@clear-capabilities/agentic-security-scanner 0.79.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/333.index.js +283 -0
- 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 +140 -1
- package/dist/agentic-security.mjs +10 -10
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +7 -5
- package/src/.agentic-security/findings.json +117732 -0
- package/src/.agentic-security/last-scan.json +117732 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +12946 -0
- package/src/.agentic-security/streak.json +21 -0
- package/src/dataflow/.agentic-security/findings.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +250 -0
- package/src/dataflow/.agentic-security/streak.json +21 -0
- package/src/dataflow/cross-service-taint.js +201 -0
- package/src/dataflow/formal-verify.js +204 -0
- package/src/dataflow/ifds-precise.js +222 -0
- package/src/dataflow/k2-summary-cache.js +153 -0
- package/src/dataflow/lib-taint-summaries.js +198 -0
- package/src/dataflow/privacy-taint.js +205 -0
- package/src/dataflow/smt-feasibility.js +189 -0
- package/src/engine.js +825 -127
- package/src/ir/.agentic-security/findings.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +193 -0
- package/src/ir/.agentic-security/streak.json +20 -0
- package/src/ir/cpp-preprocessor.js +142 -0
- package/src/ir/csharp-ir.js +604 -0
- package/src/ir/universal-ir.js +403 -0
- package/src/mcp/.agentic-security/findings.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +331 -0
- package/src/mcp/.agentic-security/streak.json +20 -0
- package/src/mcp/tools.js +140 -1
- package/src/posture/.agentic-security/findings.json +77181 -0
- package/src/posture/.agentic-security/last-scan.json +77181 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +8904 -0
- package/src/posture/.agentic-security/streak.json +21 -0
- package/src/posture/api-contract.js +193 -0
- package/src/posture/attack-taxonomy.js +227 -0
- 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/compliance-policy.js +218 -0
- package/src/posture/composite-risk.js +122 -0
- package/src/posture/cross-repo-memory.js +180 -0
- package/src/posture/csharp-analysis.js +330 -0
- package/src/posture/dep-add-guard.js +197 -0
- package/src/posture/exploit-bundle.js +210 -0
- package/src/posture/federated-learning.js +172 -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/license-attributions.js +94 -0
- package/src/posture/license-graph.js +238 -0
- package/src/posture/model-rescan.js +76 -0
- package/src/posture/pattern-propagation.js +39 -0
- package/src/posture/pqc-migration-plan.js +158 -0
- package/src/posture/pr-augment.js +234 -0
- package/src/posture/reachability-filter.js +33 -2
- package/src/posture/realtime-cve-monitor.js +214 -0
- package/src/posture/risk-dollars.js +158 -0
- package/src/posture/runtime-correlation.js +174 -0
- package/src/posture/sbom-diff.js +171 -0
- package/src/posture/sca-policy.js +235 -0
- package/src/posture/sca-upgrade.js +259 -0
- package/src/posture/threat-model-auto.js +268 -0
- package/src/posture/threat-model-grounding.js +169 -0
- package/src/posture/time-to-fix.js +129 -0
- package/src/posture/triage-learning.js +170 -0
- package/src/posture/triage-memory.js +151 -0
- package/src/posture/triage.js +40 -1
- package/src/posture/watch-mode.js +171 -0
- package/src/posture/workflow-installer.js +231 -0
- package/src/sast/.agentic-security/findings.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +941 -0
- package/src/sast/.agentic-security/streak.json +22 -0
- package/src/sast/_secret-entropy.js +145 -0
- package/src/sast/cloud-iam.js +312 -0
- package/src/sast/cpp.js +138 -4
- package/src/sast/crypto-protocol.js +388 -0
- package/src/sast/csharp-tokenizer.js +392 -0
- package/src/sast/csharp.js +924 -138
- package/src/sast/dapp-frontend.js +200 -0
- package/src/sast/k8s-admission.js +271 -0
- package/src/sast/llm-app.js +272 -0
- package/src/sast/ml-supply-chain.js +259 -0
- package/src/sast/mobile.js +224 -0
- package/src/sast/post-quantum-crypto.js +348 -0
- package/src/sast/web3-advanced.js +375 -0
- package/src/sca/.agentic-security/findings.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json.sig +1 -0
- package/src/sca/.agentic-security/scan-history.json +113 -0
- package/src/sca/.agentic-security/streak.json +21 -0
- package/src/sca/CLAUDE.md +161 -0
- package/src/sca/binary-metadata.js +37 -15
- package/src/sca/sigstore-verify.js +215 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Model-of-the-month re-scan delta.
|
|
2
|
+
//
|
|
3
|
+
// Re-runs the LLM validator (already opt-in via AGENTIC_SECURITY_LLM_VALIDATE)
|
|
4
|
+
// with a different model and produces a delta report: which findings the
|
|
5
|
+
// newer model marked TP that the prior model marked FP (or vice versa),
|
|
6
|
+
// what newer reasoning catches that older reasoning missed.
|
|
7
|
+
//
|
|
8
|
+
// Use case: every time Anthropic ships a new Claude model (or you want to
|
|
9
|
+
// A/B against gpt-5 / a custom finetune), re-validate the last scan and see
|
|
10
|
+
// which findings change verdict.
|
|
11
|
+
//
|
|
12
|
+
// Output: .agentic-security/model-rescan/<from>-vs-<to>.json with:
|
|
13
|
+
// { from, to, changed: [{ finding_id, before, after, why }], ts }
|
|
14
|
+
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
|
|
18
|
+
const STATE = '.agentic-security';
|
|
19
|
+
|
|
20
|
+
function _readJson(scanRoot, name) {
|
|
21
|
+
try { return JSON.parse(fs.readFileSync(path.join(scanRoot, STATE, name), 'utf8')); } catch { return null; }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Compare two validator runs by finding_id. Each run is a JSON like:
|
|
26
|
+
* { model: 'claude-sonnet-4', results: { findingId: { verdict, reason }, ... } }
|
|
27
|
+
*/
|
|
28
|
+
export function diffValidatorRuns(runA, runB) {
|
|
29
|
+
const a = runA && runA.results ? runA.results : {};
|
|
30
|
+
const b = runB && runB.results ? runB.results : {};
|
|
31
|
+
const ids = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
32
|
+
const changed = [];
|
|
33
|
+
for (const id of ids) {
|
|
34
|
+
const av = (a[id] && a[id].verdict) || null;
|
|
35
|
+
const bv = (b[id] && b[id].verdict) || null;
|
|
36
|
+
if (av !== bv) {
|
|
37
|
+
changed.push({
|
|
38
|
+
finding_id: id,
|
|
39
|
+
before: av,
|
|
40
|
+
after: bv,
|
|
41
|
+
before_reason: a[id]?.reason || null,
|
|
42
|
+
after_reason: b[id]?.reason || null,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return changed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Persist a model-rescan report. Returns the file path.
|
|
51
|
+
*/
|
|
52
|
+
export function persistRescanReport(scanRoot, from, to, changed) {
|
|
53
|
+
const dir = path.join(scanRoot, STATE, 'model-rescan');
|
|
54
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
55
|
+
const safe = (s) => String(s || 'unknown').replace(/[^\w.-]/g, '-');
|
|
56
|
+
const fp = path.join(dir, `${safe(from)}-vs-${safe(to)}.json`);
|
|
57
|
+
const report = { from, to, ts: new Date().toISOString(), changed };
|
|
58
|
+
try { fs.writeFileSync(fp, JSON.stringify(report, null, 2)); } catch {}
|
|
59
|
+
return fp;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Build a quick natural-language summary of the delta.
|
|
64
|
+
*/
|
|
65
|
+
export function summarizeDelta(changed) {
|
|
66
|
+
if (!Array.isArray(changed) || !changed.length) return 'No changes — validators agree on every finding.';
|
|
67
|
+
const flipsToTP = changed.filter(c => c.before === 'fp' && c.after === 'tp');
|
|
68
|
+
const flipsToFP = changed.filter(c => c.before === 'tp' && c.after === 'fp');
|
|
69
|
+
const lines = [];
|
|
70
|
+
lines.push(`${changed.length} verdict change(s) between models:`);
|
|
71
|
+
if (flipsToTP.length) lines.push(` ${flipsToTP.length} finding(s) now confirmed TP (newer model caught what older missed)`);
|
|
72
|
+
if (flipsToFP.length) lines.push(` ${flipsToFP.length} finding(s) now FP (newer model recognized as safe)`);
|
|
73
|
+
return lines.join('\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const _internals = {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Pattern propagation — annotator that surfaces cross-repo signals on
|
|
2
|
+
// findings. For each finding, queries the cross-repo store for past
|
|
3
|
+
// fixes and triage decisions on the same family from sibling repos and
|
|
4
|
+
// stamps the finding with a crossRepoSignal field.
|
|
5
|
+
//
|
|
6
|
+
// The /show-findings command / PR-augment / explain_finding MCP tool
|
|
7
|
+
// can render the signal alongside the finding so the developer sees
|
|
8
|
+
// "you already fixed this exact shape in repo X."
|
|
9
|
+
//
|
|
10
|
+
// Pure annotator — no LLM calls. Opt-out via the cross-repo-memory
|
|
11
|
+
// AGENTIC_SECURITY_NO_CROSS_REPO=1 flag.
|
|
12
|
+
|
|
13
|
+
import { findSiblingSignals, renderSiblingNote } from './cross-repo-memory.js';
|
|
14
|
+
|
|
15
|
+
export function annotateCrossRepoSignals(scanRoot, findings) {
|
|
16
|
+
if (process.env.AGENTIC_SECURITY_NO_CROSS_REPO === '1') return { annotated: 0 };
|
|
17
|
+
if (!Array.isArray(findings) || findings.length === 0) return { annotated: 0 };
|
|
18
|
+
let annotated = 0;
|
|
19
|
+
// De-duplicate signal lookups per family — many findings share a family.
|
|
20
|
+
const sigCache = new Map();
|
|
21
|
+
for (const f of findings) {
|
|
22
|
+
const fam = f.family;
|
|
23
|
+
if (!fam) continue;
|
|
24
|
+
let signals = sigCache.get(fam);
|
|
25
|
+
if (signals === undefined) {
|
|
26
|
+
signals = findSiblingSignals(scanRoot, f);
|
|
27
|
+
sigCache.set(fam, signals);
|
|
28
|
+
}
|
|
29
|
+
if (signals.siblingFixes.length || signals.siblingTriage.length) {
|
|
30
|
+
f.crossRepoSignal = {
|
|
31
|
+
fixes: signals.siblingFixes.length,
|
|
32
|
+
triage: signals.siblingTriage.length,
|
|
33
|
+
note: renderSiblingNote(signals),
|
|
34
|
+
};
|
|
35
|
+
annotated++;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return { annotated, total: findings.length };
|
|
39
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// PQC migration-plan artifact emitter.
|
|
2
|
+
//
|
|
3
|
+
// Aggregates pqc-migration findings (emitted by sast/post-quantum-crypto.js)
|
|
4
|
+
// into a structured plan suitable for an engineering organization to use as
|
|
5
|
+
// a project tracker:
|
|
6
|
+
//
|
|
7
|
+
// .agentic-security/pqc-migration-plan.json
|
|
8
|
+
// .agentic-security/pqc-migration-plan.md
|
|
9
|
+
//
|
|
10
|
+
// Buckets findings by:
|
|
11
|
+
// - HNDL criticality (high-priority — data captured today is harvest-now-decrypt-later
|
|
12
|
+
// exposure when a CRQC arrives)
|
|
13
|
+
// - Use case (signing / encryption / KEX) — drives the replacement primitive
|
|
14
|
+
// - Recommended replacement (ML-KEM-768, ML-DSA-65, etc.)
|
|
15
|
+
// - File / package locality so the plan can be carved into milestones.
|
|
16
|
+
//
|
|
17
|
+
// Cleartext markdown summarises the top recommendations and milestone
|
|
18
|
+
// suggestions; JSON-LD-shaped structured output is consumable by Vanta /
|
|
19
|
+
// Drata / SecureFrame or any custom rollup dashboard.
|
|
20
|
+
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
|
|
24
|
+
function _byHndl(findings) {
|
|
25
|
+
return {
|
|
26
|
+
hndlCritical: findings.filter(f => f.hndlCritical),
|
|
27
|
+
standard: findings.filter(f => !f.hndlCritical),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function _byUseCase(findings) {
|
|
32
|
+
const map = new Map();
|
|
33
|
+
for (const f of findings) {
|
|
34
|
+
const k = f.pqcRecommendation?.primary || 'unspecified';
|
|
35
|
+
if (!map.has(k)) map.set(k, []);
|
|
36
|
+
map.get(k).push(f);
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function _byFile(findings) {
|
|
42
|
+
const map = new Map();
|
|
43
|
+
for (const f of findings) {
|
|
44
|
+
const k = f.file || 'unknown';
|
|
45
|
+
if (!map.has(k)) map.set(k, []);
|
|
46
|
+
map.get(k).push(f);
|
|
47
|
+
}
|
|
48
|
+
return map;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildMigrationPlan(allFindings) {
|
|
52
|
+
const pqc = (allFindings || []).filter(f => f.family === 'pqc-migration');
|
|
53
|
+
if (!pqc.length) return null;
|
|
54
|
+
const bySev = _byHndl(pqc);
|
|
55
|
+
const byPrimitive = _byUseCase(pqc);
|
|
56
|
+
const byFile = _byFile(pqc);
|
|
57
|
+
const summary = {
|
|
58
|
+
total: pqc.length,
|
|
59
|
+
hndlCritical: bySev.hndlCritical.length,
|
|
60
|
+
standard: bySev.standard.length,
|
|
61
|
+
filesAffected: byFile.size,
|
|
62
|
+
primitivesNeeded: Array.from(byPrimitive.keys()),
|
|
63
|
+
};
|
|
64
|
+
const milestones = [
|
|
65
|
+
{
|
|
66
|
+
id: 'M1',
|
|
67
|
+
title: 'Inventory & policy',
|
|
68
|
+
target: '90 days',
|
|
69
|
+
owner: 'security',
|
|
70
|
+
items: [
|
|
71
|
+
'Confirm scanner findings against design docs',
|
|
72
|
+
'Adopt PQC migration policy (CNSA 2.0 / NIST IR 8547 alignment)',
|
|
73
|
+
'Establish KMS support for hybrid keys',
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'M2',
|
|
78
|
+
title: 'HNDL-critical paths to PQ-hybrid',
|
|
79
|
+
target: '180 days',
|
|
80
|
+
owner: 'platform',
|
|
81
|
+
items: bySev.hndlCritical.slice(0, 25).map(f => ({
|
|
82
|
+
finding: f.id, file: f.file, line: f.line,
|
|
83
|
+
replacement: f.pqcRecommendation?.hybrid || f.pqcRecommendation?.primary,
|
|
84
|
+
})),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'M3',
|
|
88
|
+
title: 'Standard signing/KEX migration',
|
|
89
|
+
target: '12 months',
|
|
90
|
+
owner: 'platform',
|
|
91
|
+
items: bySev.standard.slice(0, 50).map(f => ({
|
|
92
|
+
finding: f.id, file: f.file, line: f.line,
|
|
93
|
+
replacement: f.pqcRecommendation?.primary,
|
|
94
|
+
})),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 'M4',
|
|
98
|
+
title: 'Deprecate classical primitives',
|
|
99
|
+
target: '24 months',
|
|
100
|
+
owner: 'security',
|
|
101
|
+
items: ['Remove dual-stack libraries once peers are PQ-capable', 'Rotate root CA / long-lived signing keys to ML-DSA'],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
return {
|
|
105
|
+
generatedAt: new Date().toISOString(),
|
|
106
|
+
summary,
|
|
107
|
+
milestones,
|
|
108
|
+
perFile: Object.fromEntries(
|
|
109
|
+
Array.from(byFile.entries()).map(([file, fs]) => [file, {
|
|
110
|
+
count: fs.length,
|
|
111
|
+
subfamilies: Array.from(new Set(fs.map(f => f.subfamily))),
|
|
112
|
+
hndlCritical: fs.some(f => f.hndlCritical),
|
|
113
|
+
}]),
|
|
114
|
+
),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function persistMigrationPlan(scanRoot, plan) {
|
|
119
|
+
if (!plan) return null;
|
|
120
|
+
try { fs.mkdirSync(path.join(scanRoot, '.agentic-security'), { recursive: true }); } catch {}
|
|
121
|
+
try { fs.writeFileSync(path.join(scanRoot, '.agentic-security', 'pqc-migration-plan.json'), JSON.stringify(plan, null, 2)); } catch {}
|
|
122
|
+
try { fs.writeFileSync(path.join(scanRoot, '.agentic-security', 'pqc-migration-plan.md'), _markdown(plan)); } catch {}
|
|
123
|
+
return plan;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function _markdown(plan) {
|
|
127
|
+
const lines = [];
|
|
128
|
+
lines.push('# Post-quantum cryptography migration plan');
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push(`Generated ${plan.generatedAt.slice(0, 10)}.`);
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push(`**${plan.summary.total}** pre-quantum primitive sites across **${plan.summary.filesAffected}** files. `);
|
|
133
|
+
lines.push(`HNDL-critical: **${plan.summary.hndlCritical}** | Standard: **${plan.summary.standard}**`);
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('## Recommended PQ primitives');
|
|
136
|
+
for (const p of plan.summary.primitivesNeeded) lines.push(`- ${p}`);
|
|
137
|
+
lines.push('');
|
|
138
|
+
for (const m of plan.milestones) {
|
|
139
|
+
lines.push(`## ${m.id} — ${m.title} (target ${m.target}, owner ${m.owner})`);
|
|
140
|
+
if (Array.isArray(m.items) && m.items.length) {
|
|
141
|
+
for (const it of m.items.slice(0, 20)) {
|
|
142
|
+
if (typeof it === 'string') lines.push(`- ${it}`);
|
|
143
|
+
else lines.push(`- \`${it.file}:${it.line}\` → ${it.replacement || '(see finding)'}`);
|
|
144
|
+
}
|
|
145
|
+
if (m.items.length > 20) lines.push(`- … ${m.items.length - 20} more`);
|
|
146
|
+
}
|
|
147
|
+
lines.push('');
|
|
148
|
+
}
|
|
149
|
+
lines.push('## References');
|
|
150
|
+
lines.push('- NIST FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), FIPS 205 (SLH-DSA)');
|
|
151
|
+
lines.push('- NIST IR 8547 — Transition to Post-Quantum Cryptographic Standards');
|
|
152
|
+
lines.push('- CNSA 2.0 — Commercial National Security Algorithm Suite, Sept 2022');
|
|
153
|
+
lines.push('- RFC 9794 — X25519MLKEM768 hybrid key exchange for TLS 1.3');
|
|
154
|
+
lines.push('- Open Quantum Safe project (liboqs, oqs-provider for OpenSSL 3)');
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const _internals = { _byHndl, _byUseCase, _byFile, _markdown };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// PR-description auto-augmentation.
|
|
2
|
+
//
|
|
3
|
+
// Reads the current last-scan vs a baseline (git base branch or
|
|
4
|
+
// a stored snapshot), and produces a Markdown block suitable for
|
|
5
|
+
// injecting into a PR body via `gh pr edit --body` or chaining into
|
|
6
|
+
// `gh pr create`.
|
|
7
|
+
//
|
|
8
|
+
// Output sections:
|
|
9
|
+
// 1. Security delta summary (added / removed / changed by severity)
|
|
10
|
+
// 2. ATT&CK tactics covered by new findings
|
|
11
|
+
// 3. Suggested reviewers by class (auth changes → security; PII → privacy)
|
|
12
|
+
// 4. Links to relevant artifacts (threat-model, compliance-evidence,
|
|
13
|
+
// PQC plan, exploit bundles) when present
|
|
14
|
+
//
|
|
15
|
+
// Pure render — does not call git or gh; caller orchestrates that.
|
|
16
|
+
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { diffScans, summarizeDiff } from './baseline-compare.js';
|
|
20
|
+
|
|
21
|
+
const REVIEWER_TRIGGERS = [
|
|
22
|
+
{ family: /^auth/, team: 'security', why: 'Auth-related findings' },
|
|
23
|
+
{ family: /^crypto/, team: 'security', why: 'Cryptography findings' },
|
|
24
|
+
{ family: /^pii|gdpr|hipaa/i, team: 'privacy', why: 'PII / data-handling findings' },
|
|
25
|
+
{ family: /^iam|cloud|k8s/, team: 'platform', why: 'Cloud / Kubernetes posture findings' },
|
|
26
|
+
{ family: /^supply|license|vuln/, team: 'platform', why: 'Supply-chain findings' },
|
|
27
|
+
{ family: /^llm|prompt|ml/, team: 'ml', why: 'LLM / ML supply-chain findings' },
|
|
28
|
+
{ family: /^web3|defi/, team: 'security', why: 'Smart-contract findings' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function _stateFile(scanRoot, name) {
|
|
32
|
+
return path.join(scanRoot, '.agentic-security', name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _readJson(fp) {
|
|
36
|
+
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _baselinePath(scanRoot, ref) {
|
|
40
|
+
const safe = String(ref || 'main').replace(/[^\w.-]/g, '-');
|
|
41
|
+
return path.join(scanRoot, '.agentic-security', 'scan-baselines', `${safe}.json`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Persist a scan snapshot under .agentic-security/scan-baselines/<ref>.json
|
|
46
|
+
* so subsequent PRs can diff against it.
|
|
47
|
+
*/
|
|
48
|
+
export function persistBaseline(scanRoot, ref, scan) {
|
|
49
|
+
const fp = _baselinePath(scanRoot, ref);
|
|
50
|
+
try { fs.mkdirSync(path.dirname(fp), { recursive: true }); } catch {}
|
|
51
|
+
try { fs.writeFileSync(fp, JSON.stringify({ ref, ts: new Date().toISOString(), findings: scan.findings || [] }, null, 2)); } catch {}
|
|
52
|
+
return fp;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function loadBaseline(scanRoot, ref) {
|
|
56
|
+
return _readJson(_baselinePath(scanRoot, ref));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Recommended reviewers derived from the diff's added-findings.
|
|
61
|
+
*/
|
|
62
|
+
function _suggestReviewers(addedFindings) {
|
|
63
|
+
const hits = new Map(); // team → {why: Set, count: number}
|
|
64
|
+
for (const f of addedFindings) {
|
|
65
|
+
for (const t of REVIEWER_TRIGGERS) {
|
|
66
|
+
if (t.family.test(f.family || '')) {
|
|
67
|
+
if (!hits.has(t.team)) hits.set(t.team, { why: new Set(), count: 0 });
|
|
68
|
+
const ent = hits.get(t.team);
|
|
69
|
+
ent.why.add(t.why);
|
|
70
|
+
ent.count++;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return Array.from(hits.entries()).map(([team, ent]) => ({
|
|
76
|
+
team, count: ent.count, why: Array.from(ent.why),
|
|
77
|
+
})).sort((a, b) => b.count - a.count);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* ATT&CK techniques surfaced by added findings (attack-taxonomy annotator
|
|
82
|
+
* must have run for this to be populated).
|
|
83
|
+
*/
|
|
84
|
+
function _addedAttckSummary(addedFindings) {
|
|
85
|
+
const map = new Map();
|
|
86
|
+
for (const f of addedFindings) {
|
|
87
|
+
for (const t of (f.attck || [])) {
|
|
88
|
+
if (!map.has(t)) map.set(t, { count: 0, name: f.attckName || t });
|
|
89
|
+
map.get(t).count++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return Array.from(map.entries())
|
|
93
|
+
.sort((a, b) => b[1].count - a[1].count)
|
|
94
|
+
.slice(0, 8)
|
|
95
|
+
.map(([id, v]) => ({ id, name: v.name, count: v.count }));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Artifact links — pull file paths that exist under .agentic-security/.
|
|
100
|
+
*/
|
|
101
|
+
function _artifactLinks(scanRoot) {
|
|
102
|
+
const candidates = [
|
|
103
|
+
{ name: 'Threat model', file: 'threat-model.md' },
|
|
104
|
+
{ name: 'Compliance evidence', file: 'compliance-evidence.md' },
|
|
105
|
+
{ name: 'PQC migration plan', file: 'pqc-migration-plan.md' },
|
|
106
|
+
{ name: 'DPIA', file: 'dpia.md' },
|
|
107
|
+
{ name: 'ATTRIBUTIONS', file: 'ATTRIBUTIONS.md' },
|
|
108
|
+
{ name: 'NOTICE', file: 'NOTICE' },
|
|
109
|
+
];
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const c of candidates) {
|
|
112
|
+
const fp = path.join(scanRoot, '.agentic-security', c.file);
|
|
113
|
+
if (fs.existsSync(fp)) out.push({ name: c.name, path: `.agentic-security/${c.file}` });
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Render a Markdown PR-body augmentation block.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} scanRoot
|
|
122
|
+
* @param {object} opts
|
|
123
|
+
* - baselineRef: string (default 'main')
|
|
124
|
+
* - title: string section title (default 'Security review')
|
|
125
|
+
* - blocking: bool if true, prepend a 🛑 block-merge banner when new criticals added
|
|
126
|
+
*/
|
|
127
|
+
export function augmentPrBody(scanRoot, opts = {}) {
|
|
128
|
+
const baselineRef = opts.baselineRef || 'main';
|
|
129
|
+
const title = opts.title || 'Security review (automated)';
|
|
130
|
+
const blocking = opts.blocking !== false;
|
|
131
|
+
|
|
132
|
+
const current = _readJson(_stateFile(scanRoot, 'last-scan.json'));
|
|
133
|
+
if (!current) return { ok: false, error: 'No .agentic-security/last-scan.json — run a scan first.' };
|
|
134
|
+
|
|
135
|
+
const baseline = loadBaseline(scanRoot, baselineRef);
|
|
136
|
+
const diff = baseline ? diffScans(baseline, current) : { added: current.findings || [], removed: [], changed: [], unchanged: 0 };
|
|
137
|
+
const summary = summarizeDiff(diff);
|
|
138
|
+
|
|
139
|
+
const lines = [];
|
|
140
|
+
lines.push(`## ${title}`);
|
|
141
|
+
lines.push('');
|
|
142
|
+
|
|
143
|
+
if (!baseline) {
|
|
144
|
+
lines.push(`> Baseline against \`${baselineRef}\` not found — showing the full current scan as added. Run \`/pr-augment --persist-baseline ${baselineRef}\` from \`${baselineRef}\` to enable diff mode.`);
|
|
145
|
+
lines.push('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const newCriticals = summary.bySeverity.critical?.added || 0;
|
|
149
|
+
const newHighs = summary.bySeverity.high?.added || 0;
|
|
150
|
+
|
|
151
|
+
if (blocking && newCriticals > 0) {
|
|
152
|
+
lines.push(`> 🛑 **${newCriticals} new critical finding(s)** — recommend blocking merge until resolved.`);
|
|
153
|
+
lines.push('');
|
|
154
|
+
} else if (newHighs > 0) {
|
|
155
|
+
lines.push(`> ⚠️ **${newHighs} new high-severity finding(s)** — review before merging.`);
|
|
156
|
+
lines.push('');
|
|
157
|
+
} else if (summary.addedCount === 0) {
|
|
158
|
+
lines.push('> ✅ No new findings vs baseline.');
|
|
159
|
+
lines.push('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Delta table
|
|
163
|
+
lines.push('### Findings delta vs `' + baselineRef + '`');
|
|
164
|
+
lines.push('');
|
|
165
|
+
lines.push('| Severity | Added | Removed |');
|
|
166
|
+
lines.push('|---|---:|---:|');
|
|
167
|
+
for (const sev of ['critical', 'high', 'medium', 'low']) {
|
|
168
|
+
const s = summary.bySeverity[sev] || { added: 0, removed: 0 };
|
|
169
|
+
lines.push(`| ${sev} | ${s.added} | ${s.removed} |`);
|
|
170
|
+
}
|
|
171
|
+
lines.push('');
|
|
172
|
+
|
|
173
|
+
// ATT&CK tactics
|
|
174
|
+
const attck = _addedAttckSummary(diff.added);
|
|
175
|
+
if (attck.length) {
|
|
176
|
+
lines.push('### MITRE ATT&CK techniques (new findings)');
|
|
177
|
+
lines.push('');
|
|
178
|
+
for (const t of attck) lines.push(`- \`${t.id}\` ${t.name} (${t.count})`);
|
|
179
|
+
lines.push('');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Suggested reviewers
|
|
183
|
+
const reviewers = _suggestReviewers(diff.added);
|
|
184
|
+
if (reviewers.length) {
|
|
185
|
+
lines.push('### Suggested reviewers');
|
|
186
|
+
lines.push('');
|
|
187
|
+
for (const r of reviewers) lines.push(`- **${r.team}** — ${r.why.join('; ')} (${r.count})`);
|
|
188
|
+
lines.push('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Top 5 added findings
|
|
192
|
+
if (diff.added.length) {
|
|
193
|
+
const top = diff.added
|
|
194
|
+
.slice()
|
|
195
|
+
.sort((a, b) => (sevRank(b.severity) - sevRank(a.severity)) || ((b.confidence || 0) - (a.confidence || 0)))
|
|
196
|
+
.slice(0, 5);
|
|
197
|
+
lines.push('### Top added findings');
|
|
198
|
+
lines.push('');
|
|
199
|
+
for (const f of top) {
|
|
200
|
+
const where = `${f.file || '?'}:${f.line || 0}`;
|
|
201
|
+
lines.push(`- **[${(f.severity || '?').toUpperCase()}]** ${f.vuln || f.family || 'finding'} — \`${where}\``);
|
|
202
|
+
}
|
|
203
|
+
lines.push('');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Artifact links
|
|
207
|
+
const arts = _artifactLinks(scanRoot);
|
|
208
|
+
if (arts.length) {
|
|
209
|
+
lines.push('### Posture artifacts');
|
|
210
|
+
lines.push('');
|
|
211
|
+
for (const a of arts) lines.push(`- [${a.name}](${a.path})`);
|
|
212
|
+
lines.push('');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
lines.push('_Generated by [agentic-security](https://github.com/Clear-Capabilities/agentic-security)._');
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
ok: true,
|
|
219
|
+
body: lines.join('\n'),
|
|
220
|
+
summary: {
|
|
221
|
+
newCriticals,
|
|
222
|
+
newHighs,
|
|
223
|
+
added: summary.addedCount,
|
|
224
|
+
removed: summary.removedCount,
|
|
225
|
+
reviewers,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sevRank(s) {
|
|
231
|
+
return { critical: 4, high: 3, medium: 2, low: 1, info: 0 }[s] || 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export const _internals = { _suggestReviewers, _addedAttckSummary, _artifactLinks, _baselinePath };
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
// module turns that signal into a precision lever: findings marked reachable=
|
|
6
6
|
// false are demoted to severity 'info' with f.unreachable = true.
|
|
7
7
|
//
|
|
8
|
+
// Phase 2 / Item 4 (SCA improvement plan): also demote SCA findings whose
|
|
9
|
+
// reachabilityTier indicates the vulnerable code is not reached by any route
|
|
10
|
+
// handler. Critical+manifest-only on a 500-dep transitive graph would
|
|
11
|
+
// otherwise drown out real reachable bugs.
|
|
12
|
+
//
|
|
8
13
|
// Disabled when scanRoot/--include-unreachable signals are present, or when
|
|
9
14
|
// AGENTIC_SECURITY_INCLUDE_UNREACHABLE=1 is set.
|
|
10
15
|
|
|
@@ -15,6 +20,19 @@ const SEVERITY_DEMOTE = {
|
|
|
15
20
|
low: 'info',
|
|
16
21
|
};
|
|
17
22
|
|
|
23
|
+
// SCA reachability tiers, ordered from highest urgency to lowest. A tier
|
|
24
|
+
// in DEMOTE_SCA_TIERS triggers severity demotion; a tier NOT in the set
|
|
25
|
+
// keeps full severity. route-reachable-via-function and function-reachable
|
|
26
|
+
// both keep full severity because the vulnerable function is provably
|
|
27
|
+
// called; import-reachable also keeps full severity (imported = uncertain
|
|
28
|
+
// but plausible). The lower three tiers get demoted.
|
|
29
|
+
const DEMOTE_SCA_TIERS = new Set([
|
|
30
|
+
'unreachable', // function never called from project
|
|
31
|
+
'build-only', // dev/build-time dependency only
|
|
32
|
+
'manifest-only', // declared, but no use observed
|
|
33
|
+
'transitive-only', // transitive dep, scope unclear
|
|
34
|
+
]);
|
|
35
|
+
|
|
18
36
|
export function demoteUnreachable(findings, opts = {}) {
|
|
19
37
|
if (!Array.isArray(findings)) return;
|
|
20
38
|
if (opts.includeUnreachable || process.env.AGENTIC_SECURITY_INCLUDE_UNREACHABLE === '1') return;
|
|
@@ -26,9 +44,22 @@ export function demoteUnreachable(findings, opts = {}) {
|
|
|
26
44
|
if (!haveRoutes) return;
|
|
27
45
|
for (const f of findings) {
|
|
28
46
|
if (!f || typeof f !== 'object') continue;
|
|
29
|
-
if (f.reachable !== false) continue;
|
|
30
|
-
if (f.type === 'vulnerable_dep') continue;
|
|
31
47
|
if (f.unreachable) continue;
|
|
48
|
+
// SCA findings: demote based on reachabilityTier instead of f.reachable
|
|
49
|
+
// (the latter isn't meaningful for an SCA finding — components don't
|
|
50
|
+
// have call sites in the SAST sense).
|
|
51
|
+
if (f.type === 'vulnerable_dep') {
|
|
52
|
+
if (!DEMOTE_SCA_TIERS.has(f.reachabilityTier)) continue;
|
|
53
|
+
const beforeSca = f.severity;
|
|
54
|
+
const afterSca = SEVERITY_DEMOTE[beforeSca];
|
|
55
|
+
if (!afterSca || beforeSca === afterSca) continue;
|
|
56
|
+
f.severity = afterSca;
|
|
57
|
+
f.unreachable = true;
|
|
58
|
+
f._reachabilityDemoted = beforeSca;
|
|
59
|
+
f._reachabilityDemoteReason = `tier:${f.reachabilityTier}`;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (f.reachable !== false) continue;
|
|
32
63
|
// Source has an explicit HTTP/DOM/Form/URL category → engine is confident
|
|
33
64
|
// it's a user-input source even though no route was linked. Don't demote.
|
|
34
65
|
if (f.source && f.source.category && /HTTP|DOM|Form|URL|Query/i.test(f.source.category)) continue;
|