@cyclonedx/cdxgen
Version:
Creates CycloneDX Software Bill of Materials (SBOM) from source or container image
803 lines (785 loc) • 34.7 kB
YAML
# CI/CD Permission Security Rules
# Category: ci-permission
# Evaluates GitHub Actions, GitLab CI, Azure Pipelines, etc. for privilege risks
- id: CI-001
name: "Unpinned action in write-permission workflow"
description: "GitHub Actions referenced by tag/branch in workflows with write permissions pose supply chain risk"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001]
techniques: [T1195.001]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:action:isShaPinned') = 'false'
and (
$prop($, 'cdx:github:workflow:hasWritePermissions') = 'true'
or $prop($, 'cdx:github:job:hasWritePermissions') = 'true'
)
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Unpinned GitHub Action '{{ $prop($, 'cdx:github:action:uses') }}' in workflow with write permissions"
mitigation: "Pin action to full SHA: actions/setup-node@abc123def456..."
evidence: |
{
"pinningType": $prop($, 'cdx:github:action:versionPinningType'),
"workflowTriggers": $prop($, 'cdx:github:workflow:triggers'),
"jobName": $prop($, 'cdx:github:job:name')
}
- id: CI-002
name: "OIDC token issuance to non-official action"
description: "Workflows or jobs granting id-token:write to third-party actions may enable token exfiltration"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0006]
techniques: [T1528]
condition: |
$auditComponents($)[
(
$prop($, 'cdx:github:workflow:hasIdTokenWrite') = 'true'
or $prop($, 'cdx:github:job:hasIdTokenWrite') = 'true'
)
and $prop($, 'cdx:actions:isOfficial') = 'false'
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Workflow grants OIDC token access to non-official action '{{ $prop($, 'cdx:github:action:uses') }}'"
mitigation: "Restrict id-token scope or use only verified/official actions for OIDC operations"
evidence: |
{
"isVerified": $prop($, 'cdx:actions:isVerified'),
"actionGroup": group,
"jobHasIdTokenWrite": $prop($, 'cdx:github:job:hasIdTokenWrite'),
"workflowHasIdTokenWrite": $prop($, 'cdx:github:workflow:hasIdTokenWrite')
}
- id: CI-003
name: "Action pinned to mutable tag"
description: "GitHub Actions pinned to tags (vs SHA) can change behavior unexpectedly if tag is moved"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001]
techniques: [T1195.001]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:action:versionPinningType') = 'tag'
]
location: |
{ "bomRef": $."bom-ref", "purl": purl }
message: "GitHub Action '{{ $prop($, 'cdx:github:action:uses') }}' pinned to mutable tag (not SHA)"
mitigation: "Consider pinning to full commit SHA for immutability: actions/setup-node@<full-sha>"
evidence: |
{
"currentVersion": version,
"isOfficial": $prop($, 'cdx:actions:isOfficial')
}
- id: CI-004
name: "Workflow uses pull_request_target trigger"
description: "pull_request_target can execute code in the context of the base branch, risking secret exposure"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001, TA0004]
condition: |
$auditWorkflows($)[
$prop($, 'cdx:github:workflow:hasPullRequestTargetTrigger') = 'true'
]
location: |
{
"bomRef": $."bom-ref",
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Workflow '{{ $prop($, 'cdx:github:workflow:name') }}' uses pull_request_target trigger"
mitigation: "Ensure workflow does not checkout or run code from the PR head; use pull_request with careful permissions"
evidence: |
{
"triggers": $prop($, 'cdx:github:workflow:triggers'),
"hasWritePermissions": $prop($, 'cdx:github:workflow:hasWritePermissions')
}
- id: CI-005
name: "Checkout step persists credentials unnecessarily"
description: "actions/checkout with persist-credentials=true (default) exposes GITHUB_TOKEN to subsequent steps"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0006]
techniques: [T1552]
condition: |
$auditComponents($)[
$contains($nullSafeProp($, 'cdx:github:action:uses'), 'actions/checkout')
and $propBool($, 'cdx:github:checkout:persistCredentials') != false
and (
$propBool($, 'cdx:github:workflow:hasWritePermissions') = true
or $propBool($, 'cdx:github:job:hasWritePermissions') = true
)
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Checkout step uses persist-credentials=true in privileged workflow; consider persist-credentials: false;"
mitigation: "Add persist-credentials: false to checkout steps that don't require git push operations"
evidence: |
{
"persistCredentials": $prop($, 'cdx:github:checkout:persistCredentials'),
"workflowPermissions": $prop($, 'cdx:github:workflow:hasWritePermissions')
}
- id: CI-006
name: "Cache usage in untrusted trigger context"
description: "GitHub Actions cache can be poisoned when used in workflows triggered by untrusted input (e.g., pull_request from forks)"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001]
techniques: [T1195.001]
condition: |
$auditComponents($)[
$nullSafeProp($, 'cdx:github:action:uses') ~> $contains('actions/cache')
and (
$prop($, 'cdx:github:workflow:hasPullRequestTrigger') = 'true'
or $prop($, 'cdx:github:workflow:hasPullRequestTargetTrigger') = 'true'
)
and (
$propBool($, 'cdx:github:workflow:hasWritePermissions') = true
or $propBool($, 'cdx:github:job:hasWritePermissions') = true
or $prop($, 'cdx:github:cache:hasRestoreKeys') = 'true'
or $prop($, 'cdx:github:cache:keyUsesHashFiles') != 'true'
)
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Cache action used in pull-request-reachable workflow; cache key '{{ $prop($, 'cdx:github:cache:key') }}' may be writable by untrusted code"
mitigation: "Scope cache keys to PR-specific values, validate restored cache contents, or avoid caching in untrusted workflows"
evidence: |
{
"cacheKey": $prop($, 'cdx:github:cache:key'),
"hasRestoreKeys": $prop($, 'cdx:github:cache:hasRestoreKeys'),
"keyUsesHashFiles": $prop($, 'cdx:github:cache:keyUsesHashFiles'),
"cachePath": $prop($, 'cdx:github:cache:path'),
"triggers": $prop($, 'cdx:github:workflow:triggers')
}
- id: CI-007
name: "Script injection via untrusted context interpolation"
description: "Direct interpolation of github.event.* or inputs.* into run: blocks enables command injection"
severity: critical
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0002]
techniques: [T1059]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:step:hasUntrustedInterpolation') = 'true'
and $prop($, 'cdx:github:step:type') = 'run'
]
location: |
{
"bomRef": $."bom-ref",
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Untrusted input interpolated into shell command: variables '{{ $prop($, 'cdx:github:step:interpolatedVars') }}'"
mitigation: "Use intermediate environment variables: env: TITLE: ${{ github.event.pull_request.title }} then reference $TITLE in run:"
evidence: |
{
"interpolatedVars": $prop($, 'cdx:github:step:interpolatedVars'),
"stepCommand": $prop($, 'cdx:github:step:command')
}
- id: CI-008
name: "High-risk trigger with write permissions"
description: "Triggers like pull_request_target, issue_comment, or workflow_run combined with write permissions enable privilege escalation"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001, TA0004]
condition: |
$auditWorkflows($)[
(
$prop($, 'cdx:github:workflow:hasPullRequestTargetTrigger') = 'true'
or $prop($, 'cdx:github:workflow:hasIssueCommentTrigger') = 'true'
or $prop($, 'cdx:github:workflow:hasWorkflowRunTrigger') = 'true'
)
and $prop($, 'cdx:github:workflow:hasWritePermissions') = 'true'
]
location: |
{
"bomRef": $."bom-ref",
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Workflow '{{ $prop($, 'cdx:github:workflow:name') }}' uses high-risk trigger with write permissions"
mitigation: "Use pull_request instead of pull_request_target; require manual approval for issue_comment triggers; restrict permissions per job"
evidence: |
{
"triggers": $prop($, 'cdx:github:workflow:triggers'),
"hasWritePermissions": $prop($, 'cdx:github:workflow:hasWritePermissions')
}
- id: CI-009
name: "Workflow file contains hidden Unicode characters"
description: "Hidden Unicode in workflow files can disguise malicious logic, comments, or diffs and should be reviewed before merge"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0005]
techniques: [T1027]
condition: |
$auditWorkflows($)[
$prop($, 'cdx:github:workflow:hasHiddenUnicode') = 'true'
]
location: |
{
"bomRef": $."bom-ref",
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Workflow '{{ $prop($, 'cdx:github:workflow:name') }}' contains hidden Unicode characters"
mitigation: "Review the file in an editor that reveals bidirectional, zero-width, and control characters; remove suspicious hidden Unicode before merge"
evidence: |
{
"codePoints": $prop($, 'cdx:github:workflow:hiddenUnicodeCodePoints'),
"lineNumbers": $prop($, 'cdx:github:workflow:hiddenUnicodeLineNumbers'),
"inComments": $prop($, 'cdx:github:workflow:hiddenUnicodeInComments')
}
- id: CI-010
name: "Legacy token-based package publishing step"
description: "npm and PyPI publishing should prefer trusted publishing or OIDC-backed flows instead of long-lived token secrets or explicit --token arguments"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0006]
techniques: [T1528]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:step:isPublishCommand') = 'true'
and $prop($, 'cdx:github:step:usesLegacyPublishToken') = 'true'
]
location: |
{
"bomRef": $."bom-ref",
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Workflow publish step '{{ name }}' uses legacy {{ $prop($, 'cdx:github:step:publishEcosystem') }} token-based publishing"
mitigation: "Prefer trusted publishing or short-lived OIDC-backed release flows instead of persistent package tokens or explicit --token arguments"
evidence: |
{
"ecosystem": $prop($, 'cdx:github:step:publishEcosystem'),
"tokenSources": $prop($, 'cdx:github:step:legacyPublishTokenSources'),
"stepCommand": $prop($, 'cdx:github:step:command')
}
- id: CI-011
name: "External reusable workflow inherits caller secrets"
description: "Reusable workflows invoked from external repositories with secrets: inherit expand the trust boundary and can expose repository credentials"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0006]
techniques: [T1528, T1552]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:reusableWorkflow:isExternal') = 'true'
and $prop($, 'cdx:github:reusableWorkflow:secretsInherit') = 'true'
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Reusable workflow '{{ $prop($, 'cdx:github:reusableWorkflow:uses') }}' inherits caller secrets from an external repository"
mitigation: "Avoid secrets: inherit for external reusable workflows; pass only the minimum explicit secrets and pin the reusable workflow to a full SHA"
evidence: |
{
"uses": $prop($, 'cdx:github:reusableWorkflow:uses'),
"isShaPinned": $prop($, 'cdx:github:reusableWorkflow:isShaPinned'),
"withKeys": $prop($, 'cdx:github:reusableWorkflow:withKeys')
}
- id: CI-012
name: "External reusable workflow pinned to mutable ref"
description: "Reusable workflows referenced by tag or branch can change behavior without review and should be pinned to immutable SHAs"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001]
techniques: [T1195.001]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:reusableWorkflow:isExternal') = 'true'
and $prop($, 'cdx:github:reusableWorkflow:isShaPinned') = 'false'
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "External reusable workflow '{{ $prop($, 'cdx:github:reusableWorkflow:uses') }}' is pinned to a mutable ref"
mitigation: "Pin reusable workflows to full SHAs and review updates through a normal change-management process"
evidence: |
{
"pinningType": $prop($, 'cdx:github:reusableWorkflow:versionPinningType'),
"ref": $prop($, 'cdx:github:reusableWorkflow:ref')
}
- id: CI-013
name: "High-risk trigger reaches a self-hosted runner"
description: "High-risk triggers executing on self-hosted runners can expose internal network access, credentials, and long-lived runner state"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0004, TA0008]
condition: |
$auditComponents($)[
(
$prop($, 'cdx:github:workflow:hasPullRequestTargetTrigger') = 'true'
or $prop($, 'cdx:github:workflow:hasIssueCommentTrigger') = 'true'
or $prop($, 'cdx:github:workflow:hasWorkflowRunTrigger') = 'true'
)
and $prop($, 'cdx:github:job:isSelfHosted') = 'true'
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "High-risk workflow trigger reaches self-hosted runner in job '{{ $prop($, 'cdx:github:job:name') }}'"
mitigation: "Restrict self-hosted runners to trusted workflows only, isolate runner credentials, and gate high-risk triggers with manual approval"
evidence: |
{
"triggers": $prop($, 'cdx:github:workflow:triggers'),
"jobRunner": $prop($, 'cdx:github:job:runner'),
"jobName": $prop($, 'cdx:github:job:name')
}
- id: CI-014
name: "Privileged workflow mutates downstream runner state"
description: "Writing to GITHUB_ENV, GITHUB_PATH, or GITHUB_OUTPUT in privileged workflows can persist attacker-controlled state across later steps and jobs"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0002]
techniques: [T1059]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:step:type') = 'run'
and $prop($, 'cdx:github:step:mutatesRunnerState') = 'true'
and (
$prop($, 'cdx:github:workflow:hasWritePermissions') = 'true'
or $prop($, 'cdx:github:job:hasWritePermissions') = 'true'
or $prop($, 'cdx:github:workflow:hasIdTokenWrite') = 'true'
or $prop($, 'cdx:github:job:hasIdTokenWrite') = 'true'
)
]
location: |
{
"bomRef": $."bom-ref",
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Privileged workflow step '{{ name }}' mutates runner state via '{{ $prop($, 'cdx:github:step:runnerStateTargets') }}'"
mitigation: "Avoid mutating shared runner state in privileged jobs; prefer tightly-scoped step outputs and review all GITHUB_ENV/GITHUB_PATH/GITHUB_OUTPUT writes"
evidence: |
{
"runnerStateTargets": $prop($, 'cdx:github:step:runnerStateTargets'),
"stepCommand": $prop($, 'cdx:github:step:command'),
"jobName": $prop($, 'cdx:github:job:name')
}
- id: CI-015
name: "Outbound network command references sensitive context"
description: "Run steps that invoke outbound network tools while transmitting secrets, github.token, or OIDC request context are strong exfiltration indicators"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0010]
techniques: [T1048]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:step:type') = 'run'
and $prop($, 'cdx:github:step:hasOutboundNetworkCommand') = 'true'
and $prop($, 'cdx:github:step:referencesSensitiveContext') = 'true'
and $prop($, 'cdx:github:step:likelyExfiltration') = 'true'
]
location: |
{
"bomRef": $."bom-ref",
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Outbound command in step '{{ name }}' references sensitive context '{{ $prop($, 'cdx:github:step:sensitiveContextRefs') }}'"
mitigation: "Review outbound network steps for secret or token use, disable unnecessary network egress, and move sensitive values into least-privileged isolated jobs"
evidence: |
{
"networkTools": $prop($, 'cdx:github:step:outboundNetworkTools'),
"sensitiveContextRefs": $prop($, 'cdx:github:step:sensitiveContextRefs'),
"exfiltrationIndicators": $prop($, 'cdx:github:step:exfiltrationIndicators'),
"stepCommand": $prop($, 'cdx:github:step:command')
}
- id: CI-016
name: "Privileged reusable workflow accepts caller secrets"
description: "workflow_call producers that request caller-provided secrets while also holding write or OIDC permissions expand the blast radius across repositories and workflows"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0006]
techniques: [T1528, T1552]
condition: |
$auditWorkflows($)[
$prop($, 'cdx:github:workflow:isWorkflowCallProducer') = 'true'
and $safeStr($prop($, 'cdx:github:workflow:workflowCallSecrets')) != ''
and (
$propBool($, 'cdx:github:workflow:hasWritePermissions') = true
or $propBool($, 'cdx:github:workflow:hasIdTokenWrite') = true
)
]
location: |
{
"bomRef": $."bom-ref",
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Reusable workflow '{{ $prop($, 'cdx:github:workflow:name') }}' accepts caller secrets while running with privileged permissions"
mitigation: "Avoid broad secret interfaces on reusable workflows with write or OIDC permissions; split privileged work into a narrowly scoped follow-up job and require only explicit minimal secrets"
evidence: |
{
"workflowCallSecrets": $prop($, 'cdx:github:workflow:workflowCallSecrets'),
"writeScopes": $prop($, 'cdx:github:workflow:writeScopes'),
"hasWritePermissions": $prop($, 'cdx:github:workflow:hasWritePermissions'),
"hasIdTokenWrite": $prop($, 'cdx:github:workflow:hasIdTokenWrite')
}
- id: CI-017
name: "Privileged reusable workflow exports caller-influenced outputs"
description: "workflow_call producers that both accept caller-controlled inputs and emit outputs from privileged execution contexts can propagate unsafe values into downstream trusted jobs"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0003, TA0004]
condition: |
$auditWorkflows($)[
$prop($, 'cdx:github:workflow:isWorkflowCallProducer') = 'true'
and $safeStr($prop($, 'cdx:github:workflow:workflowCallInputs')) != ''
and $safeStr($prop($, 'cdx:github:workflow:workflowCallOutputs')) != ''
and (
$propBool($, 'cdx:github:workflow:hasWritePermissions') = true
or $propBool($, 'cdx:github:workflow:hasIdTokenWrite') = true
)
]
location: |
{
"bomRef": $."bom-ref",
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Reusable workflow '{{ $prop($, 'cdx:github:workflow:name') }}' exports outputs from a privileged workflow_call interface that also accepts caller inputs"
mitigation: "Review workflow_call outputs for trust-boundary crossings, sanitize caller-controlled data before emitting outputs, and avoid combining privileged permissions with broad producer interfaces"
evidence: |
{
"workflowCallInputs": $prop($, 'cdx:github:workflow:workflowCallInputs'),
"workflowCallOutputs": $prop($, 'cdx:github:workflow:workflowCallOutputs'),
"hasWritePermissions": $prop($, 'cdx:github:workflow:hasWritePermissions'),
"hasIdTokenWrite": $prop($, 'cdx:github:workflow:hasIdTokenWrite')
}
- id: CI-018
name: "Fork-reachable workflow dispatches downstream workflow or repository events"
description: "Dispatching workflow_dispatch or repository_dispatch from fork-reachable or privileged jobs can create a lateral-movement path into downstream workflows with broader credentials"
severity: high
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0006]
techniques: [T1528]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:step:dispatchesWorkflow') = 'true'
and $prop($, 'cdx:github:step:referencesSensitiveContext') = 'true'
and (
$propBool($, 'cdx:github:workflow:hasPullRequestTrigger') = true
or $propBool($, 'cdx:github:workflow:hasPullRequestTargetTrigger') = true
or $propBool($, 'cdx:github:workflow:hasIssueCommentTrigger') = true
or $propBool($, 'cdx:github:workflow:hasWorkflowRunTrigger') = true
or $propBool($, 'cdx:github:workflow:hasWritePermissions') = true
or $propBool($, 'cdx:github:job:hasWritePermissions') = true
or $propBool($, 'cdx:github:workflow:hasIdTokenWrite') = true
or $propBool($, 'cdx:github:job:hasIdTokenWrite') = true
)
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Workflow step '{{ name }}' dispatches '{{ $prop($, 'cdx:github:step:dispatchKinds') }}' toward '{{ $prop($, 'cdx:github:step:dispatchTargets') }}' from a fork-reachable or privileged context"
mitigation: "Avoid chaining downstream workflow_dispatch or repository_dispatch calls from pull-request/fork-reachable jobs; isolate release dispatchers into trusted workflows with narrowly scoped credentials"
evidence: |
{
"dispatchKinds": $prop($, 'cdx:github:step:dispatchKinds'),
"dispatchMechanisms": $prop($, 'cdx:github:step:dispatchMechanisms'),
"dispatchTargets": $prop($, 'cdx:github:step:dispatchTargets'),
"localReceiverWorkflowFiles": $prop($, 'cdx:github:step:dispatchReceiverWorkflowFiles'),
"localReceiverWorkflowNames": $prop($, 'cdx:github:step:dispatchReceiverWorkflowNames'),
"sensitiveContextRefs": $prop($, 'cdx:github:step:sensitiveContextRefs'),
"workflowTriggers": $prop($, 'cdx:github:workflow:triggers'),
"writeScopes": $prop($, 'cdx:github:workflow:writeScopes')
}
- id: CI-019
name: "Workflow dispatch step references explicit fork context"
description: "Dispatch chains that inspect pull_request head-repository or fork context before invoking downstream workflows are strong signals of fork-to-privileged lateral movement"
severity: critical
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0006]
techniques: [T1528, T1552]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:step:dispatchesWorkflow') = 'true'
and $prop($, 'cdx:github:step:referencesForkContext') = 'true'
and $prop($, 'cdx:github:step:referencesSensitiveContext') = 'true'
and (
$prop($, 'cdx:github:step:hasLocalDispatchReceiver') = 'true'
or $safeStr($prop($, 'cdx:github:step:hasLocalDispatchReceiver')) = ''
)
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Workflow step '{{ name }}' combines explicit fork/head-repository context with downstream dispatch '{{ $safeStr($prop($, 'cdx:github:step:dispatchReceiverWorkflowNames')) != '' ? $prop($, 'cdx:github:step:dispatchReceiverWorkflowNames') : $prop($, 'cdx:github:step:dispatchTargets') }}'"
mitigation: "Do not route fork-derived context into workflow_dispatch or repository_dispatch calls. Split privileged follow-up workflows behind trusted branch-only triggers and avoid sharing long-lived tokens into fork-reachable jobs"
evidence: |
{
"forkContextRefs": $prop($, 'cdx:github:step:forkContextRefs'),
"dispatchKinds": $prop($, 'cdx:github:step:dispatchKinds'),
"dispatchTargets": $prop($, 'cdx:github:step:dispatchTargets'),
"hasLocalDispatchReceiver": $prop($, 'cdx:github:step:hasLocalDispatchReceiver'),
"localReceiverWorkflowFiles": $prop($, 'cdx:github:step:dispatchReceiverWorkflowFiles'),
"localReceiverWorkflowNames": $prop($, 'cdx:github:step:dispatchReceiverWorkflowNames'),
"localReceiverMatchBasis": $prop($, 'cdx:github:step:dispatchReceiverMatchBasis'),
"sensitiveContextRefs": $prop($, 'cdx:github:step:sensitiveContextRefs')
}
- id: CI-020
name: "pull_request_target checks out PR head or fork context"
description: "Checking out github.event.pull_request.head.* repository or ref inside pull_request_target executes untrusted fork code with base-repository privileges"
severity: critical
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001, TA0006]
techniques: [T1195.001, T1552]
condition: |
$auditComponents($)[
$contains($nullSafeProp($, 'cdx:github:action:uses'), 'actions/checkout')
and $prop($, 'cdx:github:workflow:hasPullRequestTargetTrigger') = 'true'
and (
$prop($, 'cdx:github:checkout:checksOutUntrustedRef') = 'true'
or $prop($, 'cdx:github:checkout:referencesForkContext') = 'true'
)
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Checkout step '{{ $prop($, 'cdx:github:action:uses') }}' pulls PR head or fork context inside pull_request_target"
mitigation: "Do not checkout github.event.pull_request.head.* in pull_request_target workflows. Prefer pull_request with read-only permissions or split trusted follow-up work into a separate branch-only workflow"
evidence: |
{
"checkoutRef": $prop($, 'cdx:github:checkout:ref'),
"checkoutRepository": $prop($, 'cdx:github:checkout:repository'),
"untrustedRefContexts": $prop($, 'cdx:github:checkout:untrustedRefContexts'),
"forkContextRefs": $prop($, 'cdx:github:checkout:forkContextRefs'),
"persistCredentials": $prop($, 'cdx:github:checkout:persistCredentials'),
"workflowHasWritePermissions": $prop($, 'cdx:github:workflow:hasWritePermissions')
}
- id: CI-021
name: "Heuristic: high-risk trigger with implicit permissions and sensitive operations"
description: "High-risk GitHub Actions workflows that omit explicit permissions blocks while still performing sensitive operations may rely on repository-default token scopes. This is a review heuristic, not proof of write access."
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0006]
techniques: [T1528, T1552]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:workflow:hasHighRiskTrigger') = 'true'
and $prop($, 'cdx:github:workflow:hasAnyExplicitPermissionsBlock') = 'false'
and $prop($, 'cdx:github:step:hasSensitiveOperations') = 'true'
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "Heuristic review: workflow '{{ $prop($, 'cdx:github:workflow:name') }}' uses a high-risk trigger without an explicit permissions block while step '{{ $safeStr($prop($, 'cdx:github:step:name')) != '' ? $prop($, 'cdx:github:step:name') : name }}' performs sensitive operation(s) '{{ $prop($, 'cdx:github:step:sensitiveOperations') }}'"
mitigation: "Add an explicit top-level permissions: block (for example permissions: {}) and re-grant only the minimum per-job scopes needed for the sensitive step"
evidence: |
{
"workflowTriggers": $prop($, 'cdx:github:workflow:triggers'),
"workflowHasExplicitPermissionsBlock": $prop($, 'cdx:github:workflow:hasExplicitPermissionsBlock'),
"workflowHasAnyExplicitPermissionsBlock": $prop($, 'cdx:github:workflow:hasAnyExplicitPermissionsBlock'),
"jobHasExplicitPermissionsBlock": $prop($, 'cdx:github:job:hasExplicitPermissionsBlock'),
"sensitiveOperations": $prop($, 'cdx:github:step:sensitiveOperations'),
"sensitiveContextRefs": $prop($, 'cdx:github:step:sensitiveContextRefs'),
"dispatchKinds": $prop($, 'cdx:github:step:dispatchKinds')
}
- id: CI-022
name: "npm setup action disables build cache despite resolved package distributions"
description: "Explicitly disabling setup-node caching reduces tamper resistance and reviewability when npm dependencies are resolved from remote package distributions"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001]
techniques: [T1195.001]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:action:disablesBuildCache') = 'true'
and $prop($, 'cdx:github:action:buildCacheEcosystem') = 'npm'
and $count($$.components[
$startsWith(purl, 'pkg:npm/')
and (
$contains($lowercase($nullSafeProp($, 'cdx:npm:manifestSourceType')), 'git')
or $contains($lowercase($nullSafeProp($, 'cdx:npm:manifestSourceType')), 'url')
)
and $count(externalReferences[
type = 'distribution'
and (
$startsWith($lowercase(url), 'git+')
or $startsWith($lowercase(url), 'http://')
or $startsWith($lowercase(url), 'https://')
)
]) > 0
]) > 0
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "GitHub Action '{{ $prop($, 'cdx:github:action:uses') }}' explicitly disables npm build caching while resolved npm package distributions are present in the BOM"
mitigation: "Keep setup-node caching enabled unless you have a reviewed exception; disabling cache can weaken integrity checks and provenance review for resolved npm artifacts"
evidence: |
{
"cacheDisableInput": $prop($, 'cdx:github:action:buildCacheDisableInput'),
"cacheDisableValue": $prop($, 'cdx:github:action:buildCacheDisableValue'),
"matchingPackages": $$.components[
$startsWith(purl, 'pkg:npm/')
and (
$contains($lowercase($nullSafeProp($, 'cdx:npm:manifestSourceType')), 'git')
or $contains($lowercase($nullSafeProp($, 'cdx:npm:manifestSourceType')), 'url')
)
and $count(externalReferences[
type = 'distribution'
and (
$startsWith($lowercase(url), 'git+')
or $startsWith($lowercase(url), 'http://')
or $startsWith($lowercase(url), 'https://')
)
]) > 0
].purl
}
- id: CI-023
name: "Python setup action disables build cache despite resolved package distributions"
description: "Explicitly disabling setup-python caching reduces tamper resistance and reviewability when PyPI dependencies are resolved from remote archives or VCS sources"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001]
techniques: [T1195.001]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:action:disablesBuildCache') = 'true'
and $prop($, 'cdx:github:action:buildCacheEcosystem') = 'pypi'
and $count($$.components[
$startsWith(purl, 'pkg:pypi/')
and (
$contains($lowercase($nullSafeProp($, 'cdx:pypi:manifestSourceType')), 'git')
or $contains($lowercase($nullSafeProp($, 'cdx:pypi:manifestSourceType')), 'url')
)
]) > 0
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "GitHub Action '{{ $prop($, 'cdx:github:action:uses') }}' explicitly disables Python build caching while resolved PyPI package distributions are present in the BOM"
mitigation: "Keep setup-python caching enabled when lockfiles resolve remote archives or VCS sources unless you have a reviewed exception"
evidence: |
{
"cacheDisableInput": $prop($, 'cdx:github:action:buildCacheDisableInput'),
"cacheDisableValue": $prop($, 'cdx:github:action:buildCacheDisableValue'),
"matchingPackages": $$.components[
$startsWith(purl, 'pkg:pypi/')
and (
$contains($lowercase($nullSafeProp($, 'cdx:pypi:manifestSourceType')), 'git')
or $contains($lowercase($nullSafeProp($, 'cdx:pypi:manifestSourceType')), 'url')
)
].purl
}
- id: CI-024
name: "Cargo setup action disables build cache despite manifest-declared git dependencies"
description: "Explicitly disabling Cargo setup caching reduces tamper resistance and reviewability when Cargo manifests rely on git dependencies"
severity: medium
category: ci-permission
dry-run-support: full
attack:
tactics: [TA0001]
techniques: [T1195.001]
condition: |
$auditComponents($)[
$prop($, 'cdx:github:action:disablesBuildCache') = 'true'
and $prop($, 'cdx:github:action:buildCacheEcosystem') = 'cargo'
and $count($$.components[
$startsWith(purl, 'pkg:cargo/')
and $hasProp($, 'cdx:cargo:git')
]) > 0
]
location: |
{
"bomRef": $."bom-ref",
"purl": purl,
"file": $prop($, 'cdx:github:workflow:file')
}
message: "GitHub Action '{{ $prop($, 'cdx:github:action:uses') }}' explicitly disables Cargo build caching while manifest-declared Cargo git dependencies are present in the BOM"
mitigation: "Keep Cargo setup caching enabled when manifests rely on git dependencies unless you have a reviewed exception"
evidence: |
{
"cacheDisableInput": $prop($, 'cdx:github:action:buildCacheDisableInput'),
"cacheDisableValue": $prop($, 'cdx:github:action:buildCacheDisableValue'),
"matchingPackages": $$.components[
$startsWith(purl, 'pkg:cargo/')
and $hasProp($, 'cdx:cargo:git')
].purl
}