@blamejs/exceptd-skills 0.16.23 → 0.16.25

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.
@@ -89,7 +89,7 @@
89
89
  "byte_start": 21020,
90
90
  "byte_end": 23408,
91
91
  "bytes": 2388,
92
- "h3_count": 7
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": 8
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": 7
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": 9
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": 6
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": 4
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": 4
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": 5
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": 5
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": 4
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": 6
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": 7
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": 6
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": 4
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": 3
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": 9
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": 10
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": 9
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": 9
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": 9
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": 11
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": 9
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": 6
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": 10
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": 14
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": 15
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": 10
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": 13
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": 9
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": 13
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": 12
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": 10
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": 14
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": 15
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": 16
4270
+ "h3_count": 0
4271
4271
  },
4272
4272
  {
4273
4273
  "name": "Compliance Theater Check",
@@ -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
- return { hasServiceAccount: j?.type === "service_account" };
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 staticFound = false;
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
- staticFound = true;
156
+ tokenValue = value.trim();
109
157
  break;
110
158
  }
111
- return staticFound;
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 kubeStaticToken = parseKubeStaticToken(kubeContent);
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";
@@ -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
- signal_overrides["unpriv-userns-enabled"] = (v === 1) ? "hit" : "miss";
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
- signal_overrides["unpriv-bpf-allowed"] = (v === 0) ? "hit" : "miss";
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