@clear-capabilities/agentic-security-scanner 0.140.0 → 0.142.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +283 -0
  2. package/dist/113.index.js +79 -3
  3. package/dist/178.index.js +1 -1
  4. package/dist/238.index.js +77 -1
  5. package/dist/384.index.js +1 -1
  6. package/dist/435.index.js +12 -0
  7. package/dist/526.index.js +79 -3
  8. package/dist/637.index.js +1 -1
  9. package/dist/agentic-security.mjs +14 -14
  10. package/dist/agentic-security.mjs.sha256 +1 -1
  11. package/dist/compliance-frameworks/ccpa.json +34 -7
  12. package/dist/compliance-frameworks/eu-ai-act.json +65 -14
  13. package/dist/compliance-frameworks/gdpr.json +56 -12
  14. package/dist/compliance-frameworks/hipaa-security-rule.json +68 -15
  15. package/dist/compliance-frameworks/nist-ai-600-1.json +57 -12
  16. package/dist/compliance-frameworks/nist-csf-2.json +78 -16
  17. package/dist/compliance-frameworks/nist-privacy-1-1.json +3 -0
  18. package/dist/compliance-frameworks/owasp-asvs-5.json +91 -20
  19. package/dist/compliance-frameworks/owasp-llm-top-10.json +89 -20
  20. package/package.json +19 -5
  21. package/src/dataflow/CLAUDE.md +9 -0
  22. package/src/dataflow/catalog.js +61 -0
  23. package/src/dataflow/engine.js +95 -0
  24. package/src/dataflow/sanitizer-gate.js +61 -0
  25. package/src/engine.js +353 -31
  26. package/src/mcp/tools.js +12 -0
  27. package/src/posture/accuracy-scorecard.js +57 -0
  28. package/src/posture/aibom.js +110 -1
  29. package/src/posture/auditor-walkthrough.js +56 -17
  30. package/src/posture/compliance-frameworks/ccpa.json +34 -7
  31. package/src/posture/compliance-frameworks/eu-ai-act.json +65 -14
  32. package/src/posture/compliance-frameworks/gdpr.json +56 -12
  33. package/src/posture/compliance-frameworks/hipaa-security-rule.json +68 -15
  34. package/src/posture/compliance-frameworks/nist-ai-600-1.json +57 -12
  35. package/src/posture/compliance-frameworks/nist-csf-2.json +78 -16
  36. package/src/posture/compliance-frameworks/nist-privacy-1-1.json +3 -0
  37. package/src/posture/compliance-frameworks/owasp-asvs-5.json +91 -20
  38. package/src/posture/compliance-frameworks/owasp-llm-top-10.json +89 -20
  39. package/src/posture/concurrency-checker.js +3 -3
  40. package/src/posture/coverage-strength.js +182 -0
  41. package/src/posture/epss.js +17 -1
  42. package/src/posture/family-registry.js +103 -0
  43. package/src/posture/family-resolve.js +47 -0
  44. package/src/posture/fix-coverage.js +113 -0
  45. package/src/posture/fix-metrics.js +76 -0
  46. package/src/posture/mcp-rug-pull.js +144 -0
  47. package/src/posture/poc-inprocess.js +217 -1
  48. package/src/posture/proof-coverage.js +162 -0
  49. package/src/posture/reachability-filter.js +44 -0
  50. package/src/posture/sbom.js +12 -3
  51. package/src/runScan.js +56 -5
  52. package/src/sast/CLAUDE.md +2 -2
  53. package/src/sast/claude-md-prompt-injection.js +47 -3
  54. package/src/sast/cloud-iam.js +23 -0
  55. package/src/sast/convention-deviation.js +66 -3
  56. package/src/sast/crypto-protocol.js +23 -0
  57. package/src/sast/dapp-frontend.js +20 -0
  58. package/src/sast/iac-cloud-templates.js +346 -0
  59. package/src/sast/k8s-admission.js +27 -0
  60. package/src/sast/ml-supply-chain.js +22 -0
  61. package/src/sast/ruby.js +132 -0
  62. package/src/sast/web3-advanced.js +26 -0
  63. package/src/sca/CLAUDE.md +21 -4
  64. package/src/sca/container.js +18 -1
  65. package/src/sca/dep-confusion.js +69 -3
package/src/mcp/tools.js CHANGED
@@ -330,6 +330,18 @@ function _maybeOffload(sessionRoot, toolName, items) {
330
330
  }
331
331
 
332
332
  // ─── scan_diff ───────────────────────────────────────────────────────────────
