@blamejs/exceptd-skills 0.16.28 → 0.16.30
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 +28 -0
- package/README.md +1 -1
- package/bin/exceptd.js +251 -18
- package/data/_indexes/_meta.json +4 -3
- package/data/_indexes/jurisdiction-map.json +31 -158
- package/data/playbooks/crypto.json +6 -0
- package/lib/auto-discovery.js +8 -0
- package/lib/collectors/README.md +3 -2
- package/lib/collectors/library-author.js +26 -9
- package/lib/collectors/secrets.js +8 -1
- package/lib/cross-ref-api.js +96 -31
- package/lib/lint-skills.js +6 -1
- package/lib/playbook-runner.js +264 -52
- package/lib/prefetch.js +78 -6
- package/lib/refresh-external.js +106 -5
- package/lib/scoring.js +49 -5
- package/lib/validate-cve-catalog.js +14 -2
- package/lib/validate-indexes.js +5 -0
- package/lib/validate-playbooks.js +133 -38
- package/manifest.json +53 -53
- package/orchestrator/pipeline.js +16 -4
- package/package.json +1 -1
- package/sbom.cdx.json +73 -58
- package/scripts/build-indexes.js +12 -1
- package/scripts/check-sbom-currency.js +76 -14
- package/scripts/refresh-sbom.js +1 -1
- package/scripts/run-e2e-scenarios.js +41 -11
- package/scripts/sync-package-description.js +74 -0
- package/scripts/verify-shipped-tarball.js +18 -7
- package/sources/validators/cve-validator.js +16 -6
|
@@ -35,7 +35,6 @@
|
|
|
35
35
|
"policy-exception-gen",
|
|
36
36
|
"pqc-first",
|
|
37
37
|
"privacy-consent-ops",
|
|
38
|
-
"rag-pipeline-security",
|
|
39
38
|
"ransomware-response",
|
|
40
39
|
"researcher",
|
|
41
40
|
"sector-energy",
|
|
@@ -54,7 +53,7 @@
|
|
|
54
53
|
"zeroday-gap-learn"
|
|
55
54
|
],
|
|
56
55
|
"example_excerpts": {},
|
|
57
|
-
"skill_count":
|
|
56
|
+
"skill_count": 50
|
|
58
57
|
},
|
|
59
58
|
"UK": {
|
|
60
59
|
"skills": [
|
|
@@ -63,15 +62,11 @@
|
|
|
63
62
|
"ai-c2-detection",
|
|
64
63
|
"ai-risk-management",
|
|
65
64
|
"api-security",
|
|
66
|
-
"attack-surface-pentest",
|
|
67
|
-
"cloud-iam-incident",
|
|
68
65
|
"cloud-security",
|
|
69
66
|
"compliance-theater",
|
|
70
67
|
"container-runtime-security",
|
|
71
68
|
"coordinated-vuln-disclosure",
|
|
72
|
-
"decompression-dos",
|
|
73
69
|
"defensive-countermeasure-mapping",
|
|
74
|
-
"dlp-gap-analysis",
|
|
75
70
|
"email-security-anti-phishing",
|
|
76
71
|
"exploit-scoring",
|
|
77
72
|
"framework-gap-analysis",
|
|
@@ -80,36 +75,22 @@
|
|
|
80
75
|
"identity-assurance",
|
|
81
76
|
"idp-incident-response",
|
|
82
77
|
"incident-response-playbook",
|
|
83
|
-
"kernel-lpe-triage",
|
|
84
|
-
"log-injection-telemetry",
|
|
85
78
|
"mcp-agent-trust",
|
|
86
|
-
"mlops-security",
|
|
87
|
-
"multitenancy-isolation",
|
|
88
|
-
"network-trust",
|
|
89
|
-
"ot-ics-security",
|
|
90
79
|
"policy-exception-gen",
|
|
91
80
|
"pqc-first",
|
|
92
|
-
"privacy-consent-ops",
|
|
93
|
-
"rag-pipeline-security",
|
|
94
81
|
"ransomware-response",
|
|
95
82
|
"researcher",
|
|
96
83
|
"sector-energy",
|
|
97
84
|
"sector-federal-government",
|
|
98
|
-
"sector-financial",
|
|
99
|
-
"sector-healthcare",
|
|
100
85
|
"sector-telecom",
|
|
101
|
-
"security-maturity-tiers",
|
|
102
|
-
"self-update-integrity",
|
|
103
86
|
"skill-update-loop",
|
|
104
87
|
"supply-chain-integrity",
|
|
105
88
|
"threat-model-currency",
|
|
106
89
|
"threat-modeling-methodology",
|
|
107
|
-
"
|
|
108
|
-
"webapp-security",
|
|
109
|
-
"zeroday-gap-learn"
|
|
90
|
+
"webapp-security"
|
|
110
91
|
],
|
|
111
92
|
"example_excerpts": {},
|
|
112
|
-
"skill_count":
|
|
93
|
+
"skill_count": 31
|
|
113
94
|
},
|
|
114
95
|
"AU": {
|
|
115
96
|
"skills": [
|
|
@@ -138,13 +119,11 @@
|
|
|
138
119
|
"kernel-lpe-triage",
|
|
139
120
|
"log-injection-telemetry",
|
|
140
121
|
"mcp-agent-trust",
|
|
141
|
-
"mlops-security",
|
|
142
122
|
"multitenancy-isolation",
|
|
143
123
|
"ot-ics-security",
|
|
144
124
|
"policy-exception-gen",
|
|
145
125
|
"pqc-first",
|
|
146
126
|
"privacy-consent-ops",
|
|
147
|
-
"rag-pipeline-security",
|
|
148
127
|
"ransomware-response",
|
|
149
128
|
"researcher",
|
|
150
129
|
"sector-energy",
|
|
@@ -162,72 +141,51 @@
|
|
|
162
141
|
"zeroday-gap-learn"
|
|
163
142
|
],
|
|
164
143
|
"example_excerpts": {},
|
|
165
|
-
"skill_count":
|
|
144
|
+
"skill_count": 45
|
|
166
145
|
},
|
|
167
146
|
"SG": {
|
|
168
147
|
"skills": [
|
|
169
|
-
"age-gates-child-safety",
|
|
170
|
-
"ai-attack-surface",
|
|
171
|
-
"api-security",
|
|
172
|
-
"cloud-iam-incident",
|
|
173
|
-
"cloud-security",
|
|
174
148
|
"container-runtime-security",
|
|
175
|
-
"coordinated-vuln-disclosure",
|
|
176
|
-
"email-security-anti-phishing",
|
|
177
149
|
"framework-gap-analysis",
|
|
178
150
|
"global-grc",
|
|
179
151
|
"identity-assurance",
|
|
180
|
-
"incident-response-playbook",
|
|
181
|
-
"mcp-agent-trust",
|
|
182
|
-
"mlops-security",
|
|
183
152
|
"researcher",
|
|
184
|
-
"sector-federal-government",
|
|
185
153
|
"sector-financial",
|
|
186
|
-
"sector-healthcare",
|
|
187
|
-
"sector-telecom",
|
|
188
154
|
"threat-modeling-methodology",
|
|
189
155
|
"webapp-security"
|
|
190
156
|
],
|
|
191
157
|
"example_excerpts": {},
|
|
192
|
-
"skill_count":
|
|
158
|
+
"skill_count": 8
|
|
193
159
|
},
|
|
194
160
|
"JP": {
|
|
195
161
|
"skills": [
|
|
196
162
|
"age-gates-child-safety",
|
|
197
|
-
"ai-risk-management",
|
|
198
163
|
"api-security",
|
|
199
164
|
"cloud-iam-incident",
|
|
200
|
-
"cloud-security",
|
|
201
165
|
"container-runtime-security",
|
|
202
|
-
"coordinated-vuln-disclosure",
|
|
203
166
|
"dlp-gap-analysis",
|
|
204
167
|
"email-security-anti-phishing",
|
|
205
168
|
"framework-gap-analysis",
|
|
206
169
|
"global-grc",
|
|
207
170
|
"identity-assurance",
|
|
208
171
|
"incident-response-playbook",
|
|
209
|
-
"mlops-security",
|
|
210
172
|
"ot-ics-security",
|
|
211
173
|
"pqc-first",
|
|
212
|
-
"ransomware-response",
|
|
213
174
|
"sector-energy",
|
|
214
175
|
"sector-federal-government",
|
|
215
176
|
"sector-financial",
|
|
216
177
|
"sector-healthcare",
|
|
217
|
-
"sector-telecom",
|
|
218
178
|
"supply-chain-integrity",
|
|
219
179
|
"threat-modeling-methodology",
|
|
220
180
|
"webapp-security"
|
|
221
181
|
],
|
|
222
182
|
"example_excerpts": {},
|
|
223
|
-
"skill_count":
|
|
183
|
+
"skill_count": 19
|
|
224
184
|
},
|
|
225
185
|
"IN": {
|
|
226
186
|
"skills": [
|
|
227
187
|
"age-gates-child-safety",
|
|
228
188
|
"ai-risk-management",
|
|
229
|
-
"api-security",
|
|
230
|
-
"cloud-security",
|
|
231
189
|
"dlp-gap-analysis",
|
|
232
190
|
"email-security-anti-phishing",
|
|
233
191
|
"framework-gap-analysis",
|
|
@@ -242,51 +200,36 @@
|
|
|
242
200
|
"threat-modeling-methodology"
|
|
243
201
|
],
|
|
244
202
|
"example_excerpts": {},
|
|
245
|
-
"skill_count":
|
|
203
|
+
"skill_count": 14
|
|
246
204
|
},
|
|
247
205
|
"CA": {
|
|
248
206
|
"skills": [
|
|
249
207
|
"age-gates-child-safety",
|
|
250
|
-
"ai-c2-detection",
|
|
251
|
-
"cloud-iam-incident",
|
|
252
|
-
"cloud-security",
|
|
253
|
-
"defensive-countermeasure-mapping",
|
|
254
208
|
"dlp-gap-analysis",
|
|
255
209
|
"framework-gap-analysis",
|
|
256
210
|
"global-grc",
|
|
257
|
-
"identity-assurance",
|
|
258
211
|
"idp-incident-response",
|
|
259
|
-
"
|
|
260
|
-
"sector-energy",
|
|
261
|
-
"sector-federal-government",
|
|
262
|
-
"sector-financial",
|
|
263
|
-
"sector-healthcare",
|
|
264
|
-
"sector-telecom",
|
|
265
|
-
"self-update-integrity",
|
|
266
|
-
"skill-update-loop",
|
|
267
|
-
"zeroday-gap-learn"
|
|
212
|
+
"sector-financial"
|
|
268
213
|
],
|
|
269
214
|
"example_excerpts": {},
|
|
270
|
-
"skill_count":
|
|
215
|
+
"skill_count": 6
|
|
271
216
|
},
|
|
272
217
|
"BR": {
|
|
273
218
|
"skills": [
|
|
274
219
|
"age-gates-child-safety",
|
|
275
220
|
"ai-risk-management",
|
|
276
|
-
"api-security",
|
|
277
221
|
"cloud-security",
|
|
278
222
|
"dlp-gap-analysis",
|
|
279
223
|
"framework-gap-analysis",
|
|
280
224
|
"global-grc",
|
|
281
225
|
"incident-response-playbook",
|
|
282
226
|
"pqc-first",
|
|
283
|
-
"sector-financial",
|
|
284
227
|
"sector-healthcare",
|
|
285
228
|
"supply-chain-integrity",
|
|
286
229
|
"threat-modeling-methodology"
|
|
287
230
|
],
|
|
288
231
|
"example_excerpts": {},
|
|
289
|
-
"skill_count":
|
|
232
|
+
"skill_count": 11
|
|
290
233
|
},
|
|
291
234
|
"CN": {
|
|
292
235
|
"skills": [
|
|
@@ -315,53 +258,33 @@
|
|
|
315
258
|
"skill_count": 1
|
|
316
259
|
},
|
|
317
260
|
"AE": {
|
|
318
|
-
"skills": [
|
|
319
|
-
"incident-response-playbook",
|
|
320
|
-
"sector-financial"
|
|
321
|
-
],
|
|
261
|
+
"skills": [],
|
|
322
262
|
"example_excerpts": {},
|
|
323
|
-
"skill_count":
|
|
263
|
+
"skill_count": 0
|
|
324
264
|
},
|
|
325
265
|
"SA": {
|
|
326
266
|
"skills": [
|
|
327
267
|
"age-gates-child-safety",
|
|
328
|
-
"compliance-theater",
|
|
329
|
-
"defensive-countermeasure-mapping",
|
|
330
268
|
"dlp-gap-analysis",
|
|
331
|
-
"
|
|
332
|
-
"fuzz-testing-strategy",
|
|
333
|
-
"global-grc",
|
|
334
|
-
"mcp-agent-trust",
|
|
335
|
-
"mlops-security",
|
|
336
|
-
"pqc-first",
|
|
337
|
-
"sector-energy",
|
|
338
|
-
"sector-federal-government",
|
|
339
|
-
"sector-financial",
|
|
340
|
-
"sector-healthcare",
|
|
341
|
-
"supply-chain-integrity",
|
|
342
|
-
"zeroday-gap-learn"
|
|
269
|
+
"sector-financial"
|
|
343
270
|
],
|
|
344
271
|
"example_excerpts": {},
|
|
345
|
-
"skill_count":
|
|
272
|
+
"skill_count": 3
|
|
346
273
|
},
|
|
347
274
|
"NZ": {
|
|
348
|
-
"skills": [
|
|
349
|
-
"sector-financial",
|
|
350
|
-
"sector-telecom"
|
|
351
|
-
],
|
|
275
|
+
"skills": [],
|
|
352
276
|
"example_excerpts": {},
|
|
353
|
-
"skill_count":
|
|
277
|
+
"skill_count": 0
|
|
354
278
|
},
|
|
355
279
|
"KR": {
|
|
356
280
|
"skills": [
|
|
357
281
|
"age-gates-child-safety",
|
|
358
282
|
"dlp-gap-analysis",
|
|
359
283
|
"framework-gap-analysis",
|
|
360
|
-
"global-grc"
|
|
361
|
-
"supply-chain-integrity"
|
|
284
|
+
"global-grc"
|
|
362
285
|
],
|
|
363
286
|
"example_excerpts": {},
|
|
364
|
-
"skill_count":
|
|
287
|
+
"skill_count": 4
|
|
365
288
|
},
|
|
366
289
|
"CL": {
|
|
367
290
|
"skills": [],
|
|
@@ -371,8 +294,6 @@
|
|
|
371
294
|
"IL": {
|
|
372
295
|
"skills": [
|
|
373
296
|
"ai-risk-management",
|
|
374
|
-
"api-security",
|
|
375
|
-
"cloud-iam-incident",
|
|
376
297
|
"cloud-security",
|
|
377
298
|
"container-runtime-security",
|
|
378
299
|
"coordinated-vuln-disclosure",
|
|
@@ -394,7 +315,7 @@
|
|
|
394
315
|
"webapp-security"
|
|
395
316
|
],
|
|
396
317
|
"example_excerpts": {},
|
|
397
|
-
"skill_count":
|
|
318
|
+
"skill_count": 20
|
|
398
319
|
},
|
|
399
320
|
"CH": {
|
|
400
321
|
"skills": [
|
|
@@ -423,74 +344,33 @@
|
|
|
423
344
|
},
|
|
424
345
|
"TW": {
|
|
425
346
|
"skills": [
|
|
426
|
-
"cloud-security",
|
|
427
347
|
"container-runtime-security",
|
|
428
|
-
"dlp-gap-analysis",
|
|
429
|
-
"framework-gap-analysis",
|
|
430
348
|
"global-grc",
|
|
431
349
|
"ot-ics-security",
|
|
432
350
|
"pqc-first",
|
|
433
351
|
"supply-chain-integrity"
|
|
434
352
|
],
|
|
435
353
|
"example_excerpts": {},
|
|
436
|
-
"skill_count":
|
|
354
|
+
"skill_count": 5
|
|
437
355
|
},
|
|
438
356
|
"ID": {
|
|
439
357
|
"skills": [
|
|
440
|
-
"age-gates-child-safety",
|
|
441
|
-
"ai-attack-surface",
|
|
442
|
-
"ai-c2-detection",
|
|
443
358
|
"ai-risk-management",
|
|
444
|
-
"api-security",
|
|
445
|
-
"attack-surface-pentest",
|
|
446
|
-
"cloud-iam-incident",
|
|
447
|
-
"cloud-security",
|
|
448
|
-
"compliance-theater",
|
|
449
|
-
"container-runtime-security",
|
|
450
|
-
"coordinated-vuln-disclosure",
|
|
451
|
-
"defensive-countermeasure-mapping",
|
|
452
359
|
"dlp-gap-analysis",
|
|
453
|
-
"email-security-anti-phishing",
|
|
454
|
-
"exploit-scoring",
|
|
455
360
|
"framework-gap-analysis",
|
|
456
|
-
"fuzz-testing-strategy",
|
|
457
361
|
"global-grc",
|
|
458
362
|
"identity-assurance",
|
|
459
|
-
"idp-incident-response",
|
|
460
|
-
"incident-response-playbook",
|
|
461
|
-
"kernel-lpe-triage",
|
|
462
|
-
"mcp-agent-trust",
|
|
463
|
-
"mlops-security",
|
|
464
363
|
"ot-ics-security",
|
|
465
|
-
"policy-exception-gen",
|
|
466
364
|
"pqc-first",
|
|
467
|
-
"
|
|
468
|
-
"ransomware-response",
|
|
469
|
-
"researcher",
|
|
470
|
-
"sector-energy",
|
|
471
|
-
"sector-federal-government",
|
|
472
|
-
"sector-financial",
|
|
473
|
-
"sector-healthcare",
|
|
474
|
-
"sector-telecom",
|
|
475
|
-
"skill-update-loop",
|
|
476
|
-
"supply-chain-integrity",
|
|
477
|
-
"threat-model-currency",
|
|
478
|
-
"threat-modeling-methodology",
|
|
479
|
-
"webapp-security",
|
|
480
|
-
"zeroday-gap-learn"
|
|
365
|
+
"supply-chain-integrity"
|
|
481
366
|
],
|
|
482
367
|
"example_excerpts": {},
|
|
483
|
-
"skill_count":
|
|
368
|
+
"skill_count": 8
|
|
484
369
|
},
|
|
485
370
|
"VN": {
|
|
486
|
-
"skills": [
|
|
487
|
-
"dlp-gap-analysis",
|
|
488
|
-
"framework-gap-analysis",
|
|
489
|
-
"global-grc",
|
|
490
|
-
"supply-chain-integrity"
|
|
491
|
-
],
|
|
371
|
+
"skills": [],
|
|
492
372
|
"example_excerpts": {},
|
|
493
|
-
"skill_count":
|
|
373
|
+
"skill_count": 0
|
|
494
374
|
},
|
|
495
375
|
"US_NYDFS": {
|
|
496
376
|
"skills": [
|
|
@@ -520,33 +400,26 @@
|
|
|
520
400
|
},
|
|
521
401
|
"NO": {
|
|
522
402
|
"skills": [
|
|
523
|
-
"mail-server-hardening",
|
|
524
403
|
"sector-energy",
|
|
525
404
|
"skill-update-loop"
|
|
526
405
|
],
|
|
527
406
|
"example_excerpts": {},
|
|
528
|
-
"skill_count":
|
|
407
|
+
"skill_count": 2
|
|
529
408
|
},
|
|
530
409
|
"MX": {
|
|
531
|
-
"skills": [
|
|
532
|
-
"sector-financial"
|
|
533
|
-
],
|
|
410
|
+
"skills": [],
|
|
534
411
|
"example_excerpts": {},
|
|
535
|
-
"skill_count":
|
|
412
|
+
"skill_count": 0
|
|
536
413
|
},
|
|
537
414
|
"AR": {
|
|
538
|
-
"skills": [
|
|
539
|
-
"age-gates-child-safety"
|
|
540
|
-
],
|
|
415
|
+
"skills": [],
|
|
541
416
|
"example_excerpts": {},
|
|
542
|
-
"skill_count":
|
|
417
|
+
"skill_count": 0
|
|
543
418
|
},
|
|
544
419
|
"TR": {
|
|
545
|
-
"skills": [
|
|
546
|
-
"sector-telecom"
|
|
547
|
-
],
|
|
420
|
+
"skills": [],
|
|
548
421
|
"example_excerpts": {},
|
|
549
|
-
"skill_count":
|
|
422
|
+
"skill_count": 0
|
|
550
423
|
},
|
|
551
424
|
"TH": {
|
|
552
425
|
"skills": [],
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
"description": "At least one TLS library must be present and queryable. If none, the host has no crypto surface this playbook scopes.",
|
|
37
37
|
"check": "exists_any(['openssl', 'libssl.so*', 'libcrypto.so*']) == true",
|
|
38
38
|
"on_fail": "warn"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"id": "linux-platform",
|
|
42
|
+
"description": "Host must be Linux — crypto enumeration reads /etc/ssh and invokes the system openssl/ssh binaries.",
|
|
43
|
+
"check": "host.platform == 'linux'",
|
|
44
|
+
"on_fail": "halt"
|
|
39
45
|
}
|
|
40
46
|
],
|
|
41
47
|
"mutex": [],
|
package/lib/auto-discovery.js
CHANGED
|
@@ -557,6 +557,14 @@ function extractAcronymFromGroupUri(uri) {
|
|
|
557
557
|
* @param {object} opts { cap?: number, sinceDays?: number }
|
|
558
558
|
*/
|
|
559
559
|
async function discoverNewRfcs(ctx, opts = {}) {
|
|
560
|
+
// Air-gap: new-RFC discovery queries IETF Datatracker live (~one call per
|
|
561
|
+
// project working group). Refuse the egress under --air-gap so a fully
|
|
562
|
+
// offline run makes no network calls — the other discovery paths guard the
|
|
563
|
+
// same way. --from-cache alone (network-available host, e.g. the scheduled
|
|
564
|
+
// refresh) still discovers live; add --air-gap for a truly offline run.
|
|
565
|
+
if ((ctx && ctx.airGap === true) || process.env.EXCEPTD_AIR_GAP === "1") {
|
|
566
|
+
return { diffs: [], errors: 0, spilled: 0, summary: "RFC discovery: skipped under air-gap (no live Datatracker query)" };
|
|
567
|
+
}
|
|
560
568
|
const cap = opts.cap ?? DEFAULT_CAP;
|
|
561
569
|
const sinceDays = opts.sinceDays ?? 180;
|
|
562
570
|
const cutoff = new Date(Date.now() - sinceDays * 86_400_000).toISOString().slice(0, 10);
|
package/lib/collectors/README.md
CHANGED
|
@@ -62,8 +62,9 @@ exceptd collect <playbook> | exceptd run <playbook> --evidence - # full loop
|
|
|
62
62
|
Exit codes:
|
|
63
63
|
|
|
64
64
|
- `0` — submission emitted successfully (operator should check `collector_errors[]` for partial-evidence warnings)
|
|
65
|
-
- `1` — no collector exists for the playbook id (the AI-evidence path remains)
|
|
66
|
-
|
|
65
|
+
- `1` — failure: either no collector exists for the playbook id (the AI-evidence path remains) **or** the collector threw an unhandled exception (file a bug). Both go through the shared error path, so both exit `1`; the JSON envelope on stderr distinguishes them — `type: "collector_not_found"` for the missing-collector case, an `"threw an unhandled exception"` message plus a `stack` for the crash case.
|
|
66
|
+
|
|
67
|
+
Run `exceptd doctor --exit-codes` for the full exit-code map. Code `2` is reserved for the CI escalation gate (`detected` classification), not used by `collect`.
|
|
67
68
|
|
|
68
69
|
## When to write a collector
|
|
69
70
|
|
|
@@ -88,7 +88,7 @@ function looksLikePublishWorkflow(name, content) {
|
|
|
88
88
|
// filename-prefix checks above are unaffected. A `#` inside a quoted string
|
|
89
89
|
// is rare in workflow YAML and not load-bearing for these command probes
|
|
90
90
|
// (stripping can only REMOVE comment text, never create a false match).
|
|
91
|
-
const code = content
|
|
91
|
+
const code = stripYamlComments(content);
|
|
92
92
|
|
|
93
93
|
// Explicit publish-shape commands — these are commitments to push
|
|
94
94
|
// artifacts, not setup / scaffolding.
|
|
@@ -133,7 +133,22 @@ function hasIdTokenWriteAnyScope(content) {
|
|
|
133
133
|
return /\bid-token:\s*write\b/.test(content);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// Strip YAML line-comments so a `#`-commented MENTION of a publish-shape
|
|
137
|
+
// token / command / runner is not read as the real thing. The classifier
|
|
138
|
+
// (looksLikePublishWorkflow) already does this; the indicator probes — and the
|
|
139
|
+
// provenance / SBOM-capability probes in collect() — must use the same view,
|
|
140
|
+
// or a comment produces a false (often deterministic) hit, and in the
|
|
141
|
+
// provenance direction a commented `--provenance` would suppress a real gap
|
|
142
|
+
// (a false negative on a security-relevant posture check).
|
|
143
|
+
function stripYamlComments(content) {
|
|
144
|
+
return content.replace(/#.*$/gm, "");
|
|
145
|
+
}
|
|
146
|
+
|
|
136
147
|
function scanPublishWorkflow(content, rel) {
|
|
148
|
+
// Whole-content probes below run against a comment-stripped view. The
|
|
149
|
+
// `uses:` line scan stays on the raw lines — its anchored regex already
|
|
150
|
+
// rejects `#`-prefixed lines, so a commented `uses:` cannot match.
|
|
151
|
+
const code = stripYamlComments(content);
|
|
137
152
|
const hits = {
|
|
138
153
|
"publish-workflow-uses-static-token": [],
|
|
139
154
|
"publish-workflow-no-id-token-write": [],
|
|
@@ -148,11 +163,13 @@ function scanPublishWorkflow(content, rel) {
|
|
|
148
163
|
// NPM_TOKEN / PYPI_TOKEN / CARGO_TOKEN / RUBYGEMS_API_KEY /
|
|
149
164
|
// GEM_HOST_API_KEY; expand to cover the common variants for each
|
|
150
165
|
// ecosystem.
|
|
151
|
-
const usesStaticToken = /\bsecrets\.(NPM_TOKEN|PYPI_TOKEN|PYPI_API_TOKEN|CARGO_TOKEN|CARGO_REGISTRY_TOKEN|RUBYGEMS_API_KEY|GEM_HOST_API_KEY|MAVEN_TOKEN|MAVEN_CENTRAL_TOKEN|GH_TOKEN)\b/.test(
|
|
166
|
+
const usesStaticToken = /\bsecrets\.(NPM_TOKEN|PYPI_TOKEN|PYPI_API_TOKEN|CARGO_TOKEN|CARGO_REGISTRY_TOKEN|RUBYGEMS_API_KEY|GEM_HOST_API_KEY|MAVEN_TOKEN|MAVEN_CENTRAL_TOKEN|GH_TOKEN)\b/.test(code);
|
|
152
167
|
// OIDC is available when THIS publish file declares `id-token: write` at
|
|
153
168
|
// any scope (workflow or job). Scoped to the file by design — a sibling
|
|
154
|
-
// workflow's OIDC does not authenticate this publish job.
|
|
155
|
-
|
|
169
|
+
// workflow's OIDC does not authenticate this publish job. Read from the
|
|
170
|
+
// comment-stripped view so a commented `id-token: write` cannot falsely
|
|
171
|
+
// satisfy the capability (which would suppress the static-token finding).
|
|
172
|
+
const hasIdTokenWrite = hasIdTokenWriteAnyScope(code);
|
|
156
173
|
if (usesStaticToken && !hasIdTokenWrite) {
|
|
157
174
|
hits["publish-workflow-uses-static-token"].push({ file: rel, line: 0, snippet: "publish workflow uses a static long-lived token (NPM_TOKEN / PYPI / Cargo / Maven) without id-token: write for OIDC" });
|
|
158
175
|
}
|
|
@@ -186,15 +203,15 @@ function scanPublishWorkflow(content, rel) {
|
|
|
186
203
|
// non-frozen-install: workflow uses `npm install` instead of `npm ci`,
|
|
187
204
|
// or `pip install <pkg>` without `--require-hashes`, or `cargo
|
|
188
205
|
// install` without `--locked`.
|
|
189
|
-
if (/\bnpm\s+install\b/.test(
|
|
206
|
+
if (/\bnpm\s+install\b/.test(code) && !/\bnpm\s+ci\b/.test(code)) {
|
|
190
207
|
hits["release-workflow-non-frozen-install"].push({ file: rel, line: 0, snippet: "publish workflow uses `npm install` rather than `npm ci` — lockfile is not enforced" });
|
|
191
208
|
}
|
|
192
|
-
if (/\bcargo\s+(?:build|install)\b/.test(
|
|
209
|
+
if (/\bcargo\s+(?:build|install)\b/.test(code) && !/--locked\b/.test(code) && !/--frozen\b/.test(code)) {
|
|
193
210
|
hits["release-workflow-non-frozen-install"].push({ file: rel, line: 0, snippet: "cargo build/install without --locked / --frozen" });
|
|
194
211
|
}
|
|
195
212
|
|
|
196
213
|
// runs-on-self-hosted: any `runs-on: self-hosted` line.
|
|
197
|
-
if (/runs-on:\s*['"]?(?:self-hosted|\[?\s*self-hosted)/i.test(
|
|
214
|
+
if (/runs-on:\s*['"]?(?:self-hosted|\[?\s*self-hosted)/i.test(code)) {
|
|
198
215
|
hits["publish-workflow-runs-on-self-hosted"].push({ file: rel, line: 0, snippet: "publish workflow runs on a self-hosted runner — non-ephemeral execution context" });
|
|
199
216
|
}
|
|
200
217
|
|
|
@@ -265,7 +282,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
265
282
|
try {
|
|
266
283
|
const j = JSON.parse(pkgManifest.content);
|
|
267
284
|
const manifestOptIn = j?.publishConfig?.provenance === true;
|
|
268
|
-
const workflowOptIn = publishWorkflows.some(w => /npm\s+publish[^\n]*--provenance\b/.test(w.content));
|
|
285
|
+
const workflowOptIn = publishWorkflows.some(w => /npm\s+publish[^\n]*--provenance\b/.test(stripYamlComments(w.content)));
|
|
269
286
|
provenanceMissing = (manifestOptIn || workflowOptIn) ? "miss" : "hit";
|
|
270
287
|
} catch (e) {
|
|
271
288
|
errors.push({ artifact_id: "package-manifest", kind: "parse_failed", reason: `package.json: ${e.message}` });
|
|
@@ -414,7 +431,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
414
431
|
// signed-attestation capability exists at release and the indicator
|
|
415
432
|
// should not fire on the absence of a committed artifact.
|
|
416
433
|
const releaseSbomCapable = publishWorkflows.some(w => {
|
|
417
|
-
const c = w.content;
|
|
434
|
+
const c = stripYamlComments(w.content);
|
|
418
435
|
return (
|
|
419
436
|
// SBOM-generation tooling invoked in the workflow.
|
|
420
437
|
/cyclonedx/i.test(c) ||
|
|
@@ -207,7 +207,14 @@ function fpIndicesSatisfied(indicatorId, value, file, window) {
|
|
|
207
207
|
const INDICATOR_PATTERNS = [
|
|
208
208
|
{ id: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
209
209
|
{ id: "aws-secret-access-key", re: /\baws_secret_access_key\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi },
|
|
210
|
-
|
|
210
|
+
// Require a full PEM block (header, base64 body, closing marker) — not just
|
|
211
|
+
// the `-----BEGIN PRIVATE KEY-----` header — so a service-account JSON shown
|
|
212
|
+
// as a doc placeholder or a redaction/DLP library's detection-pattern literal
|
|
213
|
+
// does not register as a real embedded key. A real key is JSON-encoded with
|
|
214
|
+
// `\n` escapes, so the body class includes backslash; `-` is excluded so the
|
|
215
|
+
// run halts at `-----END` (no backtracking, ReDoS-safe). Mirrors
|
|
216
|
+
// ssh-private-key-block below.
|
|
217
|
+
{ id: "gcp-service-account-json", re: /"type"\s*:\s*"service_account"[\s\S]{0,1200}?"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----[A-Za-z0-9+/=\s\\]{40,4000}-----END/g },
|
|
211
218
|
{ id: "github-personal-access-token", re: /\bghp_[A-Za-z0-9]{36}\b/g },
|
|
212
219
|
{ id: "github-fine-grained-pat", re: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
|
|
213
220
|
{ id: "slack-bot-or-user-token", re: /\bxox[abposr]-[A-Za-z0-9-]{10,}\b/g },
|