@blamejs/exceptd-skills 0.12.41 → 0.13.1
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.
- package/CHANGELOG.md +124 -0
- package/bin/exceptd.js +52 -44
- package/data/_indexes/_meta.json +49 -49
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +2 -2
- package/data/_indexes/chains.json +1531 -575
- package/data/_indexes/jurisdiction-map.json +15 -4
- package/data/_indexes/section-offsets.json +1244 -1244
- package/data/_indexes/token-budget.json +173 -173
- package/data/atlas-ttps.json +55 -11
- package/data/attack-techniques.json +124 -19
- package/data/cve-catalog.json +194 -27
- package/data/cwe-catalog.json +15 -5
- package/data/framework-control-gaps.json +32 -10
- package/data/playbooks/ai-api.json +5 -0
- package/data/playbooks/cicd-pipeline-compromise.json +970 -0
- package/data/playbooks/cloud-iam-incident.json +4 -1
- package/data/playbooks/cred-stores.json +10 -0
- package/data/playbooks/framework.json +16 -0
- package/data/playbooks/hardening.json +4 -0
- package/data/playbooks/identity-sso-compromise.json +951 -0
- package/data/playbooks/idp-incident.json +3 -0
- package/data/playbooks/kernel.json +6 -0
- package/data/playbooks/llm-tool-use-exfil.json +963 -0
- package/data/playbooks/mcp.json +6 -0
- package/data/playbooks/runtime.json +4 -0
- package/data/playbooks/sbom.json +13 -0
- package/data/playbooks/secrets.json +6 -0
- package/data/playbooks/webhook-callback-abuse.json +916 -0
- package/data/zeroday-lessons.json +178 -0
- package/lib/cross-ref-api.js +33 -13
- package/lib/cve-curation.js +12 -1
- package/lib/exit-codes.js +29 -0
- package/lib/lint-skills.js +24 -2
- package/lib/refresh-external.js +17 -1
- package/lib/scoring.js +55 -0
- package/lib/source-advisories.js +281 -0
- package/manifest.json +83 -83
- package/orchestrator/index.js +207 -24
- package/package.json +1 -1
- package/sbom.cdx.json +134 -79
- package/scripts/predeploy.js +7 -13
- package/scripts/refresh-reverse-refs.js +86 -0
- package/scripts/refresh-sbom.js +21 -4
- package/skills/age-gates-child-safety/skill.md +1 -5
- package/skills/ai-attack-surface/skill.md +11 -4
- package/skills/ai-c2-detection/skill.md +11 -2
- package/skills/ai-risk-management/skill.md +4 -2
- package/skills/api-security/skill.md +7 -8
- package/skills/attack-surface-pentest/skill.md +2 -2
- package/skills/cloud-iam-incident/skill.md +1 -5
- package/skills/cloud-security/skill.md +0 -4
- package/skills/compliance-theater/skill.md +10 -2
- package/skills/container-runtime-security/skill.md +1 -3
- package/skills/dlp-gap-analysis/skill.md +3 -4
- package/skills/email-security-anti-phishing/skill.md +1 -8
- package/skills/exploit-scoring/skill.md +7 -2
- package/skills/framework-gap-analysis/skill.md +1 -1
- package/skills/fuzz-testing-strategy/skill.md +1 -2
- package/skills/global-grc/skill.md +3 -2
- package/skills/identity-assurance/skill.md +1 -3
- package/skills/idp-incident-response/skill.md +1 -4
- package/skills/incident-response-playbook/skill.md +1 -5
- package/skills/kernel-lpe-triage/skill.md +2 -2
- package/skills/mcp-agent-trust/skill.md +13 -3
- package/skills/mlops-security/skill.md +2 -3
- package/skills/ot-ics-security/skill.md +0 -3
- package/skills/policy-exception-gen/skill.md +11 -3
- package/skills/pqc-first/skill.md +4 -2
- package/skills/rag-pipeline-security/skill.md +2 -0
- package/skills/ransomware-response/skill.md +1 -5
- package/skills/researcher/skill.md +4 -3
- package/skills/sector-energy/skill.md +0 -4
- package/skills/sector-federal-government/skill.md +2 -3
- package/skills/sector-financial/skill.md +1 -4
- package/skills/sector-healthcare/skill.md +0 -5
- package/skills/sector-telecom/skill.md +0 -4
- package/skills/security-maturity-tiers/skill.md +1 -2
- package/skills/skill-update-loop/skill.md +4 -3
- package/skills/supply-chain-integrity/skill.md +4 -3
- package/skills/threat-model-currency/skill.md +1 -1
- package/skills/threat-modeling-methodology/skill.md +2 -1
- package/skills/webapp-security/skill.md +0 -5
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"id": "webhook-callback-abuse",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"last_threat_review": "2026-05-17",
|
|
6
|
+
"threat_currency_score": 93,
|
|
7
|
+
"changelog": [
|
|
8
|
+
{
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"date": "2026-05-17",
|
|
11
|
+
"summary": "Initial seven-phase webhook + OAuth-callback trust-model playbook. Covers the cross-cutting failure mode where an inbound webhook receiver, an OAuth redirect_uri, or a chat-platform incoming webhook (Slack / Teams / Discord / GitHub Apps) becomes the persistent trust anchor that fans out to cloud, source-control, and CI/CD planes. Walks installed receivers + their secret-handling + redirect_uri allowlists + state-parameter enforcement + per-app webhook signing verification. Closes the GRC loop with NIST 800-53 IA-9 + AC-4 gaps, ISO 27001 A.5.16 + A.8.21 gaps, NIS2 Art.21(2)(d) supply-chain-of-trust gap, and SOC 2 CC6.6 inbound-endpoint gap. Cross-cuts mcp (overlapping tool-trust model), cred-stores (token leakage path), and sbom (compromised webhook → poisoned package upload).",
|
|
12
|
+
"cves_added": [
|
|
13
|
+
"CVE-2024-1709",
|
|
14
|
+
"CVE-2026-42208"
|
|
15
|
+
],
|
|
16
|
+
"framework_gaps_updated": [
|
|
17
|
+
"nist-800-53-IA-9-webhook-trust",
|
|
18
|
+
"nist-800-53-AC-4-callback-state",
|
|
19
|
+
"iso-27001-2022-A.5.16-federated-callback",
|
|
20
|
+
"nis2-art21-2d-supply-chain-of-trust",
|
|
21
|
+
"soc2-CC6.6-inbound-endpoint"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"owner": "@blamejs/platform-security",
|
|
26
|
+
"air_gap_mode": false,
|
|
27
|
+
"scope": "service",
|
|
28
|
+
"preconditions": [
|
|
29
|
+
{
|
|
30
|
+
"id": "filesystem-or-api-read",
|
|
31
|
+
"description": "Agent must be able to enumerate either (a) the operator's source repository for webhook configuration files and OAuth client metadata, OR (b) GitHub Apps / Slack / Teams / Discord / Atlassian Connect admin APIs to list installed webhook subscriptions. Pure-runtime hosts with no repo + no admin API access mark the playbook visibility_gap=no_webhook_inventory.",
|
|
32
|
+
"check": "agent_has_filesystem_read == true OR agent_has_admin_api_token == true",
|
|
33
|
+
"on_fail": "halt"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "operator-owns-receivers",
|
|
37
|
+
"description": "The operator must own (or hold explicit written authorisation for) the OAuth client registrations + webhook subscriptions being inventoried. Probing third-party callback endpoints without authorisation is out-of-scope.",
|
|
38
|
+
"check": "operator_ownership_attested == true",
|
|
39
|
+
"on_fail": "halt"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"mutex": [],
|
|
43
|
+
"feeds_into": [
|
|
44
|
+
{
|
|
45
|
+
"playbook_id": "cred-stores",
|
|
46
|
+
"condition": "finding.includes_token_leakage == true"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"playbook_id": "sbom",
|
|
50
|
+
"condition": "finding.includes_package_upload_webhook == true"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"playbook_id": "mcp",
|
|
54
|
+
"condition": "finding.includes_agentic_tool_callback == true"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"playbook_id": "framework",
|
|
58
|
+
"condition": "analyze.compliance_theater_check.verdict == 'theater'"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
"domain": {
|
|
63
|
+
"name": "Webhook + OAuth-callback trust model abuse",
|
|
64
|
+
"attack_class": "identity-abuse",
|
|
65
|
+
"atlas_refs": [],
|
|
66
|
+
"attack_refs": [
|
|
67
|
+
"T1190",
|
|
68
|
+
"T1199",
|
|
69
|
+
"T1078.004",
|
|
70
|
+
"T1552.004",
|
|
71
|
+
"T1098.001"
|
|
72
|
+
],
|
|
73
|
+
"cve_refs": [
|
|
74
|
+
"CVE-2024-1709",
|
|
75
|
+
"CVE-2026-42208"
|
|
76
|
+
],
|
|
77
|
+
"cwe_refs": [
|
|
78
|
+
"CWE-352",
|
|
79
|
+
"CWE-345",
|
|
80
|
+
"CWE-918",
|
|
81
|
+
"CWE-522",
|
|
82
|
+
"CWE-287",
|
|
83
|
+
"CWE-200"
|
|
84
|
+
],
|
|
85
|
+
"d3fend_refs": [
|
|
86
|
+
"D3-CBAN",
|
|
87
|
+
"D3-NTA",
|
|
88
|
+
"D3-IOPR",
|
|
89
|
+
"D3-EI"
|
|
90
|
+
],
|
|
91
|
+
"frameworks_in_scope": [
|
|
92
|
+
"nist-800-53",
|
|
93
|
+
"iso-27001-2022",
|
|
94
|
+
"soc2",
|
|
95
|
+
"pci-dss-4",
|
|
96
|
+
"nis2",
|
|
97
|
+
"dora",
|
|
98
|
+
"uk-caf",
|
|
99
|
+
"au-ism"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"phases": {
|
|
103
|
+
"govern": {
|
|
104
|
+
"jurisdiction_obligations": [
|
|
105
|
+
{
|
|
106
|
+
"jurisdiction": "EU",
|
|
107
|
+
"regulation": "NIS2 Art.23",
|
|
108
|
+
"obligation": "notify_regulator",
|
|
109
|
+
"window_hours": 24,
|
|
110
|
+
"clock_starts": "detect_confirmed",
|
|
111
|
+
"evidence_required": [
|
|
112
|
+
"webhook_receiver_inventory",
|
|
113
|
+
"secret_exposure_window_estimate",
|
|
114
|
+
"downstream_token_revocation_record"
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"jurisdiction": "EU",
|
|
119
|
+
"regulation": "DORA Art.19",
|
|
120
|
+
"obligation": "notify_regulator",
|
|
121
|
+
"window_hours": 4,
|
|
122
|
+
"clock_starts": "detect_confirmed",
|
|
123
|
+
"evidence_required": [
|
|
124
|
+
"ict_third_party_callback_inventory",
|
|
125
|
+
"containment_record"
|
|
126
|
+
]
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"jurisdiction": "EU",
|
|
130
|
+
"regulation": "GDPR Art.33",
|
|
131
|
+
"obligation": "notify_regulator",
|
|
132
|
+
"window_hours": 72,
|
|
133
|
+
"clock_starts": "detect_confirmed",
|
|
134
|
+
"evidence_required": [
|
|
135
|
+
"callback_log_excerpt",
|
|
136
|
+
"data_subject_impact_assessment"
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"jurisdiction": "US-NY",
|
|
141
|
+
"regulation": "NYDFS 23 NYCRR 500.17",
|
|
142
|
+
"obligation": "notify_regulator",
|
|
143
|
+
"window_hours": 72,
|
|
144
|
+
"clock_starts": "detect_confirmed",
|
|
145
|
+
"evidence_required": [
|
|
146
|
+
"callback_log_excerpt",
|
|
147
|
+
"containment_record"
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"jurisdiction": "AU",
|
|
152
|
+
"regulation": "Privacy Act 1988 — Notifiable Data Breaches scheme (s26WK)",
|
|
153
|
+
"obligation": "notify_regulator",
|
|
154
|
+
"window_hours": 720,
|
|
155
|
+
"clock_starts": "analyze_complete",
|
|
156
|
+
"evidence_required": [
|
|
157
|
+
"australian_resident_records_affected",
|
|
158
|
+
"remediation_completed_evidence"
|
|
159
|
+
]
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
"theater_fingerprints": [
|
|
163
|
+
{
|
|
164
|
+
"pattern_id": "https-redirect-uri-as-proof",
|
|
165
|
+
"claim": "All OAuth redirect_uri values use HTTPS, so the callback flow is secure.",
|
|
166
|
+
"fast_detection_test": "Enumerate redirect_uri allowlists for every OAuth client. Look for wildcard subdomains (`https://*.example.com/cb`), open redirects on the registered host (`/redirect?to=`), and unbound localhost loopback (`http://127.0.0.1:*/cb` with no port pin). HTTPS alone does not prevent CSRF or open-redirect-chained callback hijack.",
|
|
167
|
+
"implicated_controls": [
|
|
168
|
+
"nist-800-53-IA-9",
|
|
169
|
+
"iso-27001-2022-A.5.16",
|
|
170
|
+
"soc2-cc6.6"
|
|
171
|
+
]
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"pattern_id": "webhook-secret-as-authentication",
|
|
175
|
+
"claim": "Every inbound webhook receiver validates an HMAC signature so the payload is trusted.",
|
|
176
|
+
"fast_detection_test": "Confirm the receiver (a) actually rejects requests with missing / mismatched signatures (not just logs them), (b) loads the secret from a real secret store rather than an environment variable copy-pasted in deploy YAML, and (c) the same secret is not reused across stage + prod or across multiple unrelated apps. Replay protection (timestamp window + nonce cache) must be present; signature-only is replayable indefinitely."
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
"pattern_id": "platform-incoming-webhook-as-public-url",
|
|
180
|
+
"claim": "The Slack / Teams / Discord incoming-webhook URL is just a posting endpoint — it does not need to be secret.",
|
|
181
|
+
"fast_detection_test": "Grep the source repo, CI logs, error-tracker payloads, and the GitHub Actions secrets export for any incoming-webhook URL pattern (`hooks.slack.com/services/`, `outlook.office.com/webhook/`, `discord.com/api/webhooks/`). A leaked URL is a primitive for impersonation-into-the-channel + persistent C2 for an attacker that wants to look like a legitimate bot."
|
|
182
|
+
}
|
|
183
|
+
],
|
|
184
|
+
"framework_context": {
|
|
185
|
+
"gap_summary": "Webhook + OAuth callback receivers are the most common authentication boundary that ships without an explicit owning control. NIST 800-53 IA-9 (Service Identification and Authentication) covers identifying external services to the organisation but is silent on identifying the organisation TO external services on the inbound callback path. ISO 27001 A.5.16 (Identity management) treats federated identity but not the long-tail of service-to-service callback registrations. NIS2 Art.21(2)(d) requires supply-chain security for ICT services, but inbound webhook receivers from SaaS partners (Stripe, GitHub Apps, Atlassian Connect) are routinely excluded from the supply-chain register because they're not 'suppliers' in the procurement sense. SOC 2 CC6.6 covers logical access boundary protection on inbound traffic but doesn't require redirect_uri allowlist hygiene, state-parameter enforcement, or signature-validation behaviour testing. The result: a callback URL that an OAuth provider trusts becomes an unowned, undocumented persistent trust anchor.",
|
|
186
|
+
"lag_score": 22,
|
|
187
|
+
"per_framework_gaps": [
|
|
188
|
+
{
|
|
189
|
+
"framework": "nist-800-53",
|
|
190
|
+
"control_id": "IA-9 — Service Identification and Authentication",
|
|
191
|
+
"designed_for": "Identifying external services so the organisation can decide whether to trust them.",
|
|
192
|
+
"insufficient_because": "The control names outbound trust (do we trust them?) but not the inbound callback (does the redirect_uri allowlist enforce that only the operator's domain receives the authorisation code?). Open-redirect-into-callback chains satisfy IA-9 trivially because the named service is unchanged."
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"framework": "nist-800-53",
|
|
196
|
+
"control_id": "AC-4 — Information Flow Enforcement",
|
|
197
|
+
"designed_for": "Controlling information flow within and between connected systems.",
|
|
198
|
+
"insufficient_because": "Does not require state-parameter binding on OAuth callbacks (the standard CSRF defence) and does not require replay-window enforcement on inbound webhooks. A receiver can pass AC-4 review and still accept a 4-day-old replayed payload."
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"framework": "iso-27001-2022",
|
|
202
|
+
"control_id": "A.5.16 — Identity management",
|
|
203
|
+
"designed_for": "Lifecycle management of identities in federated and non-federated relationships.",
|
|
204
|
+
"insufficient_because": "Treats human + service identities, not callback-URL identities. The redirect_uri allowlist itself is an identity decision that A.5.16 doesn't anchor."
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"framework": "nis2",
|
|
208
|
+
"control_id": "Art.21(2)(d) — Supply chain security",
|
|
209
|
+
"designed_for": "Security of supply chains including direct supplier relationships.",
|
|
210
|
+
"insufficient_because": "Inbound webhook receivers from SaaS partners are excluded from most supply-chain registers because the partner isn't a procurement vendor. Yet the receiver is the persistent trust anchor that decides whether a payload from that partner gets actioned."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"framework": "soc2",
|
|
214
|
+
"control_id": "CC6.6 — Logical and physical access controls — boundary protection",
|
|
215
|
+
"designed_for": "Logical access boundary protection on the entity's perimeter.",
|
|
216
|
+
"insufficient_because": "Inbound webhook receivers are tested for TLS + WAF rules but not for HMAC-signature-validation behaviour, redirect_uri allowlist hygiene, or state-parameter enforcement. A receiver can pass CC6.6 with TLS + WAF and still trust an unsigned payload."
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
"skill_preload": [
|
|
221
|
+
"identity-assurance",
|
|
222
|
+
"api-security",
|
|
223
|
+
"cloud-iam-incident",
|
|
224
|
+
"framework-gap-analysis",
|
|
225
|
+
"compliance-theater",
|
|
226
|
+
"policy-exception-gen"
|
|
227
|
+
]
|
|
228
|
+
},
|
|
229
|
+
"direct": {
|
|
230
|
+
"threat_context": "Q1-Q2 2026 callback-and-webhook abuse landscape. The 2024 Snowflake intrusion chain (leaked GitHub Actions token from a public CI log → snowflake.com customer-tenant access) remains the canonical 'leaked-callback-as-cloud-account-takeover' pattern; UNC5537 monetised it across ~165 tenants and the failure mode (long-lived token in CI log, no provider-side anomaly detection) has not closed at the platform level. Salt Typhoon's telecom intrusions in 2024-2025 included GitHub Apps webhook secret reuse across stage + prod, allowing a single leaked secret to fan out across environments. CVE-2024-1709 (ConnectWise ScreenConnect SetupWizard unauthenticated auth-bypass) is the canonical example of the receiver-trust-itself failure: the endpoint that should have validated callback authorisation accepted a synthesized admin session instead. Microsoft Power Automate + Power Apps incoming-webhook URLs have been observed as C2 channels in 2025 incidents — the URL is treated as 'just a posting endpoint' but a leaked one is impersonation-into-the-channel. GitHub Apps webhook secrets in plaintext CI logs continue to surface in researcher-reported bugs; the platform does not rotate them on detection. The unifying thread: an inbound trust boundary that the operator never enumerated, secured by a shared secret that nobody owns rotating.",
|
|
231
|
+
"rwep_threshold": {
|
|
232
|
+
"escalate": 85,
|
|
233
|
+
"monitor": 65,
|
|
234
|
+
"close": 30
|
|
235
|
+
},
|
|
236
|
+
"framework_lag_declaration": "NIST 800-53 IA-9 + AC-4, ISO 27001 A.5.16, NIS2 Art.21(2)(d), and SOC 2 CC6.6 collectively underspecify the inbound-callback trust boundary. None mandate redirect_uri allowlist hygiene, OAuth state-parameter binding, webhook-signature replay-window enforcement, or per-app webhook-secret isolation. The 2024-2025 Snowflake / Salt Typhoon / GitHub-Apps-secret-leak class incidents would all satisfy a literal reading of these controls. UK CAF B3 (Data security) treats inbound endpoints as part of the perimeter but does not require behaviour-tested signature validation. AU ISM-1551 (web application authentication) names HMAC signatures but not the replay-window + secret-rotation discipline that turns a signature into a real authentication boundary.",
|
|
237
|
+
"skill_chain": [
|
|
238
|
+
{
|
|
239
|
+
"skill": "identity-assurance",
|
|
240
|
+
"purpose": "Establish baseline authentication-boundary strength for every enumerated callback + webhook receiver — does the receiver actually authenticate the caller, or just terminate TLS?",
|
|
241
|
+
"required": true
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"skill": "api-security",
|
|
245
|
+
"purpose": "Score each receiver against the OWASP API Security Top 10 inbound-trust dimensions: BOLA, broken authentication, broken object property level authorisation, unrestricted resource consumption.",
|
|
246
|
+
"required": true
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"skill": "cloud-iam-incident",
|
|
250
|
+
"purpose": "When a leaked callback or webhook secret already maps to a cloud-issued credential, run the cloud-IAM incident path to rotate + revoke + assess blast radius.",
|
|
251
|
+
"skip_if": "analyze.findings_with_cloud_credential_mapping == 0",
|
|
252
|
+
"required": false
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"skill": "framework-gap-analysis",
|
|
256
|
+
"purpose": "Map each finding to the framework control that should have caught it and why it didn't.",
|
|
257
|
+
"required": true
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"skill": "compliance-theater",
|
|
261
|
+
"purpose": "Run the theater test — does the org's claimed inbound-endpoint review actually catch a wildcard redirect_uri or a missing replay window?",
|
|
262
|
+
"required": true
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"skill": "policy-exception-gen",
|
|
266
|
+
"purpose": "Generate auditor-ready exception language if a receiver cannot be hardened within the jurisdiction's window.",
|
|
267
|
+
"skip_if": "close.exception_generation.trigger_condition == false",
|
|
268
|
+
"required": false
|
|
269
|
+
}
|
|
270
|
+
],
|
|
271
|
+
"token_budget": {
|
|
272
|
+
"estimated_total": 16500,
|
|
273
|
+
"breakdown": {
|
|
274
|
+
"govern": 2200,
|
|
275
|
+
"direct": 1500,
|
|
276
|
+
"look": 1800,
|
|
277
|
+
"detect": 2200,
|
|
278
|
+
"analyze": 3600,
|
|
279
|
+
"validate": 3000,
|
|
280
|
+
"close": 2200
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
"look": {
|
|
285
|
+
"artifacts": [
|
|
286
|
+
{
|
|
287
|
+
"id": "oauth-client-inventory",
|
|
288
|
+
"type": "config_file",
|
|
289
|
+
"source": "Repository walk for OAuth client definitions: grep for client_id, client_secret, redirect_uri, authorize_url across .env.example / app.yaml / config/*.{yml,yaml,json,toml} / terraform *.tf for okta_oauth2_app / azuread_application / google_oauth_client / aws_cognito_user_pool_client resources",
|
|
290
|
+
"description": "Inventory of OAuth client registrations the operator owns, with their redirect_uri allowlists, scopes, and which secret store the client_secret lives in.",
|
|
291
|
+
"required": true,
|
|
292
|
+
"air_gap_alternative": "If admin APIs are unreachable, restrict to the repository walk and mark cloud-provider-managed clients as inventory_gap=admin_api_unavailable."
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"id": "webhook-receiver-inventory",
|
|
296
|
+
"type": "config_file",
|
|
297
|
+
"source": "Repository walk for inbound webhook receiver routes: grep for /webhooks/, /hooks/, /callback, /oauth/callback patterns in routes/*.{ts,js,py,rb,go} + GitHub Apps app.yml + Stripe webhook handler decorators + Slack Bolt receiver registrations + Atlassian Connect descriptor.json. Capture the route path, the HTTP method allowlist, the signature header expected, and the secret-resolution source.",
|
|
298
|
+
"description": "Every inbound webhook endpoint the operator hosts, with the signing scheme it expects and how the secret is loaded.",
|
|
299
|
+
"required": true,
|
|
300
|
+
"air_gap_alternative": "Repository walk only; do not call the SaaS provider's listing API."
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"id": "platform-incoming-webhook-urls",
|
|
304
|
+
"type": "config_file",
|
|
305
|
+
"source": "Grep secrets/, .env*, *.tfvars, GitHub Actions secrets (gh secret list --json), Vault paths, AWS Secrets Manager + GCP Secret Manager + Azure Key Vault for: hooks.slack.com/services/, outlook.office.com/webhook/, discord.com/api/webhooks/, *.zapier.com/hooks/, hooks.zapier.com/, *.webhook.office.com/",
|
|
306
|
+
"description": "Platform-issued incoming-webhook URLs treated as secrets. Leaked URLs are impersonation primitives.",
|
|
307
|
+
"required": true,
|
|
308
|
+
"air_gap_alternative": "Local repository + local secret-store CLI only; skip cloud-provider secret-manager enumeration if offline."
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"id": "github-apps-webhook-config",
|
|
312
|
+
"type": "api_response",
|
|
313
|
+
"source": "gh api /orgs/{org}/installations + gh api /app/installations + gh api /app + organisation webhook list (gh api /orgs/{org}/hooks)",
|
|
314
|
+
"description": "GitHub Apps installations with their webhook URLs, signing secret presence, and the events they subscribe to.",
|
|
315
|
+
"required": false,
|
|
316
|
+
"air_gap_alternative": "If `gh` is offline, fall back to the repository's .github/app.yml + capture from cached `gh api` responses if present in CI artefacts."
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"id": "ci-cd-pipeline-webhook-config",
|
|
320
|
+
"type": "config_file",
|
|
321
|
+
"source": "Walk .github/workflows/*.yml + .gitlab-ci.yml + Jenkinsfile + bitbucket-pipelines.yml + .circleci/config.yml for outbound webhook posts, inbound trigger configurations (workflow_dispatch with HTTP trigger, repository_dispatch), and any `pipelines.bitbucket.org/webhooks/` or `gitlab.com/api/v4/projects/{id}/triggers` references.",
|
|
322
|
+
"description": "CI/CD pipeline webhook trigger configuration. A leaked Bitbucket pipeline webhook is a direct path to artefact-publish access.",
|
|
323
|
+
"required": false,
|
|
324
|
+
"air_gap_alternative": "Repository-only walk; mark cloud CI inventory as inventory_gap=admin_api_unavailable."
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"id": "callback-state-enforcement",
|
|
328
|
+
"type": "config_file",
|
|
329
|
+
"source": "For each OAuth client found in oauth-client-inventory, grep its callback handler implementation for the state= parameter: passport-oauth2 `state: true`, authlib `request.state`, raw checks `session['oauth_state'] == request.args['state']`. Absence is the signal.",
|
|
330
|
+
"description": "State-parameter enforcement on OAuth callback handlers. Absence = CSRF-via-callback primitive.",
|
|
331
|
+
"required": false
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"id": "webhook-replay-window",
|
|
335
|
+
"type": "config_file",
|
|
336
|
+
"source": "For each receiver in webhook-receiver-inventory, grep the handler for a timestamp-tolerance check: stripe.Webhook.constructEvent (300s default), slack-bolt request.body.event_time, raw `abs(now - request.headers['x-timestamp']) < N`. Absence + signature-only = indefinite replay.",
|
|
337
|
+
"description": "Replay-window enforcement on inbound webhook receivers. Signature-only with no timestamp window is indefinite replay-able.",
|
|
338
|
+
"required": false
|
|
339
|
+
}
|
|
340
|
+
],
|
|
341
|
+
"collection_scope": {
|
|
342
|
+
"time_window": "current",
|
|
343
|
+
"asset_scope": "operator_owned_repos_and_oauth_clients_and_webhook_receivers",
|
|
344
|
+
"depth": "standard",
|
|
345
|
+
"sampling": "full repository walk + full admin-API enumeration where reachable. Re-run on every new OAuth client registration or webhook subscription change."
|
|
346
|
+
},
|
|
347
|
+
"environment_assumptions": [
|
|
348
|
+
{
|
|
349
|
+
"assumption": "operator owns the OAuth client registrations + webhook subscriptions being inventoried",
|
|
350
|
+
"if_false": "Halt with authorisation_required — probing third-party callback endpoints without authorisation is out-of-scope."
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
"assumption": "repository tree is readable",
|
|
354
|
+
"if_false": "Mark every artefact requiring repository walk as inventory_gap=no_repo_access and downgrade overall confidence to medium."
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"assumption": "secret-store CLIs (gh secret, aws secretsmanager, vault, az keyvault) are configured",
|
|
358
|
+
"if_false": "platform-incoming-webhook-urls artefact is collected from repo-only sources; mark provider-side secret enumeration as inventory_gap=secret_store_unreachable."
|
|
359
|
+
}
|
|
360
|
+
],
|
|
361
|
+
"fallback_if_unavailable": [
|
|
362
|
+
{
|
|
363
|
+
"artifact_id": "github-apps-webhook-config",
|
|
364
|
+
"fallback_action": "mark_inconclusive",
|
|
365
|
+
"confidence_impact": "medium"
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"artifact_id": "ci-cd-pipeline-webhook-config",
|
|
369
|
+
"fallback_action": "mark_inconclusive",
|
|
370
|
+
"confidence_impact": "medium"
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
"artifact_id": "platform-incoming-webhook-urls",
|
|
374
|
+
"fallback_action": "use_compensating_artifact",
|
|
375
|
+
"confidence_impact": "high"
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
"artifact_id": "oauth-client-inventory",
|
|
379
|
+
"fallback_action": "escalate_to_human",
|
|
380
|
+
"confidence_impact": "high"
|
|
381
|
+
}
|
|
382
|
+
]
|
|
383
|
+
},
|
|
384
|
+
"detect": {
|
|
385
|
+
"indicators": [
|
|
386
|
+
{
|
|
387
|
+
"id": "wildcard-redirect-uri",
|
|
388
|
+
"type": "log_pattern",
|
|
389
|
+
"value": "OAuth client redirect_uri allowlist contains a wildcard subdomain or path: `https://*.<domain>/...`, `https://<host>/redirect?to=`, `https://<host>/oauth/callback?next=`",
|
|
390
|
+
"description": "Wildcard or open-redirect-style redirect_uri allows an attacker who controls any matched host or any reachable open-redirect to receive the authorisation code.",
|
|
391
|
+
"confidence": "high",
|
|
392
|
+
"deterministic": false,
|
|
393
|
+
"attack_ref": "T1190",
|
|
394
|
+
"false_positive_checks_required": [
|
|
395
|
+
"Confirm the wildcard expands to a domain the operator fully controls — for github.dev / vercel preview deployments / netlify branch URLs, the wildcard is a legitimate per-PR preview pattern but only if the preview environment ALSO requires the OAuth provider's CORS or origin check. Without an origin enforcement, the wildcard is still exploitable.",
|
|
396
|
+
"Test the registered host's `/redirect`, `/r`, `/url`, `/next`, `/return_to`, `/continue`, `/callback?to=`, `/login?next=` paths for open redirect to an attacker-controlled host. If none of those paths redirect, the wildcard's blast radius is limited to genuine subdomain takeover scenarios — still a finding, but downgrade severity."
|
|
397
|
+
]
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
"id": "missing-state-parameter",
|
|
401
|
+
"type": "log_pattern",
|
|
402
|
+
"value": "OAuth callback handler does not load, compare, or invalidate a `state` parameter bound to the originating session.",
|
|
403
|
+
"description": "Missing state-parameter binding = CSRF-via-callback. Attacker initiates the OAuth flow with their own code_verifier, the victim's session links the returned account.",
|
|
404
|
+
"confidence": "deterministic",
|
|
405
|
+
"deterministic": true,
|
|
406
|
+
"attack_ref": "T1190",
|
|
407
|
+
"false_positive_checks_required": [
|
|
408
|
+
"Confirm the OAuth library in use does not enforce state by default at a layer the handler doesn't visibly call (passport-oauth2 with `state: true` in strategy options, authlib's built-in state, NextAuth's built-in state). If the library enforces it transparently, demote to medium.",
|
|
409
|
+
"Confirm PKCE (code_challenge / code_verifier) is also absent — PKCE-enforced flows offer partial CSRF defence even without state, but only for the auth-code-injection sub-class, not for full session-linking attacks."
|
|
410
|
+
]
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"id": "missing-webhook-signature-validation",
|
|
414
|
+
"type": "log_pattern",
|
|
415
|
+
"value": "Inbound webhook receiver does not invoke a signature-comparison primitive (hmac.compare_digest, crypto.timingSafeEqual, Stripe.webhooks.constructEvent, slack_bolt SignatureVerifier) before processing the payload.",
|
|
416
|
+
"description": "Missing or non-constant-time signature validation = inbound endpoint trusts any caller. Compare-with-== leaks timing.",
|
|
417
|
+
"confidence": "deterministic",
|
|
418
|
+
"deterministic": true,
|
|
419
|
+
"attack_ref": "T1190",
|
|
420
|
+
"false_positive_checks_required": [
|
|
421
|
+
"Confirm the receiver is not behind an authenticated platform-managed gateway (AWS API Gateway with IAM auth, Cloudflare Access policy, Tailscale Funnel with ACL) that itself enforces caller identity. If the gateway terminates trust and the receiver runs only on the private network, demote to medium.",
|
|
422
|
+
"Confirm the comparison primitive is constant-time. `==` / `===` / Python `str.__eq__` against the signature header IS a finding even when present, because it leaks timing. Constant-time primitives only."
|
|
423
|
+
]
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
"id": "missing-webhook-replay-window",
|
|
427
|
+
"type": "log_pattern",
|
|
428
|
+
"value": "Inbound webhook receiver validates the signature but does not check a request-timestamp tolerance window (typically 60-300s).",
|
|
429
|
+
"description": "Signature-only with no timestamp window = indefinite replay. A captured payload remains valid for as long as the secret does.",
|
|
430
|
+
"confidence": "high",
|
|
431
|
+
"deterministic": false,
|
|
432
|
+
"attack_ref": "T1199",
|
|
433
|
+
"false_positive_checks_required": [
|
|
434
|
+
"Confirm the receiver does not record + reject duplicate event_ids in an idempotency cache (Stripe event_id, Slack event_id, GitHub X-GitHub-Delivery UUID). An idempotency cache covering the receiver's retention window is a valid alternative to a strict timestamp check.",
|
|
435
|
+
"Confirm the signed payload includes a server-issued timestamp (Stripe's `t=` prefix, Slack's X-Slack-Request-Timestamp). If the signature only covers the body and there's no timestamp in scope of the signature, the replay window is unenforceable even if the receiver checks it — escalate finding severity."
|
|
436
|
+
]
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
"id": "leaked-incoming-webhook-url",
|
|
440
|
+
"type": "log_pattern",
|
|
441
|
+
"value": "A platform-issued incoming-webhook URL (hooks.slack.com/services/T*/B*/*, outlook.office.com/webhook/<guid>@<tenant>/IncomingWebhook/<id>, discord.com/api/webhooks/<id>/<token>) is present in (a) a public repository, (b) a CI log, (c) an error-tracker payload (Sentry / Datadog / Honeybadger), or (d) a frontend bundle.",
|
|
442
|
+
"description": "Leaked incoming-webhook URL = persistent impersonation primitive in the target channel.",
|
|
443
|
+
"confidence": "deterministic",
|
|
444
|
+
"deterministic": true,
|
|
445
|
+
"attack_ref": "T1552.004",
|
|
446
|
+
"false_positive_checks_required": [
|
|
447
|
+
"Confirm the URL is currently active by checking the audit trail of the parent app (Slack incoming-webhook admin page, Teams workflow audit, Discord channel webhook page). A revoked URL is a hygiene finding, not an active exposure.",
|
|
448
|
+
"Confirm the exposure surface is reachable to an attacker. A URL in a private repo with restricted access is lower-severity than the same URL in a public Stack Overflow answer; rate by exposure surface."
|
|
449
|
+
]
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"id": "webhook-secret-shared-across-apps",
|
|
453
|
+
"type": "log_pattern",
|
|
454
|
+
"value": "The same HMAC secret value appears in webhook-receiver-inventory for two or more distinct receivers (cross-checked by hashing the loaded secret).",
|
|
455
|
+
"description": "Secret reuse across receivers = one compromised app extends to every other app that shares the secret.",
|
|
456
|
+
"confidence": "deterministic",
|
|
457
|
+
"deterministic": true,
|
|
458
|
+
"attack_ref": "T1078.004",
|
|
459
|
+
"false_positive_checks_required": [
|
|
460
|
+
"Confirm the receivers are not intentionally part of a single logical application (e.g. a monorepo with a shared signing key for internal services). If intentional, the finding is per-environment-isolation rather than per-receiver-isolation: confirm stage + prod still use distinct secrets.",
|
|
461
|
+
"Hash the secret before comparing — do not log the secret value. Use a salted hash so the audit trail does not become a secret-exposure path itself."
|
|
462
|
+
]
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"id": "long-lived-callback-token-in-ci-log",
|
|
466
|
+
"type": "log_pattern",
|
|
467
|
+
"value": "A long-lived OAuth refresh_token, GitHub Apps installation-token, Atlassian Connect JWT, or platform incoming-webhook URL appears in a CI log artefact retained > 7 days.",
|
|
468
|
+
"description": "Snowflake-class pattern — long-lived credential in a CI log becomes a persistent take-over primitive.",
|
|
469
|
+
"confidence": "high",
|
|
470
|
+
"deterministic": false,
|
|
471
|
+
"attack_ref": "T1552.004",
|
|
472
|
+
"cve_ref": "CVE-2024-1709",
|
|
473
|
+
"false_positive_checks_required": [
|
|
474
|
+
"Confirm the artefact is actually retrievable by the threat model in scope. GitHub Actions logs are restricted to repo readers by default; a private repo log is lower-blast-radius than a public one. Score the exposure by the audience that can read the log.",
|
|
475
|
+
"Confirm the token is still valid. Revoked tokens are a hygiene finding, not an active exposure. Use a low-impact validity probe (e.g. GitHub Apps `gh api /app` with the installation token; do not exercise destructive scopes)."
|
|
476
|
+
]
|
|
477
|
+
}
|
|
478
|
+
],
|
|
479
|
+
"false_positive_profile": [
|
|
480
|
+
{
|
|
481
|
+
"indicator_id": "wildcard-redirect-uri",
|
|
482
|
+
"benign_pattern": "Vercel / Netlify per-PR preview deployments that legitimately need wildcard redirect_uri for `*.vercel.app` or `*.netlify.app`.",
|
|
483
|
+
"distinguishing_test": "Confirm the upstream OAuth provider ALSO enforces a Referer / Origin check on the callback (Google's per-client origin allowlist, GitHub Apps' explicit callback URL list). Wildcard with no origin check is exploitable; wildcard with origin check is the intended preview pattern."
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
"indicator_id": "missing-webhook-signature-validation",
|
|
487
|
+
"benign_pattern": "Receiver running inside a mesh that enforces caller identity at the mesh layer (Istio / Linkerd mTLS, AWS PrivateLink with VPC endpoint policy).",
|
|
488
|
+
"distinguishing_test": "Verify the mesh policy is enforcing — not just present. An Istio AuthorizationPolicy in permissive-mode logs violations without blocking; that is not a substitute for receiver-side signature validation."
|
|
489
|
+
}
|
|
490
|
+
],
|
|
491
|
+
"minimum_signal": {
|
|
492
|
+
"detected": "At least one of {missing-state-parameter, missing-webhook-signature-validation, leaked-incoming-webhook-url, webhook-secret-shared-across-apps, long-lived-callback-token-in-ci-log} fires, OR two or more high-confidence indicators fire together. Receiver inventory > 0.",
|
|
493
|
+
"inconclusive": "Receiver inventory > 0 but every indicator's false_positive_checks_required step was blocked (admin API unreachable, mesh policy state unknown). Cannot deny without resolving the inventory gaps.",
|
|
494
|
+
"not_detected": "Receiver inventory > 0, every receiver enforces signature validation + replay window + (for OAuth clients) state-parameter binding, redirect_uri allowlists are exact-host, and no leaked URLs surface in the scanned exposure surfaces."
|
|
495
|
+
}
|
|
496
|
+
},
|
|
497
|
+
"analyze": {
|
|
498
|
+
"rwep_inputs": [
|
|
499
|
+
{
|
|
500
|
+
"signal_id": "missing-state-parameter",
|
|
501
|
+
"rwep_factor": "public_poc",
|
|
502
|
+
"weight": 15,
|
|
503
|
+
"notes": "PKCE / state-binding attacks are well-documented; assume public PoC."
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
"signal_id": "missing-webhook-signature-validation",
|
|
507
|
+
"rwep_factor": "active_exploitation",
|
|
508
|
+
"weight": 25,
|
|
509
|
+
"notes": "Unsigned webhook receivers are routinely exploited in researcher disclosures + active intrusions."
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
"signal_id": "leaked-incoming-webhook-url",
|
|
513
|
+
"rwep_factor": "active_exploitation",
|
|
514
|
+
"weight": 25,
|
|
515
|
+
"notes": "Leaked Slack / Teams / Discord webhooks have documented impersonation-into-channel abuse in 2024-2025 incidents."
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
"signal_id": "long-lived-callback-token-in-ci-log",
|
|
519
|
+
"rwep_factor": "active_exploitation",
|
|
520
|
+
"weight": 25,
|
|
521
|
+
"notes": "2024 Snowflake intrusion class — confirmed widespread exploitation of long-lived CI-log-leaked tokens."
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"signal_id": "long-lived-callback-token-in-ci-log",
|
|
525
|
+
"rwep_factor": "cisa_kev",
|
|
526
|
+
"weight": 20,
|
|
527
|
+
"notes": "Multiple linked CVEs (CVE-2024-1709 included) carry KEV listing for the receiver-trust class."
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
"signal_id": "webhook-secret-shared-across-apps",
|
|
531
|
+
"rwep_factor": "blast_radius",
|
|
532
|
+
"weight": 5,
|
|
533
|
+
"notes": "Shared-secret = blast radius multiplier across all sharing receivers."
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"signal_id": "missing-webhook-replay-window",
|
|
537
|
+
"rwep_factor": "public_poc",
|
|
538
|
+
"weight": 15,
|
|
539
|
+
"notes": "Replay-without-window attacks are trivially scripted from any captured payload."
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
"signal_id": "wildcard-redirect-uri",
|
|
543
|
+
"rwep_factor": "ai_weaponization",
|
|
544
|
+
"weight": 10,
|
|
545
|
+
"notes": "Wildcard-redirect + open-redirect-chain payload generation is a documented AI-assisted attacker workflow."
|
|
546
|
+
}
|
|
547
|
+
],
|
|
548
|
+
"blast_radius_model": {
|
|
549
|
+
"scope_question": "If an attacker successfully abuses a matched webhook or callback finding, what scope of downstream compromise does the trust anchor deliver?",
|
|
550
|
+
"scoring_rubric": [
|
|
551
|
+
{
|
|
552
|
+
"condition": "Receiver is a single-app notification consumer (no token issued, no action taken beyond logging).",
|
|
553
|
+
"blast_radius_score": 1,
|
|
554
|
+
"description": "Impersonated message in one channel. Cleanup = revoke + audit log review."
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
"condition": "Receiver triggers a workflow inside the operator's CI (repository_dispatch, Slack workflow with code-execution step).",
|
|
558
|
+
"blast_radius_score": 2,
|
|
559
|
+
"description": "Workflow trigger → code execution path in the operator's CI runner."
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
"condition": "Receiver maps to a cloud-issued credential (Snowflake-class — leaked callback token = STS access).",
|
|
563
|
+
"blast_radius_score": 3,
|
|
564
|
+
"description": "Direct cloud-tenant access via the issued credential."
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
"condition": "Receiver triggers a package-publish or release-pipeline workflow (GitHub Apps with `contents: write` + `packages: write`).",
|
|
568
|
+
"blast_radius_score": 4,
|
|
569
|
+
"description": "Supply-chain primitive — attacker can push a release that downstream operators install."
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
"condition": "Receiver maps to an IdP admin scope (Slack admin webhook, GitHub Apps with `org_admin` scope, Atlassian Connect with admin install).",
|
|
573
|
+
"blast_radius_score": 5,
|
|
574
|
+
"description": "Identity-plane compromise. Full org pivot."
|
|
575
|
+
}
|
|
576
|
+
]
|
|
577
|
+
},
|
|
578
|
+
"compliance_theater_check": {
|
|
579
|
+
"claim": "All inbound endpoints are reviewed under SOC 2 CC6.6 / ISO 27001 A.5.16 / NIST IA-9 with TLS enforced, WAF rules in place, and signed agreements with every external provider — therefore the webhook + callback trust boundary is managed.",
|
|
580
|
+
"audit_evidence": "Inbound-endpoint catalogue with TLS posture report, WAF rule export, and signed Data Processing Agreement / Integration Agreement with every provider.",
|
|
581
|
+
"reality_test": "For a sample of 5 receivers from the catalogue: (a) submit a request with a deliberately-mangled signature and confirm the receiver rejects with 401/403, not 200; (b) capture a real payload and replay it 10 minutes later, confirm the receiver rejects on the replay window; (c) for an OAuth client, attempt a callback with the state parameter stripped and confirm the handler rejects. Theater verdict if any of (a)-(c) succeed: the paper compliance does not bind to the behaviour.",
|
|
582
|
+
"theater_verdict_if_gap": "Org demonstrates inbound-endpoint inventory + TLS + WAF posture that satisfies the literal control language, while the receivers themselves accept unsigned, replayed, or state-stripped payloads. Either (a) add receiver-behaviour conformance tests to the CC6.6 / IA-9 evidence package, (b) treat receivers as supply-chain artefacts and bring them under the NIS2 Art.21(2)(d) register with per-receiver secret-rotation cadence, OR (c) generate a defensible exception via policy-exception-gen acknowledging the residual risk."
|
|
583
|
+
},
|
|
584
|
+
"framework_gap_mapping": [
|
|
585
|
+
{
|
|
586
|
+
"finding_id": "webhook-receiver-trust-failure",
|
|
587
|
+
"framework": "nist-800-53",
|
|
588
|
+
"claimed_control": "IA-9 — Service Identification and Authentication",
|
|
589
|
+
"actual_gap": "Identifies outbound trust but not inbound callback authentication. Wildcard redirect_uri + open-redirect chain satisfies IA-9 trivially.",
|
|
590
|
+
"required_control": "Add an inbound-callback variant: every receiver must demonstrate (a) signature validation with constant-time comparison, (b) replay-window enforcement, (c) for OAuth, state-parameter binding to the originating session. Behaviour conformance tested per-deploy, not just per-audit."
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
"finding_id": "webhook-receiver-trust-failure",
|
|
594
|
+
"framework": "nist-800-53",
|
|
595
|
+
"claimed_control": "AC-4 — Information Flow Enforcement",
|
|
596
|
+
"actual_gap": "Does not require state-parameter binding on OAuth callbacks or replay-window enforcement on inbound webhooks. A receiver passes AC-4 review and still accepts 4-day-old replays.",
|
|
597
|
+
"required_control": "Inbound flow controls must include CSRF defence (state binding) and replay defence (timestamp tolerance + nonce cache) as explicit testable requirements."
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
"finding_id": "webhook-receiver-trust-failure",
|
|
601
|
+
"framework": "iso-27001-2022",
|
|
602
|
+
"claimed_control": "A.5.16 — Identity management",
|
|
603
|
+
"actual_gap": "Treats human + service identities but not callback-URL identities. The redirect_uri allowlist itself is an identity decision A.5.16 doesn't anchor.",
|
|
604
|
+
"required_control": "Extend A.5.16 to enumerate callback-URL identities as first-class identity artefacts, with allowlist-hygiene + secret-rotation + exposure-surface monitoring requirements."
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
"finding_id": "webhook-receiver-trust-failure",
|
|
608
|
+
"framework": "nis2",
|
|
609
|
+
"claimed_control": "Art.21(2)(d) — Supply chain security",
|
|
610
|
+
"actual_gap": "SaaS callback partners excluded from supply-chain register because they're not procurement vendors. The receiver is the persistent trust anchor.",
|
|
611
|
+
"required_control": "Bring inbound webhook receivers under the Art.21(2)(d) register. Track per-receiver secret-rotation cadence, exposure-surface scans, and downstream-credential mapping."
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"finding_id": "webhook-receiver-trust-failure",
|
|
615
|
+
"framework": "soc2",
|
|
616
|
+
"claimed_control": "CC6.6 — Logical and physical access controls — boundary protection",
|
|
617
|
+
"actual_gap": "Tests TLS + WAF rules but not HMAC-signature-validation behaviour, redirect_uri allowlist hygiene, or state-parameter enforcement.",
|
|
618
|
+
"required_control": "Add receiver-behaviour conformance tests (mangled-signature reject, replayed-payload reject, state-stripped reject) to the CC6.6 evidence package, run per-deploy."
|
|
619
|
+
}
|
|
620
|
+
],
|
|
621
|
+
"escalation_criteria": [
|
|
622
|
+
{
|
|
623
|
+
"condition": "rwep >= 90 AND blast_radius_score >= 3",
|
|
624
|
+
"action": "page_on_call"
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
"condition": "long-lived-callback-token-in-ci-log == fired AND cloud-credential-mapping_present == true",
|
|
628
|
+
"action": "trigger_playbook",
|
|
629
|
+
"target_playbook": "cred-stores"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
"condition": "blast_radius_score >= 4 AND finding.includes_package_upload_webhook == true",
|
|
633
|
+
"action": "trigger_playbook",
|
|
634
|
+
"target_playbook": "sbom"
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
"condition": "leaked-incoming-webhook-url == fired AND exposure_surface == 'public'",
|
|
638
|
+
"action": "raise_severity"
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
"condition": "compliance_theater_check.verdict == 'theater' AND jurisdiction_obligations contains 'EU'",
|
|
642
|
+
"action": "notify_legal"
|
|
643
|
+
}
|
|
644
|
+
]
|
|
645
|
+
},
|
|
646
|
+
"validate": {
|
|
647
|
+
"remediation_paths": [
|
|
648
|
+
{
|
|
649
|
+
"id": "rotate-and-revoke",
|
|
650
|
+
"description": "Rotate every flagged webhook secret + OAuth client secret + platform incoming-webhook URL. Revoke flagged long-lived tokens at the issuing provider. Confirm rotation propagated to every consumer with a synthetic-event test.",
|
|
651
|
+
"preconditions": [
|
|
652
|
+
"operator_holds_secret_rotation_authority == true",
|
|
653
|
+
"downstream_consumers_can_be_updated_in_rotation_window == true"
|
|
654
|
+
],
|
|
655
|
+
"priority": 1,
|
|
656
|
+
"compensating_controls": [
|
|
657
|
+
"rotation_recorded_in_secret_store_audit_log",
|
|
658
|
+
"synthetic_event_validates_new_secret"
|
|
659
|
+
],
|
|
660
|
+
"estimated_time_hours": 4
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
"id": "harden-receiver-behaviour",
|
|
664
|
+
"description": "Patch each flagged receiver to enforce signature validation (constant-time), replay-window, and state-parameter binding. Replace `==` comparisons with `hmac.compare_digest` / `crypto.timingSafeEqual`. Adopt the platform SDK's verified-receiver pattern (stripe.webhooks.constructEvent, slack-bolt's built-in SignatureVerifier, Octokit Webhooks).",
|
|
665
|
+
"preconditions": [
|
|
666
|
+
"receiver_source_is_operator_owned == true",
|
|
667
|
+
"deploy_window_within_72h == true"
|
|
668
|
+
],
|
|
669
|
+
"priority": 2,
|
|
670
|
+
"compensating_controls": [
|
|
671
|
+
"behaviour_test_added_to_ci",
|
|
672
|
+
"deploy_recorded_in_change_management"
|
|
673
|
+
],
|
|
674
|
+
"estimated_time_hours": 8
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
"id": "tighten-redirect-uri-allowlist",
|
|
678
|
+
"description": "For each OAuth client with a wildcard or open-redirect-chained redirect_uri, replace with the exact-host allowlist required by the deployed environments. Where per-PR previews legitimately need a wildcard, enforce a per-deploy origin-allowlist at the provider's CORS layer.",
|
|
679
|
+
"preconditions": [
|
|
680
|
+
"operator_admin_on_oauth_provider == true",
|
|
681
|
+
"downstream_app_can_tolerate_temporary_callback_outage == true"
|
|
682
|
+
],
|
|
683
|
+
"priority": 3,
|
|
684
|
+
"compensating_controls": [
|
|
685
|
+
"redirect_uri_allowlist_recorded_in_secret_store",
|
|
686
|
+
"callback_handler_logs_rejected_origins"
|
|
687
|
+
],
|
|
688
|
+
"estimated_time_hours": 4
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
"id": "policy-exception",
|
|
692
|
+
"description": "Generate an auditor-ready policy exception when faster paths are blocked (legacy receiver cannot be patched within window, provider does not offer state-parameter enforcement, etc.) with compensating controls and time-bound risk acceptance.",
|
|
693
|
+
"preconditions": [
|
|
694
|
+
"remediation_paths[1..3] all blocked",
|
|
695
|
+
"ciso_acceptance_obtainable == true"
|
|
696
|
+
],
|
|
697
|
+
"priority": 4,
|
|
698
|
+
"compensating_controls": [
|
|
699
|
+
"enhanced_receiver_log_review",
|
|
700
|
+
"weekly_secret_exposure_scan"
|
|
701
|
+
],
|
|
702
|
+
"estimated_time_hours": 6
|
|
703
|
+
}
|
|
704
|
+
],
|
|
705
|
+
"validation_tests": [
|
|
706
|
+
{
|
|
707
|
+
"id": "mangled-signature-reject",
|
|
708
|
+
"test": "Submit a synthetic payload with a deliberately-mangled signature header to each flagged receiver. Confirm response is 401/403 + no downstream side effect.",
|
|
709
|
+
"expected_result": "All flagged receivers reject with 4xx; downstream side-effect counter does not increment.",
|
|
710
|
+
"test_type": "negative"
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
"id": "replay-window-reject",
|
|
714
|
+
"test": "Capture a real signed payload, wait past the configured replay window (default 5 minutes), replay. Confirm rejection.",
|
|
715
|
+
"expected_result": "Receiver rejects with 401/403; logs entry tagged as replay_window_exceeded.",
|
|
716
|
+
"test_type": "negative"
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
"id": "state-stripped-callback-reject",
|
|
720
|
+
"test": "For each flagged OAuth client, initiate a callback with the state parameter stripped or replaced. Confirm the handler rejects without linking the OAuth provider's response to any user session.",
|
|
721
|
+
"expected_result": "Handler returns 4xx + does not link session; audit log shows state_mismatch.",
|
|
722
|
+
"test_type": "negative"
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
"id": "secret-rotation-propagated",
|
|
726
|
+
"test": "Post-rotation, submit a payload signed with the OLD secret. Confirm rejection.",
|
|
727
|
+
"expected_result": "Receiver rejects payload signed with rotated-out secret.",
|
|
728
|
+
"test_type": "negative"
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
"id": "no-regression-on-legitimate-traffic",
|
|
732
|
+
"test": "Run the receiver's standard integration test suite (or replay 24h of legitimate captured traffic with current secrets).",
|
|
733
|
+
"expected_result": "All legitimate traffic succeeds; no regression in receiver throughput.",
|
|
734
|
+
"test_type": "regression"
|
|
735
|
+
}
|
|
736
|
+
],
|
|
737
|
+
"residual_risk_statement": {
|
|
738
|
+
"risk": "Webhook + OAuth callback trust boundary contains receivers that cannot be hardened to the required behaviour conformance within the jurisdiction's notification window.",
|
|
739
|
+
"why_remains": "Either (a) a legacy receiver depends on an upstream SDK that does not expose constant-time signature validation, (b) the OAuth provider does not enforce state parameter and the receiver cannot be updated until next deploy window, OR (c) a SaaS provider rotates incoming-webhook URLs only via a manual support ticket process slower than the window.",
|
|
740
|
+
"acceptance_level": "ciso",
|
|
741
|
+
"compensating_controls_in_place": [
|
|
742
|
+
"enhanced_receiver_log_review",
|
|
743
|
+
"weekly_secret_exposure_scan",
|
|
744
|
+
"downstream_credential_mapping_monitored"
|
|
745
|
+
]
|
|
746
|
+
},
|
|
747
|
+
"evidence_requirements": [
|
|
748
|
+
{
|
|
749
|
+
"evidence_type": "config_diff",
|
|
750
|
+
"description": "Diff of receiver-handler code showing signature-validation + replay-window + state-binding additions, plus the change-management approval reference.",
|
|
751
|
+
"retention_period": "audit_cycle",
|
|
752
|
+
"framework_satisfied": [
|
|
753
|
+
"nist-800-53-IA-9",
|
|
754
|
+
"nist-800-53-AC-4",
|
|
755
|
+
"iso-27001-2022-A.5.16",
|
|
756
|
+
"soc2-cc6.6"
|
|
757
|
+
]
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
"evidence_type": "scan_report",
|
|
761
|
+
"description": "Exposure-surface scan output showing leaked-URL hits before + after remediation (CI logs, error-tracker, public repos).",
|
|
762
|
+
"retention_period": "1_year",
|
|
763
|
+
"framework_satisfied": [
|
|
764
|
+
"soc2-cc6.6",
|
|
765
|
+
"iso-27001-2022-A.8.21"
|
|
766
|
+
]
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
"evidence_type": "exploit_replay_negative",
|
|
770
|
+
"description": "Receiver-behaviour test results showing mangled-signature reject, replay-window reject, and state-stripped reject all pass post-remediation.",
|
|
771
|
+
"retention_period": "1_year",
|
|
772
|
+
"framework_satisfied": [
|
|
773
|
+
"soc2-cc6.6",
|
|
774
|
+
"iso-27001-2022-A.5.16"
|
|
775
|
+
]
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
"evidence_type": "attestation",
|
|
779
|
+
"description": "Signed exceptd attestation file with evidence_hash, receiver inventory pre/post, RWEP at detection, RWEP post-remediation, residual risk acceptance.",
|
|
780
|
+
"retention_period": "7_years",
|
|
781
|
+
"framework_satisfied": [
|
|
782
|
+
"nist-800-53-CA-7",
|
|
783
|
+
"iso-27001-2022-A.5.36",
|
|
784
|
+
"nis2-art21-2d"
|
|
785
|
+
]
|
|
786
|
+
}
|
|
787
|
+
],
|
|
788
|
+
"regression_trigger": [
|
|
789
|
+
{
|
|
790
|
+
"condition": "new_oauth_client_registered == true",
|
|
791
|
+
"interval": "on_event"
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
"condition": "new_webhook_subscription_created == true",
|
|
795
|
+
"interval": "on_event"
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
"condition": "incoming_webhook_url_added_to_secret_store == true",
|
|
799
|
+
"interval": "on_event"
|
|
800
|
+
},
|
|
801
|
+
{
|
|
802
|
+
"condition": "weekly",
|
|
803
|
+
"interval": "7d"
|
|
804
|
+
}
|
|
805
|
+
]
|
|
806
|
+
},
|
|
807
|
+
"close": {
|
|
808
|
+
"evidence_package": {
|
|
809
|
+
"bundle_format": "csaf-2.0",
|
|
810
|
+
"contents": [
|
|
811
|
+
"all_validation_tests_passed",
|
|
812
|
+
"receiver_inventory_pre_post",
|
|
813
|
+
"exploit_replay_negative",
|
|
814
|
+
"secret_rotation_audit_record",
|
|
815
|
+
"residual_risk_statement",
|
|
816
|
+
"framework_gap_mapping",
|
|
817
|
+
"compliance_theater_verdict",
|
|
818
|
+
"attestation"
|
|
819
|
+
],
|
|
820
|
+
"destination": "local_only",
|
|
821
|
+
"signed": true
|
|
822
|
+
},
|
|
823
|
+
"learning_loop": {
|
|
824
|
+
"enabled": true,
|
|
825
|
+
"lesson_template": {
|
|
826
|
+
"attack_vector": "Webhook or OAuth callback trust boundary abuse — $finding_class (e.g. missing-state-parameter, leaked-incoming-webhook-url, long-lived-callback-token-in-ci-log).",
|
|
827
|
+
"control_gap": "Inbound-endpoint inventory + TLS + WAF satisfied the literal control language but did not bind to receiver behaviour. Signature validation, replay-window, state-parameter binding were not tested per-deploy.",
|
|
828
|
+
"framework_gap": "NIST IA-9 + AC-4, ISO A.5.16, NIS2 Art.21(2)(d), SOC 2 CC6.6 all underspecify the inbound-callback trust boundary. None mandate redirect_uri allowlist hygiene, OAuth state binding, replay-window enforcement, or per-app secret isolation.",
|
|
829
|
+
"new_control_requirement": "Add receiver-behaviour conformance to each framework's inbound-trust control: mangled-signature reject + replay-window reject + state-stripped reject tested per-deploy. Bring SaaS callback partners under the supply-chain register with per-receiver secret-rotation cadence."
|
|
830
|
+
},
|
|
831
|
+
"feeds_back_to_skills": [
|
|
832
|
+
"identity-assurance",
|
|
833
|
+
"api-security",
|
|
834
|
+
"cloud-iam-incident",
|
|
835
|
+
"framework-gap-analysis",
|
|
836
|
+
"zeroday-gap-learn"
|
|
837
|
+
]
|
|
838
|
+
},
|
|
839
|
+
"notification_actions": [
|
|
840
|
+
{
|
|
841
|
+
"obligation_ref": "EU/NIS2 Art.23 24h",
|
|
842
|
+
"deadline": "computed_at_runtime",
|
|
843
|
+
"recipient": "internal_legal",
|
|
844
|
+
"evidence_attached": [
|
|
845
|
+
"webhook_receiver_inventory",
|
|
846
|
+
"secret_exposure_window_estimate",
|
|
847
|
+
"downstream_token_revocation_record"
|
|
848
|
+
],
|
|
849
|
+
"draft_notification": "Initial NIS2 Art.23 24-hour early-warning notification: webhook + OAuth callback trust-boundary compromise across ${affected_receiver_count} receiver(s). Findings include: ${finding_classes_present}. Exposure window: ${exposure_window_estimate}. Containment in place: ${containment_record}. Full incident assessment to follow within 72 hours per Art.23(4)."
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
"obligation_ref": "EU/DORA Art.19 4h",
|
|
853
|
+
"deadline": "computed_at_runtime",
|
|
854
|
+
"recipient": "internal_legal",
|
|
855
|
+
"evidence_attached": [
|
|
856
|
+
"ict_third_party_callback_inventory",
|
|
857
|
+
"containment_record"
|
|
858
|
+
],
|
|
859
|
+
"draft_notification": "DORA Art.19 initial notification: Major ICT-related incident — webhook/callback trust-boundary compromise affecting ICT third-party integration(s) ${ict_third_party_callback_inventory}. Critical or important functions affected: ${affected_functions}. Full classification + impact assessment to follow within statutory windows."
|
|
860
|
+
}
|
|
861
|
+
],
|
|
862
|
+
"exception_generation": {
|
|
863
|
+
"trigger_condition": "remediation_blocked == true OR (provider_does_not_expose_required_primitive == true AND alternative_provider_migration_eta > jurisdiction_window)",
|
|
864
|
+
"exception_template": {
|
|
865
|
+
"scope": "Webhook + OAuth callback trust-boundary residual risk across ${affected_receiver_count} receiver(s); remediation paths 1-3 blocked or partially blocked.",
|
|
866
|
+
"duration": "until_provider_primitive_available_or_30d",
|
|
867
|
+
"compensating_controls": [
|
|
868
|
+
"enhanced_receiver_log_review",
|
|
869
|
+
"weekly_secret_exposure_scan",
|
|
870
|
+
"downstream_credential_mapping_monitored",
|
|
871
|
+
"leaked_url_takedown_runbook"
|
|
872
|
+
],
|
|
873
|
+
"risk_acceptance_owner": "ciso",
|
|
874
|
+
"auditor_ready_language": "Pursuant to ${framework_id} ${control_id}, the organisation documents a time-bound risk acceptance for webhook + OAuth callback trust-boundary findings across ${affected_receiver_count} receiver(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 exposure indicator firing — whichever is first."
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
"regression_schedule": {
|
|
878
|
+
"next_run": "computed_at_runtime",
|
|
879
|
+
"trigger": "both",
|
|
880
|
+
"notify_on_skip": true
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
"directives": [
|
|
885
|
+
{
|
|
886
|
+
"id": "all-callback-and-webhook-receivers",
|
|
887
|
+
"title": "Inventory + behaviour-test every OAuth callback handler + inbound webhook receiver",
|
|
888
|
+
"applies_to": {
|
|
889
|
+
"always": true
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
"id": "snowflake-class-ci-log-leak",
|
|
894
|
+
"title": "Targeted scan for Snowflake-class long-lived-callback-token-in-CI-log exposure",
|
|
895
|
+
"applies_to": {
|
|
896
|
+
"attack_technique": "T1552.004"
|
|
897
|
+
},
|
|
898
|
+
"phase_overrides": {
|
|
899
|
+
"direct": {
|
|
900
|
+
"rwep_threshold": {
|
|
901
|
+
"escalate": 75,
|
|
902
|
+
"monitor": 50,
|
|
903
|
+
"close": 25
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
"id": "github-apps-installation-trust",
|
|
910
|
+
"title": "Targeted review for GitHub Apps installation + webhook secret hygiene",
|
|
911
|
+
"applies_to": {
|
|
912
|
+
"attack_technique": "T1199"
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
]
|
|
916
|
+
}
|