# 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
    }
