scanner

package
v0.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Index

Constants

View Source
const (
	FrameworkOWASP        = "owasp"
	FrameworkSLSABuildL1  = "slsa-build-l1"
	FrameworkSLSABuildL2  = "slsa-build-l2"
	FrameworkSLSABuildL3  = "slsa-build-l3"
	FrameworkSLSASourceL1 = "slsa-source-l1" // version controlled (trivially satisfied for any GitHub repo)
	FrameworkSLSASourceL2 = "slsa-source-l2" // preserve change history (immutable refs, tag protection)
	FrameworkSLSASourceL3 = "slsa-source-l3" // continuous technical controls (branch protection, status checks)
	FrameworkSLSASourceL4 = "slsa-source-l4" // two-party review
)

Framework labels grouping rules into compliance/security frameworks. A rule can belong to multiple frameworks at once (e.g. action-pinning is both an OWASP check and a SLSA Build L3 prerequisite). The CLI's --ruleset flag and the web app's ruleset selector accept these as filter values directly, plus the umbrella "slsa" alias that matches any "slsa-*" entry.

SLSA labels track the SLSA v1.2 specification (https://slsa.dev/spec/v1.2/). The Build track levels (L1/L2/L3) are unchanged from v1.0; the Source track (L1 Version Controlled → L2 History & Provenance → L3 Continuous Technical Controls → L4 Two-Party Review) is new/promoted-from-draft in v1.2.

View Source
const (
	RefKindAction           = "action"
	RefKindReusableWorkflow = "reusable-workflow"
)

Ref kinds distinguish a step-level action call from a job-level reusable workflow call. Both are `uses:` references but live at different levels and mean different things for inventory.

View Source
const SettingsFile = "<repository settings>"

SettingsFile is the synthetic Finding.File value used for all repository configuration findings (those that come from the GitHub API, not from a workflow YAML). Reporters and the web SPA recognize this exact value and render the findings apart from per-file findings.

Variables

This section is empty.

Functions

func AssignFingerprints

func AssignFingerprints(findings []Finding)

AssignFingerprints fills each finding's Fingerprint with a stable identity for cross-scan tracking. The hash covers what the finding *is* — rule, file, title, and the description (which names the job/step/action involved) — and deliberately excludes line/column, so unrelated edits that shift a finding down a file don't change its identity.

Two genuinely identical findings in one file (same rule, same description — e.g. copy-pasted steps) get a stable "#n" occurrence suffix, assigned in line order so the earliest occurrence keeps the bare hash across scans.

func CollectInlineIgnores

func CollectInlineIgnores(content []byte) map[int]inlineDirective

CollectInlineIgnores parses the raw file content and returns a directive per line that carries a `# pipefort: ignore` comment (1-based line numbers).

func ConfigFileNames

func ConfigFileNames() []string

ConfigFileNames returns the candidate config filenames in precedence order, so the web API can fetch them over the provider's contents API.

func FixBytes

func FixBytes(content []byte, findings []Finding) ([]byte, int, error)

FixBytes applies the auto-fix dispatcher to a CI YAML's content in-memory and returns the mutated bytes plus a count of fixes applied. Returns (nil, 0, nil) when no fix matched — callers can use this to skip opening an empty PR/MR or writing an unchanged file.

Findings are split by platform via the RuleID suffix (`-gl-` → GitLab, otherwise GitHub). Mixed batches are rare (findings come from one file), so we dispatch the whole batch to whichever fixer matches the first platform-tagged finding; the other side's findings fall through unhandled.

func FixFile

func FixFile(filePath string, findings []Finding) (int, error)

FixFile parses a single YAML file, applies AST modifications based on findings, and saves in-place. Thin wrapper around FixBytes — exists so the CLI's --fix path keeps its current behaviour while the web app uses the in-memory primitive directly.

func FixFindings

func FixFindings(targetPath string, findings []Finding) (int, error)

FixFindings groups findings by file, applies fixes to each file's YAML AST, and writes it back.

func IsGitLabCIPath

func IsGitLabCIPath(p string) bool

IsGitLabCIPath reports whether a path looks like a GitLab CI config file: .gitlab-ci.yml / .gitlab-ci.yaml at the repo root, or any *.yml/.yaml under .gitlab-ci/.

func RuleByID

func RuleByID() map[RuleID]RuleSpec

RuleByID returns a map view of Rules() for O(1) lookups, e.g. when validating a rule_id submitted by the API client.

Types

type ActionRef

type ActionRef struct {
	File           string
	Line           int
	Column         int
	Owner          string
	Repo           string
	Ref            string // tag, branch, or 40-hex SHA after '@'
	Raw            string // full "owner/repo@ref" as written
	VersionComment string // trailing "# v3" comment text, if any
	// Kind is RefKindAction for a step-level action, or RefKindReusableWorkflow
	// for a job-level reusable-workflow call. Empty on refs from callers that
	// predate this field; treat empty as RefKindAction.
	Kind string
	// Path is the reusable workflow's path within its repo (e.g.
	// ".github/workflows/build.yml"). Empty for actions.
	Path string
}

ActionRef is a single third-party `uses: owner/repo[/path]@ref` reference, with the trailing line comment captured for ref-version-mismatch.

func CollectActionRefs

func CollectActionRefs(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []ActionRef

CollectActionRefs extracts every third-party action reference from a parsed workflow. Local (`./`, `.github/`) and `docker://` references are skipped — the former isn't third-party, the latter is handled by CheckUnpinnedImages.

func CollectActionRefsFromBytes

func CollectActionRefsFromBytes(name string, content []byte) []ActionRef

CollectActionRefsFromBytes parses one GitHub workflow file's bytes and returns its action references. GitLab files have no equivalent `uses:` surface and yield nothing. Used by the web scan pipeline, which holds file content in memory.

func CollectActionRefsFromDir

func CollectActionRefsFromDir(dirPath string) []ActionRef

CollectActionRefsFromDir walks a repo's .github/workflows directory and returns every action reference across its workflow files. Used by the CLI's --audit-pins pass.

func CollectReusableWorkflowRefs added in v0.1.1

func CollectReusableWorkflowRefs(file string, jobs []JobNodeWithID) []ActionRef

CollectReusableWorkflowRefs extracts every remote reusable-workflow call from a parsed workflow — the job-level `uses: owner/repo/.github/workflows/x.yml@ref` that CollectActionRefs (which only reads step-level `uses:`) deliberately skips. Local calls (`./...`) are omitted: they aren't third-party. Kept a separate collector so callers that only want action refs (and the pin-audit / detection paths) are unaffected.

func CollectReusableWorkflowRefsFromBytes added in v0.1.1

func CollectReusableWorkflowRefsFromBytes(name string, content []byte) []ActionRef

CollectReusableWorkflowRefsFromBytes parses one GitHub workflow file's bytes and returns its remote reusable-workflow calls. GitLab files yield nothing. Mirror of CollectActionRefsFromBytes for the job-level surface.

type ActionsPermissions

type ActionsPermissions struct {
	Enabled        bool   `json:"enabled"`
	AllowedActions string `json:"allowed_actions"` // "all" | "local_only" | "selected"
}

ActionsPermissions mirrors GET /repos/.../actions/permissions.

type Advisory

type Advisory struct {
	GHSAID          string
	Summary         string
	VulnerableRange string // e.g. "< 1.2.3" or ">= 1.0.0, < 1.2.3"
	FirstPatched    string // e.g. "1.2.3" ("" if none)
}

Advisory is a minimal view of a GitHub security advisory affecting an action.

type AttackStage

type AttackStage struct {
	Order       int    `json:"order"`
	Title       string `json:"title"`
	Description string `json:"description"`
	// RuleID links a stage to the component finding responsible for it. Empty
	// for the synthetic terminal "impact" stage.
	RuleID RuleID `json:"rule_id"`
}

AttackStage is one ordered node in the attack chain drawn for a combo.

type BranchProtection

type BranchProtection struct {
	EnforceAdmins              *EnabledFlag                `json:"enforce_admins,omitempty"`
	RequiredPullRequestReviews *RequiredPullRequestReviews `json:"required_pull_request_reviews,omitempty"`
	RequiredStatusChecks       *RequiredStatusChecks       `json:"required_status_checks,omitempty"`
	RequiredSignatures         *EnabledFlag                `json:"required_signatures,omitempty"`
	AllowForcePushes           *EnabledFlag                `json:"allow_force_pushes,omitempty"`
	AllowDeletions             *EnabledFlag                `json:"allow_deletions,omitempty"`
}

BranchProtection is the subset of GET /repos/.../branches/{branch}/protection the rules need. Nested pointer fields follow GitHub's convention of omitting the sub-object entirely when the policy is not in force (e.g. RequiredPullRequestReviews == nil means "PR reviews are not required").

type ComboComponent

type ComboComponent struct {
	RuleID  RuleID  `json:"rule_id"`
	Finding Finding `json:"finding"`
}

ComboComponent ties a matched requirement back to the concrete finding (file:line) that satisfied it.

type ComboScope

type ComboScope string

ComboScope describes how a combo instance is keyed: "file" combos require their anchor ingredient inside one workflow file and emit once per such file; "repo" combos correlate repo-wide and emit at most once per scan.

const (
	ScopeFile ComboScope = "file"
	ScopeRepo ComboScope = "repo"
)

type ComboSeverity

type ComboSeverity string

ComboSeverity rates a toxic combination. It is deliberately a separate type from Finding.Severity: combos can be CRITICAL (above the per-finding scale) and must never leak into shouldFail / countBySeverity / CLI exit codes.

const (
	ComboCritical ComboSeverity = "CRITICAL"
	ComboHigh     ComboSeverity = "HIGH"
)

type Confidence

type Confidence string

Confidence expresses how certain a rule is that a finding is real (as opposed to how bad it would be — that's Severity). Deterministic checks (e.g. "no permissions block") are HIGH; pattern/heuristic checks that can misfire (e.g. secret-name matching, typosquat edit distance) are MEDIUM or LOW. Every finding gets a confidence: checks may stamp one per finding, and StampConfidence backfills the rule's default for the rest.

const (
	ConfidenceHigh   Confidence = "HIGH"
	ConfidenceMedium Confidence = "MEDIUM"
	ConfidenceLow    Confidence = "LOW"
)

type Config

type Config struct {
	Path           string   `json:"path"`
	FailOnSeverity Severity `json:"fail_on_severity"`
	Ruleset        string   `json:"ruleset"` // "all", "owasp", "slsa", "slsa-build-l1|2|3"
}

Config represents options for scanning.

type EnabledFlag

type EnabledFlag struct {
	Enabled bool `json:"enabled"`
}

EnabledFlag is GitHub's recurring `{"enabled": bool}` envelope.

type FeatureStatus

type FeatureStatus struct {
	Status string `json:"status"`
}

FeatureStatus is GitHub's `{"status": "enabled" | "disabled"}` shape.

type Finding

type Finding struct {
	File           string   `json:"file"`
	Line           int      `json:"line"`
	Column         int      `json:"column"`
	Severity       Severity `json:"severity"`
	Category       string   `json:"category"`       // OWASP Category (e.g. CICD-SEC-04)
	RuleID         RuleID   `json:"rule_id"`        // Per-check identifier (matches RuleSpec.ID). Empty for SYSTEM findings.
	Title          string   `json:"title"`          // Short description
	Description    string   `json:"description"`    // Detailed description of the risk
	Recommendation string   `json:"recommendation"` // Actionable steps to fix
	// Confidence is how certain the check is that this finding is real.
	// Backfilled from the rule's DefaultConfidence by StampConfidence; SYSTEM
	// findings (empty RuleID) default to HIGH.
	Confidence Confidence `json:"confidence,omitempty"`
	// Fingerprint is a stable identity for tracking the same finding across
	// scans (independent of line/column shifts). Populated by
	// AssignFingerprints; empty until then. Feeds the web app's new-finding
	// diffing and SARIF partialFingerprints.
	Fingerprint string `json:"fingerprint,omitempty"`
}

Finding represents a single security vulnerability/risk found in the pipeline.

func ApplyRepoConfig

func ApplyRepoConfig(findings []Finding, cfg *RepoConfig) []Finding

ApplyRepoConfig applies a repo config to a finished finding list as a pure transform: drop disabled rules, drop file/line-matched ignores, and rewrite severities. SYSTEM findings (empty RuleID) always pass. A nil config is a no-op. This runs after the file paths are normalized to repo-relative so the glob matching in IgnoreEntry.File works.

func AuditActionPins

func AuditActionPins(ctx context.Context, refs []ActionRef, auditor PinAuditor) []Finding

AuditActionPins runs the four online supply-chain audits over the collected references. Network results are memoized per repo/ref so a heavily-reused action (e.g. actions/checkout) is looked up once. Lookup errors are swallowed per-ref so a transient failure can't sink the whole pass; the affected audit simply produces no finding.

func CheckCachePoisoningRelease

func CheckCachePoisoningRelease(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckCachePoisoningRelease flags dependency caching inside a workflow that also publishes a release-shaped artifact. A cache entry poisoned by a lower-trust workflow (e.g. one triggered by a fork PR) can be restored here and flow into the published output.

func CheckCheckoutPersistCredentials

func CheckCheckoutPersistCredentials(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckCheckoutPersistCredentials flags actions/checkout steps inside a privileged-trigger workflow that don't disable persist-credentials, leaving the job token in .git/config for later untrusted steps (CICD-SEC-1).

func CheckContinueOnErrorJob

func CheckContinueOnErrorJob(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckContinueOnErrorJob flags jobs that declare continue-on-error: true at the job level. A job-level `continue-on-error: true` causes the job's conclusion to be reported as success even when its steps fail, so required- check gates, branch-protection enforcement, and audit dashboards lose visibility into the failure.

func CheckDebugLoggingEnabled

func CheckDebugLoggingEnabled(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckDebugLoggingEnabled flags workflow/job/step env blocks that turn on the GitHub Actions debug-logging knobs.

func CheckDownloadWithoutChecksum

func CheckDownloadWithoutChecksum(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckDownloadWithoutChecksum flags inline scripts that download a binary or archive without a paired integrity-verification command in the same step.

func CheckForbiddenUses

func CheckForbiddenUses(refs []ActionRef, policy *ForbiddenUses) []Finding

CheckForbiddenUses enforces a repository's action allow/deny policy from its .pipefort.yml `forbidden-uses` block. It matches each action reference's `owner/repo` (ignoring the @ref) against the policy patterns (globs, e.g. `someorg/*` or `actions/checkout`). Allow and Deny are mutually exclusive; Allow wins when both are set. Returns nil when no policy is configured, so the rule is silent by default.

func CheckGitHubEnvInjection

func CheckGitHubEnvInjection(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckGitHubEnvInjection flags writes to $GITHUB_ENV / $GITHUB_PATH whose value derives from untrusted input — either interpolated directly, or laundered through a tainted env: var. Poisoning these files injects environment variables / PATH entries into every subsequent step (CICD-SEC-4).

func CheckHardcodedSecrets

func CheckHardcodedSecrets(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckHardcodedSecrets checks for Insufficient Credential Hygiene (CICD-SEC-6) specifically finding hardcoded passwords, tokens, API keys.

func CheckLongLivedPAT

func CheckLongLivedPAT(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckLongLivedPAT flags secrets-context references whose name strongly implies a long-lived personal access token. We walk env blocks (workflow, job, step) and `with:` inputs on steps; the literal GITHUB_TOKEN is excluded.

func CheckMissingConcurrency

func CheckMissingConcurrency(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckMissingConcurrency flags deploy/release-shaped workflows that lack any concurrency: guard (workflow-level or on every job). Overlapping runs of such a workflow race on shared caches, artifacts, and deploy targets.

func CheckMissingTimeout

func CheckMissingTimeout(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckMissingTimeout checks if jobs have defined timeout-minutes to prevent resource exhaustion.

func CheckObfuscatedExpression

func CheckObfuscatedExpression(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckObfuscatedExpression flags obfuscation that hides untrusted-input flow from human review: index-notation context access and base64-decode-and-run chains in run scripts.

func CheckOverprovisionedSecrets

func CheckOverprovisionedSecrets(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckOverprovisionedSecrets flags two over-exposure patterns: toJSON(secrets) (dumps every secret at once) and workflow-level env: mapping of secrets.* (leaks the secret into every step of every job).

func CheckPBAC

func CheckPBAC(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckPBAC checks for Pipeline-Based Access Controls (CICD-SEC-5) specifically missing top-level and job-level permissions blocks.

func CheckPPE

func CheckPPE(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckPPE checks for Poisoned Pipeline Execution (CICD-SEC-4) — untrusted github context interpolated into code. Two injection surfaces are covered: inline run: scripts, and action with: inputs that are evaluated as code (most notably actions/github-script's `script:` input).

func CheckPPELaundered

func CheckPPELaundered(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckPPELaundered extends PPE detection (same rule as CheckPPE) to untrusted data laundered through a tainted env: var and re-interpolated as `${{ env.X }}` in a run script or action input — a bypass of the direct github.event.* match.

func CheckPipeToShell

func CheckPipeToShell(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckPipeToShell checks if steps run curl/wget and pipe it directly to bash/sh.

func CheckRepoDispatchUnfiltered

func CheckRepoDispatchUnfiltered(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckRepoDispatchUnfiltered flags workflows triggered by repository_dispatch without an explicit types: allowlist. Without it, any caller with a repo-scoped token can fire the workflow with arbitrary event_type and client_payload — effectively granting third-party services an unbounded trigger surface.

func CheckSLSACachePoisoning

func CheckSLSACachePoisoning(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSLSACachePoisoning flags actions/cache (or actions/cache/restore) keys derived from PR-controlled inputs in workflows that run with elevated pull_request_target privileges. An attacker controls the cache namespace and can plant payloads consumed by the trusted base branch.

func CheckSLSAOIDCTokenScope

func CheckSLSAOIDCTokenScope(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSLSAOIDCTokenScope flags jobs that use attest/cosign/slsa-generator signing tooling but don't declare id-token: write at the job level (and the workflow-level permissions don't already grant it).

func CheckSLSAPermsOverlyBroad

func CheckSLSAPermsOverlyBroad(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSLSAPermsOverlyBroad flags an explicit permissions block that grants write-all (the antipattern that's strictly worse than no block: it makes the maintainer think they applied least privilege when they actually granted everything).

func CheckSLSAProvenance

func CheckSLSAProvenance(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSLSAProvenance flags workflows that publish a release-shaped artifact without any provenance/signing step. Heuristic: presence of any "publishes" action or run-line, absence of every provenance/signing action *and* the SLSA reusable workflow.

func CheckSLSAProvenanceIsolated

func CheckSLSAProvenanceIsolated(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSLSAProvenanceIsolated flags workflows that have in-job attestation steps but no slsa-github-generator reusable workflow call. The in-job form is L2 only — L3 requires the signing context the user's run-steps can't touch, which the reusable workflow provides.

func CheckSLSAVerifyStep

func CheckSLSAVerifyStep(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSLSAVerifyStep emits an INFO finding when the workflow consumes artifacts (downloads or pulls images) but no verification step is present. This is a recommendation, not a security defect — INFO severity keeps it out of failure thresholds by default.

func CheckSecretInRunOutput

func CheckSecretInRunOutput(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSecretInRunOutput flags inline scripts that print a secret to logs or persist it to step output/env, both of which defeat GitHub's log masking.

func CheckSecretsInheritPRTarget

func CheckSecretsInheritPRTarget(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSecretsInheritPRTarget flags jobs that call a reusable workflow with secrets: inherit inside a privileged-trigger workflow, handing every repository secret to a workflow running in an attacker-influenced context (CICD-SEC-4).

func CheckSelfHostedRunners

func CheckSelfHostedRunners(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSelfHostedRunners checks if jobs run on self-hosted runners which may expose internal environments.

func CheckSpoofableActorCondition

func CheckSpoofableActorCondition(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckSpoofableActorCondition flags an `if:` gate that compares github.actor or github.triggering_actor to a bot login as a security control. Those fields are spoofable in several trigger contexts, so they must not be the sole guard on privileged logic (CICD-SEC-1).

func CheckUnpinnedActions

func CheckUnpinnedActions(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckUnpinnedActions checks for Dependency Chain Abuse (CICD-SEC-3) specifically referencing third-party actions by tag/branch instead of full commit SHA.

func CheckUnpinnedImages

func CheckUnpinnedImages(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckUnpinnedImages checks for Dependency Chain Abuse (CICD-SEC-3) via container images referenced by a mutable tag instead of an immutable digest. Three surfaces are covered, all of which pull an OCI image at run time:

  • job-level `container:` (scalar `image:tag` or a mapping with `image:`)
  • `services.<name>.image`
  • step `uses: docker://image:tag`

A tag like `:latest` or `:18` is mutable — the registry can repoint it to a different (possibly malicious) image — so the build is only reproducible when the image is pinned by `@sha256:` digest. Pure-expression images (`${{ ... }}`) are skipped: their value isn't knowable statically.

func CheckUnsoundCondition

func CheckUnsoundCondition(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckUnsoundCondition flags job/step `if:` values that GitHub evaluates to a constant-truthy string rather than a boolean. The canonical bug is mixing literal text with an expression — either multiple ${{ }} blocks joined by literal operators, or literal text outside a single ${{ }} block. GitHub then treats the whole value as a non-empty string, which is always truthy, so the guard never actually gates.

func CheckUnsoundContains

func CheckUnsoundContains(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckUnsoundContains flags contains('literal haystack', <context>) where the needle is attacker-influenceable (a ref, label, title, …). A crafted value that is a substring of the haystack satisfies the check and passes the gate.

func CheckUntrustedPullRequestTarget

func CheckUntrustedPullRequestTarget(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckUntrustedPullRequestTarget checks for Insufficient Flow Control (CICD-SEC-1): a privileged trigger (pull_request_target or workflow_run) that checks out the untrusted head ref and then runs tests/builds with secrets in scope.

func CheckUseTrustedPublishing

func CheckUseTrustedPublishing(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckUseTrustedPublishing nudges publishing steps that use a long-lived registry token toward OIDC trusted publishing (short-lived, per-run credentials). Currently targets PyPI (the most mature trusted-publishing ecosystem) plus token-based npm/cargo/rubygems publishes.

func CheckWorkflowRunArtifactPoisoning

func CheckWorkflowRunArtifactPoisoning(file string, workflow *WorkflowNode, jobs []JobNodeWithID) []Finding

CheckWorkflowRunArtifactPoisoning flags workflow_run workflows that download artifacts produced by the (untrusted) triggering run. A fork PR can upload a poisoned artifact the privileged workflow_run then trusts (CICD-SEC-1).

func FilterByConfidence

func FilterByConfidence(findings []Finding, min Confidence) []Finding

FilterByConfidence keeps findings at or above the given minimum confidence. An empty minimum (or LOW) keeps everything. SYSTEM findings always pass.

func FilterByEnabledRules

func FilterByEnabledRules(findings []Finding, disabled map[RuleID]bool) []Finding

FilterByEnabledRules drops findings whose RuleID is explicitly disabled. Findings with an empty RuleID (synthetic SYSTEM/INFO notices like parse errors or settings-audit failures) are never toggleable — they always pass. A nil/empty disabled map is a no-op; callers can pass the result of EffectiveDisabledRules unconditionally.

func FilterByPersona

func FilterByPersona(findings []Finding, persona Persona) []Finding

FilterByPersona keeps findings whose rule belongs to the selected persona tier or below: `regular` keeps only regular rules, `pedantic` adds pedantic ones, `auditor` keeps everything. SYSTEM findings and unknown rules always pass.

func FilterFindings

func FilterFindings(findings []Finding, ruleset string) []Finding

FilterFindings filters findings based on the ruleset. Supported values:

  • "", "all" — every finding (no filter).
  • "owasp" — rules tagged with FrameworkOWASP.
  • "slsa" — rules tagged with any SLSA v1.2 framework (Build or Source track).
  • "slsa-build-l1|l2|l3" — rules for that specific SLSA v1.2 Build level.
  • "slsa-source-l1|l2|l3|l4" — rules for that specific SLSA v1.2 Source level.

SYSTEM findings (no RuleID — parse errors, settings-audit notices) always pass. Rules with no framework tags only appear under "all".

func ScanBytes

func ScanBytes(name string, content []byte) ([]Finding, error)

ScanBytes scans raw CI YAML held in memory. It dispatches on the file path: .gitlab-ci.yml / .gitlab-ci/*.yml is routed through the GitLab scanner; everything else is treated as a GitHub Actions workflow. The name argument is used both for dispatch and as the Finding.File label so callers need not write content to disk before scanning.

func ScanDir

func ScanDir(dirPath string) ([]Finding, error)

ScanDir walks a directory looking for CI/CD configs to scan:

  • .github/workflows/*.yml / *.yaml (GitHub Actions)
  • .gitlab-ci.yml at the repo root and .gitlab-ci/**/*.yml (GitLab CI)

Both layouts are walked when present; findings from each are merged in path order. When neither is present we fall back to the legacy "walk everything" behaviour for ad-hoc YAML collections.

func ScanFile

func ScanFile(filePath string) ([]Finding, error)

ScanFile scans a single GitHub Actions workflow file from disk.

func ScanGitLabProjectSettings

func ScanGitLabProjectSettings(ctx GitLabProjectContext) []Finding

ScanGitLabProjectSettings runs the GitLab project-configuration checks and returns the findings. Each finding carries the synthetic SettingsFile path and Platform-gitlab rule ids so the UI groups them like the GitHub settings audit. Mirrors ScanRepositorySettings's hardcoded-sequence style.

func ScanRepositorySettings

func ScanRepositorySettings(ctx RepositoryContext) []Finding

ScanRepositorySettings runs every repository-settings audit rule against the fetched context and returns the resulting findings. It mirrors ScanBytes's hardcoded-sequence style at scanner.go so a new rule means "add a line here", with no registry to update.

func StampConfidence

func StampConfidence(findings []Finding) []Finding

StampConfidence backfills Finding.Confidence from the rule's DefaultConfidence for findings that don't carry one. SYSTEM findings (empty RuleID) and findings for unknown rules default to HIGH. Mutates in place and returns the slice for call-site convenience.

type ForbiddenUses

type ForbiddenUses struct {
	Allow []string `yaml:"allow"`
	Deny  []string `yaml:"deny"`
}

ForbiddenUses is an allow/deny policy for action references. Exactly one of Allow (only these are permitted) or Deny (these are forbidden) should be set; when both are present, Allow takes precedence.

type GitHubPinAuditor

type GitHubPinAuditor struct {
	Token  string
	Client *http.Client
}

GitHubPinAuditor implements PinAuditor against api.github.com. A token is optional but strongly recommended — anonymous requests share a 60/hour limit. It reuses the package's githubAPIBaseURL var so tests can point it at an httptest server.

func NewGitHubPinAuditor

func NewGitHubPinAuditor(token string) *GitHubPinAuditor

NewGitHubPinAuditor builds an auditor with a 10s-per-request HTTP client.

func (*GitHubPinAuditor) Advisories

func (g *GitHubPinAuditor) Advisories(ctx context.Context, owner, repo string) ([]Advisory, error)

Advisories returns GHSA entries affecting an action via the global advisories API, filtered to the actions ecosystem and this package.

func (*GitHubPinAuditor) RefKinds

func (g *GitHubPinAuditor) RefKinds(ctx context.Context, owner, repo, ref string) (bool, bool, error)

RefKinds reports whether ref exists as a branch and/or a tag via the git refs API. A 404 on a kind means that kind doesn't exist.

func (*GitHubPinAuditor) RepoArchived

func (g *GitHubPinAuditor) RepoArchived(ctx context.Context, owner, repo string) (bool, bool, error)

RepoArchived reports whether owner/repo is archived via the repo API.

func (*GitHubPinAuditor) ResolveRef

func (g *GitHubPinAuditor) ResolveRef(ctx context.Context, owner, repo, ref string) (string, bool, error)

ResolveRef resolves a tag/branch/SHA to its commit SHA via the commits API. A 404 means the ref/SHA isn't present in the repo (found=false).

func (*GitHubPinAuditor) TagSHAs

func (g *GitHubPinAuditor) TagSHAs(ctx context.Context, owner, repo string) ([]string, error)

TagSHAs returns the commit SHAs the repo's tags point at (first page only — enough for the stale-ref heuristic; older tags rarely matter).

type GitLabProjectContext

type GitLabProjectContext struct {
	ProjectPath   string
	WebURL        string
	DefaultBranch string

	// OnlyAllowMergeIfPipelineSucceeds mirrors the project setting of the same
	// name; nil when the project payload didn't include it.
	OnlyAllowMergeIfPipelineSucceeds *bool
	// PublicBuilds is the "public pipelines" project setting.
	PublicBuilds *bool
	// ApprovalsBeforeMerge is the required approval count; nil when the
	// approvals API is unavailable (Free tier returns 404).
	ApprovalsBeforeMerge *int

	// DefaultBranchProtected is true when the default branch has a Protected
	// Branch entry. DefaultBranchAllowsForcePush reflects that entry's
	// allow_force_push. Both are meaningful only when protected-branch data was
	// fetched (BranchProtectionFetched).
	BranchProtectionFetched      bool
	DefaultBranchProtected       bool
	DefaultBranchAllowsForcePush bool
}

GitLabProjectContext is the GitLab project configuration the settings audit inspects. It is the GitLab analog of RepositoryContext (which is GitHub-shaped); the API layer fetches it and hands it to ScanGitLabProjectSettings. Pointer fields distinguish "not fetched / not available on this tier" (nil) from a real value.

type IgnoreEntry

type IgnoreEntry struct {
	File  string `yaml:"file"`
	Lines []int  `yaml:"lines"`
}

IgnoreEntry suppresses a rule at a file (glob, repo-relative) and, optionally, specific 1-based line numbers. An empty Lines slice suppresses the whole file.

type JobNode

type JobNode struct {
	Name        yaml.Node `yaml:"name"`
	RunsOn      yaml.Node `yaml:"runs-on"`
	Permissions yaml.Node `yaml:"permissions"`
	Env         yaml.Node `yaml:"env"`
	Concurrency yaml.Node `yaml:"concurrency"`
	Steps       yaml.Node `yaml:"steps"`
	// Uses is a job-level `uses:` — i.e. the job calls a reusable workflow
	// (owner/repo/.github/workflows/x.yml@ref). Empty for normal step-based jobs.
	Uses           yaml.Node `yaml:"uses"`
	If             yaml.Node `yaml:"if"`
	TimeoutMinutes yaml.Node `yaml:"timeout-minutes"`
}

JobNode represents a single job in a workflow.

type JobNodeWithID

type JobNodeWithID struct {
	ID     string
	Line   int
	Column int
	Node   JobNode
}

JobNodeWithID wraps a JobNode with its mapping key (ID).

type Persona

type Persona string

Persona groups rules by signal-to-noise, zizmor-style. The CLI's --persona flag admits rules at or below the selected tier: `regular` (the default) keeps only high-signal security checks, `pedantic` adds hygiene/best-practice nits, and `auditor` adds everything — including checks that surface things worth a look but rarely actionable on their own.

const (
	PersonaRegular  Persona = "regular"
	PersonaPedantic Persona = "pedantic"
	PersonaAuditor  Persona = "auditor"
)

type PinAuditor

type PinAuditor interface {
	// ResolveRef returns the commit SHA a ref (tag/branch/sha) resolves to in
	// owner/repo, and whether it exists there at all (found=false on 404).
	ResolveRef(ctx context.Context, owner, repo, ref string) (sha string, found bool, err error)
	// Advisories returns published advisories affecting the owner/repo action.
	Advisories(ctx context.Context, owner, repo string) ([]Advisory, error)
	// RepoArchived reports whether owner/repo is archived (read-only) upstream.
	// ok=false when the repo can't be read (404/error) so the caller skips it.
	RepoArchived(ctx context.Context, owner, repo string) (archived bool, ok bool, err error)
	// RefKinds reports whether a ref exists upstream as a branch and/or a tag.
	RefKinds(ctx context.Context, owner, repo, ref string) (isBranch, isTag bool, err error)
	// TagSHAs returns the commit SHAs that owner/repo's tags point at.
	TagSHAs(ctx context.Context, owner, repo string) ([]string, error)
}

PinAuditor performs the network lookups the online audits need. It is an interface so the audits can be unit-tested with a fake (no network).

type Platform

type Platform string

Platform names the CI/CD platform a rule targets. Empty defaults to GitHub for back-compat with the existing rule catalog — every pre-multi-provider rule was implicitly GitHub Actions. New GitLab-specific rules carry PlatformGitLab. Portable rules (e.g. best-prac-1-pipe-to-shell, cicd-sec-9-download-without-checksum) carry PlatformAny.

const (
	PlatformAny    Platform = ""       // legacy/portable
	PlatformGitHub Platform = "github" // GitHub Actions YAML / repo settings
	PlatformGitLab Platform = "gitlab" // GitLab CI YAML
)

type RepoConfig

type RepoConfig struct {
	// Ruleset / MinConfidence / Persona are defaults for the corresponding CLI
	// flags; an explicit flag always wins (see LoadRepoConfig callers).
	Ruleset       string `yaml:"ruleset"`
	MinConfidence string `yaml:"min-confidence"`
	Persona       string `yaml:"persona"`

	// Rules holds per-rule overrides keyed by RuleID.
	Rules map[string]RuleOverride `yaml:"rules"`

	// ForbiddenUses drives the cicd-sec-5-forbidden-uses rule (allow XOR deny
	// list of action references). Consumed by that check; nil = rule silent.
	ForbiddenUses *ForbiddenUses `yaml:"forbidden-uses"`
}

RepoConfig is an in-repo `.pipefort.yml` — the CLI-friendly counterpart to the web app's DB-backed rule settings. It lets a repository ship its own scan preferences and per-rule suppressions alongside its code, so a `pipefort` run needs no external state. zizmor's `zizmor.yml` is the direct analog.

Every field is optional. The zero value applies nothing.

func LoadRepoConfig

func LoadRepoConfig(dir string) (*RepoConfig, string, error)

LoadRepoConfig looks for a config file under dir (in repoConfigNames order) and parses it. Returns (nil, "", nil) when none is present. The returned string is the relative path found, for user-facing messaging.

func ParseRepoConfig

func ParseRepoConfig(data []byte) (*RepoConfig, error)

ParseRepoConfig unmarshals config bytes and validates enum-ish fields so a typo (e.g. persona: auditer) is surfaced instead of silently ignored.

func (*RepoConfig) DisabledRuleIDs

func (c *RepoConfig) DisabledRuleIDs() map[RuleID]bool

DisabledRuleIDs returns the set of rules the config turns off. Used by the web layer to union config-disabled rules into the DB-backed disabled set (a repo config may further-restrict, never re-enable).

type RepoInfo

type RepoInfo struct {
	Private             bool                 `json:"private"`
	Visibility          string               `json:"visibility"` // "public" | "private" | "internal"
	DefaultBranch       string               `json:"default_branch"`
	SecurityAndAnalysis *SecurityAndAnalysis `json:"security_and_analysis,omitempty"`
}

RepoInfo is the subset of GET /repos/{owner}/{repo} the settings audit cares about. SecurityAndAnalysis is only present for repos where GitHub has decided to surface the feature toggles (always for public, conditionally for private).

type RepositoryContext

type RepositoryContext struct {
	Owner         string
	Repo          string
	DefaultBranch string
	HTMLURL       string // e.g. https://github.com/{owner}/{repo} — used to build remediation links

	Repository       *RepoInfo
	BranchProtection *BranchProtection
	WorkflowPerms    *WorkflowPermissions
	ActionsPolicy    *ActionsPermissions
	DependabotAlerts bool // true == GitHub returned 204 ("enabled")
	HasCodeowners    bool // true when any of CODEOWNERS / .github/CODEOWNERS / docs/CODEOWNERS exists

	// BranchProtectionUnavailable is true when GitHub refused to serve branch
	// protection because the repository's plan doesn't support protected
	// branches (private repo on a free plan), as opposed to it simply being
	// unconfigured (BranchProtection == nil with this false). The audit reports
	// this as an INFO note instead of a HIGH "no protection" finding the owner
	// can't act on without upgrading.
	BranchProtectionUnavailable bool
}

RepositoryContext is the bundle of GitHub-side configuration the settings-audit rules inspect. It mirrors what FetchRepositorySettings returns from pkg/api/github.go. Pointer fields are nil when the corresponding endpoint returns 404 (or is otherwise unavailable); callers must treat nil as "not configured" rather than panicking.

type RequiredPullRequestReviews

type RequiredPullRequestReviews struct {
	DismissStaleReviews          bool `json:"dismiss_stale_reviews"`
	RequireCodeOwnerReviews      bool `json:"require_code_owner_reviews"`
	RequiredApprovingReviewCount int  `json:"required_approving_review_count"`
}

RequiredPullRequestReviews mirrors the branch-protection sub-object.

type RequiredStatusChecks

type RequiredStatusChecks struct {
	Strict bool `json:"strict"`
}

RequiredStatusChecks mirrors the branch-protection sub-object.

type RuleID

type RuleID string

RuleID is the stable, per-check identifier (e.g. "best-prac-2-missing-timeout"). Finding.Category is the coarser OWASP/best-prac group ("CICD-SEC-1", "BEST-PRAC-2") that the existing "owasp" ruleset filter and the auto-fixer dispatch on. We keep both: Category preserves backward compatibility with persisted findings and the fixer; RuleID gives users a per-check toggle that doesn't accidentally mute nine sibling branch-protection rules just because they all share CICD-SEC-1.

const (
	RulePPECheckout    RuleID = "cicd-sec-1-ppe-checkout"
	RuleLongLivedPAT   RuleID = "cicd-sec-2-long-lived-pat"
	RuleUnpinnedAction RuleID = "cicd-sec-3-unpinned-action"
	RuleUnpinnedImage  RuleID = "cicd-sec-3-unpinned-image"

	// Online pinned-action audits (supplychain_online_rules.go). Opt-in: emitted
	// only by AuditActionPins (CLI --audit-pins / the web scan pipeline), never by
	// the offline ScanBytes pass.
	RuleKnownVulnerableAction RuleID = "cicd-sec-3-known-vulnerable-action"
	RuleImpostorCommit        RuleID = "cicd-sec-3-impostor-commit"
	RuleRefVersionMismatch    RuleID = "cicd-sec-3-ref-version-mismatch"
	RuleTyposquatAction       RuleID = "cicd-sec-3-typosquat-action"
	RuleArchivedAction        RuleID = "cicd-sec-3-archived-action"
	RuleStaleActionRef        RuleID = "cicd-sec-3-stale-action-ref"
	RuleRefConfusion          RuleID = "cicd-sec-3-ref-confusion"
	RulePPEShellInjection     RuleID = "cicd-sec-4-ppe-shell-injection"
	RuleMissingPermissions    RuleID = "cicd-sec-5-missing-permissions"
	RuleHardcodedSecrets      RuleID = "cicd-sec-6-hardcoded-secrets"
	RuleDebugLoggingEnabled   RuleID = "cicd-sec-7-debug-logging-enabled"
	RuleRepoDispatchUnfilt    RuleID = "cicd-sec-8-repository-dispatch-unfiltered"
	RuleDownloadNoChecksum    RuleID = "cicd-sec-9-download-without-checksum"
	RuleContinueOnErrorJob    RuleID = "cicd-sec-10-continue-on-error-job"
	RulePipeToShell           RuleID = "best-prac-1-pipe-to-shell"
	RuleMissingTimeout        RuleID = "best-prac-2-missing-timeout"
	RuleSelfHostedRunners     RuleID = "best-prac-3-self-hosted-runners"

	// Additional workflow checks (rules.go / owasp_extended_rules.go).
	RuleWorkflowRunArtifactPoisoning RuleID = "cicd-sec-1-workflow-run-artifact-poisoning"
	RuleCheckoutPersistCreds         RuleID = "cicd-sec-1-checkout-persist-credentials"
	RuleSecretsInheritPRTarget       RuleID = "cicd-sec-4-secrets-inherit-pr-target"
	RuleSecretInRunOutput            RuleID = "cicd-sec-6-secret-in-run-output"

	// Injection-depth checks (injection_rules.go).
	RuleGitHubEnvInjection      RuleID = "cicd-sec-4-github-env-injection"
	RuleSpoofableActorCondition RuleID = "cicd-sec-1-spoofable-actor-condition"

	// Offline rule-parity batch (condition_rules.go / obfuscation_rules.go /
	// cache_rules.go / publishing_rules.go / concurrency_rules.go /
	// owasp_extended_rules.go).
	RuleUnsoundCondition   RuleID = "cicd-sec-1-unsound-condition"
	RuleUnsoundContains    RuleID = "cicd-sec-1-unsound-contains"
	RuleObfuscatedExpr     RuleID = "cicd-sec-4-obfuscated-expression"
	RuleCachePoisonRelease RuleID = "cicd-sec-4-cache-poisoning-release"
	RuleOverprovSecrets    RuleID = "cicd-sec-6-overprovisioned-secrets"
	RuleUseTrustedPublish  RuleID = "cicd-sec-2-use-trusted-publishing"
	RuleMissingConcurrency RuleID = "best-prac-4-missing-concurrency"

	// Config-driven action allow/deny policy (forbidden_uses.go). Silent unless
	// a .pipefort.yml forbidden-uses block is present.
	RuleForbiddenUses RuleID = "cicd-sec-5-forbidden-uses"

	// SLSA Build-track workflow checks (slsa_rules.go) ------------------------
	RuleSLSAProvenance         RuleID = "slsa-build-l2-provenance"
	RuleSLSAProvenanceIsolated RuleID = "slsa-build-l3-provenance-isolated"
	RuleSLSAOIDCTokenScope     RuleID = "slsa-build-l2-oidc-token-scope"
	RuleSLSAPermsOverlyBroad   RuleID = "slsa-build-l2-perms-overly-broad"
	RuleSLSACachePoisoning     RuleID = "slsa-build-l3-cache-poisoning"
	RuleSLSAVerifyStep         RuleID = "slsa-build-l2-verify-step"

	// GitLab CI workflow checks (gitlab_rules.go) -----------------------------
	// One rule per OWASP CI/CD Top 10 risk that has a portable GitLab analog.
	// IDs follow the `cicd-sec-N-gl-*` convention so the GitHub IDs stay
	// stable. The portable rules (best-prac-1, cicd-sec-9) reuse their
	// existing IDs across both platforms — there is no `-gl-` parallel.
	RuleGitLabMRTarget          RuleID = "cicd-sec-1-gl-mr-target"
	RuleGitLabPATSecret         RuleID = "cicd-sec-2-gl-pat-secret"
	RuleGitLabUnpinnedInclude   RuleID = "cicd-sec-3-gl-unpinned-include"
	RuleGitLabShellInjection    RuleID = "cicd-sec-4-gl-shell-injection"
	RuleGitLabHardcodedSecrets  RuleID = "cicd-sec-6-gl-hardcoded-secrets"
	RuleGitLabDebugTrace        RuleID = "cicd-sec-7-gl-debug-trace"
	RuleGitLabTriggerUnfiltered RuleID = "cicd-sec-8-gl-trigger-unfiltered"
	RuleGitLabAllowFailure      RuleID = "cicd-sec-10-gl-allow-failure"
	RuleGitLabMissingTimeout    RuleID = "best-prac-2-gl-missing-timeout"
	RuleGitLabSelfHostedTags    RuleID = "best-prac-3-gl-self-hosted-tags"
	RuleGitLabMissingResGroup   RuleID = "best-prac-4-gl-missing-resource-group"

	// GitLab project-settings checks (settings_gitlab.go, Surface repo-settings).
	RuleGitLabBPMissing       RuleID = "cicd-sec-1-gl-bp-missing"
	RuleGitLabBPForcePush     RuleID = "cicd-sec-1-gl-bp-force-push"
	RuleGitLabMergeNoPipeline RuleID = "cicd-sec-1-gl-merge-without-pipeline"
	RuleGitLabNoApprovals     RuleID = "cicd-sec-1-gl-no-approvals"
	RuleGitLabPublicPipelines RuleID = "cicd-sec-4-gl-public-pipelines"
)

Workflow checks (ScanBytes) ------------------------------------------------- Each ID maps to one of the Check* functions in rules.go / slsa_rules.go.

const (
	RuleBPMissing            RuleID = "cicd-sec-1-bp-missing"
	RuleBPForcePush          RuleID = "cicd-sec-1-bp-force-push"
	RuleBPDeletion           RuleID = "cicd-sec-1-bp-deletion"
	RuleBPNoReview           RuleID = "cicd-sec-1-bp-no-review"
	RuleBPFewReviewers       RuleID = "cicd-sec-1-bp-few-reviewers"
	RuleBPStaleReviews       RuleID = "cicd-sec-1-bp-stale-reviews"
	RuleBPNoStatusChecks     RuleID = "cicd-sec-1-bp-no-status-checks"
	RuleBPAdminBypass        RuleID = "cicd-sec-1-bp-admin-bypass"
	RuleBPNoCodeownersReview RuleID = "cicd-sec-1-bp-no-codeowners-review"
	RuleBPNoSignedCommits    RuleID = "cicd-sec-1-bp-no-signed-commits"

	RuleWPermWrite     RuleID = "cicd-sec-4-wperm-write"
	RuleWPermPRApprove RuleID = "cicd-sec-4-wperm-pr-approve"

	RuleActionsAllAllowed RuleID = "cicd-sec-5-actions-all-allowed"

	RuleDependabotAlertsOff RuleID = "cicd-sec-3-dependabot-alerts-off"
	RuleDependabotFixesOff  RuleID = "cicd-sec-3-dependabot-fixes-off"

	RuleSecretScanningOff       RuleID = "cicd-sec-6-secret-scanning-off"
	RuleSecretPushProtectionOff RuleID = "cicd-sec-6-secret-push-protection-off"
)

Repository-settings checks (ScanRepositorySettings) ------------------------ Each ID maps to one of the helper functions in settings.go. The slugs match the existing docs/rules/<slug>.mdx pages so doc URLs are derivable.

type RuleOverride

type RuleOverride struct {
	// Enabled, when non-nil and false, disables the rule entirely. A repo
	// config can only *disable* a rule, never re-enable one an org policy
	// turned off (enforced by the web layer, not here).
	Enabled *bool `yaml:"enabled"`
	// Severity, when set, rewrites the finding severity (HIGH/MEDIUM/LOW/INFO).
	Severity string `yaml:"severity"`
	// Ignore suppresses findings of this rule at specific files/lines.
	Ignore []IgnoreEntry `yaml:"ignore"`
}

RuleOverride is a single rule's configuration.

type RuleSpec

type RuleSpec struct {
	ID              RuleID      `json:"id"`
	Category        string      `json:"category"` // "CICD-SEC-1", "BEST-PRAC-2", ...
	Title           string      `json:"title"`
	DefaultSeverity Severity    `json:"default_severity"`
	Surface         RuleSurface `json:"surface"`
	Platform        Platform    `json:"platform,omitempty"` // empty = github (legacy default)
	Description     string      `json:"description"`
	DocURL          string      `json:"doc_url"` // "/rules/<id>"
	// Frameworks the rule serves. A rule with no frameworks is unfiltered (it
	// only shows up under the "all" ruleset). Use the Framework* constants.
	Frameworks []string `json:"frameworks"`
	// DefaultConfidence is stamped onto findings that don't carry their own.
	// Left empty in the catalog literal it normalizes to ConfidenceHigh —
	// only heuristic rules need an explicit entry.
	DefaultConfidence Confidence `json:"default_confidence"`
	// Persona tiers the rule for noise filtering. Empty normalizes to
	// PersonaRegular — only pedantic/auditor rules need an explicit entry.
	Persona Persona `json:"persona"`
}

RuleSpec describes a single user-toggleable check. The web app reads this catalog over GET /api/rules to render the Rule Settings page; the Go API uses it on PUT to validate that a rule_id submitted by the client is real.

func Rules

func Rules() []RuleSpec

Rules returns the canonical catalog in the order pages should render. New scanner checks must add an entry here (and a docs/rules/<id>.mdx page — see CLAUDE.md convention 3) so users can toggle them.

Entries may leave DefaultConfidence/Persona empty; Rules() normalizes those to ConfidenceHigh/PersonaRegular so consumers never see a zero value.

type RuleSurface

type RuleSurface string

RuleSurface tells the UI which scan path emits a rule, so the Rule Settings page can group "what I check in my workflow YAMLs" apart from "what I check in GitHub's repository configuration".

const (
	SurfaceWorkflow     RuleSurface = "workflow"      // produced by ScanBytes
	SurfaceRepoSettings RuleSurface = "repo-settings" // produced by ScanRepositorySettings
)

type SecurityAndAnalysis

type SecurityAndAnalysis struct {
	SecretScanning               *FeatureStatus `json:"secret_scanning,omitempty"`
	SecretScanningPushProtection *FeatureStatus `json:"secret_scanning_push_protection,omitempty"`
	DependabotSecurityUpdates    *FeatureStatus `json:"dependabot_security_updates,omitempty"`
}

SecurityAndAnalysis mirrors the nested object GitHub returns when the repository has any of the related features available. Each sub-field is itself nullable — secret scanning, for example, is only reported on repos where GitHub Advanced Security applies.

type Severity

type Severity string

Severity represents the severity of a security finding.

const (
	SeverityHigh   Severity = "HIGH"
	SeverityMedium Severity = "MEDIUM"
	SeverityLow    Severity = "LOW"
	SeverityInfo   Severity = "INFO"
)

type StepNode

type StepNode struct {
	Name yaml.Node `yaml:"name"`
	Uses yaml.Node `yaml:"uses"`
	Run  yaml.Node `yaml:"run"`
	If   yaml.Node `yaml:"if"`
	Env  yaml.Node `yaml:"env"`
	With yaml.Node `yaml:"with"`
}

StepNode represents a single step in a job.

type ToxicCombo

type ToxicCombo struct {
	ID             string           `json:"id"`
	Title          string           `json:"title"`
	Severity       ComboSeverity    `json:"severity"`
	Scope          ComboScope       `json:"scope"`
	File           string           `json:"file"` // set for file-scoped instances; "" repo-wide
	Impact         string           `json:"impact"`
	BreakChain     string           `json:"break_chain"`
	BreakChainRule RuleID           `json:"break_chain_rule"`
	Stages         []AttackStage    `json:"stages"`
	Components     []ComboComponent `json:"components"`
}

ToxicCombo is one detected toxic combination instance.

func DetectToxicCombinations

func DetectToxicCombinations(findings []Finding) []ToxicCombo

DetectToxicCombinations correlates the given findings into toxic combinations. Callers must pass findings AFTER rule/ruleset filtering so a disabled rule can never form a combo. The result is deterministically ordered (severity, then id, then file) and is nil for empty input.

type WorkflowNode

type WorkflowNode struct {
	Name        yaml.Node `yaml:"name"`
	On          yaml.Node `yaml:"on"`
	Permissions yaml.Node `yaml:"permissions"`
	Env         yaml.Node `yaml:"env"`
	Concurrency yaml.Node `yaml:"concurrency"`
	Jobs        yaml.Node `yaml:"jobs"`
}

WorkflowNode is a wrapper around the yaml.Node to parse a GitHub Actions workflow.

type WorkflowPermissions

type WorkflowPermissions struct {
	DefaultWorkflowPermissions   string `json:"default_workflow_permissions"`     // "read" | "write"
	CanApprovePullRequestReviews bool   `json:"can_approve_pull_request_reviews"` // self-approval
}

WorkflowPermissions mirrors GET /repos/.../actions/permissions/workflow.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL