@clear-capabilities/agentic-security-scanner 0.80.0 → 0.84.1
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/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/839.index.js +170 -0
- package/dist/985.index.js +51 -1
- package/dist/agentic-security.mjs +83 -83
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/.agentic-security/findings.json +21283 -8189
- package/src/.agentic-security/last-scan.json +21283 -8189
- package/src/.agentic-security/last-scan.json.sig +1 -1
- package/src/.agentic-security/scan-history.json +512 -128
- package/src/.agentic-security/streak.json +3 -3
- package/src/engine.js +41 -0
- package/src/mcp/.agentic-security/findings.json +4 -4
- package/src/mcp/.agentic-security/last-scan.json +4 -4
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -1
- package/src/mcp/.agentic-security/scan-history.json +188 -0
- package/src/mcp/.agentic-security/streak.json +5 -5
- package/src/mcp/tools.js +51 -1
- package/src/posture/.agentic-security/findings.json +17234 -4057
- package/src/posture/.agentic-security/last-scan.json +17234 -4057
- package/src/posture/.agentic-security/last-scan.json.sig +1 -1
- package/src/posture/.agentic-security/scan-history.json +1942 -200
- package/src/posture/.agentic-security/streak.json +3 -3
- package/src/posture/auditor-walkthrough.js +252 -0
- package/src/posture/claude-authorship.js +197 -0
- package/src/posture/compliance-frameworks/.agentic-security/findings.json +80 -0
- package/src/posture/compliance-frameworks/.agentic-security/last-scan.json +80 -0
- package/src/posture/compliance-frameworks/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/compliance-frameworks/.agentic-security/scan-history.json +90 -0
- package/src/posture/compliance-frameworks/.agentic-security/streak.json +22 -0
- package/src/posture/compliance-frameworks/ccpa.json +32 -0
- package/src/posture/compliance-frameworks/eu-ai-act.json +51 -0
- package/src/posture/compliance-frameworks/gdpr.json +45 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +56 -0
- package/src/posture/compliance-frameworks/nist-ai-600-1.json +51 -0
- package/src/posture/compliance-frameworks/nist-csf-2.json +73 -0
- package/src/posture/compliance-frameworks/owasp-asvs-5.json +79 -0
- package/src/posture/compliance-frameworks/owasp-llm-top-10.json +69 -0
- package/src/posture/cross-repo-memory.js +180 -0
- package/src/posture/dep-add-guard.js +197 -0
- package/src/posture/findings-memory.js +152 -0
- package/src/posture/fix-style-mirror.js +118 -0
- package/src/posture/git-history.js +141 -0
- package/src/posture/intent-context.js +175 -0
- package/src/posture/model-rescan.js +76 -0
- package/src/posture/pattern-propagation.js +39 -0
- package/src/posture/pr-augment.js +234 -0
- package/src/posture/risk-dollars.js +158 -0
- package/src/posture/threat-model-grounding.js +169 -0
- package/src/posture/time-to-fix.js +129 -0
- package/src/posture/triage-memory.js +151 -0
- package/src/posture/triage.js +15 -1
- package/src/posture/watch-mode.js +171 -0
- package/src/posture/workflow-installer.js +231 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"firstScanDate": "2026-05-28T17:47:06.515Z",
|
|
3
|
-
"lastScanDate": "2026-05-
|
|
4
|
-
"totalScans":
|
|
3
|
+
"lastScanDate": "2026-05-29T22:40:15.935Z",
|
|
4
|
+
"totalScans": 63,
|
|
5
5
|
"daysCleanCritical": 0,
|
|
6
6
|
"lastCleanDate": null,
|
|
7
7
|
"lastCriticalDate": "2026-05-29",
|
|
8
8
|
"hasEverHadCritical": true,
|
|
9
9
|
"bestDaysCleanCritical": 0,
|
|
10
10
|
"totalFindingsAtFirstScan": 256,
|
|
11
|
-
"totalFindingsAtLastScan":
|
|
11
|
+
"totalFindingsAtLastScan": 381,
|
|
12
12
|
"totalFixesInferred": 0,
|
|
13
13
|
"lastGrade": "C",
|
|
14
14
|
"bestGrade": "C",
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// Auditor-walkthrough generator.
|
|
2
|
+
//
|
|
3
|
+
// Produces a step-by-step narrative an engineering team can follow to
|
|
4
|
+
// demonstrate evidence for a compliance framework's controls to an
|
|
5
|
+
// external auditor.
|
|
6
|
+
//
|
|
7
|
+
// Built-in frameworks (all public-domain — no copyrighted text reproduced):
|
|
8
|
+
//
|
|
9
|
+
// nist-csf-2 NIST Cybersecurity Framework 2.0
|
|
10
|
+
// nist-ai-600-1 NIST AI Risk Management Framework, GenAI profile
|
|
11
|
+
// owasp-asvs-5 OWASP Application Security Verification Standard 5.0
|
|
12
|
+
// owasp-llm-top-10 OWASP Top 10 for LLM Applications 2025
|
|
13
|
+
// eu-ai-act EU AI Act (Regulation 2024/1689)
|
|
14
|
+
// gdpr General Data Protection Regulation
|
|
15
|
+
// hipaa-security-rule HIPAA Security Rule (45 CFR Part 164)
|
|
16
|
+
// ccpa California Consumer Privacy Act
|
|
17
|
+
//
|
|
18
|
+
// Proprietary frameworks (SOC2 Trust Services Criteria, ISO 27001/27002,
|
|
19
|
+
// PCI-DSS, HITRUST CSF) are intentionally NOT bundled because their
|
|
20
|
+
// control text is copyrighted by their respective publishers. For those,
|
|
21
|
+
// the BYO mechanism is:
|
|
22
|
+
//
|
|
23
|
+
// .agentic-security/compliance/<framework>/controls.json
|
|
24
|
+
//
|
|
25
|
+
// User supplies their own control mapping in the same shape as the
|
|
26
|
+
// bundled ones. The auditor-walkthrough renders evidence against it.
|
|
27
|
+
//
|
|
28
|
+
// Disclaimer: this module organizes scanner evidence into a narrative.
|
|
29
|
+
// It does not certify compliance. A licensed assessor (CPA / auditor /
|
|
30
|
+
// DPO) is responsible for the final attestation.
|
|
31
|
+
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
|
|
35
|
+
const BUNDLED_DIR = path.join(path.dirname(new URL(import.meta.url).pathname), 'compliance-frameworks');
|
|
36
|
+
const STATE = '.agentic-security';
|
|
37
|
+
|
|
38
|
+
function _readJson(fp) {
|
|
39
|
+
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List the available frameworks (bundled + project-byo).
|
|
44
|
+
*/
|
|
45
|
+
export function listFrameworks(scanRoot) {
|
|
46
|
+
const out = [];
|
|
47
|
+
try {
|
|
48
|
+
for (const fn of fs.readdirSync(BUNDLED_DIR)) {
|
|
49
|
+
if (!fn.endsWith('.json')) continue;
|
|
50
|
+
const fw = _readJson(path.join(BUNDLED_DIR, fn));
|
|
51
|
+
if (fw && fw.id) out.push({ id: fw.id, name: fw.name, source: 'bundled', license: fw.license });
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
if (scanRoot) {
|
|
55
|
+
const projDir = path.join(scanRoot, STATE, 'compliance');
|
|
56
|
+
if (fs.existsSync(projDir)) {
|
|
57
|
+
try {
|
|
58
|
+
for (const sub of fs.readdirSync(projDir)) {
|
|
59
|
+
const fp = path.join(projDir, sub, 'controls.json');
|
|
60
|
+
if (fs.existsSync(fp)) {
|
|
61
|
+
const fw = _readJson(fp);
|
|
62
|
+
if (fw && fw.id) out.push({ id: fw.id, name: fw.name, source: 'project', license: fw.license || 'user-provided' });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load a framework definition by id. Project BYO overrides bundled.
|
|
73
|
+
*/
|
|
74
|
+
export function loadFramework(scanRoot, id) {
|
|
75
|
+
if (scanRoot) {
|
|
76
|
+
const projFp = path.join(scanRoot, STATE, 'compliance', id, 'controls.json');
|
|
77
|
+
if (fs.existsSync(projFp)) return _readJson(projFp);
|
|
78
|
+
}
|
|
79
|
+
for (const fn of fs.readdirSync(BUNDLED_DIR)) {
|
|
80
|
+
if (!fn.endsWith('.json')) continue;
|
|
81
|
+
const fw = _readJson(path.join(BUNDLED_DIR, fn));
|
|
82
|
+
if (fw && fw.id === id) return fw;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* For each control, evaluate evidence against the current scan.
|
|
89
|
+
*
|
|
90
|
+
* Returns an array of:
|
|
91
|
+
* { control, status, observations[] }
|
|
92
|
+
*
|
|
93
|
+
* Status:
|
|
94
|
+
* 'present' — all mapsTo families have zero open critical findings AND
|
|
95
|
+
* module artifacts exist
|
|
96
|
+
* 'partial' — some signal present but with open issues
|
|
97
|
+
* 'absent' — no signal / open critical findings on every mapsTo family
|
|
98
|
+
* 'manual' — control has no mapsTo (requires manual attestation)
|
|
99
|
+
*/
|
|
100
|
+
export function evaluateFramework(scanRoot, fw, scan) {
|
|
101
|
+
const findings = (scan && Array.isArray(scan.findings)) ? scan.findings : [];
|
|
102
|
+
const components = (scan && Array.isArray(scan.components)) ? scan.components : [];
|
|
103
|
+
const families = new Map();
|
|
104
|
+
for (const f of findings) {
|
|
105
|
+
const k = f.family || 'unknown';
|
|
106
|
+
if (!families.has(k)) families.set(k, []);
|
|
107
|
+
families.get(k).push(f);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const results = [];
|
|
111
|
+
for (const c of fw.controls || []) {
|
|
112
|
+
const obs = [];
|
|
113
|
+
let status = 'manual';
|
|
114
|
+
const maps = Array.isArray(c.mapsTo) ? c.mapsTo : [];
|
|
115
|
+
|
|
116
|
+
if (maps.length === 0) {
|
|
117
|
+
obs.push('No automated mapping — requires manual evidence collection.');
|
|
118
|
+
results.push({ control: c, status, observations: obs });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let allCleared = true;
|
|
123
|
+
let anySignal = false;
|
|
124
|
+
for (const m of maps) {
|
|
125
|
+
if (m.startsWith('family:')) {
|
|
126
|
+
const fam = m.slice('family:'.length).split(':')[0];
|
|
127
|
+
const open = (families.get(fam) || []).filter(f => !f.intentSuppressed && !f.pastDecision && (f.severity === 'critical' || f.severity === 'high'));
|
|
128
|
+
if (open.length) {
|
|
129
|
+
allCleared = false;
|
|
130
|
+
obs.push(`${open.length} open ${fam} finding(s) at high/critical.`);
|
|
131
|
+
} else {
|
|
132
|
+
obs.push(`✓ ${fam}: no open critical/high findings.`);
|
|
133
|
+
}
|
|
134
|
+
anySignal = true;
|
|
135
|
+
} else if (m.startsWith('module:')) {
|
|
136
|
+
const mod = m.slice('module:'.length);
|
|
137
|
+
const ARTIFACT = {
|
|
138
|
+
'sbom-diff': 'sbom-history/',
|
|
139
|
+
'license-attributions': 'ATTRIBUTIONS.md',
|
|
140
|
+
'threat-model-auto': 'threat-model.json',
|
|
141
|
+
'compliance-policy': 'compliance-evidence.json',
|
|
142
|
+
'mcp-audit': 'mcp-audit.log',
|
|
143
|
+
'fix-history': 'fix-history/log.json',
|
|
144
|
+
'privacy-taint': 'dpia.md',
|
|
145
|
+
'aibom': 'aibom.json',
|
|
146
|
+
'attack-taxonomy': 'last-scan.json',
|
|
147
|
+
'why-fired': 'last-scan.json',
|
|
148
|
+
'scan-history': 'scan-history/',
|
|
149
|
+
'integrity': 'last-scan.json.sig',
|
|
150
|
+
'watch-mode': 'watch-status.json',
|
|
151
|
+
'cve-alert-daemon': 'cve-alerts/',
|
|
152
|
+
'triage': 'triage.json',
|
|
153
|
+
'triage-memory': 'triage-memory.jsonl',
|
|
154
|
+
'verifier': 'verifier-runs/',
|
|
155
|
+
'calibration': 'calibration-seed.json',
|
|
156
|
+
'holdout-eval': 'holdout-eval.jsonl',
|
|
157
|
+
'sigstore-verify': 'sigstore-attestations/',
|
|
158
|
+
'pre-edit-bodyguard': '.../hooks/pre-edit-bodyguard.js',
|
|
159
|
+
'apply-fix': 'fix-history/log.json',
|
|
160
|
+
'security-fixer': '.../agents/security-fixer.md',
|
|
161
|
+
'mcp-tools': '.../scanner/src/mcp/tools.js',
|
|
162
|
+
};
|
|
163
|
+
const target = ARTIFACT[mod];
|
|
164
|
+
if (target && fs.existsSync(path.join(scanRoot, STATE, target))) {
|
|
165
|
+
obs.push(`✓ ${mod}: ${target} present.`);
|
|
166
|
+
anySignal = true;
|
|
167
|
+
} else {
|
|
168
|
+
obs.push(`✗ ${mod}: expected ${target || '(unmapped)'} not present.`);
|
|
169
|
+
allCleared = false;
|
|
170
|
+
}
|
|
171
|
+
} else if (m.startsWith('rule:')) {
|
|
172
|
+
// Could check whether a custom rule fires zero — leave a hint for now.
|
|
173
|
+
obs.push(`(rule mapping) ${m} — verify manually that the bodyguard rule is enabled.`);
|
|
174
|
+
anySignal = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!anySignal) status = 'manual';
|
|
179
|
+
else if (allCleared) status = 'present';
|
|
180
|
+
else status = 'partial';
|
|
181
|
+
|
|
182
|
+
results.push({ control: c, status, observations: obs });
|
|
183
|
+
}
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Render the walkthrough Markdown narrative.
|
|
189
|
+
*/
|
|
190
|
+
export function renderWalkthrough(fw, evaluation, opts = {}) {
|
|
191
|
+
const lines = [];
|
|
192
|
+
lines.push(`# Auditor walkthrough — ${fw.name}`);
|
|
193
|
+
lines.push('');
|
|
194
|
+
lines.push(`> Publisher: ${fw.publisher}`);
|
|
195
|
+
lines.push(`> License: ${fw.license}`);
|
|
196
|
+
if (fw.url) lines.push(`> Source: ${fw.url}`);
|
|
197
|
+
lines.push('');
|
|
198
|
+
lines.push('> **This walkthrough organizes scanner evidence into a narrative for an external auditor.** It does NOT certify compliance. A licensed assessor is responsible for the final attestation.');
|
|
199
|
+
lines.push('');
|
|
200
|
+
|
|
201
|
+
const present = evaluation.filter(e => e.status === 'present').length;
|
|
202
|
+
const partial = evaluation.filter(e => e.status === 'partial').length;
|
|
203
|
+
const absent = evaluation.filter(e => e.status === 'absent').length;
|
|
204
|
+
const manual = evaluation.filter(e => e.status === 'manual').length;
|
|
205
|
+
const total = evaluation.length;
|
|
206
|
+
lines.push(`## Summary`);
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(`Controls evaluated: **${total}**`);
|
|
209
|
+
lines.push(`- ✅ Evidence present: **${present}**`);
|
|
210
|
+
lines.push(`- 🟡 Partial evidence: **${partial}**`);
|
|
211
|
+
lines.push(`- ⛔ No evidence: **${absent}**`);
|
|
212
|
+
lines.push(`- 📝 Manual attestation required: **${manual}**`);
|
|
213
|
+
lines.push('');
|
|
214
|
+
|
|
215
|
+
lines.push(`## Controls — step by step`);
|
|
216
|
+
lines.push('');
|
|
217
|
+
for (const ev of evaluation) {
|
|
218
|
+
const c = ev.control;
|
|
219
|
+
const glyph = { present: '✅', partial: '🟡', absent: '⛔', manual: '📝' }[ev.status] || '?';
|
|
220
|
+
lines.push(`### ${glyph} ${c.id}${c.function ? ` (${c.function})` : ''} — ${c.summary}`);
|
|
221
|
+
lines.push('');
|
|
222
|
+
if (c.evidence && c.evidence.length) {
|
|
223
|
+
lines.push('**Evidence the auditor expects:**');
|
|
224
|
+
for (const e of c.evidence) lines.push(`- ${e}`);
|
|
225
|
+
lines.push('');
|
|
226
|
+
}
|
|
227
|
+
if (ev.observations.length) {
|
|
228
|
+
lines.push('**Current state:**');
|
|
229
|
+
for (const o of ev.observations) lines.push(`- ${o}`);
|
|
230
|
+
lines.push('');
|
|
231
|
+
}
|
|
232
|
+
if (ev.status === 'absent' || ev.status === 'partial') {
|
|
233
|
+
lines.push(`**Remediation:** address the bullet(s) above, then re-run \`/auditor-walkthrough ${fw.id}\` to update this report.`);
|
|
234
|
+
lines.push('');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Persist the walkthrough at .agentic-security/auditor-walkthroughs/<id>.md
|
|
243
|
+
*/
|
|
244
|
+
export function persistWalkthrough(scanRoot, fw, body) {
|
|
245
|
+
const dir = path.join(scanRoot, STATE, 'auditor-walkthroughs');
|
|
246
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
247
|
+
const fp = path.join(dir, `${fw.id}.md`);
|
|
248
|
+
try { fs.writeFileSync(fp, body); } catch {}
|
|
249
|
+
return fp;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const _internals = { _readJson };
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// Claude-authorship analysis — closes the prompt-engineering loop.
|
|
2
|
+
//
|
|
3
|
+
// Builds on posture/git-history.js (which already tags individual findings
|
|
4
|
+
// with aiAuthored=true via the "Co-Authored-By: Claude" trailer detection).
|
|
5
|
+
//
|
|
6
|
+
// What this module adds:
|
|
7
|
+
//
|
|
8
|
+
// 1. analyzeAuthorshipPatterns(scanRoot, findings)
|
|
9
|
+
// Aggregates findings by (family, aiAuthored, file-pattern). Returns
|
|
10
|
+
// patterns like:
|
|
11
|
+
// "12 of 47 Claude-authored commits introduced auth-missing findings"
|
|
12
|
+
// "Claude-authored commits 3.2× more likely to ship SQLi than human-authored"
|
|
13
|
+
//
|
|
14
|
+
// 2. suggestClaudeMdEvolution(scanRoot, findings)
|
|
15
|
+
// For each clustered Claude pattern, drafts a one-paragraph addition
|
|
16
|
+
// to CLAUDE.md that would have prevented it. The user reviews + accepts.
|
|
17
|
+
//
|
|
18
|
+
// 3. extractOriginatingPromptCluster(findings)
|
|
19
|
+
// When findings carry `originatingPrompt` (set by git-history.js), clusters
|
|
20
|
+
// similar prompts to surface "the same kind of ask repeatedly produces
|
|
21
|
+
// vulnerable code."
|
|
22
|
+
//
|
|
23
|
+
// Pure analysis — no LLM calls in this module. The result is structured
|
|
24
|
+
// data that a /claude-vuln-audit command formats for the user.
|
|
25
|
+
|
|
26
|
+
const SEVERITY_RANK = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
|
|
27
|
+
|
|
28
|
+
function _normalizePrompt(p) {
|
|
29
|
+
return String(p || '')
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/['"`]/g, '')
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function _promptKeyTerms(p) {
|
|
37
|
+
// Heuristic clustering — extract content nouns / verbs typical of
|
|
38
|
+
// request shapes. v1 = stop-word removal + token bag.
|
|
39
|
+
const STOP = new Set(['a','an','the','and','or','but','to','for','of','in','on','at','by','from','with','as','is','are','be','was','were','this','that','it','i','you','we','please','can','could','would','should','add','make','create','write','build','give','need','want','have']);
|
|
40
|
+
return _normalizePrompt(p).split(/[^a-z0-9]+/).filter(t => t.length > 2 && !STOP.has(t));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function _jaccard(a, b) {
|
|
44
|
+
const sa = new Set(a), sb = new Set(b);
|
|
45
|
+
let inter = 0;
|
|
46
|
+
for (const x of sa) if (sb.has(x)) inter++;
|
|
47
|
+
const union = sa.size + sb.size - inter;
|
|
48
|
+
return union === 0 ? 0 : inter / union;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Aggregate findings into pattern statistics.
|
|
53
|
+
*/
|
|
54
|
+
export function analyzeAuthorshipPatterns(findings) {
|
|
55
|
+
if (!Array.isArray(findings)) return null;
|
|
56
|
+
const ai = findings.filter(f => f.aiAuthored);
|
|
57
|
+
const hu = findings.filter(f => f.introducedBy && !f.aiAuthored);
|
|
58
|
+
const total = ai.length + hu.length;
|
|
59
|
+
if (total === 0) return { total: 0, ai: 0, human: 0, patterns: [] };
|
|
60
|
+
|
|
61
|
+
// Per-family breakdown.
|
|
62
|
+
const byFamily = new Map();
|
|
63
|
+
for (const f of ai) {
|
|
64
|
+
const k = f.family || 'unknown';
|
|
65
|
+
if (!byFamily.has(k)) byFamily.set(k, { ai: 0, human: 0, severity: 0, files: new Set() });
|
|
66
|
+
const ent = byFamily.get(k);
|
|
67
|
+
ent.ai++;
|
|
68
|
+
ent.severity = Math.max(ent.severity, SEVERITY_RANK[f.severity] || 0);
|
|
69
|
+
if (f.file) ent.files.add(f.file);
|
|
70
|
+
}
|
|
71
|
+
for (const f of hu) {
|
|
72
|
+
const k = f.family || 'unknown';
|
|
73
|
+
if (!byFamily.has(k)) byFamily.set(k, { ai: 0, human: 0, severity: 0, files: new Set() });
|
|
74
|
+
byFamily.get(k).human++;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const patterns = [];
|
|
78
|
+
for (const [family, ent] of byFamily) {
|
|
79
|
+
if (ent.ai === 0) continue;
|
|
80
|
+
const familyTotal = ent.ai + ent.human;
|
|
81
|
+
const aiShare = familyTotal > 0 ? ent.ai / familyTotal : 0;
|
|
82
|
+
const expectedAiShare = ai.length / total;
|
|
83
|
+
const lift = expectedAiShare > 0 ? aiShare / expectedAiShare : 0;
|
|
84
|
+
patterns.push({
|
|
85
|
+
family,
|
|
86
|
+
aiCount: ent.ai,
|
|
87
|
+
humanCount: ent.human,
|
|
88
|
+
aiShare: Number(aiShare.toFixed(3)),
|
|
89
|
+
expectedShare: Number(expectedAiShare.toFixed(3)),
|
|
90
|
+
lift: Number(lift.toFixed(2)),
|
|
91
|
+
maxSeverity: Object.keys(SEVERITY_RANK).find(k => SEVERITY_RANK[k] === ent.severity) || 'unknown',
|
|
92
|
+
fileCount: ent.files.size,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
patterns.sort((a, b) => b.lift - a.lift || b.aiCount - a.aiCount);
|
|
96
|
+
return {
|
|
97
|
+
total,
|
|
98
|
+
ai: ai.length,
|
|
99
|
+
human: hu.length,
|
|
100
|
+
aiShare: Number((ai.length / total).toFixed(3)),
|
|
101
|
+
patterns,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Cluster findings by similar originating prompts. Returns groups of
|
|
107
|
+
* findings whose prompts share at least JACCARD_FLOOR token overlap.
|
|
108
|
+
*/
|
|
109
|
+
export function extractOriginatingPromptCluster(findings, opts = {}) {
|
|
110
|
+
const floor = opts.jaccardFloor || 0.35;
|
|
111
|
+
const withPrompts = (findings || []).filter(f => f.originatingPrompt);
|
|
112
|
+
if (withPrompts.length === 0) return [];
|
|
113
|
+
const termCache = new Map();
|
|
114
|
+
const term = (f) => {
|
|
115
|
+
if (!termCache.has(f.id || f.stableId)) {
|
|
116
|
+
termCache.set(f.id || f.stableId, _promptKeyTerms(f.originatingPrompt));
|
|
117
|
+
}
|
|
118
|
+
return termCache.get(f.id || f.stableId);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const used = new Set();
|
|
122
|
+
const clusters = [];
|
|
123
|
+
for (let i = 0; i < withPrompts.length; i++) {
|
|
124
|
+
if (used.has(i)) continue;
|
|
125
|
+
const seed = withPrompts[i];
|
|
126
|
+
const seedTerms = term(seed);
|
|
127
|
+
const group = [seed];
|
|
128
|
+
used.add(i);
|
|
129
|
+
for (let j = i + 1; j < withPrompts.length; j++) {
|
|
130
|
+
if (used.has(j)) continue;
|
|
131
|
+
const sim = _jaccard(seedTerms, term(withPrompts[j]));
|
|
132
|
+
if (sim >= floor) {
|
|
133
|
+
group.push(withPrompts[j]);
|
|
134
|
+
used.add(j);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (group.length >= 2) {
|
|
138
|
+
clusters.push({
|
|
139
|
+
size: group.length,
|
|
140
|
+
samplePrompt: seed.originatingPrompt,
|
|
141
|
+
families: Array.from(new Set(group.map(f => f.family).filter(Boolean))),
|
|
142
|
+
findings: group.map(f => ({ id: f.id, file: f.file, line: f.line, family: f.family, severity: f.severity })),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
clusters.sort((a, b) => b.size - a.size);
|
|
147
|
+
return clusters;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Draft CLAUDE.md additions for the top patterns. Each suggestion is a
|
|
152
|
+
* short stanza the user can paste into CLAUDE.md or AGENTS.md to
|
|
153
|
+
* pre-empt the recurring AI-authored vuln pattern.
|
|
154
|
+
*/
|
|
155
|
+
export function suggestClaudeMdEvolution(analysis) {
|
|
156
|
+
if (!analysis || !Array.isArray(analysis.patterns)) return [];
|
|
157
|
+
const out = [];
|
|
158
|
+
for (const p of analysis.patterns.slice(0, 5)) {
|
|
159
|
+
if (p.lift < 1.2 || p.aiCount < 2) continue; // not enough signal
|
|
160
|
+
out.push({
|
|
161
|
+
family: p.family,
|
|
162
|
+
aiCount: p.aiCount,
|
|
163
|
+
lift: p.lift,
|
|
164
|
+
maxSeverity: p.maxSeverity,
|
|
165
|
+
suggestion: _draftSuggestion(p),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function _draftSuggestion(p) {
|
|
172
|
+
const FAMILY_HINTS = {
|
|
173
|
+
'sqli': 'when asked to add a database query, always use parameterized queries via the existing helper rather than string interpolation. If no helper exists, use the driver\'s prepared-statement API directly (`db.prepare(sql).run(params)`).',
|
|
174
|
+
'sql-injection': 'when asked to add a database query, always use parameterized queries via the existing helper rather than string interpolation. If no helper exists, use the driver\'s prepared-statement API directly (`db.prepare(sql).run(params)`).',
|
|
175
|
+
'xss': 'when asked to render user-supplied content, default to text rendering and escape HTML explicitly. Avoid `dangerouslySetInnerHTML` / `v-html` / `eval` / template-literal HTML construction without sanitization.',
|
|
176
|
+
'command-injection': 'when asked to shell out, use `spawn(cmd, [args], { shell: false })` with explicit argv arrays. Never interpolate user input into a shell string.',
|
|
177
|
+
'auth-missing': 'when asked to add a route or endpoint, surface the route\'s authn/authz requirement explicitly. Default to auth-required unless the route is on the explicit public allowlist.',
|
|
178
|
+
'authz': 'when asked to look up a resource by id, also assert that the caller owns the resource (or has a relevant permission tier). Use the project\'s `assertResourceOwner` / equivalent helper.',
|
|
179
|
+
'csrf': 'when asked to add a state-changing endpoint, ensure CSRF protection is in place. Use the existing middleware or add it as part of the same change.',
|
|
180
|
+
'hardcoded-secret': 'when asked to use an API key or credential, read it from `process.env.X` (or the project\'s secret manager) — never embed the literal in source.',
|
|
181
|
+
'path-traversal': 'when asked to read/write a file based on user input, resolve to absolute path and assert it stays under a trusted root before any fs call.',
|
|
182
|
+
'crypto-weak-cipher':'when asked to encrypt, default to AES-256-GCM or ChaCha20-Poly1305 with crypto.randomBytes IVs. Never DES/3DES/RC4/Blowfish/ECB.',
|
|
183
|
+
'crypto-weak-hash': 'when asked to hash for security purposes (signing, password storage, integrity), use SHA-256+ for hashes and Argon2id/bcrypt/scrypt for passwords. MD5/SHA-1 only for non-security checksums, explicitly tagged.',
|
|
184
|
+
};
|
|
185
|
+
const advice = FAMILY_HINTS[p.family] || `when working in code that could introduce a ${p.family} finding, default to the project's safest established pattern.`;
|
|
186
|
+
return [
|
|
187
|
+
`## Security default — ${p.family}`,
|
|
188
|
+
'',
|
|
189
|
+
`Past Claude-authored work in this repo has introduced ${p.aiCount} ${p.family} finding(s) (${p.lift}× the rate of human-authored work). To pre-empt:`,
|
|
190
|
+
'',
|
|
191
|
+
`> ${advice}`,
|
|
192
|
+
'',
|
|
193
|
+
`Consider this a hard default unless the user explicitly asks for an exception.`,
|
|
194
|
+
].join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const _internals = { _normalizePrompt, _promptKeyTerms, _jaccard, _draftSuggestion };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scanId": "aeb3cf41-7bb6-49c6-b4ae-b126e5afd726",
|
|
3
|
+
"startedAt": "2026-05-29T21:02:56.188Z",
|
|
4
|
+
"durationMs": 15,
|
|
5
|
+
"scanned": {
|
|
6
|
+
"files": 0,
|
|
7
|
+
"lines": 0
|
|
8
|
+
},
|
|
9
|
+
"findings": [],
|
|
10
|
+
"bundles": [],
|
|
11
|
+
"routes": [],
|
|
12
|
+
"components": [],
|
|
13
|
+
"suppressedCount": 0,
|
|
14
|
+
"blastRadiusSignals": {
|
|
15
|
+
"industry": "generic",
|
|
16
|
+
"industryConfidence": "low",
|
|
17
|
+
"jurisdictions": [],
|
|
18
|
+
"controls": [],
|
|
19
|
+
"estimatedUsers": 50,
|
|
20
|
+
"revenueIndicator": "pre-revenue",
|
|
21
|
+
"hasStripe": false,
|
|
22
|
+
"hasAuth": false,
|
|
23
|
+
"hasUserTable": false,
|
|
24
|
+
"hasPII": false,
|
|
25
|
+
"hasPHI": false,
|
|
26
|
+
"hasS3": false
|
|
27
|
+
},
|
|
28
|
+
"_v3": {
|
|
29
|
+
"counterfactual": {
|
|
30
|
+
"spofControls": [],
|
|
31
|
+
"note": "no-controls-detected"
|
|
32
|
+
},
|
|
33
|
+
"threatModel": {
|
|
34
|
+
"summary": {
|
|
35
|
+
"assetCount": 0,
|
|
36
|
+
"boundaryCount": 0,
|
|
37
|
+
"strideCounts": {
|
|
38
|
+
"spoofing": 0,
|
|
39
|
+
"tampering": 0,
|
|
40
|
+
"repudiation": 0,
|
|
41
|
+
"informationDisclosure": 0,
|
|
42
|
+
"denialOfService": 0,
|
|
43
|
+
"elevationOfPrivilege": 0
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"assets": [],
|
|
47
|
+
"trustBoundaries": [],
|
|
48
|
+
"stride": {
|
|
49
|
+
"spoofing": [],
|
|
50
|
+
"tampering": [],
|
|
51
|
+
"repudiation": [],
|
|
52
|
+
"informationDisclosure": [],
|
|
53
|
+
"denialOfService": [],
|
|
54
|
+
"elevationOfPrivilege": []
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"trustBoundaryDiagram": {
|
|
58
|
+
"mermaid": "flowchart LR\n INTERNET((Internet))\n APP[\"Application\"]\n classDef sev_critical fill:#ffcccc,stroke:#a00,stroke-width:2px;\n classDef sev_high fill:#ffe0b2,stroke:#c60,stroke-width:2px;\n classDef sev_medium fill:#fff3cd,stroke:#a80;\n classDef sev_low fill:#e8eaf6,stroke:#557;",
|
|
59
|
+
"nodes": [
|
|
60
|
+
{
|
|
61
|
+
"id": "INTERNET",
|
|
62
|
+
"kind": "external",
|
|
63
|
+
"label": "Internet"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "APP",
|
|
67
|
+
"kind": "app",
|
|
68
|
+
"label": "Application"
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"edges": [],
|
|
72
|
+
"decorations": []
|
|
73
|
+
},
|
|
74
|
+
"calibrationDrift": {
|
|
75
|
+
"alarms": [],
|
|
76
|
+
"note": "no-feedback-data"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"annotatorErrors": []
|
|
80
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scanId": "aeb3cf41-7bb6-49c6-b4ae-b126e5afd726",
|
|
3
|
+
"startedAt": "2026-05-29T21:02:56.188Z",
|
|
4
|
+
"durationMs": 15,
|
|
5
|
+
"scanned": {
|
|
6
|
+
"files": 0,
|
|
7
|
+
"lines": 0
|
|
8
|
+
},
|
|
9
|
+
"findings": [],
|
|
10
|
+
"bundles": [],
|
|
11
|
+
"routes": [],
|
|
12
|
+
"components": [],
|
|
13
|
+
"suppressedCount": 0,
|
|
14
|
+
"blastRadiusSignals": {
|
|
15
|
+
"industry": "generic",
|
|
16
|
+
"industryConfidence": "low",
|
|
17
|
+
"jurisdictions": [],
|
|
18
|
+
"controls": [],
|
|
19
|
+
"estimatedUsers": 50,
|
|
20
|
+
"revenueIndicator": "pre-revenue",
|
|
21
|
+
"hasStripe": false,
|
|
22
|
+
"hasAuth": false,
|
|
23
|
+
"hasUserTable": false,
|
|
24
|
+
"hasPII": false,
|
|
25
|
+
"hasPHI": false,
|
|
26
|
+
"hasS3": false
|
|
27
|
+
},
|
|
28
|
+
"_v3": {
|
|
29
|
+
"counterfactual": {
|
|
30
|
+
"spofControls": [],
|
|
31
|
+
"note": "no-controls-detected"
|
|
32
|
+
},
|
|
33
|
+
"threatModel": {
|
|
34
|
+
"summary": {
|
|
35
|
+
"assetCount": 0,
|
|
36
|
+
"boundaryCount": 0,
|
|
37
|
+
"strideCounts": {
|
|
38
|
+
"spoofing": 0,
|
|
39
|
+
"tampering": 0,
|
|
40
|
+
"repudiation": 0,
|
|
41
|
+
"informationDisclosure": 0,
|
|
42
|
+
"denialOfService": 0,
|
|
43
|
+
"elevationOfPrivilege": 0
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"assets": [],
|
|
47
|
+
"trustBoundaries": [],
|
|
48
|
+
"stride": {
|
|
49
|
+
"spoofing": [],
|
|
50
|
+
"tampering": [],
|
|
51
|
+
"repudiation": [],
|
|
52
|
+
"informationDisclosure": [],
|
|
53
|
+
"denialOfService": [],
|
|
54
|
+
"elevationOfPrivilege": []
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"trustBoundaryDiagram": {
|
|
58
|
+
"mermaid": "flowchart LR\n INTERNET((Internet))\n APP[\"Application\"]\n classDef sev_critical fill:#ffcccc,stroke:#a00,stroke-width:2px;\n classDef sev_high fill:#ffe0b2,stroke:#c60,stroke-width:2px;\n classDef sev_medium fill:#fff3cd,stroke:#a80;\n classDef sev_low fill:#e8eaf6,stroke:#557;",
|
|
59
|
+
"nodes": [
|
|
60
|
+
{
|
|
61
|
+
"id": "INTERNET",
|
|
62
|
+
"kind": "external",
|
|
63
|
+
"label": "Internet"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "APP",
|
|
67
|
+
"kind": "app",
|
|
68
|
+
"label": "Application"
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"edges": [],
|
|
72
|
+
"decorations": []
|
|
73
|
+
},
|
|
74
|
+
"calibrationDrift": {
|
|
75
|
+
"alarms": [],
|
|
76
|
+
"note": "no-feedback-data"
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"annotatorErrors": []
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ccd53b45945cf534ddf095b46bb3a987246676e48f0308cf01946295f9d3e448
|