333
+ // Test seam for the write boundary (PRD F6.4).
334
+ //
335
+ // `_confine` and `isReservedWrite` ARE the confinement contract in
336
+ // agents/_CONFINEMENT.md. A boundary is only worth what its refusals are worth,
337
+ // and refusals cannot be adversarially tested through the public tools without
338
+ // also exercising a real scan, a real patch and a real filesystem write — so
339
+ // the check would be measuring four things and attributing failure to one.
340
+ //
341
+ // Exported under the `_internals` convention this codebase already uses
342
+ // (see posture/poc-inprocess.js). Not part of the MCP tool surface.
343
+ export const _internals = { _confine, isReservedWrite: _isReservedWritePath };
344
+
333
345
  export const scan_diff = {
334
346
  name: 'scan_diff',
335
347
  description: 'Scan a list of files for security findings. Use BEFORE writing a Write/Edit to disk so the agent can self-correct. Returns findings with severity, file:line, title, remediation. Snippets are redacted of obvious secret patterns. Paths confined to the session root; symlinks are refused.',
@@ -428,6 +428,63 @@ export function renderScorecardMarkdown(m) {
428
428
  }
429
429
  L.push('Per-file counts are in `docs/scorecard.json`.');
430
430
  L.push('');
431
+ // PRD F12.6 — the honest scorecard publishes the LIMITS too, not only the
432
+ // rates. Three claims this project makes are only meaningful with their
433
+ // caveat attached, and each caveat was invisible before this section:
434
+ // proof coverage — "provable" means provable by a JAVASCRIPT-ONLY harness
435
+ // calibration — the confidence surface is currently UNVERIFIED
436
+ // compliance — most controls are backed by weak or unmeasured detectors
437
+ // Publishing a rate without its ceiling is how a number comes to mean
438
+ // whatever the reader assumes.
439
+ if (m.limits) {
440
+ L.push('## Stated limits — what these capabilities cannot do');
441
+ L.push('');
442
+ if (m.limits.proof) {
443
+ const pc = m.limits.proof;
444
+ L.push('### Execution proof coverage, with its ceiling');
445
+ L.push('');
446
+ L.push('| Bucket | Share of all findings |');
447
+ L.push('| --- | --- |');
448
+ L.push(`| Provable (a proof class exists) | ${pc.provable.n}/${pc.total} |`);
449
+ L.push(`| Declined on purpose, reason stated | ${pc.indeterminate.n}/${pc.total} |`);
450
+ L.push(`| No proof class yet (backlog) | ${pc.unclassified.n}/${pc.total} |`);
451
+ L.push(`| **Out of harness scope (not JavaScript)** | **${pc.outOfScope.n}/${pc.total}** |`);
452
+ L.push('');
453
+ L.push(`**The ceiling.** The in-process proof harness only loads JavaScript. Of ${pc.total} findings`);
454
+ L.push(`measured on the CVE corpus, ${pc.reachable.n} are reachable by it at all. Proof coverage is`);
455
+ L.push(`**${pc.provable.n}/${pc.total}** of ALL findings and **${pc.provable.n}/${pc.reachable.n}** of the reachable ones.`);
456
+ L.push('Both are given because they differ by a lot, and a reader shown only one will');
457
+ L.push('draw the wrong conclusion in whichever direction that one flatters.');
458
+ L.push('');
459
+ L.push('Adding more proof classes does not move the ceiling. A Python or Java finding is');
460
+ L.push('not backlog — it is unreachable by this harness at any effort.');
461
+ L.push('');
462
+ }
463
+ if (m.limits.calibration) {
464
+ L.push('### Confidence calibration');
465
+ L.push('');
466
+ L.push(`**${m.limits.calibration}**`);
467
+ L.push('');
468
+ L.push('Every finding carries a confidence number. That number is a claim about how');
469
+ L.push('often the engine is right, and it is only worth what the evidence behind it is');
470
+ L.push('worth. `calibration-seed.json` is FITTING data, so measuring against it would');
471
+ L.push('reproduce the error it was fitted to. The release gate fails on this by default;');
472
+ L.push('a dated waiver is what currently allows a release, and it expires.');
473
+ L.push('');
474
+ }
475
+ if (m.limits.compliance) {
476
+ const c = m.limits.compliance;
477
+ L.push('### Compliance control strength');
478
+ L.push('');
479
+ L.push(`Of ${c.total} bundled controls, **${c.partiallyEvidenced} are backed by a detector that is weak or was never`);
480
+ L.push('measured against an independent corpus**, and are flagged `partiallyEvidenced`.');
481
+ L.push(`A further **${c.notCodeTestable} are organisational or artifact-existence only** and can never`);
482
+ L.push('read as evidenced by a scanner. A control appearing in a coverage map is not a');
483
+ L.push('statement that the control is satisfied.');
484
+ L.push('');
485
+ }
486
+ }
487
+
431
488
  const ind = m.committedInputs.independent;
432
489
  if (ind && ind.overall) {
433
490
  L.push('## Independent evaluation population — the number that matters');
@@ -201,7 +201,19 @@ export function buildAIBOM(scan, fileContents = {}, meta = {}) {
201
201
  return {
202
202
  aibomFormat: 'agentic-security AI-BOM',
203
203
  version: '1',
204
- cyclonedxCompatible: '1.7-ml-bom',
204
+ // PRD F5.5 — this document is PROPRIETARY and now says so.
205
+ //
206
+ // It previously carried `cyclonedxCompatible: '1.7-ml-bom'`, which was an
207
+ // unverified claim: nothing validated it, and the document has no
208
+ // `bomFormat`, no `specVersion` and no CycloneDX `components` array, so it
209
+ // would not have parsed as CycloneDX at all. A consumer reading that field
210
+ // and feeding this to a CycloneDX tool would have got an error, not a BOM.
211
+ //
212
+ // The honest options F5.5 gives are "validate mechanically, or be labelled
213
+ // proprietary". Both are taken: the field below states the truth, and
214
+ // `toCycloneDXMLBOM()` emits a REAL ML-BOM that `validateMLBOM()` checks.
215
+ proprietary: true,
216
+ cyclonedxMlBom: 'not this document — call toCycloneDXMLBOM() for a CycloneDX 1.6 ML-BOM view',
205
217
  generatedAt: meta.startedAt || new Date().toISOString(),
206
218
  models,
207
219
  promptTemplates,
@@ -286,3 +298,100 @@ export function aibomToMarkdown(aibom) {
286
298
 
287
299
  return out.join('\n');
288
300
  }
301
+
302
+
303
+ // ── CycloneDX ML-BOM (PRD F5.5) ────────────────────────────────────────────
304
+ //
305
+ // A real CycloneDX 1.6 document describing the models this scan found, using the
306
+ // ML-BOM shape: `components[].type: 'machine-learning-model'` carrying a
307
+ // `modelCard`. Emitted separately from the proprietary AI-BOM rather than
308
+ // pretending the proprietary one already conforms.
309
+ //
310
+ // The serial number derives from content under --deterministic, for the same
311
+ // reason toCycloneDX's does: an attestation over a BOM is meaningless if the BOM
312
+ // changes on every run.
313
+ import * as _crypto from 'node:crypto';
314
+ import { isDeterministic as _isDet } from './deterministic.js';
315
+
316
+ function _mlbomSerial(seed) {
317
+ if (!_isDet()) return `urn:uuid:${_crypto.randomUUID()}`;
318
+ const h = _crypto.createHash('sha256').update(String(seed)).digest('hex');
319
+ return `urn:uuid:${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${((parseInt(h[16], 16) & 0x3) | 0x8).toString(16)}${h.slice(17, 20)}-${h.slice(20, 32)}`;
320
+ }
321
+
322
+ export function toCycloneDXMLBOM(aibom, meta = {}) {
323
+ const models = (aibom && aibom.models) || [];
324
+ const components = models.map((m) => ({
325
+ type: 'machine-learning-model',
326
+ 'bom-ref': `model:${m.provider || 'unknown'}/${m.name || 'unknown'}${m.version ? `@${m.version}` : ''}`,
327
+ name: m.name || 'unknown',
328
+ ...(m.version ? { version: m.version } : {}),
329
+ modelCard: {
330
+ modelParameters: {
331
+ ...(m.provider ? { approach: { type: 'supervised' } } : {}),
332
+ ...(m.task ? { task: m.task } : {}),
333
+ },
334
+ // `considerations` is where CycloneDX expects known risks to live. Pinning
335
+ // status is a real supply-chain property of a model reference: an unpinned
336
+ // model can change under you between runs.
337
+ considerations: {
338
+ technicalLimitations: m.pinned
339
+ ? []
340
+ : ['model reference is not pinned to a version; the served model can change between runs'],
341
+ },
342
+ },
343
+ properties: [
344
+ ...(m.provider ? [{ name: 'agentic-security:provider', value: String(m.provider) }] : []),
345
+ ...(m.file ? [{ name: 'agentic-security:declaredIn', value: String(m.file) }] : []),
346
+ { name: 'agentic-security:pinned', value: String(!!m.pinned) },
347
+ ],
348
+ }));
349
+
350
+ return {
351
+ bomFormat: 'CycloneDX',
352
+ specVersion: '1.6',
353
+ serialNumber: _mlbomSerial(JSON.stringify(components.map((c) => c['bom-ref']))),
354
+ version: 1,
355
+ metadata: {
356
+ timestamp: meta.startedAt || (aibom && aibom.generatedAt) || new Date().toISOString(),
357
+ tools: [{ vendor: 'Clear Capabilities', name: 'agentic-security', version: meta.engineVersion || 'dev' }],
358
+ component: { type: 'application', name: 'scan-target', version: '1.0.0' },
359
+ },
360
+ components,
361
+ };
362
+ }
363
+
364
+ /**
365
+ * Mechanical validation of an ML-BOM.
366
+ *
367
+ * STRUCTURAL, not full JSON-Schema validation: fetching the official CycloneDX
368
+ * schema at scan time would break the no-runtime-network rule, and vendoring it
369
+ * would add a file that silently rots against upstream. So this checks the
370
+ * required fields and the ML-BOM-specific shape, and SAYS that is what it does —
371
+ * a check labelled "validates against CycloneDX" that only tests a few keys
372
+ * would be the same unverified claim this item exists to remove.
373
+ */
374
+ export function validateMLBOM(doc) {
375
+ const errors = [];
376
+ const req = (cond, msg) => { if (!cond) errors.push(msg); };
377
+
378
+ req(doc && typeof doc === 'object', 'not an object');
379
+ if (!doc || typeof doc !== 'object') return { ok: false, errors, checked: 'structural' };
380
+
381
+ req(doc.bomFormat === 'CycloneDX', `bomFormat must be "CycloneDX", got ${JSON.stringify(doc.bomFormat)}`);
382
+ req(/^1\.[4-9]$/.test(String(doc.specVersion || '')), `specVersion must be 1.4-1.9, got ${JSON.stringify(doc.specVersion)}`);
383
+ req(Number.isInteger(doc.version) && doc.version >= 1, 'version must be a positive integer');
384
+ req(/^urn:uuid:[0-9a-f-]{36}$/i.test(String(doc.serialNumber || '')), 'serialNumber must be a urn:uuid');
385
+ req(doc.metadata && typeof doc.metadata === 'object', 'metadata is required');
386
+ req(Array.isArray(doc.components), 'components must be an array');
387
+
388
+ for (const [i, c] of (Array.isArray(doc.components) ? doc.components : []).entries()) {
389
+ req(typeof c.name === 'string' && c.name, `components[${i}].name is required`);
390
+ req(typeof c.type === 'string' && c.type, `components[${i}].type is required`);
391
+ if (c.type === 'machine-learning-model') {
392
+ req(c.modelCard && typeof c.modelCard === 'object',
393
+ `components[${i}] is a machine-learning-model but carries no modelCard — that is the whole ML-BOM extension`);
394
+ }
395
+ }
396
+ return { ok: errors.length === 0, errors, checked: 'structural (required fields + ML-BOM component shape), NOT full JSON-Schema validation' };
397
+ }
@@ -35,6 +35,11 @@ import * as fs from 'node:fs';
35
35
  import * as path from 'node:path';
36
36
 
37
37
  import { statePath, stateWritesEnabled } from './state-dir.js';
38
+ import { COMPLIANCE_FAMILY_ALIAS, resolveFamilyKeys } from './family-resolve.js';
39
+ import { strengthOfControl as _strengthOfControl } from './coverage-strength.js';
40
+
41
+ // Re-exported so existing callers/tests keep importing these from here.
42
+ export { COMPLIANCE_FAMILY_ALIAS, resolveFamilyKeys };
38
43
  const BUNDLED_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), 'compliance-frameworks');
39
44
  function _readJson(fp) {
40
45
  try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
@@ -118,15 +123,6 @@ export function loadFramework(scanRoot, id) {
118
123
  // `k8s-pod-privileged`; `nist-csf-2.json`/`hipaa-security-rule.json` map to
119
124
  // the compliance-side spelling `k8s-pod-security-privileged`, which no
120
125
  // detector ever emitted.
121
- export const COMPLIANCE_FAMILY_ALIAS = {
122
- // ASVS spells it `sqli`; every detector emits `sql-injection` (or a
123
- // language-prefixed variant, which the suffix rule below does NOT cover
124
- // because the prefix is on the wrong end).
125
- 'sqli': ['sql-injection', 'dart-sql-injection', 'laravel-sql-injection'],
126
- 'auth-missing': ['broken-access-control', 'fastapi-missing-auth', 'springboot-missing-authz', 'laravel-missing-auth', 'quarkus-missing-authz'],
127
- 'authz': ['broken-access-control', 'idor', 'springboot-missing-authz', 'quarkus-missing-authz'],
128
- 'k8s-pod-security-privileged': ['k8s-pod-privileged'],
129
- };
130
126
 
131
127
  // CMP-1 audit trail: every `family:` string referenced by the bundled
132
128
  // compliance-frameworks/*.json files was cross-checked against real
@@ -223,7 +219,19 @@ export function evaluateFramework(scanRoot, fw, scan) {
223
219
 
224
220
  if (maps.length === 0) {
225
221
  obs.push('No automated mapping — requires manual evidence collection.');
226
- results.push({ control: c, status, observations: obs });
222
+ // PRD F10.2: carry the MEASURED strength of the backing detector, so a
223
+ // control mapped to a detector that finds 3 of 18 independent advisories
224
+ // cannot read the same as one backed by a detector that finds nearly
225
+ // everything. Import is lazy so the evaluator keeps working if the bench
226
+ // artifacts are absent (they degrade to `unmeasured`, never to a default).
227
+ let evidence = null;
228
+ try { evidence = _strengthOfControl(c); } catch { /* strength is additive; never block evaluation */ }
229
+ results.push({
230
+ control: c,
231
+ status,
232
+ observations: obs,
233
+ ...(evidence ? { evidence, partiallyEvidenced: evidence.tier === 'weak' || evidence.tier === 'unmeasured' } : {}),
234
+ });
227
235
  continue;
228
236
  }
229
237
 
@@ -268,7 +276,6 @@ export function evaluateFramework(scanRoot, fw, scan) {
268
276
  hasUnverifiableMapping = true;
269
277
  continue;
270
278
  }
271
- const aliasFams = COMPLIANCE_FAMILY_ALIAS[fam] || [];
272
279
  // Several detectors emit the family as `<family>-<rule-slug>` — the
273
280
  // observed vocabulary holds `prompt-injection-http-user-input-in-llm-`,
274
281
  // `xpath-injection-query-built-via-string-c` and similar. This lookup
@@ -281,11 +288,8 @@ export function evaluateFramework(scanRoot, fw, scan) {
281
288
  // prefix test would let `nosql-injection` satisfy a `sql-injection`
282
289
  // mapping, silently merging two different vulnerability classes. A test
283
290
  // pins that boundary in the failing direction.
284
- const bases = [fam, ...aliasFams];
285
- const candidates = [];
286
- for (const [key, list] of families) {
287
- if (bases.some(b => key === b || key.startsWith(`${b}-`))) candidates.push(...list);
288
- }
291
+ const candidates = resolveFamilyKeys(fam, families.keys())
292
+ .flatMap(k => families.get(k) || []);
289
293
  const scoped = subfam ? candidates.filter(f => !f.subfamily || f.subfamily === subfam) : candidates;
290
294
  const open = scoped.filter(f => !f.intentSuppressed && !f.pastDecision && (f.severity === 'critical' || f.severity === 'high'));
291
295
  if (open.length) {
@@ -359,13 +363,48 @@ export function evaluateFramework(scanRoot, fw, scan) {
359
363
  // any control that already reads 'partial' because of an inherently
360
364
  // unverifiable rule: mapping — that precedent (rule: caps at 'partial',
361
365
  // never reaching 'present' OR 'absent') is unchanged.
366
+ // PRD F10.3: a control NIST/the framework rates as not code-testable must
367
+ // never read as evidenced by this tool. Two shapes were reaching 'present'
368
+ // that should not have:
369
+ //
370
+ // codeTestable:'no' — organisational (policy, training, governance).
371
+ // Nothing a scanner observes can satisfy it.
372
+ // codeTestable:'partial' — the only mappings are `module:` artifact
373
+ // EXISTENCE checks. "threat-model.json is
374
+ // present" is not evidence that threat modelling
375
+ // happened; it is evidence a file exists.
376
+ //
377
+ // Both are capped at 'partial' via the same unverifiable-mapping path a
378
+ // `rule:` mapping already uses, and the reason is stated in the
379
+ // observations so a reader is told WHY rather than left to infer it.
380
+ if (c.codeTestable === 'no') {
381
+ obs.push('⚠ this control is organisational, not code-testable — a scanner cannot evidence it (codeTestable: no).');
382
+ hasUnverifiableMapping = true;
383
+ anySignal = true;
384
+ } else if (c.codeTestable === 'partial' && maps.every(m => !m.startsWith('family:'))) {
385
+ obs.push('⚠ backed only by artifact-existence checks — a present file is weaker evidence than a detector finding nothing (codeTestable: partial).');
386
+ hasUnverifiableMapping = true;
387
+ }
388
+
362
389
  if (!anySignal) status = 'manual';
363
390
  else if (hasUnverifiableMapping) status = 'partial';
364
391
  else if (allCleared) status = 'present';
365
392
  else if (!anyCleared) status = 'absent';
366
393
  else status = 'partial';
367
394
 
368
- results.push({ control: c, status, observations: obs });
395
+ // PRD F10.2: carry the MEASURED strength of the backing detector, so a
396
+ // control mapped to a detector that finds 3 of 18 independent advisories
397
+ // cannot read the same as one backed by a detector that finds nearly
398
+ // everything. Import is lazy so the evaluator keeps working if the bench
399
+ // artifacts are absent (they degrade to `unmeasured`, never to a default).
400
+ let evidence = null;
401
+ try { evidence = _strengthOfControl(c); } catch { /* strength is additive; never block evaluation */ }
402
+ results.push({
403
+ control: c,
404
+ status,
405
+ observations: obs,
406
+ ...(evidence ? { evidence, partiallyEvidenced: evidence.tier === 'weak' || evidence.tier === 'unmeasured' } : {}),
407
+ });
369
408
  }
370
409
  return results;
371
410
  }
@@ -4,29 +4,56 @@
4
4
  "publisher": "California Legislature",
5
5
  "license": "California statute (public)",
6
6
  "url": "https://leginfo.legislature.ca.gov/faces/codes_displayText.xhtml?division=3.&part=4.&lawCode=CIV&title=1.81.5",
7
+ "scope": "SELECTIVE SUBSET. 4 of the CCPA/CPRA obligations, chosen because a code scanner can produce evidence for them. The statute is far broader; the majority of its duties (notice, consumer request handling, contracts, retention policy) are organisational and are NOT represented here. Absence of a control is not a statement of compliance.",
8
+ "controlsDigest": "ebc1f708c329ab42",
9
+ "controlCount": 4,
7
10
  "controls": [
8
11
  {
9
12
  "id": "§1798.100",
10
13
  "summary": "Consumer right to know — businesses inform consumers about categories of personal info collected.",
11
- "evidence": ["DPIA artifact lists every PII field with file:line provenance."],
12
- "mapsTo": ["module:privacy-taint"]
14
+ "codeTestable": "partial",
15
+ "evidence": [
16
+ "DPIA artifact lists every PII field with file:line provenance."
17
+ ],
18
+ "mapsTo": [
19
+ "module:privacy-taint"
20
+ ]
13
21
  },
14
22
  {
15
23
  "id": "§1798.105",
16
24
  "summary": "Right to delete — businesses comply with verified deletion requests.",
17
- "evidence": ["No data-exposure findings on PII storage paths."]
25
+ "codeTestable": "no",
26
+ "evidence": [
27
+ "No data-exposure findings on PII storage paths."
28
+ ]
18
29
  },
19
30
  {
20
31
  "id": "§1798.150",
21
32
  "summary": "Civil action for security breach affecting personal information.",
22
- "evidence": ["Crypto findings cleared on PII handling code.", "TLS pinning checks pass."],
23
- "mapsTo": ["family:crypto-weak-cipher", "family:crypto-tls-version", "family:crypto-tls-no-verify"]
33
+ "codeTestable": "yes",
34
+ "evidence": [
35
+ "Crypto findings cleared on PII handling code.",
36
+ "TLS pinning checks pass."
37
+ ],
38
+ "mapsTo": [
39
+ "family:crypto-weak-cipher",
40
+ "family:crypto-tls-version",
41
+ "family:crypto-tls-no-verify"
42
+ ]
24
43
  },
25
44
  {
26
45
  "id": "§1798.81.5",
27
46
  "summary": "Reasonable security procedures and practices.",
28
- "evidence": ["No critical findings on user-data endpoints.", "Auth-missing zero on PII paths."],
29
- "mapsTo": ["family:auth-missing", "family:authz", "family:hardcoded-secret"]
47
+ "codeTestable": "yes",
48
+ "evidence": [
49
+ "No critical findings on user-data endpoints.",
50
+ "Auth-missing zero on PII paths."
51
+ ],
52
+ "mapsTo": [
53
+ "family:auth-missing",
54
+ "family:authz",
55
+ "family:hardcoded-secret"
56
+ ]
30
57
  }
31
58
  ]
32
59
  }
@@ -4,48 +4,99 @@
4
4
  "publisher": "European Parliament & Council",
5
5
  "license": "EU law (Official Journal)",
6
6
  "url": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj",
7
+ "scope": "SELECTIVE SUBSET. 7 obligations drawn from the high-risk-system and GPAI articles where a code signal exists. The Act is far broader; conformity assessment, registration, human oversight and post-market monitoring are organisational and are NOT represented here.",
8
+ "controlsDigest": "16d15998a686a19b",
9
+ "controlCount": 7,
7
10
  "controls": [
8
11
  {
9
12
  "id": "Art.9",
10
13
  "summary": "Risk management system — continuous, iterative risk identification and mitigation.",
11
- "evidence": ["Threat model artifact (.agentic-security/threat-model.{json,md}).", "Continuous scan history present."],
12
- "mapsTo": ["module:threat-model-auto", "module:scan-history"]
14
+ "codeTestable": "partial",
15
+ "evidence": [
16
+ "Threat model artifact (.agentic-security/threat-model.{json,md}).",
17
+ "Continuous scan history present."
18
+ ],
19
+ "mapsTo": [
20
+ "module:threat-model-auto",
21
+ "module:scan-history"
22
+ ]
13
23
  },
14
24
  {
15
25
  "id": "Art.10",
16
26
  "summary": "Data governance — training/validation data is relevant, representative, and bias-checked.",
17
- "evidence": ["No training-data-pii findings.", "DPIA artifact for any PII handling."],
18
- "mapsTo": ["family:training-data-pii", "family:pii-exposure"]
27
+ "codeTestable": "yes",
28
+ "evidence": [
29
+ "No training-data-pii findings.",
30
+ "DPIA artifact for any PII handling."
31
+ ],
32
+ "mapsTo": [
33
+ "family:training-data-pii",
34
+ "family:pii-exposure"
35
+ ]
19
36
  },
20
37
  {
21
38
  "id": "Art.11",
22
39
  "summary": "Technical documentation — system architecture, components, training procedures documented.",
23
- "evidence": ["AIBOM artifact at .agentic-security/aibom.json.", "Threat model + DPIA + compliance-evidence present."],
24
- "mapsTo": ["module:aibom", "module:threat-model-auto", "module:compliance-policy"]
40
+ "codeTestable": "partial",
41
+ "evidence": [
42
+ "AIBOM artifact at .agentic-security/aibom.json.",
43
+ "Threat model + DPIA + compliance-evidence present."
44
+ ],
45
+ "mapsTo": [
46
+ "module:aibom",
47
+ "module:threat-model-auto",
48
+ "module:compliance-policy"
49
+ ]
25
50
  },
26
51
  {
27
52
  "id": "Art.12",
28
53
  "summary": "Record-keeping — automatic logging of system events for traceability.",
29
- "evidence": ["MCP audit log .agentic-security/mcp-audit.log present.", "Scan history retained."],
30
- "mapsTo": ["module:mcp-audit", "module:scan-history"]
54
+ "codeTestable": "partial",
55
+ "evidence": [
56
+ "MCP audit log .agentic-security/mcp-audit.log present.",
57
+ "Scan history retained."
58
+ ],
59
+ "mapsTo": [
60
+ "module:mcp-audit",
61
+ "module:scan-history"
62
+ ]
31
63
  },
32
64
  {
33
65
  "id": "Art.13",
34
66
  "summary": "Transparency — instructions for use enable users to interpret the system's output correctly.",
35
- "evidence": ["why-fired annotation surfaces detection provenance on every finding."],
36
- "mapsTo": ["module:why-fired"]
67
+ "codeTestable": "partial",
68
+ "evidence": [
69
+ "why-fired annotation surfaces detection provenance on every finding."
70
+ ],
71
+ "mapsTo": [
72
+ "module:why-fired"
73
+ ]
37
74
  },
38
75
  {
39
76
  "id": "Art.14",
40
77
  "summary": "Human oversight — system permits humans to override / interrupt.",
41
- "evidence": ["Fix application requires confirm:true.", "Bodyguard hook can refuse risky edits."],
42
- "mapsTo": ["module:pre-edit-bodyguard", "module:apply-fix"]
78
+ "codeTestable": "partial",
79
+ "evidence": [
80
+ "Fix application requires confirm:true.",
81
+ "Bodyguard hook can refuse risky edits."
82
+ ],
83
+ "mapsTo": [
84
+ "module:pre-edit-bodyguard",
85
+ "module:apply-fix"
86
+ ]
43
87
  },
44
88
  {
45
89
  "id": "Art.15",
46
90
  "summary": "Accuracy, robustness, cybersecurity — appropriate level of accuracy and resilience.",
47
- "evidence": ["Calibration + held-out evaluation present (.agentic-security/calibration-seed.json).", "OWASP Benchmark regression gate."],
48
- "mapsTo": ["module:calibration", "module:holdout-eval"]
91
+ "codeTestable": "partial",
92
+ "evidence": [
93
+ "Calibration + held-out evaluation present (.agentic-security/calibration-seed.json).",
94
+ "OWASP Benchmark regression gate."
95
+ ],
96
+ "mapsTo": [
97
+ "module:calibration",
98
+ "module:holdout-eval"
99
+ ]
49
100
  }
50
101
  ]
51
102
  }
@@ -4,42 +4,86 @@
4
4
  "publisher": "European Parliament & Council",
5
5
  "license": "EU law (Official Journal)",
6
6
  "url": "https://eur-lex.europa.eu/eli/reg/2016/679/oj",
7
+ "scope": "SELECTIVE SUBSET. 6 articles where a code scanner can produce evidence (security of processing, data minimisation, DPIA inputs). GDPR has 99 articles; lawful basis, data-subject rights, transfers and records of processing are organisational and are NOT represented here.",
8
+ "controlsDigest": "749f9dad2b5657cb",
9
+ "controlCount": 6,
7
10
  "controls": [
8
11
  {
9
12
  "id": "Art.5",
10
13
  "summary": "Principles relating to processing of personal data (lawfulness, fairness, transparency, purpose limitation, data minimization, accuracy, storage limitation, integrity, accountability).",
11
- "evidence": ["DPIA artifact at .agentic-security/dpia.md.", "PII fields enumerated by the privacy-taint module."],
12
- "mapsTo": ["family:pii-exposure", "module:privacy-taint"]
14
+ "codeTestable": "yes",
15
+ "evidence": [
16
+ "DPIA artifact at .agentic-security/dpia.md.",
17
+ "PII fields enumerated by the privacy-taint module."
18
+ ],
19
+ "mapsTo": [
20
+ "family:pii-exposure",
21
+ "module:privacy-taint"
22
+ ]
13
23
  },
14
24
  {
15
25
  "id": "Art.25",
16
26
  "summary": "Data protection by design and by default.",
17
- "evidence": ["Threat model produced before code.", "PII annotation runs at scan time, not as an afterthought."],
18
- "mapsTo": ["module:threat-model-auto", "module:privacy-taint"]
27
+ "codeTestable": "partial",
28
+ "evidence": [
29
+ "Threat model produced before code.",
30
+ "PII annotation runs at scan time, not as an afterthought."
31
+ ],
32
+ "mapsTo": [
33
+ "module:threat-model-auto",
34
+ "module:privacy-taint"
35
+ ]
19
36
  },
20
37
  {
21
38
  "id": "Art.30",
22
39
  "summary": "Records of processing activities — controller maintains a register.",
23
- "evidence": ["DPIA + privacy-taint annotations cover each PII field with file:line provenance."],
24
- "mapsTo": ["module:privacy-taint"]
40
+ "codeTestable": "partial",
41
+ "evidence": [
42
+ "DPIA + privacy-taint annotations cover each PII field with file:line provenance."
43
+ ],
44
+ "mapsTo": [
45
+ "module:privacy-taint"
46
+ ]
25
47
  },
26
48
  {
27
49
  "id": "Art.32",
28
50
  "summary": "Security of processing — appropriate technical and organizational measures.",
29
- "evidence": ["Zero open critical crypto findings.", "Auth-missing findings zero on user-data endpoints.", "TLS pinning checks pass."],
30
- "mapsTo": ["family:crypto-weak-cipher", "family:crypto-tls-version", "family:crypto-tls-no-verify", "family:auth-missing"]
51
+ "codeTestable": "yes",
52
+ "evidence": [
53
+ "Zero open critical crypto findings.",
54
+ "Auth-missing findings zero on user-data endpoints.",
55
+ "TLS pinning checks pass."
56
+ ],
57
+ "mapsTo": [
58
+ "family:crypto-weak-cipher",
59
+ "family:crypto-tls-version",
60
+ "family:crypto-tls-no-verify",
61
+ "family:auth-missing"
62
+ ]
31
63
  },
32
64
  {
33
65
  "id": "Art.33",
34
66
  "summary": "Notification of personal data breach to the supervisory authority within 72 hours.",
35
- "evidence": ["Fix history retained.", "Audit log preserves the breach-window evidence."],
36
- "mapsTo": ["module:fix-history", "module:mcp-audit"]
67
+ "codeTestable": "partial",
68
+ "evidence": [
69
+ "Fix history retained.",
70
+ "Audit log preserves the breach-window evidence."
71
+ ],
72
+ "mapsTo": [
73
+ "module:fix-history",
74
+ "module:mcp-audit"
75
+ ]
37
76
  },
38
77
  {
39
78
  "id": "Art.35",
40
79
  "summary": "Data protection impact assessment (DPIA) for high-risk processing.",
41
- "evidence": ["DPIA artifact present at .agentic-security/dpia.md."],
42
- "mapsTo": ["module:privacy-taint:emitDpiaArtifact"]
80
+ "codeTestable": "partial",
81
+ "evidence": [
82
+ "DPIA artifact present at .agentic-security/dpia.md."
83
+ ],
84
+ "mapsTo": [
85
+ "module:privacy-taint:emitDpiaArtifact"
86
+ ]
43
87
  }
44
88
  ]
45
89
  }