@blamejs/exceptd-skills 0.9.5 → 0.10.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.
- package/AGENTS.md +45 -0
- package/CHANGELOG.md +66 -0
- package/README.md +30 -5
- package/bin/exceptd.js +400 -1
- package/data/_indexes/_meta.json +2 -2
- package/data/playbooks/ai-api.json +763 -0
- package/data/playbooks/containers.json +766 -0
- package/data/playbooks/cred-stores.json +715 -0
- package/data/playbooks/crypto.json +726 -0
- package/data/playbooks/framework.json +725 -0
- package/data/playbooks/hardening.json +672 -0
- package/data/playbooks/kernel.json +549 -0
- package/data/playbooks/mcp.json +727 -0
- package/data/playbooks/runtime.json +649 -0
- package/data/playbooks/sbom.json +893 -0
- package/data/playbooks/secrets.json +690 -0
- package/lib/cross-ref-api.js +224 -0
- package/lib/playbook-runner.js +826 -0
- package/lib/schemas/playbook.schema.json +652 -0
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/orchestrator/scanner.js +23 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"id": "cred-stores",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"last_threat_review": "2026-05-11",
|
|
6
|
+
"threat_currency_score": 95,
|
|
7
|
+
"changelog": [
|
|
8
|
+
{
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"date": "2026-05-11",
|
|
11
|
+
"summary": "Initial seven-phase credential-store inventory playbook. Inspects ~/.aws/credentials, ~/.kube/config, ~/.config/gcloud/credentials.db, ~/.docker/config.json, ~/.npmrc, ~/.pypirc, GPG keychain. Distinguishes long-lived static credentials from short-lived federated (SSO-issued kube tokens, IAM Identity Center / Workforce Identity / OIDC-issued service account tokens). Reports identity-assurance posture per store with framework-gap mapping.",
|
|
12
|
+
"framework_gaps_updated": ["nist-800-53-IA-5", "nist-800-63b-aal", "iso-27001-2022-A.5.16", "nis2-art21-2j"]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"owner": "@blamejs/identity",
|
|
16
|
+
"air_gap_mode": true,
|
|
17
|
+
"preconditions": [
|
|
18
|
+
{
|
|
19
|
+
"id": "home-dir-readable",
|
|
20
|
+
"description": "Playbook inspects files in the current user's $HOME. Agent must be able to read the running user's home directory.",
|
|
21
|
+
"check": "agent_can_read('$HOME') == true",
|
|
22
|
+
"on_fail": "halt"
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"mutex": [],
|
|
26
|
+
"feeds_into": [
|
|
27
|
+
{
|
|
28
|
+
"playbook_id": "secrets",
|
|
29
|
+
"condition": "finding.severity >= 'high'"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"playbook_id": "runtime",
|
|
33
|
+
"condition": "finding.severity == 'critical'"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"domain": {
|
|
39
|
+
"name": "Per-user credential store inventory + identity-assurance posture",
|
|
40
|
+
"attack_class": "identity-abuse",
|
|
41
|
+
"atlas_refs": [],
|
|
42
|
+
"attack_refs": ["T1078", "T1552.001", "T1552.004", "T1528", "T1606.001"],
|
|
43
|
+
"cve_refs": [],
|
|
44
|
+
"cwe_refs": ["CWE-522", "CWE-256", "CWE-798", "CWE-732"],
|
|
45
|
+
"d3fend_refs": ["D3-CAA", "D3-FCR", "D3-ANCI"],
|
|
46
|
+
"frameworks_in_scope": [
|
|
47
|
+
"nist-800-53", "iso-27001-2022", "soc2", "pci-dss-4",
|
|
48
|
+
"nis2", "dora", "uk-caf", "au-ism", "au-essential-8"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"phases": {
|
|
53
|
+
|
|
54
|
+
"govern": {
|
|
55
|
+
"jurisdiction_obligations": [
|
|
56
|
+
{
|
|
57
|
+
"jurisdiction": "EU",
|
|
58
|
+
"regulation": "NIS2 Art.23",
|
|
59
|
+
"obligation": "notify_regulator",
|
|
60
|
+
"window_hours": 24,
|
|
61
|
+
"clock_starts": "detect_confirmed",
|
|
62
|
+
"evidence_required": ["credential_inventory", "exposure_window_estimate", "rotation_status"]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"jurisdiction": "EU",
|
|
66
|
+
"regulation": "DORA Art.19",
|
|
67
|
+
"obligation": "notify_regulator",
|
|
68
|
+
"window_hours": 4,
|
|
69
|
+
"clock_starts": "detect_confirmed",
|
|
70
|
+
"evidence_required": ["initial_notification", "ict_third_party_dependencies"]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"jurisdiction": "EU",
|
|
74
|
+
"regulation": "GDPR Art.33",
|
|
75
|
+
"obligation": "notify_regulator",
|
|
76
|
+
"window_hours": 72,
|
|
77
|
+
"clock_starts": "detect_confirmed",
|
|
78
|
+
"evidence_required": ["credential_inventory", "data_subject_impact_assessment"]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"jurisdiction": "AU",
|
|
82
|
+
"regulation": "APRA CPS 234",
|
|
83
|
+
"obligation": "notify_regulator",
|
|
84
|
+
"window_hours": 72,
|
|
85
|
+
"clock_starts": "validate_complete",
|
|
86
|
+
"evidence_required": ["materiality_assessment", "remediation_completed_evidence"]
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"theater_fingerprints": [
|
|
90
|
+
{
|
|
91
|
+
"pattern_id": "sso-attested-but-aws-static-keys-live",
|
|
92
|
+
"claim": "We use SSO / Identity Center / Workforce Identity for all cloud access — no static keys.",
|
|
93
|
+
"fast_detection_test": "Cat ~/.aws/credentials. If file is non-empty AND contains an [profile] block with aws_access_key_id (not just aws_sso_session-shaped config), SSO attestation is incomplete. Repeat for ~/.config/gcloud/credentials.db (sqlite query for service_account-shaped rows) and ~/.kube/config (token: field with no exec: federated flow).",
|
|
94
|
+
"implicated_controls": ["nist-800-53-IA-2", "iso-27001-2022-A.5.16", "soc2-cc6.1"]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"pattern_id": "mfa-attested-but-pat-bypasses",
|
|
98
|
+
"claim": "MFA is enforced for all developer access.",
|
|
99
|
+
"fast_detection_test": "GitHub PATs, NPM tokens, PyPI tokens, container registry tokens bypass MFA by design — they are long-lived bearer tokens. Inspect ~/.npmrc, ~/.pypirc, ~/.docker/config.json for raw tokens. Any non-OAuth-flow token is an MFA bypass primitive whose existence undermines the MFA attestation.",
|
|
100
|
+
"implicated_controls": ["nist-800-53-IA-2(1)", "iso-27001-2022-A.5.17", "pci-dss-4-req-8.4"]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"pattern_id": "passkey-attested-but-ssh-keys-stale",
|
|
104
|
+
"claim": "Passkeys / FIDO2 are deployed for all auth — phishing-resistant.",
|
|
105
|
+
"fast_detection_test": "ls -la ~/.ssh/ and check mtime on private keys. SSH keys older than 12 months on a workstation that's been online during that time are stale credentials that exist outside the FIDO2 trust model. Any FIDO2 / Passkey attestation that does not also address legacy SSH key lifecycle is incomplete.",
|
|
106
|
+
"implicated_controls": ["nist-800-63b-aal3", "iso-27001-2022-A.5.16", "uk-caf-b2"]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"pattern_id": "credential-age-not-tracked",
|
|
110
|
+
"claim": "Credentials are rotated quarterly per policy.",
|
|
111
|
+
"fast_detection_test": "Stat each credential file: mtime is the upper bound on credential age. If any ~/.aws/credentials or ~/.kube/config or ~/.docker/config.json mtime is > 90 days, the policy is not enforced on this workstation. Compare against the policy text: if policy says 'rotated' but no rotation cadence is technically enforced, it is theater.",
|
|
112
|
+
"implicated_controls": ["nist-800-53-IA-5(1)", "iso-27001-2022-A.5.17", "pci-dss-4-req-8.3.7"]
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
"framework_context": {
|
|
116
|
+
"gap_summary": "Credential store inventory sits at the intersection of identity management (NIST 800-63 AAL, NIST 800-53 IA family, ISO A.5.16 / A.5.17, NIS2 Art.21(2)(j)) and key management (PCI Req.8, SOC 2 CC6). Frameworks specify what credentials should be (AAL3 / phishing-resistant / short-lived) but do not require enumeration of what is actually present on developer workstations. The 2025-2026 IR pattern: developer workstation compromise → ~/.aws/credentials harvest → cloud account pivot. Long-lived static keys live alongside SSO-attested identity layers. SSO attestation accepted by auditors; static keys not audited because they are not in the SSO-attested system. This is the dominant gap.",
|
|
117
|
+
"lag_score": 25,
|
|
118
|
+
"per_framework_gaps": [
|
|
119
|
+
{
|
|
120
|
+
"framework": "nist-800-53",
|
|
121
|
+
"control_id": "IA-5(1)",
|
|
122
|
+
"designed_for": "Password-based authenticator management — complexity, lifetime, history.",
|
|
123
|
+
"insufficient_because": "Names passwords. Does not name long-lived bearer tokens (PATs, NPM, PyPI, AWS access keys) as authenticators-requiring-management. These tokens are functionally passwords but escape IA-5 specification."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"framework": "nist-800-53",
|
|
127
|
+
"control_id": "IA-2",
|
|
128
|
+
"designed_for": "Identification + authentication of organizational users.",
|
|
129
|
+
"insufficient_because": "Permits SSO attestation as the answer. Does not require enumeration of credential stores on user devices that may bypass SSO."
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"framework": "nist-800-63b",
|
|
133
|
+
"control_id": "AAL3",
|
|
134
|
+
"designed_for": "Authenticator Assurance Level 3 — phishing-resistant authenticators.",
|
|
135
|
+
"insufficient_because": "Describes target authenticators. Coexistence of AAL3 with AAL1-shaped bearer tokens (long-lived PATs, static cloud keys) is permissible. Auditor evaluates AAL3 presence, not AAL3 exclusivity."
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"framework": "iso-27001-2022",
|
|
139
|
+
"control_id": "A.5.16",
|
|
140
|
+
"designed_for": "Identity management — full lifecycle of user identities.",
|
|
141
|
+
"insufficient_because": "Identity lifecycle but not credential-store enumeration. Permits attested SSO-only narrative while local stores hold parallel non-SSO credentials."
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"framework": "nis2",
|
|
145
|
+
"control_id": "Art.21(2)(j)",
|
|
146
|
+
"designed_for": "Cryptography and access control policies.",
|
|
147
|
+
"insufficient_because": "Policy shape. Permits 'we use SSO' as evidence without requiring per-device credential-store inventory."
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
},
|
|
151
|
+
"skill_preload": ["identity-assurance", "framework-gap-analysis", "compliance-theater", "policy-exception-gen"]
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
"direct": {
|
|
155
|
+
"threat_context": "Developer workstation credential harvest is the dominant 2025-2026 cloud-account-compromise vector. Mandiant / CrowdStrike Q1 2026 IR data: ~38% of cloud account intrusions traced to harvest of ~/.aws/credentials, ~/.kube/config, ~/.config/gcloud, or ~/.npmrc on a developer endpoint. Stealer malware families (Lumma, Rhadamanthys, Stealc) ship dedicated parsers for these files. AI-coding-assistant agentic flows expand the attack surface: any tool a developer trusts to read their workspace can be prompt-injected to exfiltrate these stores. Identity-assurance posture matters: workstations using exclusively federated short-lived credentials (SSO → STS:AssumeRoleWithWebIdentity → 1h credentials in ~/.aws/cli/cache) deny the harvest its target; workstations with long-lived static credentials provide it directly. Frameworks attest AAL3 + SSO without requiring the workstation-inventory evidence that distinguishes the two postures.",
|
|
156
|
+
"rwep_threshold": {
|
|
157
|
+
"escalate": 85,
|
|
158
|
+
"monitor": 60,
|
|
159
|
+
"close": 30
|
|
160
|
+
},
|
|
161
|
+
"framework_lag_declaration": "NIST 800-63b AAL specs, NIST 800-53 IA-2/IA-5, ISO A.5.16/A.5.17, NIS2 Art.21(2)(j), PCI-DSS Req.8 collectively specify target authenticator properties (phishing-resistant, short-lived, MFA-gated). They do not require enumeration of credentials actually present on developer endpoints. SSO + MFA attestation is accepted as evidence even when local credential stores hold long-lived bearer tokens that bypass SSO. Gap = ~25 days between credential-store drift (new PAT issued, new long-lived AWS key created) and any framework's review cadence.",
|
|
162
|
+
"skill_chain": [
|
|
163
|
+
{ "skill": "identity-assurance", "purpose": "Score each inventoried credential against AAL/IAL/FAL framework. Distinguish AAL3 short-lived from AAL1 long-lived. Map to NIST 800-63 obligations.", "required": true },
|
|
164
|
+
{ "skill": "framework-gap-analysis", "purpose": "Map credential-store findings to which IA/A.5 controls claim to cover them and where the gap is.", "required": true },
|
|
165
|
+
{ "skill": "compliance-theater", "purpose": "Run the theater test on the org's SSO / MFA / passkey attestation against the live credential-store state.", "required": true },
|
|
166
|
+
{ "skill": "policy-exception-gen", "purpose": "Generate auditor-ready exception language for long-lived credentials that cannot be eliminated (e.g. CI bootstrap secrets, legacy SaaS without OIDC).", "skip_if": "close.exception_generation.trigger_condition == false", "required": false }
|
|
167
|
+
],
|
|
168
|
+
"token_budget": {
|
|
169
|
+
"estimated_total": 18000,
|
|
170
|
+
"breakdown": {
|
|
171
|
+
"govern": 2400,
|
|
172
|
+
"direct": 1600,
|
|
173
|
+
"look": 2000,
|
|
174
|
+
"detect": 2800,
|
|
175
|
+
"analyze": 4000,
|
|
176
|
+
"validate": 2800,
|
|
177
|
+
"close": 2400
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
"look": {
|
|
183
|
+
"artifacts": [
|
|
184
|
+
{
|
|
185
|
+
"id": "aws-credentials",
|
|
186
|
+
"type": "config_file",
|
|
187
|
+
"source": "~/.aws/credentials (INI); ~/.aws/config (INI)",
|
|
188
|
+
"description": "AWS CLI credentials + profile config. INI parse: [profile] blocks with aws_access_key_id are static; sso_session = blocks + role_arn with credential_process are federated.",
|
|
189
|
+
"required": true,
|
|
190
|
+
"air_gap_alternative": "Stat the file for presence + mtime; parse offline without API calls."
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"id": "aws-sso-cache",
|
|
194
|
+
"type": "file",
|
|
195
|
+
"source": "~/.aws/sso/cache/*.json",
|
|
196
|
+
"description": "AWS SSO session cache. Presence + expiration field indicates federated short-lived credentials in use.",
|
|
197
|
+
"required": false
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
"id": "kube-config",
|
|
201
|
+
"type": "config_file",
|
|
202
|
+
"source": "~/.kube/config (YAML)",
|
|
203
|
+
"description": "Kubernetes client config. users[].user has token: (static), client-certificate (static), OR exec: (dynamic / federated — e.g. aws eks get-token, gke-gcloud-auth-plugin, oidc-login).",
|
|
204
|
+
"required": true,
|
|
205
|
+
"air_gap_alternative": "Read the file; parse YAML offline."
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"id": "gcloud-credentials",
|
|
209
|
+
"type": "config_file",
|
|
210
|
+
"source": "~/.config/gcloud/credentials.db (SQLite); ~/.config/gcloud/access_tokens.db; ~/.config/gcloud/application_default_credentials.json",
|
|
211
|
+
"description": "gcloud credential database. SQLite query for type=service_account (static JSON key), type=authorized_user (federated user creds), type=external_account (workload identity / workforce identity / OIDC). ADC JSON shape differentiates service account from federated.",
|
|
212
|
+
"required": true,
|
|
213
|
+
"air_gap_alternative": "If sqlite3 unavailable, stat for file presence + mtime; mark token-type as inconclusive."
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"id": "docker-config",
|
|
217
|
+
"type": "config_file",
|
|
218
|
+
"source": "~/.docker/config.json",
|
|
219
|
+
"description": "Docker / OCI registry auth. auths[].auth fields are base64(user:password) — long-lived. credHelpers / credsStore directs to an OS keychain — better. credsStore=ecr-login / gcloud / acr is federated.",
|
|
220
|
+
"required": true
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
"id": "npmrc",
|
|
224
|
+
"type": "config_file",
|
|
225
|
+
"source": "~/.npmrc; project-level .npmrc",
|
|
226
|
+
"description": "NPM registry auth. //registry.npmjs.org/:_authToken=npm_* is a long-lived PAT. //registry.npmjs.org/:_auth=base64(user:pass) is even worse (deprecated).",
|
|
227
|
+
"required": true
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"id": "pypirc",
|
|
231
|
+
"type": "config_file",
|
|
232
|
+
"source": "~/.pypirc",
|
|
233
|
+
"description": "PyPI upload auth. password = pypi-A... is a long-lived PyPI token.",
|
|
234
|
+
"required": true
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"id": "gpg-keys",
|
|
238
|
+
"type": "config_file",
|
|
239
|
+
"source": "gpg --list-secret-keys --keyid-format=long --with-fingerprint 2>/dev/null",
|
|
240
|
+
"description": "GPG private key inventory. Each secret key is a long-lived signing credential. Key creation date + expiration + algorithm + bit-strength.",
|
|
241
|
+
"required": false,
|
|
242
|
+
"air_gap_alternative": "ls ~/.gnupg/private-keys-v1.d/ for presence count if gpg(1) unavailable; cannot resolve algorithm/strength offline."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"id": "ssh-keys-inventory",
|
|
246
|
+
"type": "config_file",
|
|
247
|
+
"source": "ls -la ~/.ssh/id_* 2>/dev/null; for k in ~/.ssh/id_*; do ssh-keygen -l -f $k 2>/dev/null; done",
|
|
248
|
+
"description": "SSH private key inventory. Fingerprint + algorithm + bit-strength + file mtime (proxy for credential age).",
|
|
249
|
+
"required": true
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
"id": "ssh-config",
|
|
253
|
+
"type": "config_file",
|
|
254
|
+
"source": "~/.ssh/config",
|
|
255
|
+
"description": "SSH config. ProxyJump / ForwardAgent / CertificateFile settings reveal whether SSH access is bastion-mediated and cert-based (better) vs. raw key (worse).",
|
|
256
|
+
"required": false
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
"id": "keychain-inventory",
|
|
260
|
+
"type": "config_file",
|
|
261
|
+
"source": "(linux) secret-tool search * * 2>/dev/null | head -100; (mac, if relevant) security dump-keychain -d ~/Library/Keychains/login.keychain-db 2>/dev/null",
|
|
262
|
+
"description": "OS keychain summary. Counts and item-class breakdown, not the secret values.",
|
|
263
|
+
"required": false
|
|
264
|
+
}
|
|
265
|
+
],
|
|
266
|
+
"collection_scope": {
|
|
267
|
+
"time_window": "current",
|
|
268
|
+
"asset_scope": "current_user_home_directory",
|
|
269
|
+
"depth": "standard",
|
|
270
|
+
"sampling": "single-user point-in-time snapshot; re-collect on regression triggers (new credential issued, monthly cadence)"
|
|
271
|
+
},
|
|
272
|
+
"environment_assumptions": [
|
|
273
|
+
{
|
|
274
|
+
"assumption": "agent runs as the user whose credential stores are to be inventoried",
|
|
275
|
+
"if_false": "Cross-user inventory requires root or sudo. Mark per-user-store findings inconclusive unless explicit cross-user authority is documented."
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"assumption": "user is a developer / engineer with non-trivial credential surface",
|
|
279
|
+
"if_false": "Most stores will be empty/absent. That is itself the desired posture; emit baseline_clean and skip false-positive concerns."
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
"assumption": "host platform is linux / mac / wsl (POSIX home-directory layout)",
|
|
283
|
+
"if_false": "Windows users have %USERPROFILE%\\.aws, %APPDATA%\\gcloud, %APPDATA%\\npm; same conceptual artifacts but different paths."
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
"assumption": "agent can call gpg / ssh-keygen / sqlite3",
|
|
287
|
+
"if_false": "Use air-gap alternatives for each artifact; some structural detail (key algorithm, sqlite-stored token-type) becomes inconclusive."
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
"fallback_if_unavailable": [
|
|
291
|
+
{ "artifact_id": "aws-credentials", "fallback_action": "mark_inconclusive", "confidence_impact": "high" },
|
|
292
|
+
{ "artifact_id": "kube-config", "fallback_action": "mark_inconclusive", "confidence_impact": "high" },
|
|
293
|
+
{ "artifact_id": "gcloud-credentials", "fallback_action": "mark_inconclusive", "confidence_impact": "high" },
|
|
294
|
+
{ "artifact_id": "docker-config", "fallback_action": "mark_inconclusive", "confidence_impact": "medium" },
|
|
295
|
+
{ "artifact_id": "npmrc", "fallback_action": "mark_inconclusive", "confidence_impact": "medium" },
|
|
296
|
+
{ "artifact_id": "pypirc", "fallback_action": "mark_inconclusive", "confidence_impact": "medium" },
|
|
297
|
+
{ "artifact_id": "gpg-keys", "fallback_action": "use_compensating_artifact", "confidence_impact": "low" },
|
|
298
|
+
{ "artifact_id": "ssh-keys-inventory", "fallback_action": "escalate_to_human", "confidence_impact": "high" },
|
|
299
|
+
{ "artifact_id": "keychain-inventory", "fallback_action": "mark_inconclusive", "confidence_impact": "low" }
|
|
300
|
+
]
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
"detect": {
|
|
304
|
+
"indicators": [
|
|
305
|
+
{
|
|
306
|
+
"id": "aws-static-key-present",
|
|
307
|
+
"type": "log_pattern",
|
|
308
|
+
"value": "~/.aws/credentials contains a [profile] block with aws_access_key_id = AKIA* AND no sso_session / credential_process",
|
|
309
|
+
"description": "Long-lived AWS IAM user key. AAL1-equivalent. Static credential.",
|
|
310
|
+
"confidence": "deterministic",
|
|
311
|
+
"deterministic": true,
|
|
312
|
+
"attack_ref": "T1552.001"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"id": "kube-static-token",
|
|
316
|
+
"type": "log_pattern",
|
|
317
|
+
"value": "~/.kube/config users[].user.token: field present AND no exec: federated flow alongside",
|
|
318
|
+
"description": "Static Kubernetes service-account or admin token. Long-lived bearer. Indistinguishable from a federated kube context that uses cached token unless exec: is also present.",
|
|
319
|
+
"confidence": "deterministic",
|
|
320
|
+
"deterministic": true,
|
|
321
|
+
"attack_ref": "T1528"
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"id": "gcp-service-account-json-adc",
|
|
325
|
+
"type": "log_pattern",
|
|
326
|
+
"value": "~/.config/gcloud/application_default_credentials.json contains \"type\": \"service_account\"",
|
|
327
|
+
"description": "ADC pointing at a service-account JSON private key. Long-lived. The recommended posture is type=external_account (workforce identity) or type=authorized_user (federated user creds).",
|
|
328
|
+
"confidence": "deterministic",
|
|
329
|
+
"deterministic": true,
|
|
330
|
+
"attack_ref": "T1552.004"
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
"id": "docker-cleartext-auth",
|
|
334
|
+
"type": "log_pattern",
|
|
335
|
+
"value": "~/.docker/config.json contains auths[].auth: field with base64(user:password) AND no credHelpers / credsStore for that registry",
|
|
336
|
+
"description": "Docker registry credentials in cleartext (base64 is not encryption). credHelpers/credsStore would route to OS keychain or cloud-IAM-federated path.",
|
|
337
|
+
"confidence": "deterministic",
|
|
338
|
+
"deterministic": true,
|
|
339
|
+
"attack_ref": "T1552.001"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
"id": "npm-pat-present",
|
|
343
|
+
"type": "log_pattern",
|
|
344
|
+
"value": "~/.npmrc contains :_authToken=npm_[A-Za-z0-9]{36,}",
|
|
345
|
+
"description": "Long-lived NPM Personal Access Token. MFA bypass; publish-capable tokens enable supply-chain attacks.",
|
|
346
|
+
"confidence": "deterministic",
|
|
347
|
+
"deterministic": true,
|
|
348
|
+
"attack_ref": "T1552.001"
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"id": "pypi-token-present",
|
|
352
|
+
"type": "log_pattern",
|
|
353
|
+
"value": "~/.pypirc contains password = pypi-[A-Za-z0-9_-]{40,}",
|
|
354
|
+
"description": "Long-lived PyPI upload token. Same MFA-bypass + supply-chain implications as NPM.",
|
|
355
|
+
"confidence": "deterministic",
|
|
356
|
+
"deterministic": true,
|
|
357
|
+
"attack_ref": "T1552.001"
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
"id": "ssh-key-rsa-short-bits",
|
|
361
|
+
"type": "log_pattern",
|
|
362
|
+
"value": "ssh-keygen reports RSA key with bit-length < 3072 OR DSA key of any size",
|
|
363
|
+
"description": "Weak SSH key. RSA-2048 is acceptable but trending out; RSA-1024 / DSA are deprecated. Cryptographic posture issue.",
|
|
364
|
+
"confidence": "high",
|
|
365
|
+
"deterministic": false
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"id": "ssh-key-old",
|
|
369
|
+
"type": "behavioral_signal",
|
|
370
|
+
"value": "Any ~/.ssh/id_* file with mtime > 365 days ago",
|
|
371
|
+
"description": "Stale SSH key. Predates likely org rotation cadence; predates likely AAL/FIDO2 enrollment.",
|
|
372
|
+
"confidence": "high",
|
|
373
|
+
"deterministic": false
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"id": "gpg-key-old-or-weak",
|
|
377
|
+
"type": "log_pattern",
|
|
378
|
+
"value": "gpg --list-secret-keys reports a secret key with algorithm DSA OR RSA<3072 OR creation date > 5 years AND no expiration set",
|
|
379
|
+
"description": "Weak or never-expiring GPG private key. Long-lived signing credential outside any rotation cadence.",
|
|
380
|
+
"confidence": "high",
|
|
381
|
+
"deterministic": false
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"id": "credentials-file-bad-perms",
|
|
385
|
+
"type": "file_path",
|
|
386
|
+
"value": "Any of ~/.aws/credentials, ~/.kube/config, ~/.docker/config.json, ~/.npmrc, ~/.pypirc, ~/.config/gcloud/* with mode != 0600 (and not 0700 for the gcloud directory)",
|
|
387
|
+
"description": "Permissive permissions on a credential file. Local-user theft primitive.",
|
|
388
|
+
"confidence": "deterministic",
|
|
389
|
+
"deterministic": true,
|
|
390
|
+
"attack_ref": "T1552.004"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"id": "all-stores-empty-or-federated",
|
|
394
|
+
"type": "behavioral_signal",
|
|
395
|
+
"value": "Inventory shows zero static credentials AND every present store uses exec: / sso_session / credsStore / type=external_account (federated paths) OR is empty",
|
|
396
|
+
"description": "Clean federated posture. AAL3-equivalent. Not a finding; emit as positive evidence.",
|
|
397
|
+
"confidence": "high",
|
|
398
|
+
"deterministic": false
|
|
399
|
+
}
|
|
400
|
+
],
|
|
401
|
+
"false_positive_profile": [
|
|
402
|
+
{
|
|
403
|
+
"indicator_id": "aws-static-key-present",
|
|
404
|
+
"benign_pattern": "Profile is a deliberately-isolated break-glass credential stored encrypted at rest with passphrase-on-decrypt (e.g. macOS keychain-wrapped, sops-managed); the user has temporarily decrypted it for use.",
|
|
405
|
+
"distinguishing_test": "Check whether the credentials file mtime is very recent (< 24h) AND the user has documented break-glass procedure. If yes, downgrade to medium and emit a 'break-glass credential in use' visibility finding rather than 'static credential present'. Persistent presence (mtime > 30d) cannot be distinguished from accidental — hold as deterministic."
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
"indicator_id": "kube-static-token",
|
|
409
|
+
"benign_pattern": "Token field is populated by a federated exec: hook (e.g. some kube clients write the result back to token: for caching). Check whether exec: is also present on the same user entry.",
|
|
410
|
+
"distinguishing_test": "Parse the kubeconfig: if users[N].user contains BOTH token: AND exec:, the token is exec-refreshable and effectively short-lived. If only token: is present, it is static. Disambiguates by structure alone."
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"indicator_id": "ssh-key-old",
|
|
414
|
+
"benign_pattern": "Legacy host that still requires RSA key but is on its way to being decommissioned.",
|
|
415
|
+
"distinguishing_test": "Check ~/.ssh/config for Host stanzas referencing this key as IdentityFile AND check whether those hosts are in a documented decommission inventory. If yes, downgrade to medium + emit 'pending-decommission' finding. Untracked legacy keys hold high confidence."
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
"indicator_id": "ssh-key-rsa-short-bits",
|
|
419
|
+
"benign_pattern": "Yubikey-resident RSA-2048 key (some Yubikey models cap at 2048). Functionally hardware-backed despite short bit-length.",
|
|
420
|
+
"distinguishing_test": "Check ssh-keygen output: if the key file is a stub referencing a PKCS#11 module (PKCS11Provider in ssh_config or a `# YubiKey` comment) the key is hardware-backed. Hardware backing trumps bit-length for AAL purposes."
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
"indicator_id": "npm-pat-present",
|
|
424
|
+
"benign_pattern": "Read-only PAT scoped to specific package(s) for CI download; cannot publish.",
|
|
425
|
+
"distinguishing_test": "PAT token type and scope cannot be determined offline in air-gap mode. In air-gap mode, hold high confidence and document as inconclusive. With network, call npm /-/npm/v1/tokens to verify scope."
|
|
426
|
+
}
|
|
427
|
+
],
|
|
428
|
+
"minimum_signal": {
|
|
429
|
+
"detected": "At least one deterministic indicator fires (static-credential of any provider class OR credentials-file-bad-perms).",
|
|
430
|
+
"inconclusive": "Files exist but agent cannot read or parse them (different UID, encrypted-at-rest store, sqlite without sqlite3 binary). Distinguish 'static-credential cannot be detected' from 'no static credential present'.",
|
|
431
|
+
"not_detected": "All credential stores inspected AND no deterministic indicators fired AND all-stores-empty-or-federated fires positively AND all credential files have mode 0600."
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
|
|
435
|
+
"analyze": {
|
|
436
|
+
"rwep_inputs": [
|
|
437
|
+
{ "signal_id": "aws-static-key-present", "rwep_factor": "active_exploitation", "weight": 30, "notes": "Stealer malware ships dedicated AWS-credentials parsers. Treat as exploitation-imminent on any compromised endpoint." },
|
|
438
|
+
{ "signal_id": "kube-static-token", "rwep_factor": "active_exploitation", "weight": 25, "notes": "Static kube tokens enable cluster pivot; ATT&CK T1528 in published threat actor playbooks." },
|
|
439
|
+
{ "signal_id": "gcp-service-account-json-adc", "rwep_factor": "active_exploitation", "weight": 30, "notes": "Service account JSON = same risk class as AWS access keys." },
|
|
440
|
+
{ "signal_id": "docker-cleartext-auth", "rwep_factor": "blast_radius", "weight": 15, "notes": "Registry compromise = supply-chain pivot." },
|
|
441
|
+
{ "signal_id": "npm-pat-present", "rwep_factor": "active_exploitation", "weight": 25, "notes": "Publish-capable NPM PAT = supply-chain attack primitive (event-stream, ua-parser-js, chalk classes of incidents)." },
|
|
442
|
+
{ "signal_id": "pypi-token-present", "rwep_factor": "active_exploitation", "weight": 25, "notes": "PyPI publish token = same supply-chain pattern as NPM." },
|
|
443
|
+
{ "signal_id": "ssh-key-rsa-short-bits", "rwep_factor": "ai_weaponization", "weight": 5, "notes": "AI-accelerated cryptanalysis is not yet a practical threat to RSA-2048, but DSA + RSA-1024 cracking is operational." },
|
|
444
|
+
{ "signal_id": "ssh-key-old", "rwep_factor": "blast_radius", "weight": 10, "notes": "Stale key = unknown exposure history." },
|
|
445
|
+
{ "signal_id": "credentials-file-bad-perms", "rwep_factor": "active_exploitation", "weight": 20, "notes": "Permissive perms on a creds file = same-host LPE chains to credential theft." }
|
|
446
|
+
],
|
|
447
|
+
"blast_radius_model": {
|
|
448
|
+
"scope_question": "Given the inventoried credential stores, what is the realistic blast radius if this endpoint is compromised by stealer malware or a prompt-injected agent?",
|
|
449
|
+
"scoring_rubric": [
|
|
450
|
+
{ "condition": "All stores empty or federated (all-stores-empty-or-federated fires), all permissions correct, SSH keys ed25519 + recent + agent-mediated", "blast_radius_score": 1, "description": "Endpoint compromise yields no useful long-lived credentials. AAL3-equivalent." },
|
|
451
|
+
{ "condition": "One static credential present in a single bounded scope (e.g. one read-only PAT) AND federated paths exist for all primary providers", "blast_radius_score": 2, "description": "Limited bounded scope from one static credential. Cleanup feasible without org-wide impact." },
|
|
452
|
+
{ "condition": "Multiple static credentials across one provider OR a single static credential with broad scope (admin / wildcard / cross-account-trust)", "blast_radius_score": 3, "description": "Provider-account-level pivot from a single endpoint compromise." },
|
|
453
|
+
{ "condition": "Static credentials across multiple providers (cloud + SCM + registry) OR publish-capable supply-chain token (NPM/PyPI/container) present", "blast_radius_score": 4, "description": "Cross-provider pivot + supply-chain attack primitive." },
|
|
454
|
+
{ "condition": "All of: static cloud admin key + publish-capable supply-chain token + permissive permissions + stale SSH keys", "blast_radius_score": 5, "description": "Maximum endpoint-derived blast radius. Identity boundary collapse from this single compromise." }
|
|
455
|
+
]
|
|
456
|
+
},
|
|
457
|
+
"compliance_theater_check": {
|
|
458
|
+
"claim": "Org enforces SSO + MFA + FIDO2/Passkey for all identity assurance per IA-2 / A.5.16 / AAL3.",
|
|
459
|
+
"audit_evidence": "Identity Provider configuration documentation; SSO enrollment reports; MFA/FIDO2 enrollment rates.",
|
|
460
|
+
"reality_test": "Inspect the workstation credential stores. Count: (a) static cloud credentials (~/.aws/credentials AKIA*, ~/.config/gcloud/*.json service_account), (b) static SCM/registry tokens (~/.npmrc, ~/.pypirc, ~/.docker/config.json auths.auth), (c) static kube tokens, (d) SSH keys with mtime > 12 months. Any count > 0 contradicts the AAL3 attestation: the workstation has parallel non-SSO credentials that bypass the entire identity stack. Additionally check whether the IdP's audit logs contain login events for the developer in the last 30 days — if not, the developer is operating exclusively via the local static credentials, the SSO attestation is theater for this user.",
|
|
461
|
+
"theater_verdict_if_gap": "SSO/MFA/AAL3 attestation describes a posture that local credential stores contradict. Static long-lived credentials exist alongside the attested federated identity layer. Either (a) eliminate static credentials and route all access via SSO + federated short-lived tokens, (b) update attestation to acknowledge the bounded exceptions + name compensating controls (credential vaulting, audit-log monitoring), OR (c) generate a policy exception per scope/duration documenting the gap."
|
|
462
|
+
},
|
|
463
|
+
"framework_gap_mapping": [
|
|
464
|
+
{
|
|
465
|
+
"finding_id": "credential-store-static",
|
|
466
|
+
"framework": "nist-800-53",
|
|
467
|
+
"claimed_control": "IA-2 — Identification and Authentication",
|
|
468
|
+
"actual_gap": "SSO attestation accepted. No required enumeration of bypass credentials present on user endpoints.",
|
|
469
|
+
"required_control": "Add an IA-2 sub-control requiring quarterly endpoint-credential-store inventory for all users in scope, with documented disposition for any non-federated credentials found."
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
"finding_id": "credential-store-static",
|
|
473
|
+
"framework": "nist-800-53",
|
|
474
|
+
"claimed_control": "IA-5(1) — Password-Based Authenticator Management",
|
|
475
|
+
"actual_gap": "Names passwords. Long-lived bearer tokens (PATs, AWS keys, PyPI/NPM tokens) escape the specification despite being functionally passwords.",
|
|
476
|
+
"required_control": "Extend IA-5 scope to all bearer-token authenticators with the same management requirements: lifetime, rotation, complexity (token strength), inventory."
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
"finding_id": "credential-store-static",
|
|
480
|
+
"framework": "nist-800-63b",
|
|
481
|
+
"claimed_control": "AAL3 — Phishing-Resistant Authenticator Assurance",
|
|
482
|
+
"actual_gap": "Names target authenticator. Coexistence with AAL1 bearer tokens not addressed; auditor evaluates AAL3 presence, not AAL3 exclusivity.",
|
|
483
|
+
"required_control": "Add an 'AAL3-exclusive' criterion that requires no AAL1-equivalent credentials present in scope for users attested as AAL3."
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
"finding_id": "credential-store-static",
|
|
487
|
+
"framework": "iso-27001-2022",
|
|
488
|
+
"claimed_control": "A.5.16 — Identity Management",
|
|
489
|
+
"actual_gap": "Identity lifecycle does not require endpoint inventory of parallel credentials.",
|
|
490
|
+
"required_control": "Statement of Applicability footnote requiring evidence that user endpoints do not hold parallel non-SSO credentials."
|
|
491
|
+
}
|
|
492
|
+
],
|
|
493
|
+
"escalation_criteria": [
|
|
494
|
+
{ "condition": "rwep >= 90 AND credential_is_publish_capable == true", "action": "page_on_call" },
|
|
495
|
+
{ "condition": "blast_radius_score >= 4", "action": "trigger_playbook", "target_playbook": "secrets" },
|
|
496
|
+
{ "condition": "compliance_theater_check.verdict == 'theater' AND jurisdiction_obligations contains 'EU'", "action": "notify_legal" },
|
|
497
|
+
{ "condition": "any_static_admin_credential == true", "action": "raise_severity" }
|
|
498
|
+
]
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
"validate": {
|
|
502
|
+
"remediation_paths": [
|
|
503
|
+
{
|
|
504
|
+
"id": "migrate-aws-to-sso",
|
|
505
|
+
"description": "Replace static aws_access_key_id entries with sso_session profiles using IAM Identity Center (or AWS SSO). Remove old key from ~/.aws/credentials AND deactivate at IAM console.",
|
|
506
|
+
"preconditions": ["org_has_identity_center == true", "user_enrolled_in_sso == true"],
|
|
507
|
+
"priority": 1,
|
|
508
|
+
"compensating_controls": ["iam-key-deactivated", "cloudtrail-monitor-on-old-key-for-residual-use"],
|
|
509
|
+
"estimated_time_hours": 1
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
"id": "migrate-gcp-to-workforce-identity",
|
|
513
|
+
"description": "Replace ~/.config/gcloud/application_default_credentials.json service_account with workforce identity (gcloud auth login or workforce-pool federation). Delete the service account key at GCP console.",
|
|
514
|
+
"preconditions": ["org_has_workforce_identity_pool == true OR user_has_authorized_user_credentials == true"],
|
|
515
|
+
"priority": 1,
|
|
516
|
+
"compensating_controls": ["gcp-key-deleted", "gcp-audit-log-monitor-on-old-key"],
|
|
517
|
+
"estimated_time_hours": 1
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
"id": "migrate-kube-to-oidc",
|
|
521
|
+
"description": "Replace static token: kubeconfig entries with exec: hooks (oidc-login, aws eks get-token, gke-gcloud-auth-plugin). Revoke old service-account token via kubectl delete secret.",
|
|
522
|
+
"preconditions": ["cluster_supports_oidc == true OR cluster_is_managed_cloud_k8s == true"],
|
|
523
|
+
"priority": 1,
|
|
524
|
+
"compensating_controls": ["kube-token-revoked", "k8s-audit-log-monitor-on-old-token"],
|
|
525
|
+
"estimated_time_hours": 1
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"id": "migrate-docker-to-cred-helpers",
|
|
529
|
+
"description": "Replace ~/.docker/config.json auths.auth with credHelpers/credsStore directives routing to OS keychain or cloud-IAM (ecr-login, gcloud, acr).",
|
|
530
|
+
"preconditions": ["target_registry_supports_cred_helper == true"],
|
|
531
|
+
"priority": 1,
|
|
532
|
+
"compensating_controls": ["docker-token-rotated"],
|
|
533
|
+
"estimated_time_hours": 0.5
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"id": "rotate-supply-chain-tokens",
|
|
537
|
+
"description": "For ~/.npmrc / ~/.pypirc tokens: rotate AND scope down (read-only where possible) AND move to OS keychain (npm config set _authToken in keychain-backed config OR pypi via keyring backend).",
|
|
538
|
+
"preconditions": ["org_authority_to_rotate == true"],
|
|
539
|
+
"priority": 2,
|
|
540
|
+
"compensating_controls": ["token-scope-tightened", "publish-mfa-required"],
|
|
541
|
+
"estimated_time_hours": 1
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
"id": "fix-credentials-perms",
|
|
545
|
+
"description": "chmod 0600 on all credential files; chmod 0700 on ~/.config/gcloud/, ~/.aws/, ~/.ssh/.",
|
|
546
|
+
"preconditions": ["file_owner_is_current_user"],
|
|
547
|
+
"priority": 2,
|
|
548
|
+
"compensating_controls": [],
|
|
549
|
+
"estimated_time_hours": 0.25
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
"id": "modernize-ssh-keys",
|
|
553
|
+
"description": "Generate fresh ed25519 SSH key; deploy public key to all currently-authorized hosts; remove old RSA/DSA private keys from disk. Optionally use ssh-agent + Yubikey for hardware backing.",
|
|
554
|
+
"preconditions": ["all_authorized_hosts_known == true"],
|
|
555
|
+
"priority": 2,
|
|
556
|
+
"compensating_controls": ["ssh-key-inventory-updated"],
|
|
557
|
+
"estimated_time_hours": 2
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"id": "policy-exception",
|
|
561
|
+
"description": "If static credential cannot be eliminated (e.g. legacy SaaS without OIDC), generate exception with bounded compensating controls and time-bound rotation cadence.",
|
|
562
|
+
"preconditions": ["federated_alternative_unavailable == true"],
|
|
563
|
+
"priority": 4,
|
|
564
|
+
"compensating_controls": ["credential-vaulted", "rotation-cadence-30d", "audit-log-monitor-on-credential"],
|
|
565
|
+
"estimated_time_hours": 4
|
|
566
|
+
}
|
|
567
|
+
],
|
|
568
|
+
"validation_tests": [
|
|
569
|
+
{
|
|
570
|
+
"id": "re-inventory-clean",
|
|
571
|
+
"test": "Re-run the credential-store inventory. Expect zero deterministic static-credential indicators.",
|
|
572
|
+
"expected_result": "No deterministic static-credential findings; all-stores-empty-or-federated fires positively.",
|
|
573
|
+
"test_type": "negative"
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
"id": "providers-confirm-revocation",
|
|
577
|
+
"test": "At each provider (AWS IAM list-access-keys, GCP keys list, GitHub /user/keys, NPM /-/npm/v1/tokens, PyPI account tokens), confirm old credentials are deactivated/revoked.",
|
|
578
|
+
"expected_result": "All old credentials inactive or deleted at the provider.",
|
|
579
|
+
"test_type": "functional"
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
"id": "sso-paths-functional",
|
|
583
|
+
"test": "Functionally test each replaced flow: aws sts get-caller-identity via SSO; gcloud auth list via workforce identity; kubectl auth can-i via exec: flow; docker pull via cred-helper.",
|
|
584
|
+
"expected_result": "All federated flows succeed; user can perform prior workflow without static credentials.",
|
|
585
|
+
"test_type": "functional"
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
"id": "perms-correct",
|
|
589
|
+
"test": "stat -c '%a' on credential files and directories. Expect 0600 for files, 0700 for directories.",
|
|
590
|
+
"expected_result": "All credential files/directories have correct permissions.",
|
|
591
|
+
"test_type": "negative"
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
"id": "no-residual-use",
|
|
595
|
+
"test": "Review provider audit logs (CloudTrail, GCP audit, NPM/PyPI download events) for the old credentials' last-use timestamps. Confirm no use after rotation.",
|
|
596
|
+
"expected_result": "No activity on old credentials post-rotation.",
|
|
597
|
+
"test_type": "regression"
|
|
598
|
+
}
|
|
599
|
+
],
|
|
600
|
+
"residual_risk_statement": {
|
|
601
|
+
"risk": "Even after migration to federated identity, transient short-lived tokens (~/.aws/cli/cache/*.json, ~/.config/gcloud/access_tokens.db, ~/.kube/cache/) remain on disk during their TTL. Stealer malware that runs during a session window can still harvest live federated tokens.",
|
|
602
|
+
"why_remains": "Short-lived tokens still exist; the trade-off is shorter exposure window (1h vs. perpetual) but not zero. The compensating control is endpoint protection + IdP risk-based authentication (impossible-travel, device-binding), not absence of tokens-on-disk.",
|
|
603
|
+
"acceptance_level": "manager",
|
|
604
|
+
"compensating_controls_in_place": ["all-federated-paths", "edr-active-on-endpoint", "idp-risk-based-authn", "quarterly-credential-inventory-cadence"]
|
|
605
|
+
},
|
|
606
|
+
"evidence_requirements": [
|
|
607
|
+
{
|
|
608
|
+
"evidence_type": "scan_report",
|
|
609
|
+
"description": "Pre-remediation credential-store inventory + post-remediation rescan.",
|
|
610
|
+
"retention_period": "1_year",
|
|
611
|
+
"framework_satisfied": ["nist-800-53-IA-2", "nist-800-53-IA-5", "iso-27001-2022-A.5.16"]
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"evidence_type": "log_excerpt",
|
|
615
|
+
"description": "Provider audit log excerpt for the lifetime of each rotated credential.",
|
|
616
|
+
"retention_period": "7_years",
|
|
617
|
+
"framework_satisfied": ["nis2-art21-2c", "soc2-cc7.2"]
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
"evidence_type": "ticket_reference",
|
|
621
|
+
"description": "Credential rotation tickets at each provider; SSO/Workforce Identity migration approvals.",
|
|
622
|
+
"retention_period": "7_years",
|
|
623
|
+
"framework_satisfied": ["soc2-cc8.1", "iso-27001-2022-A.8.32"]
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
"evidence_type": "attestation",
|
|
627
|
+
"description": "Signed exceptd attestation: user identity, endpoint identity, inventory hash, RWEP at detection, RWEP post-remediation, AAL claimed/supported by evidence.",
|
|
628
|
+
"retention_period": "7_years",
|
|
629
|
+
"framework_satisfied": ["nist-800-63b-aal", "nist-800-53-CA-7", "iso-27001-2022-A.5.36"]
|
|
630
|
+
}
|
|
631
|
+
],
|
|
632
|
+
"regression_trigger": [
|
|
633
|
+
{ "condition": "monthly", "interval": "30d" },
|
|
634
|
+
{ "condition": "post_new_provider_onboarded", "interval": "on_event" },
|
|
635
|
+
{ "condition": "post_employee_role_change", "interval": "on_event" },
|
|
636
|
+
{ "condition": "post_credential_compromise_incident", "interval": "on_event" }
|
|
637
|
+
]
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
"close": {
|
|
641
|
+
"evidence_package": {
|
|
642
|
+
"bundle_format": "json",
|
|
643
|
+
"contents": ["scan_report", "log_excerpt", "ticket_reference", "attestation", "framework_gap_mapping", "compliance_theater_verdict", "residual_risk_statement"],
|
|
644
|
+
"destination": "local_only",
|
|
645
|
+
"signed": true
|
|
646
|
+
},
|
|
647
|
+
"learning_loop": {
|
|
648
|
+
"enabled": true,
|
|
649
|
+
"lesson_template": {
|
|
650
|
+
"attack_vector": "Endpoint credential harvest via $store_class (~/.aws/credentials / ~/.kube/config / ~/.config/gcloud/* / ~/.docker/config.json / ~/.npmrc / ~/.pypirc / ~/.ssh/id_* / GPG keychain).",
|
|
651
|
+
"control_gap": "Identity-assurance controls (IA-2, IA-5, AAL3, A.5.16, NIS2 Art.21(2)(j)) accept SSO + MFA attestation as evidence without requiring endpoint-credential-store inventory. Long-lived bearer tokens coexist with attested AAL3 posture.",
|
|
652
|
+
"framework_gap": "NIST 800-63 AAL specifies target authenticators, not bypass-credential exclusion. NIST 800-53 IA family names password authenticators, not all bearer-token authenticators. ISO and NIS2 are policy-shaped on identity lifecycle.",
|
|
653
|
+
"new_control_requirement": "Quarterly endpoint-credential-store inventory required for all users in scope of an AAL3 attestation. Static-credential presence on an AAL3-attested user's endpoint = automatic finding requiring remediation OR documented exception."
|
|
654
|
+
},
|
|
655
|
+
"feeds_back_to_skills": ["identity-assurance", "framework-gap-analysis", "compliance-theater"]
|
|
656
|
+
},
|
|
657
|
+
"notification_actions": [
|
|
658
|
+
{
|
|
659
|
+
"obligation_ref": "EU/NIS2 Art.23 24h",
|
|
660
|
+
"deadline": "computed_at_runtime",
|
|
661
|
+
"recipient": "internal_legal",
|
|
662
|
+
"evidence_attached": ["credential_inventory", "exposure_window_estimate", "rotation_status"],
|
|
663
|
+
"draft_notification": "NIS2 Art.23 early-warning notification: Credential-store finding on ${affected_user_count} user endpoint(s); static long-lived credentials identified across ${provider_count} provider(s). Credentials rotated to federated equivalents. Exposure window: ${exposure_window}. Full incident assessment within 72h per Art.23(4)."
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
"obligation_ref": "EU/DORA Art.19 4h",
|
|
667
|
+
"deadline": "computed_at_runtime",
|
|
668
|
+
"recipient": "internal_legal",
|
|
669
|
+
"evidence_attached": ["initial_notification", "ict_third_party_dependencies"],
|
|
670
|
+
"draft_notification": "DORA Art.19 initial notification: Major ICT-related incident — endpoint credential exposure on financial-entity user endpoint(s). ICT third-party dependencies: ${ict_dependencies}. Full classification + impact assessment to follow within statutory windows."
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
"obligation_ref": "EU/GDPR Art.33 72h",
|
|
674
|
+
"deadline": "computed_at_runtime",
|
|
675
|
+
"recipient": "internal_legal",
|
|
676
|
+
"evidence_attached": ["credential_inventory", "data_subject_impact_assessment"],
|
|
677
|
+
"draft_notification": "GDPR Art.33 72-hour notification: Endpoint credential exposure event affecting credentials with potential access to systems processing personal data. Categories of data subjects potentially affected: ${data_subject_categories}. Containment: credentials rotated; provider audit logs reviewed for misuse window."
|
|
678
|
+
}
|
|
679
|
+
],
|
|
680
|
+
"exception_generation": {
|
|
681
|
+
"trigger_condition": "federated_alternative_unavailable == true OR (legacy_saas_no_oidc == true AND business_continuity_requires_static_credential == true)",
|
|
682
|
+
"exception_template": {
|
|
683
|
+
"scope": "Static credential of class ${credential_class} for provider ${provider_name} on user endpoint(s) ${affected_endpoint_count}; federated alternative unavailable due to ${blocking_reason}.",
|
|
684
|
+
"duration": "until_next_audit",
|
|
685
|
+
"compensating_controls": ["credential-vaulted-with-passphrase", "rotation-cadence-30d", "audit-log-monitor-on-credential", "edr-alert-on-credential-file-read", "ip-allowlist-tightened"],
|
|
686
|
+
"risk_acceptance_owner": "ciso",
|
|
687
|
+
"auditor_ready_language": "Pursuant to ${framework_id} ${control_id}, the organization documents a time-bound risk acceptance for static credential ${credential_class} on ${affected_endpoint_count} endpoint(s). Provider: ${provider_name}. Blocking reason for federation: ${blocking_reason_narrative}. Compensating controls in place: credential vaulted with passphrase-on-use; ${rotation_cadence_days}-day forced rotation; provider audit log monitored daily for anomalous use; EDR alerts on read of the credential file outside the documented use pattern. Residual exposure: static credential remains valid during its rotation window; vaulting + passphrase reduces but does not eliminate stealer-malware harvest if endpoint is fully compromised. Risk accepted by ${ciso_name} on ${acceptance_date}. Time-bound until ${duration_expiry} OR provider OIDC availability, whichever is first. The exception will be re-evaluated on (a) provider OIDC availability, (b) listed expiry date, (c) any audit-log anomaly on the credential — whichever is first."
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
"regression_schedule": {
|
|
691
|
+
"next_run": "computed_at_runtime",
|
|
692
|
+
"trigger": "both",
|
|
693
|
+
"notify_on_skip": true
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
},
|
|
697
|
+
|
|
698
|
+
"directives": [
|
|
699
|
+
{
|
|
700
|
+
"id": "full-credential-store-inventory",
|
|
701
|
+
"title": "Full per-user credential-store inventory across cloud, k8s, container registry, package managers, SSH, GPG",
|
|
702
|
+
"applies_to": { "always": true }
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
"id": "cloud-static-keys",
|
|
706
|
+
"title": "Targeted directive — long-lived cloud credentials (AWS access keys, GCP service account JSON, Azure SP secrets)",
|
|
707
|
+
"applies_to": { "attack_technique": "T1552.001" }
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
"id": "supply-chain-tokens",
|
|
711
|
+
"title": "Targeted directive — publish-capable package-manager tokens (NPM, PyPI, container registry)",
|
|
712
|
+
"applies_to": { "attack_technique": "T1528" }
|
|
713
|
+
}
|
|
714
|
+
]
|
|
715
|
+
}
|