@clear-capabilities/agentic-security-scanner 0.84.1 → 0.118.2
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 +404 -0
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +84 -84
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +12 -6
- package/src/.agentic-security/dpia.md +26 -0
- package/src/.agentic-security/pqc-migration-plan.json +80 -0
- package/src/.agentic-security/pqc-migration-plan.md +32 -0
- package/src/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
- package/src/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
- package/src/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
- package/src/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
- package/src/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
- package/src/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
- package/src/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
- package/src/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
- package/src/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
- package/src/.agentic-security/threat-model.json +3702 -0
- package/src/.agentic-security/threat-model.md +118 -0
- package/src/dataflow/.agentic-security/dpia.md +26 -0
- package/src/dataflow/.agentic-security/sbom-history/15d7c4d20f9d968ebec29445c9e9b3fc6f74e6a1.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/8869fe64035bec0152d92ece961e9369d8637076.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
- package/src/dataflow/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
- package/src/dataflow/.agentic-security/threat-model.json +161 -0
- package/src/dataflow/.agentic-security/threat-model.md +43 -0
- package/src/dataflow/CLAUDE.md +17 -5
- package/src/dataflow/catalog.js +9 -0
- package/src/dataflow/engine.js +30 -4
- package/src/dataflow/index.js +8 -0
- package/src/dataflow/proof-gate.js +95 -0
- package/src/dataflow/summaries.js +44 -5
- package/src/engine.js +199 -5
- package/src/ir/.agentic-security/dpia.md +26 -0
- package/src/ir/.agentic-security/exploit-bundles.json +77 -0
- package/src/ir/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
- package/src/ir/.agentic-security/sbom-history/b5f468a158f6a145aedd8680841c106b9809f16b.json +6 -0
- package/src/ir/.agentic-security/threat-model.json +143 -0
- package/src/ir/.agentic-security/threat-model.md +39 -0
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/callgraph.js +12 -1
- package/src/ir/tree-sitter-loader.js +108 -0
- package/src/posture/.agentic-security/dpia.md +26 -0
- package/src/posture/.agentic-security/pqc-migration-plan.json +65 -0
- package/src/posture/.agentic-security/pqc-migration-plan.md +30 -0
- package/src/posture/.agentic-security/sbom-history/0724c133d2943c87555695b5b2adcfcfc3373a2d.json +6 -0
- package/src/posture/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
- package/src/posture/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/posture/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
- package/src/posture/.agentic-security/threat-model.json +2038 -0
- package/src/posture/.agentic-security/threat-model.md +73 -0
- package/src/posture/auditor-walkthrough.js +1 -1
- package/src/posture/corpus-status.js +74 -0
- package/src/posture/coverage-report.js +113 -0
- package/src/posture/holdout-eval.js +57 -0
- package/src/posture/pr-augment.js +1 -1
- package/src/posture/router.js +37 -7
- package/src/report/.agentic-security/dpia.md +26 -0
- package/src/report/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
- package/src/report/.agentic-security/sbom-history/f94be839aa6a1011f53da016416a561698b8f338.json +6 -0
- package/src/report/.agentic-security/threat-model.json +7 -0
- package/src/report/.agentic-security/threat-model.md +22 -0
- package/src/report/index.js +18 -1
- package/src/sast/.agentic-security/dpia.md +26 -0
- package/src/sast/.agentic-security/pqc-migration-plan.json +65 -0
- package/src/sast/.agentic-security/pqc-migration-plan.md +30 -0
- package/src/sast/.agentic-security/sbom-history/043a7b401c68f5a298c83d4c42e94f118ff2a89a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
- package/src/sast/.agentic-security/sbom-history/20d77ad7363a82777d506dedbbb1ecfed52f094a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3c6b3205f04e26f7654943d465281d00e0fa395e.json +6 -0
- package/src/sast/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
- package/src/sast/.agentic-security/sbom-history/5282502e090013219eb006f7e02d8c983bc29c8e.json +6 -0
- package/src/sast/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
- package/src/sast/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
- package/src/sast/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
- package/src/sast/.agentic-security/sbom-history/73514b485d4770f3c5ee57d87338bccd1c61d8b7.json +6 -0
- package/src/sast/.agentic-security/sbom-history/96fa584fc3134512237d58aac63d6f3778b0cbcb.json +6 -0
- package/src/sast/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
- package/src/sast/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
- package/src/sast/.agentic-security/sbom-history/a4a34ab8b3d758de2adc8c3dc3e42d4cb0db00a3.json +6 -0
- package/src/sast/.agentic-security/sbom-history/ac236ec3602cfc8fb133f389ec3687e409084673.json +6 -0
- package/src/sast/.agentic-security/sbom-history/b57b3e1292f1f9f0de6c5a34a436e330c78008a3.json +6 -0
- package/src/sast/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
- package/src/sast/.agentic-security/sbom-history/dcbfd4935922dd165093c08a1003c338eed937c5.json +6 -0
- package/src/sast/.agentic-security/sbom-history/fa017f725615b0293814344c8ef7dfeb06775ca0.json +6 -0
- package/src/sast/.agentic-security/threat-model.json +816 -0
- package/src/sast/.agentic-security/threat-model.md +91 -0
- package/src/sast/CLAUDE.md +9 -3
- package/src/sast/code-injection-multilang.js +117 -0
- package/src/sast/crypto-protocol.js +43 -1
- package/src/sast/csharp-structural.js +65 -0
- package/src/sast/csharp.js +3 -0
- package/src/sast/csrf.js +24 -3
- package/src/sast/csv-injection.js +84 -0
- package/src/sast/frontend-hygiene.js +106 -0
- package/src/sast/go-structural.js +60 -0
- package/src/sast/java-structural.js +81 -0
- package/src/sast/js-framework-structural.js +115 -0
- package/src/sast/kotlin.js +72 -0
- package/src/sast/ldap-injection.js +73 -17
- package/src/sast/open-redirect.js +47 -5
- package/src/sast/php.js +35 -0
- package/src/sast/python-structural.js +83 -0
- package/src/sast/redos-nfa.js +75 -0
- package/src/sast/response-splitting.js +48 -19
- package/src/sast/ruby.js +38 -0
- package/src/sast/secret-concat.js +60 -0
- package/src/sast/ssrf-cloud-metadata.js +13 -0
- package/src/sast/stored-taint.js +88 -0
- package/src/sast/tree-sitter-sinks.js +62 -0
- package/src/sast/weak-password-hash.js +28 -0
- package/src/sast/weak-randomness.js +46 -5
- package/src/sast/wrong-context-sanitizer.js +94 -0
- package/src/sast/xpath-injection.js +30 -6
- package/src/sast/xss-reflected-multilang.js +116 -0
- package/src/sast/xxe.js +67 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Threat Model (auto-generated)
|
|
2
|
+
|
|
3
|
+
Generated by agentic-security on 2026-05-31.
|
|
4
|
+
|
|
5
|
+
This threat model is derived from static analysis of the current codebase and is regenerated on every scan. It is intended as a working artifact, not a finished compliance document.
|
|
6
|
+
|
|
7
|
+
## Entities + boundaries
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart TB
|
|
11
|
+
subgraph External
|
|
12
|
+
route_GET__api_users_2["http-route: GET /api/users/2"]
|
|
13
|
+
route_GET__api_admin_users["http-route: GET /api/admin/users"]
|
|
14
|
+
route_GET__api_health["http-route: GET /api/health"]
|
|
15
|
+
end
|
|
16
|
+
subgraph Application
|
|
17
|
+
end
|
|
18
|
+
External --> route_GET__api_users_2
|
|
19
|
+
External --> route_GET__api_admin_users
|
|
20
|
+
External --> route_GET__api_health
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Assets
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
## STRIDE threats
|
|
27
|
+
|
|
28
|
+
### Tampering (148)
|
|
29
|
+
|
|
30
|
+
- [medium] **dos-sync-io** (CWE-400) at `deploy-platform.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
31
|
+
- [low] **mass-assignment** (CWE-915) at `integrity.js:undefined` — Mass Assignment (req.body Direct to Model)
|
|
32
|
+
- [low] **dos-sync-io** (CWE-400) at `agents-memory.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
33
|
+
- [low] **dos-sync-io** (CWE-400) at `api-contract.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
34
|
+
- [low] **dos-sync-io** (CWE-400) at `auditor-walkthrough.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
35
|
+
- [low] **dos-sync-io** (CWE-400) at `auth-posture-import.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
36
|
+
- [low] **dos-sync-io** (CWE-400) at `blast-radius.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
37
|
+
- [low] **dos-sync-io** (CWE-400) at `calibration-drift.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
38
|
+
- [low] **dos-sync-io** (CWE-400) at `calibration.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
39
|
+
- [low] **dos-sync-io** (CWE-400) at `compliance-policy.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
40
|
+
- [low] **dos-sync-io** (CWE-400) at `cross-repo-memory.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
41
|
+
- [low] **dos-sync-io** (CWE-400) at `custom-rules.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
42
|
+
- [low] **dos-sync-io** (CWE-400) at `cve-alert-daemon.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
43
|
+
- [low] **dos-sync-io** (CWE-400) at `cve-lookup.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
44
|
+
- [low] **dos-sync-io** (CWE-400) at `dep-add-guard.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
45
|
+
- [low] **dos-sync-io** (CWE-400) at `deterministic.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
46
|
+
- [low] **dos-sync-io** (CWE-400) at `epss.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
47
|
+
- [low] **dos-sync-io** (CWE-400) at `exploitability-probability.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
48
|
+
- [low] **dos-sync-io** (CWE-400) at `feature-flags.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
49
|
+
- [low] **dos-sync-io** (CWE-400) at `federated-learning.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
50
|
+
- [low] **dos-sync-io** (CWE-400) at `findings-memory.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
51
|
+
- [low] **dos-sync-io** (CWE-400) at `fix-history.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
52
|
+
- [low] **dos-sync-io** (CWE-400) at `fix-plan.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
53
|
+
- [low] **dos-sync-io** (CWE-400) at `fix-style-mirror.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
54
|
+
- [low] **dos-sync-io** (CWE-400) at `fix-verify-loop.js:undefined` — Synchronous Blocking I/O (DoS Risk in Server Context)
|
|
55
|
+
- … and 123 more
|
|
56
|
+
|
|
57
|
+
### Information Disclosure (5)
|
|
58
|
+
|
|
59
|
+
- [low] **ssrf** (CWE-918) at `attack-playbooks.js:72` — SSRF: explicit reference to cloud instance-metadata endpoint
|
|
60
|
+
- [low] **ssrf** (CWE-918) at `defender-agent.js:41` — SSRF: explicit reference to cloud instance-metadata endpoint
|
|
61
|
+
- [low] **ssrf** (CWE-918) at `flow-narration.js:24` — SSRF: explicit reference to cloud instance-metadata endpoint
|
|
62
|
+
- [low] **pqc-migration** (CWE-327) at `rule-pack-signing.js:187` — Pre-quantum ED25519 (ed25519) — replace with ML-DSA-65 before CRQC arrives
|
|
63
|
+
- [low] **ssrf** (CWE-918) at `verifier.js:55` — SSRF: explicit reference to cloud instance-metadata endpoint
|
|
64
|
+
|
|
65
|
+
### Elevation of Privilege (5)
|
|
66
|
+
|
|
67
|
+
- [low] **ssrf** (CWE-918) at `attack-playbooks.js:72` — SSRF: explicit reference to cloud instance-metadata endpoint
|
|
68
|
+
- [low] **prototype-pollution** (CWE-1321) at `adversarial-self-test.js:60` — Prototype Pollution: Direct write to __proto__ / constructor.prototype
|
|
69
|
+
- [low] **ssrf** (CWE-918) at `defender-agent.js:41` — SSRF: explicit reference to cloud instance-metadata endpoint
|
|
70
|
+
- [low] **ssrf** (CWE-918) at `flow-narration.js:24` — SSRF: explicit reference to cloud instance-metadata endpoint
|
|
71
|
+
- [low] **ssrf** (CWE-918) at `verifier.js:55` — SSRF: explicit reference to cloud instance-metadata endpoint
|
|
72
|
+
|
|
73
|
+
## Attack trees
|
|
@@ -230,7 +230,7 @@ export function renderWalkthrough(fw, evaluation, opts = {}) {
|
|
|
230
230
|
lines.push('');
|
|
231
231
|
}
|
|
232
232
|
if (ev.status === 'absent' || ev.status === 'partial') {
|
|
233
|
-
lines.push(`**Remediation:** address the bullet(s) above, then re-run \`/
|
|
233
|
+
lines.push(`**Remediation:** address the bullet(s) above, then re-run \`/compliance --walkthrough ${fw.id}\` to update this report.`);
|
|
234
234
|
lines.push('');
|
|
235
235
|
}
|
|
236
236
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Corpus coverage / gap reporter (roadmap #10).
|
|
2
|
+
//
|
|
3
|
+
// "Scale the ground-truth corpus" is a data-acquisition effort, not a code one
|
|
4
|
+
// — but you can't prioritize what you can't see. This turns the CVE-replay
|
|
5
|
+
// corpus into an actionable coverage map: progress toward the target size, and
|
|
6
|
+
// exactly which CWE×language cells have zero ground-truth entries. It measures
|
|
7
|
+
// the existing corpus only; it never invents entries.
|
|
8
|
+
|
|
9
|
+
// The corpus's declared target families/languages (mirrors bench manifest).
|
|
10
|
+
export const TARGET_CWES = [
|
|
11
|
+
'CWE-22', 'CWE-78', 'CWE-79', 'CWE-89', 'CWE-90', 'CWE-94', 'CWE-113',
|
|
12
|
+
'CWE-327', 'CWE-329', 'CWE-338', 'CWE-352', 'CWE-502', 'CWE-601', 'CWE-611',
|
|
13
|
+
'CWE-643', 'CWE-798', 'CWE-916', 'CWE-918', 'CWE-1321', 'CWE-1333',
|
|
14
|
+
];
|
|
15
|
+
export const TARGET_LANGUAGES = [
|
|
16
|
+
'javascript', 'python', 'java', 'go', 'ruby', 'php', 'csharp', 'kotlin',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Pure analyzer. `entries` = [{ cwe, language, family, cve, tier? }, …].
|
|
20
|
+
export function analyzeCorpus(entries, opts = {}) {
|
|
21
|
+
const target = opts.target || 500;
|
|
22
|
+
const cwes = opts.cweFamilies || TARGET_CWES;
|
|
23
|
+
const langs = opts.languages || TARGET_LANGUAGES;
|
|
24
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
25
|
+
|
|
26
|
+
const byLanguage = {}, byCwe = {}, matrix = {};
|
|
27
|
+
for (const e of list) {
|
|
28
|
+
const lang = (e && e.language) || 'unknown';
|
|
29
|
+
const cwe = (e && e.cwe) || 'unknown';
|
|
30
|
+
byLanguage[lang] = (byLanguage[lang] || 0) + 1;
|
|
31
|
+
byCwe[cwe] = (byCwe[cwe] || 0) + 1;
|
|
32
|
+
(matrix[cwe] || (matrix[cwe] = {}))[lang] = (matrix[cwe][lang] || 0) + 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Empty cells in the target CWE×language matrix = prioritized gaps.
|
|
36
|
+
const gaps = [];
|
|
37
|
+
for (const cwe of cwes) {
|
|
38
|
+
for (const lang of langs) {
|
|
39
|
+
if (!(matrix[cwe] && matrix[cwe][lang])) gaps.push({ cwe, language: lang });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const cellsTotal = cwes.length * langs.length;
|
|
43
|
+
return {
|
|
44
|
+
total: list.length,
|
|
45
|
+
target,
|
|
46
|
+
progressPct: Math.round((list.length / target) * 100),
|
|
47
|
+
remainingToTarget: Math.max(0, target - list.length),
|
|
48
|
+
byLanguage,
|
|
49
|
+
byCwe,
|
|
50
|
+
matrix,
|
|
51
|
+
cellsTotal,
|
|
52
|
+
cellsCovered: cellsTotal - gaps.length,
|
|
53
|
+
gapCount: gaps.length,
|
|
54
|
+
gaps,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function summarizeCorpusStatus(r) {
|
|
59
|
+
if (!r) return 'corpus: (no data)';
|
|
60
|
+
const lines = [
|
|
61
|
+
`corpus: ${r.total}/${r.target} entries (${r.progressPct}%, ${r.remainingToTarget} to target)`,
|
|
62
|
+
`matrix: ${r.cellsCovered}/${r.cellsTotal} CWE×language cells covered (${r.gapCount} gaps)`,
|
|
63
|
+
];
|
|
64
|
+
// Top under-covered languages (fewest entries among target languages).
|
|
65
|
+
const langCounts = Object.entries(r.byLanguage).sort((a, b) => a[1] - b[1]);
|
|
66
|
+
if (langCounts.length) {
|
|
67
|
+
lines.push('thinnest languages: ' + langCounts.slice(0, 3).map(([l, n]) => `${l}=${n}`).join(', '));
|
|
68
|
+
}
|
|
69
|
+
// A few example gaps to act on first.
|
|
70
|
+
if (r.gaps.length) {
|
|
71
|
+
lines.push('example gaps: ' + r.gaps.slice(0, 6).map(g => `${g.cwe}/${g.language}`).join(', '));
|
|
72
|
+
}
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Analysis-coverage honesty report (roadmap #5 + #6).
|
|
2
|
+
//
|
|
3
|
+
// A "perfect" SAST scanner must publish its blind spots, not hide them. This
|
|
4
|
+
// module turns three things the engine already knows internally into an
|
|
5
|
+
// explicit, user-facing coverage summary:
|
|
6
|
+
//
|
|
7
|
+
// 1. Per-language ANALYSIS TIER — which languages in this scan got the full
|
|
8
|
+
// IR + taint engine vs. only pattern (regex) detectors. The taint engine
|
|
9
|
+
// is wired for js/ts/py/java/cs/kt/go/php/rb; everything else
|
|
10
|
+
// (c/c++/rust/swift/solidity/dart/…) is pattern-only today.
|
|
11
|
+
// 2. Files SKIPPED and why (too large, too dense, timed out).
|
|
12
|
+
// 3. UNMODELED-SINK candidates (#5) — calls that match a dangerous shape but
|
|
13
|
+
// have NO finding at their line, i.e. likely recall blind spots to verify.
|
|
14
|
+
//
|
|
15
|
+
// All of this is informational; it never changes a finding's severity.
|
|
16
|
+
|
|
17
|
+
// Languages that flow through the Layer-1 IR + Layer-2 taint engine.
|
|
18
|
+
export const IR_TAINT_LANGS = new Set(['js', 'ts', 'py', 'java', 'cs', 'kt', 'go', 'php', 'rb']);
|
|
19
|
+
|
|
20
|
+
const EXT_TO_LANG = {
|
|
21
|
+
js: 'js', jsx: 'js', mjs: 'js', cjs: 'js', ts: 'ts', tsx: 'ts',
|
|
22
|
+
py: 'py', pyw: 'py', java: 'java', go: 'go', rb: 'rb', php: 'php',
|
|
23
|
+
cs: 'cs', kt: 'kt', kts: 'kt', swift: 'swift', rs: 'rs',
|
|
24
|
+
c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp',
|
|
25
|
+
sol: 'sol', dart: 'dart', scala: 'scala', m: 'objc',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function langOfFile(file) {
|
|
29
|
+
if (typeof file !== 'string') return null;
|
|
30
|
+
const ext = file.split('.').pop().toLowerCase();
|
|
31
|
+
return EXT_TO_LANG[ext] || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Given the list of scanned files, report which languages got IR-taint vs.
|
|
35
|
+
// pattern-only analysis, with a per-language file count.
|
|
36
|
+
export function computeAnalysisTiers(fileList) {
|
|
37
|
+
const counts = {};
|
|
38
|
+
for (const f of fileList || []) {
|
|
39
|
+
const l = langOfFile(f);
|
|
40
|
+
if (!l) continue;
|
|
41
|
+
counts[l] = (counts[l] || 0) + 1;
|
|
42
|
+
}
|
|
43
|
+
const irTaint = {}, patternOnly = {};
|
|
44
|
+
for (const [lang, n] of Object.entries(counts)) {
|
|
45
|
+
(IR_TAINT_LANGS.has(lang) ? irTaint : patternOnly)[lang] = n;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
irTaint, // { js: 12, py: 4 }
|
|
49
|
+
patternOnly, // { rs: 3, sol: 1 } → flow analysis NOT applied
|
|
50
|
+
languages: Object.keys(counts).sort(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Dangerous-call shapes worth surfacing when no finding lands on their line.
|
|
55
|
+
// Deliberately conservative + cross-language; this is a "did we miss a sink?"
|
|
56
|
+
// prompt for the user, emitted at info level only.
|
|
57
|
+
const DANGER_TOKENS = [
|
|
58
|
+
[/\beval\s*\(/, 'eval'],
|
|
59
|
+
[/\bnew\s+Function\s*\(/, 'Function-constructor'],
|
|
60
|
+
[/\bvm\.\s*runIn\w+\s*\(/, 'vm.runIn*'],
|
|
61
|
+
[/\bchild_process\b|\bexecSync?\s*\(|\bspawnSync?\s*\(/, 'child_process'],
|
|
62
|
+
[/\bdeserialize\s*\(|readObject\s*\(|ObjectInputStream\b/, 'deserialize'],
|
|
63
|
+
[/\byaml\.\s*load\s*\(|\bsafe_load\b/, 'yaml.load'],
|
|
64
|
+
[/\bpickle\.\s*loads?\s*\(/, 'pickle.load'],
|
|
65
|
+
[/\bMarshal\.\s*load\b|\bunserialize\s*\(/, 'unserialize'],
|
|
66
|
+
[/\bRuntime\b[\s\S]{0,40}\.exec\s*\(|\bProcessBuilder\b/, 'Runtime.exec/ProcessBuilder'],
|
|
67
|
+
[/\bos\.\s*system\s*\(|\bsubprocess\.\s*(?:call|run|Popen)\s*\(/, 'os.system/subprocess'],
|
|
68
|
+
[/\.innerHTML\s*=|\bdangerouslySetInnerHTML\b/, 'innerHTML'],
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Scan file contents for danger tokens that have NO finding on their line.
|
|
72
|
+
// findings: array with {file,line}. fc: { path: content }. Returns a capped
|
|
73
|
+
// example list + total count. Lines are 1-based to match finding lines.
|
|
74
|
+
export function countUnmodeledSinkCandidates(fc, findings, opts = {}) {
|
|
75
|
+
const cap = opts.cap || 12;
|
|
76
|
+
const covered = new Set();
|
|
77
|
+
for (const f of findings || []) {
|
|
78
|
+
if (f && f.file && typeof f.line === 'number') covered.add(`${f.file}:${f.line}`);
|
|
79
|
+
}
|
|
80
|
+
const examples = [];
|
|
81
|
+
let count = 0;
|
|
82
|
+
for (const [file, content] of Object.entries(fc || {})) {
|
|
83
|
+
if (typeof content !== 'string') continue;
|
|
84
|
+
const lines = content.split('\n');
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
for (const [re, token] of DANGER_TOKENS) {
|
|
87
|
+
if (!re.test(lines[i])) continue;
|
|
88
|
+
if (covered.has(`${file}:${i + 1}`)) continue; // already flagged → modelled
|
|
89
|
+
count++;
|
|
90
|
+
if (examples.length < cap) examples.push({ file, line: i + 1, token });
|
|
91
|
+
break; // one token per line is enough
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { count, examples };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// One-line CLI summary of an analysis-coverage object.
|
|
99
|
+
export function summarizeCoverage(meta) {
|
|
100
|
+
if (!meta) return 'coverage: (none)';
|
|
101
|
+
const t = meta.analysisTier || {};
|
|
102
|
+
const irLangs = Object.keys(t.irTaint || {});
|
|
103
|
+
const patLangs = Object.keys(t.patternOnly || {});
|
|
104
|
+
const parts = [
|
|
105
|
+
`scanned=${meta.filesScanned ?? '?'}`,
|
|
106
|
+
`skipped=${(meta.filesSkipped || 0) + (meta.filesDenseSkipped || 0)}`,
|
|
107
|
+
`timedOut=${meta.filesTimedOut || 0}`,
|
|
108
|
+
`ir-taint=[${irLangs.join(',')}]`,
|
|
109
|
+
];
|
|
110
|
+
if (patLangs.length) parts.push(`pattern-only=[${patLangs.join(',')}]`);
|
|
111
|
+
if (meta.unmodeledSinkCandidates?.count) parts.push(`unmodeled-sink-candidates=${meta.unmodeledSinkCandidates.count}`);
|
|
112
|
+
return parts.join(' · ');
|
|
113
|
+
}
|
|
@@ -23,6 +23,25 @@ import { brierScore, computeBrierOnHeldOut, wilsonInterval } from './calibration
|
|
|
23
23
|
|
|
24
24
|
const ECE_BINS_DEFAULT = 10;
|
|
25
25
|
|
|
26
|
+
// Map a labeled sample to a source language. Prefer an explicit `language`
|
|
27
|
+
// field; otherwise derive it from the `file` extension. Keeps per-language
|
|
28
|
+
// metrics meaningful even for older corpora that only carry a file path.
|
|
29
|
+
const EXT_TO_LANG = {
|
|
30
|
+
js: 'js', jsx: 'js', mjs: 'js', cjs: 'js', ts: 'ts', tsx: 'ts',
|
|
31
|
+
py: 'py', java: 'java', go: 'go', rb: 'rb', php: 'php',
|
|
32
|
+
cs: 'cs', kt: 'kt', kts: 'kt', swift: 'swift', rs: 'rs',
|
|
33
|
+
c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp',
|
|
34
|
+
sol: 'sol', dart: 'dart',
|
|
35
|
+
};
|
|
36
|
+
export function languageOf(o) {
|
|
37
|
+
if (o && typeof o.language === 'string' && o.language) return o.language;
|
|
38
|
+
if (o && typeof o.file === 'string') {
|
|
39
|
+
const ext = o.file.split('.').pop().toLowerCase();
|
|
40
|
+
if (EXT_TO_LANG[ext]) return EXT_TO_LANG[ext];
|
|
41
|
+
}
|
|
42
|
+
return 'unknown';
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
export function parseLabeledJsonl(text) {
|
|
27
46
|
if (typeof text !== 'string' || !text.length) return [];
|
|
28
47
|
const out = [];
|
|
@@ -39,6 +58,7 @@ export function parseLabeledJsonl(text) {
|
|
|
39
58
|
if (p === null || a === null) continue;
|
|
40
59
|
out.push({
|
|
41
60
|
family: typeof o.family === 'string' ? o.family : 'unknown',
|
|
61
|
+
language: languageOf(o),
|
|
42
62
|
stableId: typeof o.stableId === 'string' ? o.stableId : null,
|
|
43
63
|
predicted: Math.max(0, Math.min(1, p)),
|
|
44
64
|
actual: a,
|
|
@@ -98,6 +118,27 @@ export function perFamily(samples) {
|
|
|
98
118
|
return fams;
|
|
99
119
|
}
|
|
100
120
|
|
|
121
|
+
// Per-language precision/recall breakdown (roadmap #9). The "perfect
|
|
122
|
+
// multi-language SAST" claim is only defensible with per-language ground
|
|
123
|
+
// truth — a corpus that's 90% JS can hide poor Ruby precision behind a
|
|
124
|
+
// healthy aggregate. Mirrors perFamily, keyed by sample.language, and adds
|
|
125
|
+
// a per-language precision so a regression in one language is visible.
|
|
126
|
+
export function perLanguage(samples) {
|
|
127
|
+
const langs = {};
|
|
128
|
+
for (const s of samples) {
|
|
129
|
+
const l = s.language || 'unknown';
|
|
130
|
+
if (!langs[l]) langs[l] = { tp: 0, fp: 0, n: 0, precision: 0 };
|
|
131
|
+
langs[l].n++;
|
|
132
|
+
if (s.actual === 1) langs[l].tp++;
|
|
133
|
+
else if (s.actual === 0) langs[l].fp++;
|
|
134
|
+
}
|
|
135
|
+
for (const l of Object.keys(langs)) {
|
|
136
|
+
const { tp, fp } = langs[l];
|
|
137
|
+
langs[l].precision = (tp + fp) > 0 ? Number((tp / (tp + fp)).toFixed(4)) : 0;
|
|
138
|
+
}
|
|
139
|
+
return langs;
|
|
140
|
+
}
|
|
141
|
+
|
|
101
142
|
// One-shot evaluation: Brier + ECE + per-family TP/FP + overall precision.
|
|
102
143
|
// Returns null only when there's truly no data; never returns a tautological
|
|
103
144
|
// zero.
|
|
@@ -124,14 +165,30 @@ export function evaluateHeldOut(samples) {
|
|
|
124
165
|
precision,
|
|
125
166
|
precisionCi95: ci,
|
|
126
167
|
perFamily: fams,
|
|
168
|
+
perLanguage: perLanguage(samples),
|
|
127
169
|
notes: [
|
|
128
170
|
...(samples.length < 100 ? ['n<100: Brier and ECE have wide confidence; treat as directional, not decision-grade.'] : []),
|
|
129
171
|
...(brierR.brier !== null && brierR.brier > 0.10 ? [`brier=${brierR.brier.toFixed(3)} exceeds PRD target 0.10`] : []),
|
|
130
172
|
...(ece && ece.ece > 0.05 ? [`ece=${ece.ece.toFixed(3)} exceeds 0.05 calibration target`] : []),
|
|
173
|
+
// Per-language regression surfacing: a language with enough samples
|
|
174
|
+
// whose precision trails the aggregate by >0.15 is the failure the
|
|
175
|
+
// aggregate would otherwise mask.
|
|
176
|
+
...Object.entries(perLanguage(samples))
|
|
177
|
+
.filter(([, m]) => m.n >= 20 && precision - m.precision > 0.15)
|
|
178
|
+
.map(([lang, m]) => `language '${lang}' precision=${m.precision.toFixed(3)} (n=${m.n}) trails aggregate ${precision.toFixed(3)}`),
|
|
131
179
|
],
|
|
132
180
|
};
|
|
133
181
|
}
|
|
134
182
|
|
|
183
|
+
// Per-language dashboard line: one row per language, sorted by sample count.
|
|
184
|
+
export function summarizePerLanguage(result) {
|
|
185
|
+
if (!result || !result.ok || !result.perLanguage) return 'per-language: no data';
|
|
186
|
+
return Object.entries(result.perLanguage)
|
|
187
|
+
.sort((a, b) => b[1].n - a[1].n)
|
|
188
|
+
.map(([lang, m]) => `${lang}: precision=${m.precision.toFixed(3)} (tp=${m.tp} fp=${m.fp} n=${m.n})`)
|
|
189
|
+
.join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
135
192
|
// CLI-friendly summary line.
|
|
136
193
|
export function summarize(result) {
|
|
137
194
|
if (!result || !result.ok) return `held-out: ${result?.reason || 'unknown error'}`;
|
|
@@ -141,7 +141,7 @@ export function augmentPrBody(scanRoot, opts = {}) {
|
|
|
141
141
|
lines.push('');
|
|
142
142
|
|
|
143
143
|
if (!baseline) {
|
|
144
|
-
lines.push(`> Baseline against \`${baselineRef}\` not found — showing the full current scan as added. Run \`/pr
|
|
144
|
+
lines.push(`> Baseline against \`${baselineRef}\` not found — showing the full current scan as added. Run \`/compliance --pr --persist-baseline ${baselineRef}\` from \`${baselineRef}\` to enable diff mode.`);
|
|
145
145
|
lines.push('');
|
|
146
146
|
}
|
|
147
147
|
|
package/src/posture/router.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// Decision tree (cheap, no scan):
|
|
8
8
|
// - No prior scan? → run /scan first
|
|
9
9
|
// - Prior scan, criticals open? → run /fix --all --critical
|
|
10
|
-
// - Prior scan, highs open? → /fix --all --high OR /show
|
|
11
|
-
// - Prior scan, only mediums? → /report-card
|
|
10
|
+
// - Prior scan, highs open? → /fix --all --high OR /triage --show
|
|
11
|
+
// - Prior scan, only mediums? → /posture --report-card
|
|
12
12
|
// - All clean? → /security-badge (celebrate + share)
|
|
13
13
|
// - Pre-deploy intent (--launch flag, or no scan in 7 days)? → /launch-check
|
|
14
14
|
//
|
|
@@ -27,7 +27,34 @@ function ageHours(fp) {
|
|
|
27
27
|
return (Date.now() - fs.statSync(fp).mtimeMs) / 3_600_000;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// Compare the two most-recent scan-history entries to tell the user whether
|
|
31
|
+
// they're trending safer or more exposed. Returns {} when there isn't enough
|
|
32
|
+
// history to say anything honest. (premortem: never invent a trend from one
|
|
33
|
+
// data point.)
|
|
34
|
+
export function computeScanTrend(scanRoot) {
|
|
35
|
+
const hist = readJson(path.join(scanRoot, '.agentic-security', 'scan-history.json'));
|
|
36
|
+
if (!Array.isArray(hist) || hist.length < 2) return {};
|
|
37
|
+
const cur = hist[hist.length - 1];
|
|
38
|
+
const prev = hist[hist.length - 2];
|
|
39
|
+
const sevOf = (e) => (e?.critical || 0) + (e?.high || 0);
|
|
40
|
+
const delta = sevOf(cur) - sevOf(prev);
|
|
41
|
+
let trend = 'flat';
|
|
42
|
+
if (delta < 0) trend = 'improving';
|
|
43
|
+
else if (delta > 0) trend = 'regressing';
|
|
44
|
+
const n = Math.abs(delta);
|
|
45
|
+
const whatChanged =
|
|
46
|
+
trend === 'improving' ? `${n} fewer critical+high than last scan — keep going.`
|
|
47
|
+
: trend === 'regressing' ? `${n} more critical+high than last scan — new risk crept in.`
|
|
48
|
+
: 'No change in critical+high since last scan.';
|
|
49
|
+
return { trend, whatChanged };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function decide(opts) {
|
|
53
|
+
const base = baseDecision(opts);
|
|
54
|
+
return { ...base, ...computeScanTrend(opts.scanRoot) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function baseDecision({ scanRoot, intent }) {
|
|
31
58
|
const stateDir = path.join(scanRoot, '.agentic-security');
|
|
32
59
|
const lastScan = readJson(path.join(stateDir, 'last-scan.json'));
|
|
33
60
|
const scanAge = ageHours(path.join(stateDir, 'last-scan.json'));
|
|
@@ -71,7 +98,7 @@ export function decide({ scanRoot, intent }) {
|
|
|
71
98
|
if (sev.high > 0) {
|
|
72
99
|
return {
|
|
73
100
|
action: 'review-high',
|
|
74
|
-
command: 'claude /show
|
|
101
|
+
command: 'claude /triage --show',
|
|
75
102
|
reason: `${sev.high} high finding(s). Review and triage before fixing.`,
|
|
76
103
|
};
|
|
77
104
|
}
|
|
@@ -85,7 +112,7 @@ export function decide({ scanRoot, intent }) {
|
|
|
85
112
|
if (sev.medium > 0) {
|
|
86
113
|
return {
|
|
87
114
|
action: 'report-card',
|
|
88
|
-
command: 'claude /report-card',
|
|
115
|
+
command: 'claude /posture --report-card',
|
|
89
116
|
reason: `Only mediums remain. Get a letter-grade snapshot and pick what's worth fixing.`,
|
|
90
117
|
};
|
|
91
118
|
}
|
|
@@ -102,8 +129,11 @@ export function explain(decision) {
|
|
|
102
129
|
``,
|
|
103
130
|
` Action: ${decision.action}`,
|
|
104
131
|
` Why: ${decision.reason}`,
|
|
105
|
-
` Run: ${decision.command}`,
|
|
106
|
-
``,
|
|
107
132
|
];
|
|
133
|
+
if (decision.trend) {
|
|
134
|
+
const arrow = decision.trend === 'improving' ? '↓' : decision.trend === 'regressing' ? '↑' : '→';
|
|
135
|
+
lines.push(` Trend: ${arrow} ${decision.whatChanged}`);
|
|
136
|
+
}
|
|
137
|
+
lines.push(` Run: ${decision.command}`, ``);
|
|
108
138
|
return lines.join('\n');
|
|
109
139
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Data Protection Impact Assessment (DPIA)
|
|
2
|
+
|
|
3
|
+
Generated by agentic-security scanner on 2026-05-31.
|
|
4
|
+
|
|
5
|
+
This is an automated DPIA scaffold derived from static analysis.
|
|
6
|
+
It must be reviewed and completed by a privacy officer before use.
|
|
7
|
+
|
|
8
|
+
## Data classes identified
|
|
9
|
+
|
|
10
|
+
## Privacy-related findings
|
|
11
|
+
|
|
12
|
+
| Severity | File:Line | Class → Sink | Description |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
|
|
15
|
+
## Regulatory framework mapping
|
|
16
|
+
|
|
17
|
+
- **GDPR Art. 35** — DPIA required when processing is likely to result in high risk to data subjects.
|
|
18
|
+
- **CCPA §1798.130** — Notice + access rights for collected personal information.
|
|
19
|
+
|
|
20
|
+
## Reviewer checklist
|
|
21
|
+
|
|
22
|
+
- [ ] Confirm each PII field's collection has a documented lawful basis
|
|
23
|
+
- [ ] Confirm retention period for each class is documented
|
|
24
|
+
- [ ] Confirm DSAR (data subject access request) workflow exists
|
|
25
|
+
- [ ] Confirm encryption at rest + in transit for each class
|
|
26
|
+
- [ ] Confirm logging of PII access for audit (where applicable)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Threat Model (auto-generated)
|
|
2
|
+
|
|
3
|
+
Generated by agentic-security on 2026-05-31.
|
|
4
|
+
|
|
5
|
+
This threat model is derived from static analysis of the current codebase and is regenerated on every scan. It is intended as a working artifact, not a finished compliance document.
|
|
6
|
+
|
|
7
|
+
## Entities + boundaries
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
flowchart TB
|
|
11
|
+
subgraph External
|
|
12
|
+
end
|
|
13
|
+
subgraph Application
|
|
14
|
+
end
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Assets
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## STRIDE threats
|
|
21
|
+
|
|
22
|
+
## Attack trees
|
package/src/report/index.js
CHANGED
|
@@ -973,7 +973,7 @@ export function toShipVerdict(scan, options = {}) {
|
|
|
973
973
|
// CONFIRMED: surface validator-confirmed criticals as a trust signal —
|
|
974
974
|
// distinguishes "tool said so" from "tool built a PoC and it ran."
|
|
975
975
|
if (confirmedCount > 0) {
|
|
976
|
-
lines.push(c(` ✓ ${confirmedCount} CONFIRMED (PoC built by /validate
|
|
976
|
+
lines.push(c(` ✓ ${confirmedCount} CONFIRMED (PoC built by /triage --validate)`, '\x1b[1;32m'));
|
|
977
977
|
}
|
|
978
978
|
lines.push('');
|
|
979
979
|
|
|
@@ -1001,6 +1001,23 @@ export function toShipVerdict(scan, options = {}) {
|
|
|
1001
1001
|
} else if (advisoryCount > 0) {
|
|
1002
1002
|
lines.push(c(` ${advisoryCount} advisory item${advisoryCount === 1 ? '' : 's'} — run /security-scan-all --firehose to see them.`, DIM));
|
|
1003
1003
|
}
|
|
1004
|
+
// Coverage-honesty line (#5/#6): show the scan's blind spots — which
|
|
1005
|
+
// languages got flow analysis vs pattern-only, what was skipped, and how
|
|
1006
|
+
// many dangerous-looking calls had no finding. One concise line, not bloat.
|
|
1007
|
+
const _covMeta = scan._scanMeta;
|
|
1008
|
+
if (_covMeta && _covMeta.analysisTier) {
|
|
1009
|
+
const t = _covMeta.analysisTier;
|
|
1010
|
+
const pat = Object.keys(t.patternOnly || {});
|
|
1011
|
+
const segs = [`${_covMeta.filesScanned ?? '?'} files`];
|
|
1012
|
+
const ir = Object.keys(t.irTaint || {});
|
|
1013
|
+
if (ir.length) segs.push(`flow=[${ir.join(',')}]`);
|
|
1014
|
+
if (pat.length) segs.push(`pattern-only=[${pat.join(',')}]`);
|
|
1015
|
+
const skipped = (_covMeta.filesSkipped || 0) + (_covMeta.filesDenseSkipped || 0);
|
|
1016
|
+
if (skipped) segs.push(`${skipped} skipped`);
|
|
1017
|
+
if (_covMeta.unmodeledSinkCandidates?.count) segs.push(`${_covMeta.unmodeledSinkCandidates.count} unmodeled-sink candidate${_covMeta.unmodeledSinkCandidates.count === 1 ? '' : 's'}`);
|
|
1018
|
+
lines.push('');
|
|
1019
|
+
lines.push(c(' Coverage: ' + segs.join(' · '), DIM));
|
|
1020
|
+
}
|
|
1004
1021
|
lines.push('');
|
|
1005
1022
|
lines.push(c(' 🛡 agentic-security · created by ClearCapabilities.Com', DIM));
|
|
1006
1023
|
return lines.join('\n');
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Data Protection Impact Assessment (DPIA)
|
|
2
|
+
|
|
3
|
+
Generated by agentic-security scanner on 2026-06-01.
|
|
4
|
+
|
|
5
|
+
This is an automated DPIA scaffold derived from static analysis.
|
|
6
|
+
It must be reviewed and completed by a privacy officer before use.
|
|
7
|
+
|
|
8
|
+
## Data classes identified
|
|
9
|
+
|
|
10
|
+
## Privacy-related findings
|
|
11
|
+
|
|
12
|
+
| Severity | File:Line | Class → Sink | Description |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
|
|
15
|
+
## Regulatory framework mapping
|
|
16
|
+
|
|
17
|
+
- **GDPR Art. 35** — DPIA required when processing is likely to result in high risk to data subjects.
|
|
18
|
+
- **CCPA §1798.130** — Notice + access rights for collected personal information.
|
|
19
|
+
|
|
20
|
+
## Reviewer checklist
|
|
21
|
+
|
|
22
|
+
- [ ] Confirm each PII field's collection has a documented lawful basis
|
|
23
|
+
- [ ] Confirm retention period for each class is documented
|
|
24
|
+
- [ ] Confirm DSAR (data subject access request) workflow exists
|
|
25
|
+
- [ ] Confirm encryption at rest + in transit for each class
|
|
26
|
+
- [ ] Confirm logging of PII access for audit (where applicable)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generatedAt": "2026-06-01T03:15:31.819Z",
|
|
3
|
+
"summary": {
|
|
4
|
+
"total": 1,
|
|
5
|
+
"hndlCritical": 1,
|
|
6
|
+
"standard": 0,
|
|
7
|
+
"filesAffected": 1,
|
|
8
|
+
"primitivesNeeded": [
|
|
9
|
+
"ML-KEM-768"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"milestones": [
|
|
13
|
+
{
|
|
14
|
+
"id": "M1",
|
|
15
|
+
"title": "Inventory & policy",
|
|
16
|
+
"target": "90 days",
|
|
17
|
+
"owner": "security",
|
|
18
|
+
"items": [
|
|
19
|
+
"Confirm scanner findings against design docs",
|
|
20
|
+
"Adopt PQC migration policy (CNSA 2.0 / NIST IR 8547 alignment)",
|
|
21
|
+
"Establish KMS support for hybrid keys"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "M2",
|
|
26
|
+
"title": "HNDL-critical paths to PQ-hybrid",
|
|
27
|
+
"target": "180 days",
|
|
28
|
+
"owner": "platform",
|
|
29
|
+
"items": [
|
|
30
|
+
{
|
|
31
|
+
"finding": "pqc-rsa-encrypt:post-quantum-crypto.js:136",
|
|
32
|
+
"file": "post-quantum-crypto.js",
|
|
33
|
+
"line": 136,
|
|
34
|
+
"replacement": "X25519+ML-KEM-768 (CNSA 2.0 transitional)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "M3",
|
|
40
|
+
"title": "Standard signing/KEX migration",
|
|
41
|
+
"target": "12 months",
|
|
42
|
+
"owner": "platform",
|
|
43
|
+
"items": []
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "M4",
|
|
47
|
+
"title": "Deprecate classical primitives",
|
|
48
|
+
"target": "24 months",
|
|
49
|
+
"owner": "security",
|
|
50
|
+
"items": [
|
|
51
|
+
"Remove dual-stack libraries once peers are PQ-capable",
|
|
52
|
+
"Rotate root CA / long-lived signing keys to ML-DSA"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
"perFile": {
|
|
57
|
+
"post-quantum-crypto.js": {
|
|
58
|
+
"count": 1,
|
|
59
|
+
"subfamilies": [
|
|
60
|
+
"pqc-rsa-encrypt"
|
|
61
|
+
],
|
|
62
|
+
"hndlCritical": true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|