@blamejs/exceptd-skills 0.16.23 → 0.16.24
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 +18 -0
- package/agents/report-generator.md +2 -2
- package/bin/exceptd.js +72 -25
- package/data/_indexes/_meta.json +2 -2
- package/data/_indexes/chains.json +354 -177
- package/data/_indexes/section-offsets.json +35 -35
- package/lib/collectors/ai-api.js +112 -7
- package/lib/collectors/citation-hygiene.js +27 -0
- package/lib/collectors/crypto-codebase.js +25 -0
- package/lib/collectors/kernel.js +32 -2
- package/lib/collectors/library-author.js +30 -0
- package/lib/collectors/runtime.js +38 -3
- package/lib/collectors/sbom.js +21 -2
- package/lib/collectors/secrets.js +125 -0
- package/lib/cve-regression-watcher.js +5 -2
- package/lib/playbook-runner.js +16 -3
- package/manifest.json +53 -53
- package/orchestrator/README.md +1 -1
- package/orchestrator/index.js +17 -3
- package/package.json +1 -1
- package/sbom.cdx.json +50 -50
- package/scripts/builders/cwe-chains.js +1 -0
- package/scripts/builders/section-offsets.js +10 -2
- package/scripts/builders/token-budget.js +3 -3
- package/scripts/check-changelog-extract.js +38 -1
- package/scripts/check-version-tags.js +5 -0
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
"byte_start": 21020,
|
|
90
90
|
"byte_end": 23408,
|
|
91
91
|
"bytes": 2388,
|
|
92
|
-
"h3_count":
|
|
92
|
+
"h3_count": 0
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"name": "Detection Rules",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
"byte_start": 23183,
|
|
184
184
|
"byte_end": 24873,
|
|
185
185
|
"bytes": 1690,
|
|
186
|
-
"h3_count":
|
|
186
|
+
"h3_count": 0
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"name": "Defensive Countermeasure Mapping",
|
|
@@ -268,7 +268,7 @@
|
|
|
268
268
|
"byte_start": 23413,
|
|
269
269
|
"byte_end": 24950,
|
|
270
270
|
"bytes": 1537,
|
|
271
|
-
"h3_count":
|
|
271
|
+
"h3_count": 0
|
|
272
272
|
},
|
|
273
273
|
{
|
|
274
274
|
"name": "Hand-Off / Related Skills",
|
|
@@ -371,7 +371,7 @@
|
|
|
371
371
|
"byte_start": 20951,
|
|
372
372
|
"byte_end": 22669,
|
|
373
373
|
"bytes": 1718,
|
|
374
|
-
"h3_count":
|
|
374
|
+
"h3_count": 4
|
|
375
375
|
},
|
|
376
376
|
{
|
|
377
377
|
"name": "Universal Gaps (No Framework Covers These Adequately)",
|
|
@@ -389,7 +389,7 @@
|
|
|
389
389
|
"byte_start": 23764,
|
|
390
390
|
"byte_end": 25703,
|
|
391
391
|
"bytes": 1939,
|
|
392
|
-
"h3_count":
|
|
392
|
+
"h3_count": 0
|
|
393
393
|
},
|
|
394
394
|
{
|
|
395
395
|
"name": "Compliance Theater Check",
|
|
@@ -492,7 +492,7 @@
|
|
|
492
492
|
"byte_start": 29995,
|
|
493
493
|
"byte_end": 31948,
|
|
494
494
|
"bytes": 1953,
|
|
495
|
-
"h3_count":
|
|
495
|
+
"h3_count": 0
|
|
496
496
|
},
|
|
497
497
|
{
|
|
498
498
|
"name": "Compliance Theater Check",
|
|
@@ -613,7 +613,7 @@
|
|
|
613
613
|
"byte_start": 22327,
|
|
614
614
|
"byte_end": 23972,
|
|
615
615
|
"bytes": 1645,
|
|
616
|
-
"h3_count":
|
|
616
|
+
"h3_count": 0
|
|
617
617
|
},
|
|
618
618
|
{
|
|
619
619
|
"name": "Compliance Theater Check",
|
|
@@ -743,7 +743,7 @@
|
|
|
743
743
|
"byte_start": 23244,
|
|
744
744
|
"byte_end": 24789,
|
|
745
745
|
"bytes": 1545,
|
|
746
|
-
"h3_count":
|
|
746
|
+
"h3_count": 0
|
|
747
747
|
},
|
|
748
748
|
{
|
|
749
749
|
"name": "Hand-Off / Related Skills",
|
|
@@ -882,7 +882,7 @@
|
|
|
882
882
|
"byte_start": 35308,
|
|
883
883
|
"byte_end": 36801,
|
|
884
884
|
"bytes": 1493,
|
|
885
|
-
"h3_count":
|
|
885
|
+
"h3_count": 0
|
|
886
886
|
},
|
|
887
887
|
{
|
|
888
888
|
"name": "Hand-Off / Related Skills",
|
|
@@ -1106,7 +1106,7 @@
|
|
|
1106
1106
|
"byte_start": 26199,
|
|
1107
1107
|
"byte_end": 28181,
|
|
1108
1108
|
"bytes": 1982,
|
|
1109
|
-
"h3_count":
|
|
1109
|
+
"h3_count": 0
|
|
1110
1110
|
},
|
|
1111
1111
|
{
|
|
1112
1112
|
"name": "Defensive Countermeasure Mapping",
|
|
@@ -1227,7 +1227,7 @@
|
|
|
1227
1227
|
"byte_start": 39587,
|
|
1228
1228
|
"byte_end": 41090,
|
|
1229
1229
|
"bytes": 1503,
|
|
1230
|
-
"h3_count":
|
|
1230
|
+
"h3_count": 0
|
|
1231
1231
|
}
|
|
1232
1232
|
]
|
|
1233
1233
|
},
|
|
@@ -1321,7 +1321,7 @@
|
|
|
1321
1321
|
"byte_start": 34986,
|
|
1322
1322
|
"byte_end": 36548,
|
|
1323
1323
|
"bytes": 1562,
|
|
1324
|
-
"h3_count":
|
|
1324
|
+
"h3_count": 0
|
|
1325
1325
|
},
|
|
1326
1326
|
{
|
|
1327
1327
|
"name": "Compliance Theater Check",
|
|
@@ -1469,7 +1469,7 @@
|
|
|
1469
1469
|
"byte_start": 31973,
|
|
1470
1470
|
"byte_end": 33464,
|
|
1471
1471
|
"bytes": 1491,
|
|
1472
|
-
"h3_count":
|
|
1472
|
+
"h3_count": 0
|
|
1473
1473
|
},
|
|
1474
1474
|
{
|
|
1475
1475
|
"name": "Defensive Countermeasure Mapping",
|
|
@@ -1581,7 +1581,7 @@
|
|
|
1581
1581
|
"byte_start": 35586,
|
|
1582
1582
|
"byte_end": 36640,
|
|
1583
1583
|
"bytes": 1054,
|
|
1584
|
-
"h3_count":
|
|
1584
|
+
"h3_count": 0
|
|
1585
1585
|
},
|
|
1586
1586
|
{
|
|
1587
1587
|
"name": "Framework Lag Declaration",
|
|
@@ -1711,7 +1711,7 @@
|
|
|
1711
1711
|
"byte_start": 15155,
|
|
1712
1712
|
"byte_end": 16753,
|
|
1713
1713
|
"bytes": 1598,
|
|
1714
|
-
"h3_count":
|
|
1714
|
+
"h3_count": 0
|
|
1715
1715
|
},
|
|
1716
1716
|
{
|
|
1717
1717
|
"name": "Compliance Theater Check",
|
|
@@ -1926,7 +1926,7 @@
|
|
|
1926
1926
|
"byte_start": 25273,
|
|
1927
1927
|
"byte_end": 29270,
|
|
1928
1928
|
"bytes": 3997,
|
|
1929
|
-
"h3_count":
|
|
1929
|
+
"h3_count": 0
|
|
1930
1930
|
},
|
|
1931
1931
|
{
|
|
1932
1932
|
"name": "Compliance Theater Check",
|
|
@@ -2011,7 +2011,7 @@
|
|
|
2011
2011
|
"byte_start": 21811,
|
|
2012
2012
|
"byte_end": 25605,
|
|
2013
2013
|
"bytes": 3794,
|
|
2014
|
-
"h3_count":
|
|
2014
|
+
"h3_count": 0
|
|
2015
2015
|
},
|
|
2016
2016
|
{
|
|
2017
2017
|
"name": "Compliance Theater Check",
|
|
@@ -2096,7 +2096,7 @@
|
|
|
2096
2096
|
"byte_start": 32852,
|
|
2097
2097
|
"byte_end": 35950,
|
|
2098
2098
|
"bytes": 3098,
|
|
2099
|
-
"h3_count":
|
|
2099
|
+
"h3_count": 0
|
|
2100
2100
|
},
|
|
2101
2101
|
{
|
|
2102
2102
|
"name": "Compliance Theater Check",
|
|
@@ -2181,7 +2181,7 @@
|
|
|
2181
2181
|
"byte_start": 34907,
|
|
2182
2182
|
"byte_end": 37660,
|
|
2183
2183
|
"bytes": 2753,
|
|
2184
|
-
"h3_count":
|
|
2184
|
+
"h3_count": 0
|
|
2185
2185
|
},
|
|
2186
2186
|
{
|
|
2187
2187
|
"name": "Compliance Theater Check",
|
|
@@ -2360,7 +2360,7 @@
|
|
|
2360
2360
|
"byte_start": 21674,
|
|
2361
2361
|
"byte_end": 25090,
|
|
2362
2362
|
"bytes": 3416,
|
|
2363
|
-
"h3_count":
|
|
2363
|
+
"h3_count": 0
|
|
2364
2364
|
},
|
|
2365
2365
|
{
|
|
2366
2366
|
"name": "Compliance Theater Check",
|
|
@@ -2454,7 +2454,7 @@
|
|
|
2454
2454
|
"byte_start": 23877,
|
|
2455
2455
|
"byte_end": 26649,
|
|
2456
2456
|
"bytes": 2772,
|
|
2457
|
-
"h3_count":
|
|
2457
|
+
"h3_count": 0
|
|
2458
2458
|
},
|
|
2459
2459
|
{
|
|
2460
2460
|
"name": "Compliance Theater Check",
|
|
@@ -2660,7 +2660,7 @@
|
|
|
2660
2660
|
"byte_start": 24615,
|
|
2661
2661
|
"byte_end": 26923,
|
|
2662
2662
|
"bytes": 2308,
|
|
2663
|
-
"h3_count":
|
|
2663
|
+
"h3_count": 0
|
|
2664
2664
|
},
|
|
2665
2665
|
{
|
|
2666
2666
|
"name": "Compliance Theater Check",
|
|
@@ -2754,7 +2754,7 @@
|
|
|
2754
2754
|
"byte_start": 20363,
|
|
2755
2755
|
"byte_end": 24124,
|
|
2756
2756
|
"bytes": 3761,
|
|
2757
|
-
"h3_count":
|
|
2757
|
+
"h3_count": 0
|
|
2758
2758
|
},
|
|
2759
2759
|
{
|
|
2760
2760
|
"name": "Compliance Theater Check",
|
|
@@ -2857,7 +2857,7 @@
|
|
|
2857
2857
|
"byte_start": 25454,
|
|
2858
2858
|
"byte_end": 28983,
|
|
2859
2859
|
"bytes": 3529,
|
|
2860
|
-
"h3_count":
|
|
2860
|
+
"h3_count": 0
|
|
2861
2861
|
},
|
|
2862
2862
|
{
|
|
2863
2863
|
"name": "Compliance Theater Check",
|
|
@@ -2951,7 +2951,7 @@
|
|
|
2951
2951
|
"byte_start": 33880,
|
|
2952
2952
|
"byte_end": 37432,
|
|
2953
2953
|
"bytes": 3552,
|
|
2954
|
-
"h3_count":
|
|
2954
|
+
"h3_count": 0
|
|
2955
2955
|
},
|
|
2956
2956
|
{
|
|
2957
2957
|
"name": "Compliance Theater Check",
|
|
@@ -3045,7 +3045,7 @@
|
|
|
3045
3045
|
"byte_start": 35068,
|
|
3046
3046
|
"byte_end": 38475,
|
|
3047
3047
|
"bytes": 3407,
|
|
3048
|
-
"h3_count":
|
|
3048
|
+
"h3_count": 0
|
|
3049
3049
|
},
|
|
3050
3050
|
{
|
|
3051
3051
|
"name": "Compliance Theater Check",
|
|
@@ -3139,7 +3139,7 @@
|
|
|
3139
3139
|
"byte_start": 34560,
|
|
3140
3140
|
"byte_end": 37765,
|
|
3141
3141
|
"bytes": 3205,
|
|
3142
|
-
"h3_count":
|
|
3142
|
+
"h3_count": 0
|
|
3143
3143
|
},
|
|
3144
3144
|
{
|
|
3145
3145
|
"name": "Compliance Theater Check",
|
|
@@ -3233,7 +3233,7 @@
|
|
|
3233
3233
|
"byte_start": 38416,
|
|
3234
3234
|
"byte_end": 41876,
|
|
3235
3235
|
"bytes": 3460,
|
|
3236
|
-
"h3_count":
|
|
3236
|
+
"h3_count": 0
|
|
3237
3237
|
},
|
|
3238
3238
|
{
|
|
3239
3239
|
"name": "Compliance Theater Check",
|
|
@@ -3421,7 +3421,7 @@
|
|
|
3421
3421
|
"byte_start": 28125,
|
|
3422
3422
|
"byte_end": 32523,
|
|
3423
3423
|
"bytes": 4398,
|
|
3424
|
-
"h3_count":
|
|
3424
|
+
"h3_count": 0
|
|
3425
3425
|
},
|
|
3426
3426
|
{
|
|
3427
3427
|
"name": "Compliance Theater Check",
|
|
@@ -3515,7 +3515,7 @@
|
|
|
3515
3515
|
"byte_start": 40269,
|
|
3516
3516
|
"byte_end": 43223,
|
|
3517
3517
|
"bytes": 2954,
|
|
3518
|
-
"h3_count":
|
|
3518
|
+
"h3_count": 0
|
|
3519
3519
|
},
|
|
3520
3520
|
{
|
|
3521
3521
|
"name": "Compliance Theater Check",
|
|
@@ -3609,7 +3609,7 @@
|
|
|
3609
3609
|
"byte_start": 32702,
|
|
3610
3610
|
"byte_end": 36368,
|
|
3611
3611
|
"bytes": 3666,
|
|
3612
|
-
"h3_count":
|
|
3612
|
+
"h3_count": 0
|
|
3613
3613
|
},
|
|
3614
3614
|
{
|
|
3615
3615
|
"name": "Compliance Theater Check",
|
|
@@ -3703,7 +3703,7 @@
|
|
|
3703
3703
|
"byte_start": 33207,
|
|
3704
3704
|
"byte_end": 36629,
|
|
3705
3705
|
"bytes": 3422,
|
|
3706
|
-
"h3_count":
|
|
3706
|
+
"h3_count": 0
|
|
3707
3707
|
},
|
|
3708
3708
|
{
|
|
3709
3709
|
"name": "Compliance Theater Check",
|
|
@@ -4079,7 +4079,7 @@
|
|
|
4079
4079
|
"byte_start": 49575,
|
|
4080
4080
|
"byte_end": 54909,
|
|
4081
4081
|
"bytes": 5334,
|
|
4082
|
-
"h3_count":
|
|
4082
|
+
"h3_count": 0
|
|
4083
4083
|
},
|
|
4084
4084
|
{
|
|
4085
4085
|
"name": "Compliance Theater Check",
|
|
@@ -4173,7 +4173,7 @@
|
|
|
4173
4173
|
"byte_start": 30423,
|
|
4174
4174
|
"byte_end": 32621,
|
|
4175
4175
|
"bytes": 2198,
|
|
4176
|
-
"h3_count":
|
|
4176
|
+
"h3_count": 0
|
|
4177
4177
|
},
|
|
4178
4178
|
{
|
|
4179
4179
|
"name": "Compliance Theater Check",
|
|
@@ -4267,7 +4267,7 @@
|
|
|
4267
4267
|
"byte_start": 30054,
|
|
4268
4268
|
"byte_end": 33237,
|
|
4269
4269
|
"bytes": 3183,
|
|
4270
|
-
"h3_count":
|
|
4270
|
+
"h3_count": 0
|
|
4271
4271
|
},
|
|
4272
4272
|
{
|
|
4273
4273
|
"name": "Compliance Theater Check",
|
package/lib/collectors/ai-api.js
CHANGED
|
@@ -49,6 +49,16 @@ const AI_KEY_PATTERNS = [
|
|
|
49
49
|
{ id: "cohere", re: /(?:^|\n)\s*(?:export\s+|set\s+-gx\s+)?COHERE_API_KEY\s*[= ]\s*['"]?[A-Za-z0-9-]{30,}/m },
|
|
50
50
|
];
|
|
51
51
|
|
|
52
|
+
// Capture the exported value so the false_positive_checks_required entries
|
|
53
|
+
// (placeholder demotion, entropy floor) can be evaluated. The export
|
|
54
|
+
// patterns above end at the prefix; widen to grab the trailing token.
|
|
55
|
+
const AI_KEY_VALUE_RE = {
|
|
56
|
+
openai: /OPENAI_API_KEY\s*[= ]\s*['"]?(sk-[A-Za-z0-9_-]+)/,
|
|
57
|
+
anthropic: /ANTHROPIC_API_KEY\s*[= ]\s*['"]?(sk-ant-[A-Za-z0-9_-]+)/,
|
|
58
|
+
huggingface: /(?:HUGGINGFACE_TOKEN|HF_TOKEN)\s*[= ]\s*['"]?(hf_[A-Za-z0-9]+)/,
|
|
59
|
+
};
|
|
60
|
+
const PLACEHOLDER_RE = /placeholder|example|redacted|dummy|x{4,}|0{6,}|test-/i;
|
|
61
|
+
|
|
52
62
|
function scanShellRc(content) {
|
|
53
63
|
if (!content) return [];
|
|
54
64
|
const hits = [];
|
|
@@ -58,6 +68,31 @@ function scanShellRc(content) {
|
|
|
58
68
|
return hits;
|
|
59
69
|
}
|
|
60
70
|
|
|
71
|
+
// Deterministic false_positive_checks_required evaluation for
|
|
72
|
+
// cleartext-api-key-in-dotfile. Returns the satisfiable indices for the
|
|
73
|
+
// exports found across the canonical dotfiles (intersection — an index is
|
|
74
|
+
// only attested if every export satisfies it). Canonical home rc / dotfile
|
|
75
|
+
// paths are never under examples/tests/fixtures, so the path check [1] is
|
|
76
|
+
// always satisfied here.
|
|
77
|
+
function cleartextFpIndices(content) {
|
|
78
|
+
const sat = new Set(["0", "1", "2"]);
|
|
79
|
+
let sawAny = false;
|
|
80
|
+
for (const [vendor, re] of Object.entries(AI_KEY_VALUE_RE)) {
|
|
81
|
+
const m = content.match(re);
|
|
82
|
+
if (!m) continue;
|
|
83
|
+
sawAny = true;
|
|
84
|
+
const value = m[1];
|
|
85
|
+
// [0] not a documented placeholder / sk-test- fixture
|
|
86
|
+
if (PLACEHOLDER_RE.test(value)) sat.delete("0");
|
|
87
|
+
// [2] entropy floor: OpenAI sk-* >= 48 post-prefix, Anthropic sk-ant-* >= 40,
|
|
88
|
+
// HuggingFace hf_* >= 30.
|
|
89
|
+
const floor = vendor === "openai" ? 48 : vendor === "anthropic" ? 40 : 30;
|
|
90
|
+
const body = value.replace(/^sk-ant-(?:api03|admin01)-|^sk-(?:proj-|svcacct-|admin-)?|^hf_/, "");
|
|
91
|
+
if (body.length < floor) sat.delete("2");
|
|
92
|
+
}
|
|
93
|
+
return sawAny ? sat : new Set();
|
|
94
|
+
}
|
|
95
|
+
|
|
61
96
|
function parseAwsCredentials(content) {
|
|
62
97
|
if (!content) return { staticProfiles: [] };
|
|
63
98
|
const lines = content.split(/\r?\n/);
|
|
@@ -74,30 +109,43 @@ function parseAwsCredentials(content) {
|
|
|
74
109
|
profiles[current][kv[1].trim().toLowerCase()] = kv[2].trim();
|
|
75
110
|
}
|
|
76
111
|
const staticProfiles = [];
|
|
112
|
+
const accessKeyIds = [];
|
|
77
113
|
for (const [name, kv] of Object.entries(profiles)) {
|
|
78
114
|
// long-lived-aws-keys: aws_access_key_id present AND no
|
|
79
115
|
// aws_session_token sibling (STS temporary creds carry the
|
|
80
116
|
// session token; IAM-user long-lived keys do not).
|
|
81
117
|
if (kv["aws_access_key_id"] && !kv["aws_session_token"]) {
|
|
82
118
|
staticProfiles.push(name);
|
|
119
|
+
accessKeyIds.push(kv["aws_access_key_id"]);
|
|
83
120
|
}
|
|
84
121
|
}
|
|
85
|
-
return { staticProfiles };
|
|
122
|
+
return { staticProfiles, accessKeyIds };
|
|
86
123
|
}
|
|
87
124
|
|
|
125
|
+
// AWS-published sample credential pair — long-lived-aws-keys FP[0] demotes it.
|
|
126
|
+
const AWS_EXAMPLE_KEY_PARTS = new Set([
|
|
127
|
+
"AKIAIOSFODNN7EXAMPLE",
|
|
128
|
+
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
129
|
+
]);
|
|
130
|
+
|
|
88
131
|
function parseGcloudAdc(content) {
|
|
89
132
|
if (!content) return { hasServiceAccount: false };
|
|
90
133
|
try {
|
|
91
134
|
const j = JSON.parse(content);
|
|
92
|
-
|
|
135
|
+
const hasServiceAccount = j?.type === "service_account";
|
|
136
|
+
return {
|
|
137
|
+
hasServiceAccount,
|
|
138
|
+
privateKey: typeof j?.private_key === "string" ? j.private_key : "",
|
|
139
|
+
clientEmail: typeof j?.client_email === "string" ? j.client_email : "",
|
|
140
|
+
};
|
|
93
141
|
} catch { return { hasServiceAccount: false }; }
|
|
94
142
|
}
|
|
95
143
|
|
|
96
144
|
function parseKubeStaticToken(content) {
|
|
97
|
-
if (!content) return false;
|
|
145
|
+
if (!content) return { found: false };
|
|
98
146
|
// Same shape as cred-stores: token under user:, not auth-provider.
|
|
99
147
|
const userKvRe = /^(\s+)(token|token-data)\s*:\s*(\S[^\n]*)/gm;
|
|
100
|
-
let
|
|
148
|
+
let tokenValue = null;
|
|
101
149
|
for (const m of content.matchAll(userKvRe)) {
|
|
102
150
|
const upto = content.slice(0, m.index);
|
|
103
151
|
const lastUserAt = upto.lastIndexOf("\n user:");
|
|
@@ -105,12 +153,17 @@ function parseKubeStaticToken(content) {
|
|
|
105
153
|
if (lastAuthProviderAt > lastUserAt) continue;
|
|
106
154
|
const value = m[3];
|
|
107
155
|
if (!value || value.startsWith("null")) continue;
|
|
108
|
-
|
|
156
|
+
tokenValue = value.trim();
|
|
109
157
|
break;
|
|
110
158
|
}
|
|
111
|
-
|
|
159
|
+
// Cluster server URL — FP[0] demotes local-only clusters.
|
|
160
|
+
const serverM = content.match(/^\s*server:\s*(\S+)/m);
|
|
161
|
+
return { found: tokenValue !== null, tokenValue, serverUrl: serverM ? serverM[1] : "" };
|
|
112
162
|
}
|
|
113
163
|
|
|
164
|
+
const LOCAL_CLUSTER_RE = /https?:\/\/(?:127\.0\.0\.1|localhost|\[::1\])[:/]|\.kind\b|minikube|k3d|docker-for-desktop|docker-desktop/i;
|
|
165
|
+
const CI_RUNNER_PATH_RE = /(?:^|[\\/])(?:home[\\/]runner|github[\\/]workspace|builds|workspace)[\\/]/i;
|
|
166
|
+
|
|
114
167
|
function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
115
168
|
const errors = [];
|
|
116
169
|
const startTime = Date.now();
|
|
@@ -140,6 +193,7 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
140
193
|
|
|
141
194
|
const allKeyCarriers = [...shellRcs, ...dotfileKeys];
|
|
142
195
|
const cleartextHitsByFile = {};
|
|
196
|
+
let cleartextFp = null;
|
|
143
197
|
for (const p of allKeyCarriers) {
|
|
144
198
|
if (!fileExists(p)) continue;
|
|
145
199
|
const c = readSafe(p);
|
|
@@ -147,6 +201,11 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
147
201
|
const hits = scanShellRc(c);
|
|
148
202
|
if (hits.length > 0) {
|
|
149
203
|
cleartextHitsByFile[path.relative(home, p)] = hits;
|
|
204
|
+
const fp = cleartextFpIndices(c);
|
|
205
|
+
if (fp.size) {
|
|
206
|
+
if (cleartextFp === null) cleartextFp = new Set(fp);
|
|
207
|
+
else for (const idx of [...cleartextFp]) if (!fp.has(idx)) cleartextFp.delete(idx);
|
|
208
|
+
}
|
|
150
209
|
}
|
|
151
210
|
}
|
|
152
211
|
const cleartextAnyHit = Object.keys(cleartextHitsByFile).length > 0;
|
|
@@ -163,7 +222,8 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
163
222
|
|
|
164
223
|
const kubeCfgPath = (env && env.KUBECONFIG) || path.join(home, ".kube", "config");
|
|
165
224
|
const kubeContent = fileExists(kubeCfgPath) ? readSafe(kubeCfgPath) : null;
|
|
166
|
-
const
|
|
225
|
+
const kubeParsed = parseKubeStaticToken(kubeContent);
|
|
226
|
+
const kubeStaticToken = kubeParsed.found;
|
|
167
227
|
|
|
168
228
|
const signal_overrides = {
|
|
169
229
|
"cleartext-api-key-in-dotfile": cleartextAnyHit ? "hit" : "miss",
|
|
@@ -172,6 +232,51 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
172
232
|
"kubeconfig-with-static-token": kubeStaticToken ? "hit" : "miss",
|
|
173
233
|
};
|
|
174
234
|
|
|
235
|
+
// Per-indicator __fp_checks attestation. Each canonical-path credential
|
|
236
|
+
// store the collector reads is never under an examples/tests/fixtures path,
|
|
237
|
+
// so the path-based FP checks are satisfied; value-based checks (placeholder,
|
|
238
|
+
// entropy, sample-credential, cluster-locality) are evaluated deterministically.
|
|
239
|
+
// Network / sts-validity checks are left unattested so the runner still
|
|
240
|
+
// downgrades those. Without this, a real cleartext key or static token
|
|
241
|
+
// surfaced by `collect` is downgraded to inconclusive after `run`.
|
|
242
|
+
if (cleartextAnyHit && cleartextFp && cleartextFp.size) {
|
|
243
|
+
const att = {};
|
|
244
|
+
for (const idx of cleartextFp) att[idx] = true;
|
|
245
|
+
signal_overrides["cleartext-api-key-in-dotfile__fp_checks"] = att;
|
|
246
|
+
}
|
|
247
|
+
if (longLivedAws) {
|
|
248
|
+
const att = {};
|
|
249
|
+
// [0] none of the access-key ids are the AWS-published sample pair
|
|
250
|
+
if (!(awsParsed.accessKeyIds || []).some((k) => AWS_EXAMPLE_KEY_PARTS.has(k))) att["0"] = true;
|
|
251
|
+
// [1] ~/.aws/credentials is a canonical home path, not an examples/test path
|
|
252
|
+
att["1"] = true;
|
|
253
|
+
// [2] sts get-caller-identity needs network — left unattested.
|
|
254
|
+
if (Object.keys(att).length) signal_overrides["long-lived-aws-keys__fp_checks"] = att;
|
|
255
|
+
}
|
|
256
|
+
if (gcloudParsed.hasServiceAccount) {
|
|
257
|
+
const att = {};
|
|
258
|
+
// [0] private_key is a real PEM body (>= 1000 chars), not PLACEHOLDER/REDACTED
|
|
259
|
+
const pk = gcloudParsed.privateKey || "";
|
|
260
|
+
if (pk.length >= 1000 && !/PLACEHOLDER|REDACTED/i.test(pk)) att["0"] = true;
|
|
261
|
+
// [1] client_email is a real *@*.gserviceaccount.com (not example/test)
|
|
262
|
+
const ce = gcloudParsed.clientEmail || "";
|
|
263
|
+
if (/@[^@\s]+\.gserviceaccount\.com$/i.test(ce) && !/@example\.com$|@test\./i.test(ce)) att["1"] = true;
|
|
264
|
+
// [2] canonical ADC path (not under examples/) AND no GOOGLE_APPLICATION_CREDENTIALS
|
|
265
|
+
// redirecting away from it
|
|
266
|
+
if (!(env && env.GOOGLE_APPLICATION_CREDENTIALS)) att["2"] = true;
|
|
267
|
+
if (Object.keys(att).length) signal_overrides["gcp-service-account-json__fp_checks"] = att;
|
|
268
|
+
}
|
|
269
|
+
if (kubeStaticToken) {
|
|
270
|
+
const att = {};
|
|
271
|
+
// [0] cluster server URL is not a local-only dev cluster
|
|
272
|
+
if (kubeParsed.serverUrl && !LOCAL_CLUSTER_RE.test(kubeParsed.serverUrl)) att["0"] = true;
|
|
273
|
+
// [1] token is not a short kind/minikube bootstrap-token shape
|
|
274
|
+
if (kubeParsed.tokenValue && kubeParsed.tokenValue.length >= 40 && !/^[a-z0-9]{6}\.[a-z0-9]{16}$/.test(kubeParsed.tokenValue)) att["1"] = true;
|
|
275
|
+
// [2] kubeconfig is not inside a CI runner workspace
|
|
276
|
+
if (!CI_RUNNER_PATH_RE.test(kubeCfgPath)) att["2"] = true;
|
|
277
|
+
if (Object.keys(att).length) signal_overrides["kubeconfig-with-static-token__fp_checks"] = att;
|
|
278
|
+
}
|
|
279
|
+
|
|
175
280
|
const artifacts = {
|
|
176
281
|
"shell-rc-files": {
|
|
177
282
|
value: cleartextAnyHit
|
|
@@ -421,6 +421,33 @@ function collect({ cwd = process.cwd() } = {}) {
|
|
|
421
421
|
signal_overrides["cve-citation-needs-external-verification"] = "inconclusive";
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
// __fp_checks attestation for the FP-gated indicators the collector decides
|
|
425
|
+
// deterministically. Each hit already excludes illustrative (template /
|
|
426
|
+
// fixture / doc-snippet) paths and is keyed off the shipped catalogs, so the
|
|
427
|
+
// path / catalog-cross-reference / same-citation checks the collector ran
|
|
428
|
+
// are attested; surrounding-text-acknowledgement remains operator judgement.
|
|
429
|
+
// Without this the runner downgrades a real bad citation to inconclusive.
|
|
430
|
+
if (signal_overrides["fabricated-cve-id"] === "hit") {
|
|
431
|
+
// [0] not under a fixture / regex-example / doc-snippet path (illustrative
|
|
432
|
+
// paths are excluded before the hit). [1] placeholder forms (CVE-TBD /
|
|
433
|
+
// pending) never match the numeric citation regex, so a fired hit is
|
|
434
|
+
// not a placeholder.
|
|
435
|
+
signal_overrides["fabricated-cve-id__fp_checks"] = { "0": true, "1": true };
|
|
436
|
+
}
|
|
437
|
+
if (signal_overrides["rejected-or-disputed-cve"] === "hit") {
|
|
438
|
+
// [1] the catalog note marks THIS exact identifier rejected/disputed.
|
|
439
|
+
// [2] the identifier is present in the catalog (absence does not fire).
|
|
440
|
+
// [0] inline dispute-acknowledgement in surrounding prose is operator
|
|
441
|
+
// judgement — left unattested.
|
|
442
|
+
signal_overrides["rejected-or-disputed-cve__fp_checks"] = { "1": true, "2": true };
|
|
443
|
+
}
|
|
444
|
+
if (signal_overrides["rfc-number-title-mismatch"] === "hit") {
|
|
445
|
+
// [0] a paraphrase / nickname (no title claim) does not fire. [1] numbers
|
|
446
|
+
// absent from the shipped RFC index do not fire. [2] the stated title is
|
|
447
|
+
// extracted from the SAME citation line.
|
|
448
|
+
signal_overrides["rfc-number-title-mismatch__fp_checks"] = { "0": true, "1": true, "2": true };
|
|
449
|
+
}
|
|
450
|
+
|
|
424
451
|
const summarize = (list) => {
|
|
425
452
|
if (list.length === 0) return "0 hits";
|
|
426
453
|
const head = list.slice(0, 5).map((h) => {
|
|
@@ -405,6 +405,31 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
405
405
|
if (noMlKemImpl !== undefined) signal_overrides["no-ml-kem-implementation"] = noMlKemImpl;
|
|
406
406
|
if (fipsTheater !== undefined) signal_overrides["fips-claim-without-runtime-activation"] = fipsTheater;
|
|
407
407
|
|
|
408
|
+
// Per-indicator __fp_checks attestation for the FP-gated call-site
|
|
409
|
+
// indicators. Every surviving hit is in a non-test source file (isTest is
|
|
410
|
+
// excluded before the scan) and, for weak-hash, flows into a security sink
|
|
411
|
+
// (scanWeakHash). The remaining false_positive_checks_required entries are
|
|
412
|
+
// legacy-protocol-shim / feature-flag / later-override judgements the
|
|
413
|
+
// collector does not make, so they stay unattested and the runner keeps
|
|
414
|
+
// those indicators inconclusive. Without attesting what it DID check, the
|
|
415
|
+
// collector's real call-site hits are downgraded to inconclusive after run.
|
|
416
|
+
if (signal_overrides["weak-hash-import"] === "hit") {
|
|
417
|
+
// [0] not under test + non-security-file demotion; [2] hash flows to an
|
|
418
|
+
// authn/integrity sink. [1] legacy-protocol shim is operator judgement.
|
|
419
|
+
signal_overrides["weak-hash-import__fp_checks"] = { "0": true, "2": true };
|
|
420
|
+
}
|
|
421
|
+
if (signal_overrides["weak-cipher-mode"] === "hit") {
|
|
422
|
+
// [0] not under test/KAT-vector path; [2] construction is in a scanned
|
|
423
|
+
// (production) source file. [1] legacy-protocol-parser scope is operator.
|
|
424
|
+
signal_overrides["weak-cipher-mode__fp_checks"] = { "0": true, "2": true };
|
|
425
|
+
}
|
|
426
|
+
if (signal_overrides["tls-old-protocol"] === "hit") {
|
|
427
|
+
// [0] the hit is in non-test production code, not a test asserting the
|
|
428
|
+
// library REJECTS the legacy protocol. [1] feature-flag-default-off and
|
|
429
|
+
// [2] later-override are not inspected by the collector.
|
|
430
|
+
signal_overrides["tls-old-protocol__fp_checks"] = { "0": true };
|
|
431
|
+
}
|
|
432
|
+
|
|
408
433
|
const summarize = (id) => {
|
|
409
434
|
const list = hits[id];
|
|
410
435
|
if (list.length === 0) return "0 hits";
|
package/lib/collectors/kernel.js
CHANGED
|
@@ -109,6 +109,18 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// Running-kernel build config (/boot/config-$(uname -r)) backs the
|
|
113
|
+
// CONFIG_* false_positive_checks_required entries: a sysctl is moot if the
|
|
114
|
+
// feature is compiled out of the kernel. Best-effort — unreadable on many
|
|
115
|
+
// hardened hosts.
|
|
116
|
+
let kernelConfig = null;
|
|
117
|
+
if (linuxPlatform && unameR.ok) {
|
|
118
|
+
try {
|
|
119
|
+
const fs = require("node:fs");
|
|
120
|
+
kernelConfig = fs.readFileSync(`/boot/config-${unameR.value}`, "utf8");
|
|
121
|
+
} catch { /* config not readable — CONFIG_* checks stay unattested */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
112
124
|
// Signal overrides: we can't decide kver-in-affected-range without
|
|
113
125
|
// the CVE-affected-version catalog (the runner does that
|
|
114
126
|
// correlation). But we CAN flip the deterministic indicators that
|
|
@@ -127,13 +139,31 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
127
139
|
// unpriv-userns-enabled: clone == 1 means enabled (risky).
|
|
128
140
|
if (parsed.unprivileged_userns_clone != null) {
|
|
129
141
|
const v = parseInt(parsed.unprivileged_userns_clone, 10);
|
|
130
|
-
|
|
142
|
+
const hit = v === 1;
|
|
143
|
+
signal_overrides["unpriv-userns-enabled"] = hit ? "hit" : "miss";
|
|
144
|
+
// FP[1]: CONFIG_USER_NS=y — the sysctl is live only when userns is
|
|
145
|
+
// compiled in. Attested when /boot/config confirms it. FP[0]
|
|
146
|
+
// (rootless-runtime exception + LSM enforcement) is operator
|
|
147
|
+
// judgement and stays unattested.
|
|
148
|
+
if (hit && kernelConfig && /^CONFIG_USER_NS=y$/m.test(kernelConfig)) {
|
|
149
|
+
signal_overrides["unpriv-userns-enabled__fp_checks"] = { "1": true };
|
|
150
|
+
}
|
|
131
151
|
}
|
|
132
152
|
// unpriv-bpf-allowed: bpf_disabled == 0 means unprivileged BPF
|
|
133
153
|
// is allowed (risky).
|
|
134
154
|
if (parsed.unprivileged_bpf_disabled != null) {
|
|
135
155
|
const v = parseInt(parsed.unprivileged_bpf_disabled, 10);
|
|
136
|
-
|
|
156
|
+
const hit = v === 0;
|
|
157
|
+
signal_overrides["unpriv-bpf-allowed"] = hit ? "hit" : "miss";
|
|
158
|
+
// FP[0]: CONFIG_BPF_SYSCALL=y AND CONFIG_BPF_JIT=y — the sysctl is
|
|
159
|
+
// moot if BPF is compiled out. Attested when /boot/config confirms
|
|
160
|
+
// both. FP[1] (enforcing LSM bpf() restriction) is operator
|
|
161
|
+
// judgement and stays unattested.
|
|
162
|
+
if (hit && kernelConfig &&
|
|
163
|
+
/^CONFIG_BPF_SYSCALL=y$/m.test(kernelConfig) &&
|
|
164
|
+
/^CONFIG_BPF_JIT=y$/m.test(kernelConfig)) {
|
|
165
|
+
signal_overrides["unpriv-bpf-allowed__fp_checks"] = { "0": true };
|
|
166
|
+
}
|
|
137
167
|
}
|
|
138
168
|
}
|
|
139
169
|
}
|
|
@@ -496,6 +496,36 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
496
496
|
signal_overrides["lockfile-missing-integrity"] = lockfileMissingIntegrity;
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
+
// __fp_checks attestation for publish-workflow-action-refs-mutable. Both
|
|
500
|
+
// false_positive_checks_required entries are deterministic from the repo:
|
|
501
|
+
// [0] Dependabot configured for github-actions on a weekly+ schedule
|
|
502
|
+
// demotes the finding — attest survival when no such config exists.
|
|
503
|
+
// [1] every mutable ref pointing to a github-owned action is lower risk —
|
|
504
|
+
// attest survival when at least one mutable ref is third-party.
|
|
505
|
+
if (signal_overrides["publish-workflow-action-refs-mutable"] === "hit") {
|
|
506
|
+
let dependabotActions = false;
|
|
507
|
+
try {
|
|
508
|
+
const dbContent =
|
|
509
|
+
readSafe(path.join(root, ".github", "dependabot.yml")) ||
|
|
510
|
+
readSafe(path.join(root, ".github", "dependabot.yaml")) || "";
|
|
511
|
+
dependabotActions = /package-ecosystem:\s*['"]?github-actions/i.test(dbContent) &&
|
|
512
|
+
/\binterval:\s*['"]?(?:daily|weekly)/i.test(dbContent);
|
|
513
|
+
} catch { /* no dependabot config */ }
|
|
514
|
+
const mutableRefSnippets = (workflowHits["publish-workflow-action-refs-mutable"] || []).map(h => h.snippet || "");
|
|
515
|
+
const refOf = (s) => {
|
|
516
|
+
const m = s.match(/uses:\s*['"]?([^'"\s]+)/);
|
|
517
|
+
return m ? m[1] : "";
|
|
518
|
+
};
|
|
519
|
+
const anyThirdParty = mutableRefSnippets.some(s => {
|
|
520
|
+
const r = refOf(s);
|
|
521
|
+
return r && !/^(?:actions|github)\//i.test(r);
|
|
522
|
+
});
|
|
523
|
+
const att = {};
|
|
524
|
+
if (!dependabotActions) att["0"] = true;
|
|
525
|
+
if (anyThirdParty) att["1"] = true;
|
|
526
|
+
if (Object.keys(att).length) signal_overrides["publish-workflow-action-refs-mutable__fp_checks"] = att;
|
|
527
|
+
}
|
|
528
|
+
|
|
499
529
|
// Per-indicator file locations for the publish-workflow indicators
|
|
500
530
|
// flipped to "hit", so a SARIF result points at the workflow file (and,
|
|
501
531
|
// for mutable action refs, the offending `uses:` line). The other
|