@clear-capabilities/agentic-security-scanner 0.139.1 → 0.141.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +221 -0
- package/bin/agentic-security.js +40 -11
- package/dist/113.index.js +79 -3
- package/dist/178.index.js +1 -1
- package/dist/238.index.js +77 -1
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +12 -0
- package/dist/526.index.js +79 -3
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +14 -14
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +34 -7
- package/dist/compliance-frameworks/eu-ai-act.json +65 -14
- package/dist/compliance-frameworks/gdpr.json +56 -12
- package/dist/compliance-frameworks/hipaa-security-rule.json +68 -15
- package/dist/compliance-frameworks/nist-ai-600-1.json +57 -12
- package/dist/compliance-frameworks/nist-csf-2.json +78 -16
- package/dist/compliance-frameworks/nist-privacy-1-1.json +3 -0
- package/dist/compliance-frameworks/owasp-asvs-5.json +91 -20
- package/dist/compliance-frameworks/owasp-llm-top-10.json +89 -20
- package/package.json +16 -5
- package/src/dataflow/catalog.js +61 -0
- package/src/engine.js +281 -23
- package/src/mcp/tools.js +12 -0
- package/src/posture/accuracy-scorecard.js +57 -0
- package/src/posture/aibom.js +110 -1
- package/src/posture/auditor-walkthrough.js +137 -21
- package/src/posture/compliance-frameworks/ccpa.json +34 -7
- package/src/posture/compliance-frameworks/eu-ai-act.json +65 -14
- package/src/posture/compliance-frameworks/gdpr.json +56 -12
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +68 -15
- package/src/posture/compliance-frameworks/nist-ai-600-1.json +57 -12
- package/src/posture/compliance-frameworks/nist-csf-2.json +78 -16
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +3 -0
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +91 -20
- package/src/posture/compliance-frameworks/owasp-llm-top-10.json +89 -20
- package/src/posture/concurrency-checker.js +42 -5
- package/src/posture/coverage-strength.js +182 -0
- package/src/posture/epss.js +17 -1
- package/src/posture/family-registry.js +103 -0
- package/src/posture/family-resolve.js +47 -0
- package/src/posture/fix-coverage.js +113 -0
- package/src/posture/fix-metrics.js +76 -0
- package/src/posture/integrity.js +59 -8
- package/src/posture/mcp-rug-pull.js +144 -0
- package/src/posture/poc-generator.js +17 -1
- package/src/posture/poc-inprocess.js +217 -1
- package/src/posture/proof-coverage.js +162 -0
- package/src/posture/reachability-filter.js +44 -0
- package/src/posture/sbom.js +50 -7
- package/src/runScan.js +56 -5
- package/src/sast/CLAUDE.md +2 -2
- package/src/sast/claude-md-prompt-injection.js +47 -3
- package/src/sast/cloud-iam.js +23 -0
- package/src/sast/convention-deviation.js +66 -3
- package/src/sast/crypto-protocol.js +23 -0
- package/src/sast/dapp-frontend.js +20 -0
- package/src/sast/iac-cloud-templates.js +337 -0
- package/src/sast/k8s-admission.js +27 -0
- package/src/sast/ml-supply-chain.js +22 -0
- package/src/sast/ruby.js +132 -0
- package/src/sast/web3-advanced.js +26 -0
- package/src/sca/CLAUDE.md +21 -4
- package/src/sca/container.js +18 -1
- package/src/sca/dep-confusion.js +69 -3
|
@@ -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');
|
package/src/posture/aibom.js
CHANGED
|
@@ -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
|
-
|
|
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,11 +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
|
-
const COMPLIANCE_FAMILY_ALIAS = {
|
|
122
|
-
'auth-missing': ['broken-access-control', 'fastapi-missing-auth', 'springboot-missing-authz', 'laravel-missing-auth', 'quarkus-missing-authz'],
|
|
123
|
-
'authz': ['broken-access-control', 'idor', 'springboot-missing-authz', 'quarkus-missing-authz'],
|
|
124
|
-
'k8s-pod-security-privileged': ['k8s-pod-privileged'],
|
|
125
|
-
};
|
|
126
126
|
|
|
127
127
|
// CMP-1 audit trail: every `family:` string referenced by the bundled
|
|
128
128
|
// compliance-frameworks/*.json files was cross-checked against real
|
|
@@ -130,18 +130,63 @@ const COMPLIANCE_FAMILY_ALIAS = {
|
|
|
130
130
|
// real detector to alias, and `mcp-audit.js`/`sca/dep-confusion.js` were
|
|
131
131
|
// fixed at the SOURCE (they now set `family` explicitly) rather than
|
|
132
132
|
// aliased, since the finding constructors themselves were the root cause.
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
133
|
+
//
|
|
134
|
+
// CORRECTION (measured, not read): this comment used to claim four families
|
|
135
|
+
// had no producer at all — `crypto-tls-version`, `nosql-injection`,
|
|
136
|
+
// `pii-exposure` and `data-exposure` — and that every control mapped to one
|
|
137
|
+
// of them therefore read `manual`/`engine-gap` rather than a false `present`.
|
|
138
|
+
// BOTH halves were wrong. Nothing implemented the engine-gap treatment (see
|
|
139
|
+
// COMPLIANCE_FAMILY_GAPS below), and all four families DO have producers:
|
|
140
|
+
// a sweep of 331 real scan roots (bench/family-producers/OBSERVED.json)
|
|
141
|
+
// observed crypto-tls-version, nosql-injection and data-exposure twice each,
|
|
142
|
+
// and `pii-exposure` is emitted by dataflow/privacy-taint.js:129.
|
|
143
|
+
//
|
|
144
|
+
// The lesson is recorded because it cost a wrong commit: a family is not a
|
|
145
|
+
// gap because a grep or a source comment says so. Several detectors pass
|
|
146
|
+
// `family` POSITIONALLY (`_shape(file, line, ruleId, vuln, fam, …)` in
|
|
147
|
+
// cloud-iam.js, crypto-protocol.js, k8s-admission.js, ml-supply-chain.js),
|
|
148
|
+
// so no textual search enumerates the real vocabulary. Only running the
|
|
149
|
+
// engine does.
|
|
150
|
+
|
|
151
|
+
// ...which the prose above ASSERTED but nothing enforced. A `family:` mapping
|
|
152
|
+
// with no detector behind it produces an empty bucket, and an empty bucket was
|
|
153
|
+
// reported as `✓ no open critical/high findings` — identical to a genuine pass.
|
|
154
|
+
// So the four known-unevidenceable families read as fully evidenced controls,
|
|
155
|
+
// which is precisely the outcome the comment says is avoided.
|
|
156
|
+
//
|
|
157
|
+
// Declaring them here makes the claim load-bearing: the evaluator consults this
|
|
158
|
+
// map (see the `family:` branch below) and caps such a control at `partial`
|
|
159
|
+
// with an explicit disclosure, and `test/compliance-mapping-liveness.test.js`
|
|
160
|
+
// fails if an entry loses its reason, duplicates an alias, or stops being
|
|
161
|
+
// referenced by any framework.
|
|
162
|
+
//
|
|
163
|
+
// Membership is deliberately conservative — these four come from the CMP-1
|
|
164
|
+
// audit recorded above, each confirmed to be referenced only by CONSUMER
|
|
165
|
+
// tables (attack-taxonomy, risk-dollars, threat-model classifiers) with no
|
|
166
|
+
// producing detector. A family is NOT added here merely because a static grep
|
|
167
|
+
// or a fixture sweep failed to observe it: several detectors pass `family`
|
|
168
|
+
// positionally (`_shape(file, line, ruleId, vuln, fam, …)` in cloud-iam.js,
|
|
169
|
+
// crypto-protocol.js, k8s-admission.js, ml-supply-chain.js), so neither source
|
|
170
|
+
// enumerates the real vocabulary on its own. Wrongly declaring a live family a
|
|
171
|
+
// gap would suppress a control that does work.
|
|
172
|
+
export const COMPLIANCE_FAMILY_GAPS = {
|
|
173
|
+
// Deliberately EMPTY. Every family currently mapped by a bundled framework
|
|
174
|
+
// has a producer, verified against bench/family-producers/OBSERVED.json.
|
|
175
|
+
//
|
|
176
|
+
// The mechanism below is retained because the hazard is real and structural:
|
|
177
|
+
// the evaluator resolves `family:X` against a Map keyed by finding.family, so
|
|
178
|
+
// a mapping nothing produces yields an empty bucket, and an empty bucket used
|
|
179
|
+
// to render as `✓ no open critical/high findings` — a pass nothing checked.
|
|
180
|
+
// An entry here caps such a control at `partial` with an explicit disclosure
|
|
181
|
+
// instead.
|
|
182
|
+
//
|
|
183
|
+
// Adding an entry requires EVIDENCE that no detector emits the family — a
|
|
184
|
+
// sweep that does not observe it is a lower bound, not proof (SCA families
|
|
185
|
+
// need network access, and some rules need shapes no corpus entry has).
|
|
186
|
+
// Wrongly declaring a live family a gap silently suppresses a control that
|
|
187
|
+
// works, which is the mirror image of the bug this prevents.
|
|
188
|
+
};
|
|
189
|
+
|
|
145
190
|
export function evaluateFramework(scanRoot, fw, scan) {
|
|
146
191
|
// CMP-2: last-scan.json (what this is actually handed in production) carries
|
|
147
192
|
// findings across four separate channels — SAST (`findings`), secrets,
|
|
@@ -174,7 +219,19 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
174
219
|
|
|
175
220
|
if (maps.length === 0) {
|
|
176
221
|
obs.push('No automated mapping — requires manual evidence collection.');
|
|
177
|
-
|
|
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
|
+
});
|
|
178
235
|
continue;
|
|
179
236
|
}
|
|
180
237
|
|
|
@@ -207,8 +264,32 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
207
264
|
// filtering is safe; findings with no subfamily set still count
|
|
208
265
|
// (recall-preserving default — same precedent as relevance.js).
|
|
209
266
|
const [fam, subfam] = m.slice('family:'.length).split(':');
|
|
210
|
-
|
|
211
|
-
|
|
267
|
+
// No detector produces this family, so its bucket is empty on EVERY
|
|
268
|
+
// scan. Reporting that as "no open findings" is a pass nothing checked
|
|
269
|
+
// — disclose it and cap the control at 'partial' (the same treatment a
|
|
270
|
+
// `rule:` mapping gets, and for the same reason: unverifiable is not
|
|
271
|
+
// evidence). Deliberately does NOT clear allCleared — the control is
|
|
272
|
+
// unknown, not failing.
|
|
273
|
+
if (COMPLIANCE_FAMILY_GAPS[fam]) {
|
|
274
|
+
obs.push(`⚠ ${fam}: no detector can evidence this control (engine-gap) — ${COMPLIANCE_FAMILY_GAPS[fam]}`);
|
|
275
|
+
anySignal = true;
|
|
276
|
+
hasUnverifiableMapping = true;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// Several detectors emit the family as `<family>-<rule-slug>` — the
|
|
280
|
+
// observed vocabulary holds `prompt-injection-http-user-input-in-llm-`,
|
|
281
|
+
// `xpath-injection-query-built-via-string-c` and similar. This lookup
|
|
282
|
+
// used to be an exact Map key read, so `family:prompt-injection`
|
|
283
|
+
// matched none of them and the control read as evidenced whatever the
|
|
284
|
+
// scan found. That silenced LLM01 (Prompt Injection — the FIRST control
|
|
285
|
+
// of the OWASP LLM Top 10), ASVS V5.1 and NIST AI 600-1 MG-3.2-005.
|
|
286
|
+
//
|
|
287
|
+
// The `-` separator is load-bearing, not cosmetic: a bare substring or
|
|
288
|
+
// prefix test would let `nosql-injection` satisfy a `sql-injection`
|
|
289
|
+
// mapping, silently merging two different vulnerability classes. A test
|
|
290
|
+
// pins that boundary in the failing direction.
|
|
291
|
+
const candidates = resolveFamilyKeys(fam, families.keys())
|
|
292
|
+
.flatMap(k => families.get(k) || []);
|
|
212
293
|
const scoped = subfam ? candidates.filter(f => !f.subfamily || f.subfamily === subfam) : candidates;
|
|
213
294
|
const open = scoped.filter(f => !f.intentSuppressed && !f.pastDecision && (f.severity === 'critical' || f.severity === 'high'));
|
|
214
295
|
if (open.length) {
|
|
@@ -282,13 +363,48 @@ export function evaluateFramework(scanRoot, fw, scan) {
|
|
|
282
363
|
// any control that already reads 'partial' because of an inherently
|
|
283
364
|
// unverifiable rule: mapping — that precedent (rule: caps at 'partial',
|
|
284
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
|
+
|
|
285
389
|
if (!anySignal) status = 'manual';
|
|
286
390
|
else if (hasUnverifiableMapping) status = 'partial';
|
|
287
391
|
else if (allCleared) status = 'present';
|
|
288
392
|
else if (!anyCleared) status = 'absent';
|
|
289
393
|
else status = 'partial';
|
|
290
394
|
|
|
291
|
-
|
|
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
|
+
});
|
|
292
408
|
}
|
|
293
409
|
return results;
|
|
294
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
|
-
"
|
|
12
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
23
|
-
"
|
|
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
|
-
"
|
|
29
|
-
"
|
|
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
|
-
"
|
|
12
|
-
"
|
|
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
|
-
"
|
|
18
|
-
"
|
|
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
|
-
"
|
|
24
|
-
"
|
|
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
|
-
"
|
|
30
|
-
"
|
|
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
|
-
"
|
|
36
|
-
"
|
|
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
|
-
"
|
|
42
|
-
"
|
|
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
|
-
"
|
|
48
|
-
"
|
|
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
|
}
|