@clear-capabilities/agentic-security-scanner 0.78.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/.agentic-security/findings.json +16 -16
- package/bin/.agentic-security/last-scan.json +16 -16
- package/bin/.agentic-security/last-scan.json.sig +1 -1
- package/bin/.agentic-security/scan-history.json +51 -0
- package/bin/.agentic-security/streak.json +5 -5
- package/bin/agentic-security.js +22 -7
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/476.index.js +5 -5
- package/dist/637.index.js +1 -1
- package/dist/700.index.js +138 -0
- package/dist/718.index.js +53 -0
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +5 -0
- package/dist/agentic-security.mjs +1 -1
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +2 -2
- package/src/dataflow/engine.js +52 -8
- package/src/engine.js +107 -6
- package/src/integrations/index.js +2 -1
- package/src/ir/callgraph.js +27 -7
- package/src/llm-validator/index.js +7 -5
- package/src/mcp/audit.js +5 -0
- package/src/posture/calibration-drift.js +2 -1
- package/src/posture/calibration.js +3 -2
- package/src/posture/fix-history.js +8 -2
- package/src/posture/profile.js +4 -5
- package/src/posture/rule-overrides.js +2 -3
- package/src/posture/rule-pack-signing.js +2 -3
- package/src/posture/rule-synthesis.js +5 -6
- package/src/posture/security-trend.js +4 -7
- package/src/posture/state-dir.js +124 -0
- package/src/posture/streak.js +3 -0
- package/src/posture/suppressions.js +5 -8
- package/src/posture/triage.js +3 -5
- package/src/posture/validator-metrics.js +3 -6
- package/src/sast/db-taint.js +24 -0
- package/src/sast/rust.js +26 -0
- package/src/sca/binary-metadata.js +124 -0
- package/src/sca/py-package-functions.js +118 -0
- package/src/sca/vendor-detect.js +53 -0
- package/src/.agentic-security/findings.json +0 -82642
- package/src/.agentic-security/last-scan.json +0 -82642
- package/src/.agentic-security/last-scan.json.sig +0 -1
- package/src/.agentic-security/scan-history.json +0 -10054
- package/src/.agentic-security/streak.json +0 -21
- package/src/dataflow/.agentic-security/findings.json +0 -3515
- package/src/dataflow/.agentic-security/last-scan.json +0 -3515
- package/src/dataflow/.agentic-security/last-scan.json.sig +0 -1
- package/src/dataflow/.agentic-security/scan-history.json +0 -702
- package/src/dataflow/.agentic-security/streak.json +0 -22
- package/src/ir/.agentic-security/findings.json +0 -3777
- package/src/ir/.agentic-security/last-scan.json +0 -3777
- package/src/ir/.agentic-security/last-scan.json.sig +0 -1
- package/src/ir/.agentic-security/scan-history.json +0 -771
- package/src/ir/.agentic-security/streak.json +0 -21
- package/src/posture/.agentic-security/findings.json +0 -51562
- package/src/posture/.agentic-security/last-scan.json +0 -51562
- package/src/posture/.agentic-security/last-scan.json.sig +0 -1
- package/src/posture/.agentic-security/scan-history.json +0 -650
- package/src/posture/.agentic-security/streak.json +0 -20
- package/src/report/.agentic-security/findings.json +0 -80
- package/src/report/.agentic-security/last-scan.json +0 -80
- package/src/report/.agentic-security/last-scan.json.sig +0 -1
- package/src/report/.agentic-security/scan-history.json +0 -35
- package/src/report/.agentic-security/streak.json +0 -22
- package/src/sast/.agentic-security/findings.json +0 -5190
- package/src/sast/.agentic-security/last-scan.json +0 -5190
- package/src/sast/.agentic-security/last-scan.json.sig +0 -1
- package/src/sast/.agentic-security/scan-history.json +0 -408
- package/src/sast/.agentic-security/streak.json +0 -20
- package/src/sca/.agentic-security/findings.json +0 -1587
- package/src/sca/.agentic-security/last-scan.json +0 -1587
- package/src/sca/.agentic-security/last-scan.json.sig +0 -1
- package/src/sca/.agentic-security/scan-history.json +0 -36
- package/src/sca/.agentic-security/streak.json +0 -21
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3ec7e435269654d09e2168e1cd8a6586f88b9c2edce86f48c18a2dc5f321e358 agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin \u2014 SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, SARIF ingest, pipeline integrity, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"test": "npm run test:smoke && npm run test:sast && npm run test:posture && npm run test:dataflow && npm run test:mcp && npm run test:report && npm run test:bench-modules && npm run test:lifecycle && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
|
|
56
56
|
"test:smoke": "node --test test/smoke.test.js",
|
|
57
57
|
"test:sast": "node --test test/llm.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/redos-nfa.test.js test/weak-randomness.test.js",
|
|
58
|
-
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js",
|
|
58
|
+
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js",
|
|
59
59
|
"test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js",
|
|
60
60
|
"test:mcp": "node --test test/mcp.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js",
|
|
61
61
|
"test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js",
|
package/src/dataflow/engine.js
CHANGED
|
@@ -62,13 +62,13 @@ function _addPathAliasAware(state, path, callContext) {
|
|
|
62
62
|
return s;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
let _activeConstantVars = null;
|
|
66
|
+
|
|
65
67
|
function exprTaint(expr, state) {
|
|
66
|
-
// Returns true iff this expression evaluates to a tainted value under the
|
|
67
|
-
// given taint state. ALSO treats catalog-registered source patterns as
|
|
68
|
-
// tainted at-read — `req.body.host` used inline (no intermediate local)
|
|
69
|
-
// is tainted because the source resolves at the read site.
|
|
70
68
|
if (expr && expr.kind === 'member' && exprIsSource(expr)) return true;
|
|
71
69
|
if (!expr) return false;
|
|
70
|
+
// Constant propagation: variables assigned from literals are never tainted
|
|
71
|
+
if (expr.kind === 'ident' && _activeConstantVars && _activeConstantVars.has(expr.name)) return false;
|
|
72
72
|
// P1.1 — field-sensitive access path: if the expression is a pure
|
|
73
73
|
// ident/member chain ("x.y.z"), ask the access-path lattice whether any
|
|
74
74
|
// shorter prefix in the state covers it. This is what makes
|
|
@@ -157,13 +157,35 @@ function exprIsSource(expr) {
|
|
|
157
157
|
const hit = matchSource(expr);
|
|
158
158
|
if (hit) return hit;
|
|
159
159
|
}
|
|
160
|
-
// Recurse — `req.body.name` should still find `req.body` as source.
|
|
161
160
|
if (expr.kind === 'member' && expr.object) {
|
|
162
161
|
return exprIsSource(expr.object);
|
|
163
162
|
}
|
|
164
163
|
return null;
|
|
165
164
|
}
|
|
166
165
|
|
|
166
|
+
const _SQL_KEYWORDS = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|UNION|WHERE|FROM|JOIN|INTO|VALUES|SET|EXEC|EXECUTE)\b/i;
|
|
167
|
+
const _HTML_META = /[<>'"&]|innerHTML|outerHTML|document\.write/;
|
|
168
|
+
const _SHELL_META = /[;|`$(){}]|&&|\|\|/;
|
|
169
|
+
|
|
170
|
+
function _literalPartsOfExpr(expr) {
|
|
171
|
+
if (!expr) return [];
|
|
172
|
+
if (expr.kind === 'literal') return [String(expr.value || '')];
|
|
173
|
+
if (expr.kind === 'tpl') return (expr.parts || []).filter(p => p.kind === 'literal').map(p => String(p.value || ''));
|
|
174
|
+
if (expr.kind === 'binary') return [..._literalPartsOfExpr(expr.left), ..._literalPartsOfExpr(expr.right)];
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function literalSkeletonMatchesFamily(expr, cwe) {
|
|
179
|
+
const literals = _literalPartsOfExpr(expr);
|
|
180
|
+
if (!literals.length) return true;
|
|
181
|
+
const joined = literals.join(' ');
|
|
182
|
+
if (!joined.trim()) return true;
|
|
183
|
+
if (cwe === 'CWE-89' || cwe === 'CWE-943') return _SQL_KEYWORDS.test(joined);
|
|
184
|
+
if (cwe === 'CWE-79') return _HTML_META.test(joined);
|
|
185
|
+
if (cwe === 'CWE-78') return _SHELL_META.test(joined);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
167
189
|
// Apply a CFG node to a taint-state. Returns the new state + any finding emitted.
|
|
168
190
|
function step(node, stateIn, callContext) {
|
|
169
191
|
const state = new Set(stateIn);
|
|
@@ -177,9 +199,13 @@ function step(node, stateIn, callContext) {
|
|
|
177
199
|
return { state, findings };
|
|
178
200
|
|
|
179
201
|
case 'assign': {
|
|
180
|
-
// Source detection on RHS.
|
|
181
202
|
const src = exprIsSource(node.source);
|
|
182
203
|
const target = typeof node.target === 'string' ? node.target : null;
|
|
204
|
+
// Constant propagation: track variables assigned from literals
|
|
205
|
+
if (target && _activeConstantVars) {
|
|
206
|
+
if (node.source && node.source.kind === 'literal') _activeConstantVars.set(target, node.source.value);
|
|
207
|
+
else _activeConstantVars.delete(target);
|
|
208
|
+
}
|
|
183
209
|
let newState = state;
|
|
184
210
|
// Premortem #7: interprocedural return-taint via SummaryCache. If the
|
|
185
211
|
// RHS is a call to a known callee whose empty-entry-state summary says
|
|
@@ -340,6 +366,8 @@ function step(node, stateIn, callContext) {
|
|
|
340
366
|
const taintedArgIdx = e.argIndex === 'all'
|
|
341
367
|
? argTaints.findIndex(Boolean) : e.argIndex;
|
|
342
368
|
const taintedArgExpr = (node.args || [])[taintedArgIdx];
|
|
369
|
+
// String content analysis: skip if literal skeleton doesn't match injection family
|
|
370
|
+
if (e.vuln && taintedArgExpr && !literalSkeletonMatchesFamily(taintedArgExpr, e.vuln.cwe)) continue;
|
|
343
371
|
// Premortem #10: attribute the source for THIS sink to the
|
|
344
372
|
// source(s) that taint the actual argument expression — not the
|
|
345
373
|
// first source the worklist happened to record. We walk the
|
|
@@ -438,12 +466,13 @@ function step(node, stateIn, callContext) {
|
|
|
438
466
|
// every 100 iterations. A pathological CFG (large generated file with dense
|
|
439
467
|
// control flow) can otherwise hold past the global timeout.
|
|
440
468
|
function analyzeFunction(fn, entryState, callContext) {
|
|
441
|
-
const nodes = fn.cfg.nodes;
|
|
469
|
+
const nodes = fn.cfg.nodes;
|
|
442
470
|
const work = [];
|
|
443
|
-
const inStates = new Map();
|
|
471
|
+
const inStates = new Map();
|
|
444
472
|
const outStates = new Map();
|
|
445
473
|
inStates.set(fn.cfg.entry, new Set(entryState));
|
|
446
474
|
work.push(fn.cfg.entry);
|
|
475
|
+
_activeConstantVars = new Map();
|
|
447
476
|
// v0.70 #2 — points-to context for the step() transfer. Setting it here
|
|
448
477
|
// (instead of plumbing through step's signature) keeps the worklist loop
|
|
449
478
|
// unchanged and lets `step` consult `aliasesForVar` when callContext._pointsTo
|
|
@@ -712,6 +741,21 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
712
741
|
}
|
|
713
742
|
}
|
|
714
743
|
// v0.69 — expose cache to caller (runDeepAnalysis) for incremental persistence.
|
|
744
|
+
// Dead code suppression: demote findings in functions with zero callers
|
|
745
|
+
// (except route handlers which are entry points)
|
|
746
|
+
const calledQids = new Set();
|
|
747
|
+
if (callGraph.edges) for (const e of callGraph.edges) calledQids.add(typeof e.to === 'string' ? e.to : e.to?.qid);
|
|
748
|
+
if (callGraph.callersOf) for (const [qid, callers] of callGraph.callersOf) { if (callers && callers.size) calledQids.add(qid); }
|
|
749
|
+
for (const f of all) {
|
|
750
|
+
if (!f._funcQid) continue;
|
|
751
|
+
const fn = callGraph.functions?.get(f._funcQid);
|
|
752
|
+
if (!fn) continue;
|
|
753
|
+
if (calledQids.has(f._funcQid)) continue;
|
|
754
|
+
if (/handler|route|controller|middleware|endpoint/i.test(fn.name || '')) continue;
|
|
755
|
+
f._inDeadCode = true;
|
|
756
|
+
const dg = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
|
|
757
|
+
if (dg[f.severity]) f.severity = dg[f.severity];
|
|
758
|
+
}
|
|
715
759
|
Object.defineProperty(all, '_summaryCache', { value: summaryCache, enumerable: false });
|
|
716
760
|
return all;
|
|
717
761
|
}
|
package/src/engine.js
CHANGED
|
@@ -55,7 +55,7 @@ import { scanCpp } from './sast/cpp.js';
|
|
|
55
55
|
import { scanJulietShape, applyJulietJavaSuppressions, applyJulietCsSuppressions } from './sast/juliet-shape.js';
|
|
56
56
|
import { scanCppDataflow, _parseErrorCount as _cppDataflowParseErrors } from './sast/cpp-dataflow.js';
|
|
57
57
|
import { scanSolidity } from './sast/solidity.js';
|
|
58
|
-
import { scanRust } from './sast/rust.js';
|
|
58
|
+
import { scanRust, extractRustImportMap } from './sast/rust.js';
|
|
59
59
|
import { scanGoExtended } from './sast/go-extended.js';
|
|
60
60
|
import { scanDatabaseRLS } from './sast/db-rls.js';
|
|
61
61
|
import { scanRateLimit } from './sast/rate-limit.js';
|
|
@@ -4563,10 +4563,14 @@ function scanMiddlewareOrdering(fp, raw){
|
|
|
4563
4563
|
if (isAuth && line < firstAuthAt) firstAuthAt = line;
|
|
4564
4564
|
if (mountPath) mounts.push({ line, mountPath, handlerArgs, isAuth });
|
|
4565
4565
|
}
|
|
4566
|
+
let firstRateLimitAt = Infinity;
|
|
4567
|
+
for (const mt of mounts) {
|
|
4568
|
+
if (/rateLimit|rate.?limit|throttle|slowDown/i.test(mt.handlerArgs) && mt.line < firstRateLimitAt) firstRateLimitAt = mt.line;
|
|
4569
|
+
}
|
|
4566
4570
|
for (const mt of mounts) {
|
|
4567
4571
|
if (mt.isAuth) continue;
|
|
4568
4572
|
if (!_SENSITIVE_PATH_RE.test(mt.mountPath)) continue;
|
|
4569
|
-
if (mt.line >= firstAuthAt) continue;
|
|
4573
|
+
if (mt.line >= firstAuthAt) continue;
|
|
4570
4574
|
findings.push({
|
|
4571
4575
|
vuln: `Sensitive Route Mounted Before Auth Middleware (${mt.mountPath})`,
|
|
4572
4576
|
severity: 'high', cwe: 'CWE-285', stride: 'Elevation of Privilege',
|
|
@@ -4574,6 +4578,27 @@ function scanMiddlewareOrdering(fp, raw){
|
|
|
4574
4578
|
fix: `Register your auth middleware before mounting ${mt.mountPath}. Either move app.use(authMiddleware) above this line, or pass authMiddleware directly: app.use('${mt.mountPath}', authMiddleware, router).`,
|
|
4575
4579
|
});
|
|
4576
4580
|
}
|
|
4581
|
+
// Check rate-limiting before auth (auth endpoints should be rate-limited)
|
|
4582
|
+
if (firstAuthAt < Infinity && firstRateLimitAt > firstAuthAt) {
|
|
4583
|
+
findings.push({
|
|
4584
|
+
vuln: 'Rate Limiting After Auth Middleware — brute-force attacks bypass rate limits',
|
|
4585
|
+
severity: 'medium', cwe: 'CWE-307', stride: 'Denial of Service',
|
|
4586
|
+
file: fp, line: firstAuthAt, snippet: lines[firstAuthAt - 1]?.trim() || '',
|
|
4587
|
+
fix: 'Register rate-limiting middleware BEFORE auth middleware so brute-force login attempts are throttled: app.use(rateLimit({...})); app.use(authMiddleware);',
|
|
4588
|
+
});
|
|
4589
|
+
}
|
|
4590
|
+
// Check for method-level auth on sensitive routes
|
|
4591
|
+
const routeRe = /\b(?:app|router)\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]*(?:admin|user|account|settings|profile|payment|billing|dashboard)[^'"]*)['"]\s*,\s*(?!.*(?:auth|protect|verify|guard))/gi;
|
|
4592
|
+
for (const rm of cleaned.matchAll(routeRe)) {
|
|
4593
|
+
const routeLine = lineAt(cleaned, rm.index);
|
|
4594
|
+
if (routeLine >= firstAuthAt) continue;
|
|
4595
|
+
findings.push({
|
|
4596
|
+
vuln: `Sensitive Route Without Auth — ${rm[1].toUpperCase()} ${rm[2]}`,
|
|
4597
|
+
severity: 'high', cwe: 'CWE-306', stride: 'Elevation of Privilege',
|
|
4598
|
+
file: fp, line: routeLine, snippet: lines[routeLine - 1]?.trim() || '',
|
|
4599
|
+
fix: `Add auth middleware to this route: router.${rm[1]}('${rm[2]}', authMiddleware, handler). Or ensure app.use(authMiddleware) appears before this route definition.`,
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4577
4602
|
return findings;
|
|
4578
4603
|
}
|
|
4579
4604
|
|
|
@@ -5507,17 +5532,50 @@ const VULN_FUNCTION_HINTS = {
|
|
|
5507
5532
|
...(typeof _VULN_FUNCTION_HINTS_GENERATED === 'object' && !Array.isArray(_VULN_FUNCTION_HINTS_GENERATED) ? Object.fromEntries(Object.entries(_VULN_FUNCTION_HINTS_GENERATED).filter(([k])=>!k.startsWith('_'))) : {}),
|
|
5508
5533
|
...(typeof _VULN_FUNCTION_HINTS_DATA === 'object' && !Array.isArray(_VULN_FUNCTION_HINTS_DATA) ? Object.fromEntries(Object.entries(_VULN_FUNCTION_HINTS_DATA).filter(([k])=>!k.startsWith('_'))) : {}),
|
|
5509
5534
|
};
|
|
5535
|
+
function _semverSatisfies(ver,range){
|
|
5536
|
+
if(!ver||!range)return false;
|
|
5537
|
+
const parse=v=>{const m=(v||'').match(/^[=v]*(\d+)\.(\d+)\.(\d+)/);return m?[+m[1],+m[2],+m[3]]:null;};
|
|
5538
|
+
const cmp=(a,b)=>{for(let i=0;i<3;i++){if(a[i]!==b[i])return a[i]-b[i];}return 0;};
|
|
5539
|
+
const v=parse(ver);if(!v)return false;
|
|
5540
|
+
for(const part of range.split(/\s*\|\|\s*/)){
|
|
5541
|
+
let ok=true;
|
|
5542
|
+
for(const cond of part.split(/\s+/).filter(Boolean)){
|
|
5543
|
+
const m=cond.match(/^([<>=!]+)(.+)$/);if(!m)continue;
|
|
5544
|
+
const t=parse(m[2]);if(!t){ok=false;break;}
|
|
5545
|
+
const c=cmp(v,t);const op=m[1];
|
|
5546
|
+
if(op==='>='&&c<0){ok=false;break;}
|
|
5547
|
+
if(op==='>'&&c<=0){ok=false;break;}
|
|
5548
|
+
if(op==='<='&&c>0){ok=false;break;}
|
|
5549
|
+
if(op==='<'&&c>=0){ok=false;break;}
|
|
5550
|
+
if(op==='='&&c!==0){ok=false;break;}
|
|
5551
|
+
if(op==='!='&&c===0){ok=false;break;}
|
|
5552
|
+
}
|
|
5553
|
+
if(ok)return true;
|
|
5554
|
+
}
|
|
5555
|
+
return false;
|
|
5556
|
+
}
|
|
5510
5557
|
function markUsedVulnFunctions(supplyChain,fc){
|
|
5511
5558
|
const used={};
|
|
5512
5559
|
const perFile={};
|
|
5513
5560
|
for(const[fp,content] of Object.entries(fc)){
|
|
5514
5561
|
const lines=content.split('\n');
|
|
5562
|
+
// Rust import-aware matching: build import map for .rs files
|
|
5563
|
+
let _rustImports=null;
|
|
5564
|
+
if(/\.rs$/i.test(fp)){try{_rustImports=extractRustImportMap(content);}catch(_){}}
|
|
5515
5565
|
for(const[pkg,fns] of Object.entries(VULN_FUNCTION_HINTS)){
|
|
5516
5566
|
if(!perFile[pkg])perFile[pkg]=[];
|
|
5517
5567
|
for(const fn of fns){
|
|
5518
5568
|
const re=new RegExp(`\\b(?:${pkg.replace(/\W/g,'\\$&')}|_)\\.${fn}\\b`,'g');
|
|
5569
|
+
// Rust: also match bare function calls if import map traces them to this package
|
|
5570
|
+
const rustBareRe=_rustImports?new RegExp(`\\b${fn.replace(/\W/g,'\\$&')}\\s*[(<]`,'g'):null;
|
|
5519
5571
|
for(let li=0;li<lines.length;li++){
|
|
5520
|
-
|
|
5572
|
+
let matched=re.test(lines[li]);
|
|
5573
|
+
re.lastIndex=0;
|
|
5574
|
+
if(!matched&&rustBareRe){
|
|
5575
|
+
if(rustBareRe.test(lines[li])&&(_rustImports.map.get(fn)===pkg||_rustImports.map.get(fn)===pkg.replace(/-/g,'_')||_rustImports.globs.has(pkg)||_rustImports.globs.has(pkg.replace(/-/g,'_')))){matched=true;}
|
|
5576
|
+
rustBareRe.lastIndex=0;
|
|
5577
|
+
}
|
|
5578
|
+
if(matched){
|
|
5521
5579
|
perFile[pkg].push({pkg,fn,file:fp,line:li+1});
|
|
5522
5580
|
if(!used[pkg])used[pkg]=new Set();
|
|
5523
5581
|
used[pkg].add(fn);
|
|
@@ -5529,10 +5587,20 @@ function markUsedVulnFunctions(supplyChain,fc){
|
|
|
5529
5587
|
}
|
|
5530
5588
|
for(const sc of supplyChain||[]){
|
|
5531
5589
|
if(sc.type!=='vulnerable_dep')continue;
|
|
5532
|
-
// Merge hints: hardcoded → OSV ecosystem_specific → skip if none
|
|
5590
|
+
// Merge hints: versioned → hardcoded → OSV ecosystem_specific → skip if none
|
|
5533
5591
|
const hardcoded=VULN_FUNCTION_HINTS[sc.name]||[];
|
|
5592
|
+
// Version-scoped hints: check pkg@range keys
|
|
5593
|
+
const versionedFns=[];
|
|
5594
|
+
for(const[hk,hv] of Object.entries(VULN_FUNCTION_HINTS)){
|
|
5595
|
+
if(!hk.includes('@'))continue;
|
|
5596
|
+
const atIdx=hk.indexOf('@');
|
|
5597
|
+
const hPkg=hk.slice(0,atIdx);
|
|
5598
|
+
const hRange=hk.slice(atIdx+1);
|
|
5599
|
+
if(hPkg!==sc.name)continue;
|
|
5600
|
+
try{if(_semverSatisfies(sc.version,hRange))versionedFns.push(...hv);}catch(_){}
|
|
5601
|
+
}
|
|
5534
5602
|
const osvFns=Array.isArray(sc.osvVulnFunctions)?sc.osvVulnFunctions.map(f=>{const d=f.lastIndexOf('.');return d>0?f.slice(d+1):f;}):[];
|
|
5535
|
-
const allFns=[...new Set([...hardcoded,...osvFns])];
|
|
5603
|
+
const allFns=[...new Set([...versionedFns,...hardcoded,...osvFns])];
|
|
5536
5604
|
if(!allFns.length){sc.functionReachable='unknown';sc.noKnownCallSite=true;sc._hintSource='none';continue;}
|
|
5537
5605
|
sc._hintSource=osvFns.length?(hardcoded.length?'hardcoded+osv':'osv'):'hardcoded';
|
|
5538
5606
|
// Search codebase for these functions (if not already searched via VULN_FUNCTION_HINTS)
|
|
@@ -6339,8 +6407,33 @@ function _parsePubspecLock(text,filePath){
|
|
|
6339
6407
|
}return out;
|
|
6340
6408
|
}
|
|
6341
6409
|
|
|
6410
|
+
const _APT_TO_LIB={'libssl-dev':'openssl','libssl3':'openssl','openssl':'openssl','zlib1g-dev':'zlib','zlib1g':'zlib','libcurl4-openssl-dev':'libcurl','libcurl4':'libcurl','libxml2-dev':'libxml2','libxml2':'libxml2','libpq-dev':'postgresql','libsqlite3-dev':'sqlite','libjpeg-dev':'libjpeg','libpng-dev':'libpng','libfreetype6-dev':'freetype','libexpat1-dev':'expat','libyaml-dev':'libyaml','libffi-dev':'libffi','libgmp-dev':'gmp','libncurses-dev':'ncurses','libreadline-dev':'readline'};
|
|
6411
|
+
function _parseCMakeLists(text,filePath){
|
|
6412
|
+
const out=[];
|
|
6413
|
+
for(const m of text.matchAll(/find_package\s*\(\s*(\w+)(?:\s+(\d+[\d.]*))?\s*(?:REQUIRED|COMPONENTS)?/gi)){
|
|
6414
|
+
const name=m[1].toLowerCase();const ver=m[2]||'0.0.0';
|
|
6415
|
+
out.push({name,version:ver,group:'',scope:'required',purl:`pkg:generic/${name}@${ver}`,ecosystem:'system',filePath,isUnpinned:!m[2]});
|
|
6416
|
+
}
|
|
6417
|
+
return out;
|
|
6418
|
+
}
|
|
6419
|
+
function _parseConanfile(text,filePath){
|
|
6420
|
+
const out=[];
|
|
6421
|
+
for(const m of text.matchAll(/(?:requires|build_requires)\s*[=(]\s*["']([^/]+)\/([^"'@]+)/g)){
|
|
6422
|
+
out.push({name:m[1].toLowerCase(),version:m[2],group:'',scope:'required',purl:`pkg:generic/${m[1].toLowerCase()}@${m[2]}`,ecosystem:'system',filePath,isUnpinned:false});
|
|
6423
|
+
}
|
|
6424
|
+
return out;
|
|
6425
|
+
}
|
|
6426
|
+
function _parseVcpkgJson(text,filePath){
|
|
6427
|
+
const out=[];try{const d=JSON.parse(text);
|
|
6428
|
+
for(const dep of(d.dependencies||[])){
|
|
6429
|
+
const name=typeof dep==='string'?dep:dep.name;
|
|
6430
|
+
const ver=(typeof dep==='object'&&dep['version>='])?dep['version>=']:'0.0.0';
|
|
6431
|
+
if(name)out.push({name:name.toLowerCase(),version:ver,group:'',scope:'required',purl:`pkg:generic/${name.toLowerCase()}@${ver}`,ecosystem:'system',filePath,isUnpinned:ver==='0.0.0'});
|
|
6432
|
+
}
|
|
6433
|
+
}catch(_){}return out;
|
|
6434
|
+
}
|
|
6342
6435
|
function parseManifests(allFileContents){
|
|
6343
|
-
const PARSERS={'package.json':_parsePackageJson,'package-lock.json':_parsePackageLockJson,'yarn.lock':_parseYarnLock,'pnpm-lock.yaml':_parsePnpmLock,'requirements.txt':_parseRequirementsTxt,'pyproject.toml':_parsePyprojectToml,'poetry.lock':_parsePoetryLock,'Pipfile.lock':_parsePipfileLock,'composer.json':_parseComposerJson,'composer.lock':_parseComposerLock,'Gemfile':_parseGemfile,'Gemfile.lock':_parseGemfileLock,'go.mod':_parseGoMod,'Cargo.toml':_parseCargoToml,'Cargo.lock':_parseCargoLock,'pom.xml':_parsePomXml,'build.gradle':_parseBuildGradle,'build.gradle.kts':_parseBuildGradle,'pubspec.yaml':_parsePubspecYaml,'pubspec.lock':_parsePubspecLock};
|
|
6436
|
+
const PARSERS={'package.json':_parsePackageJson,'package-lock.json':_parsePackageLockJson,'yarn.lock':_parseYarnLock,'pnpm-lock.yaml':_parsePnpmLock,'requirements.txt':_parseRequirementsTxt,'pyproject.toml':_parsePyprojectToml,'poetry.lock':_parsePoetryLock,'Pipfile.lock':_parsePipfileLock,'composer.json':_parseComposerJson,'composer.lock':_parseComposerLock,'Gemfile':_parseGemfile,'Gemfile.lock':_parseGemfileLock,'go.mod':_parseGoMod,'Cargo.toml':_parseCargoToml,'Cargo.lock':_parseCargoLock,'pom.xml':_parsePomXml,'build.gradle':_parseBuildGradle,'build.gradle.kts':_parseBuildGradle,'pubspec.yaml':_parsePubspecYaml,'pubspec.lock':_parsePubspecLock,'CMakeLists.txt':_parseCMakeLists,'conanfile.txt':_parseConanfile,'vcpkg.json':_parseVcpkgJson};
|
|
6344
6437
|
const out=[],seen=new Set();
|
|
6345
6438
|
for(const[fp,content]of Object.entries(allFileContents)){
|
|
6346
6439
|
const base=fp.split('/').pop();
|
|
@@ -6931,6 +7024,8 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6931
7024
|
// 0.10.0: enrich SCA findings with CISA KEV (CISA KEV catalog)
|
|
6932
7025
|
try{supplyChain=await _enrichWithKEV(supplyChain);}catch(_){}
|
|
6933
7026
|
try{markUsedVulnFunctions(supplyChain,fc);}catch(_){}
|
|
7027
|
+
// Python AST-based function validation (deep mode only)
|
|
7028
|
+
if(process.env.AGENTIC_SECURITY_DEEP==='1'){try{const{validateOsvFunctionsExist}=await import('./sca/py-package-functions.js');for(const sc of supplyChain){if(sc.type!=='vulnerable_dep'||sc.ecosystem!=='pypi')continue;if(!sc.osvVulnFunctions||!sc.osvVulnFunctions.length)continue;const{validated,missing}=validateOsvFunctionsExist(sc.name,sc.osvVulnFunctions,scanRoot);if(validated.length)sc._pyAstValidated=validated;if(missing.length)sc._pyAstMissing=missing;}}catch(_){}}
|
|
6934
7029
|
// LLM-assisted function extraction for CVEs without hints (opt-in: AGENTIC_SECURITY_LLM_SCA=1)
|
|
6935
7030
|
if(process.env.AGENTIC_SECURITY_LLM_SCA==='1'){try{const{extractVulnFunctionsViaLLM}=await import('./sca/llm-function-extract.js');const enriched=await extractVulnFunctionsViaLLM(supplyChain);if(enriched.length){markUsedVulnFunctions(supplyChain,fc);}}catch(_){}}
|
|
6936
7031
|
setProgress({current:i,total:files.length,file:"Registry metadata...",phase:"SCA"});
|
|
@@ -7457,6 +7552,12 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7457
7552
|
// v3 next-gen: why-fired provenance is captured LAST so it reflects the
|
|
7458
7553
|
// final state of each finding after every other annotator has run.
|
|
7459
7554
|
_runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, {}); });
|
|
7555
|
+
// SCA-SAST correlation: link SAST findings to SCA vulnerable packages
|
|
7556
|
+
try{for(const f of finalFindings){if(!f.chain||!f.chain.length)continue;const src=f.chain[0]?.label||'';for(const sc of supplyChain){if(sc.type!=='vulnerable_dep')continue;if(src.includes(sc.name)||f.vuln?.toLowerCase().includes(sc.name)){f.scaCorrelation={osvId:sc.osvId,package:sc.name,version:sc.version,confirmed:true};sc.sastConfirmed=true;break;}}}}catch(_){}
|
|
7557
|
+
// Multi-sink chain detection: group findings by source variable
|
|
7558
|
+
try{const srcGroups=new Map();for(const f of finalFindings){const src=f.chain?.[0]?.label;if(!src)continue;if(!srcGroups.has(src))srcGroups.set(src,[]);srcGroups.get(src).push(f);}for(const[src,group]of srcGroups){if(group.length<2)continue;const groupId=`multi-sink:${src}:${group.length}`;for(const f of group)f._multiSinkGroupId=groupId;finalFindings.push({id:groupId,file:group[0].file,line:group[0].line,vuln:`Multi-Sink Taint Chain — ${src} reaches ${group.length} sinks`,severity:group.some(f=>f.severity==='critical')?'critical':'high',cwe:'CWE-20',parser:'MULTI-SINK',confidence:0.85,sinks:group.map(f=>({file:f.file,line:f.line,vuln:f.vuln})),_aggregated:true});}}catch(_){}
|
|
7559
|
+
// SCA transitive dedup: collapse duplicate CVEs across dep chains
|
|
7560
|
+
try{const osvGroups=new Map();for(const sc of supplyChain){if(sc.type!=='vulnerable_dep'||!sc.osvId)continue;if(!osvGroups.has(sc.osvId))osvGroups.set(sc.osvId,[]);osvGroups.get(sc.osvId).push(sc);}for(const[osvId,group]of osvGroups){if(group.length<=1)continue;const primary=group.find(s=>s.isDirect)||group[0];primary.dependents=group.filter(s=>s!==primary).map(s=>({name:s.name,version:s.version,depChain:s.depChain,isDirect:s.isDirect}));primary._transitiveDeduped=group.length-1;for(const dup of group){if(dup!==primary)dup._deduplicatedInto=primary.osvId;}supplyChain.splice(0,supplyChain.length,...supplyChain.filter(s=>!s._deduplicatedInto));}}catch(_){}
|
|
7460
7561
|
const _scanMeta={filesScanned:files.length,filesSkipped:_filesSkipped,filesTimedOut:_filesTimedOut,fileTimings:_fileTimings.sort((a,b)=>b.ms-a.ms).slice(0,20),findingsBySeverity:{critical:finalFindings.filter(f=>f.severity==='critical').length,high:finalFindings.filter(f=>f.severity==='high').length,medium:finalFindings.filter(f=>f.severity==='medium').length,low:finalFindings.filter(f=>f.severity==='low').length,info:finalFindings.filter(f=>f.severity==='info').length}};
|
|
7461
7562
|
return{routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,suppressions:_getSuppressions(),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors};}
|
|
7462
7563
|
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
import * as fs from 'node:fs';
|
|
14
14
|
import * as path from 'node:path';
|
|
15
15
|
import * as yaml from 'js-yaml';
|
|
16
|
+
import { statePath } from '../posture/state-dir.js';
|
|
16
17
|
|
|
17
18
|
function _configPath(scanRoot) {
|
|
18
|
-
return
|
|
19
|
+
return statePath(scanRoot, 'integrations.yml');
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function loadIntegrationConfig(scanRoot) {
|
package/src/ir/callgraph.js
CHANGED
|
@@ -8,11 +8,25 @@
|
|
|
8
8
|
// 4. Anything else → unresolved; the dataflow engine treats the callee as
|
|
9
9
|
// an opaque sink for taint.
|
|
10
10
|
|
|
11
|
-
export function buildCallGraph(perFileIR) {
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
11
|
+
export function buildCallGraph(perFileIR, fileContents) {
|
|
12
|
+
const functions = new Map();
|
|
13
|
+
const byNameInFile = new Map();
|
|
14
|
+
const classMethods = new Map();
|
|
15
|
+
// Re-export resolution: track `export { x } from './y'` and `module.exports = require('./y')`
|
|
16
|
+
const reexportMap = new Map();
|
|
17
|
+
if (fileContents) {
|
|
18
|
+
for (const [file, code] of Object.entries(fileContents)) {
|
|
19
|
+
if (!code || typeof code !== 'string') continue;
|
|
20
|
+
for (const m of code.matchAll(/export\s*\{\s*([^}]+)\s*\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
21
|
+
const names = m[1].split(',').map(n => n.trim().split(/\s+as\s+/));
|
|
22
|
+
for (const [orig, alias] of names) {
|
|
23
|
+
reexportMap.set(`${file}::${alias || orig}`, { sourceFile: m[2], sourceName: orig.trim() });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const cjsReexport = code.match(/module\.exports\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
27
|
+
if (cjsReexport) reexportMap.set(`${file}::*`, { sourceFile: cjsReexport[1], sourceName: '*' });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
16
30
|
|
|
17
31
|
for (const file of Object.keys(perFileIR || {})) {
|
|
18
32
|
const ir = perFileIR[file];
|
|
@@ -21,7 +35,6 @@ export function buildCallGraph(perFileIR) {
|
|
|
21
35
|
for (const fn of ir.functions) {
|
|
22
36
|
functions.set(fn.qid, fn);
|
|
23
37
|
byNameInFile.get(file).set(fn.name, fn.qid);
|
|
24
|
-
// Class methods: qid carries the class name as the scope.
|
|
25
38
|
const m = fn.qid.match(/::([A-Z]\w*)::(\w+)@/);
|
|
26
39
|
if (m) classMethods.set(`${m[1]}.${m[2]}`, fn.qid);
|
|
27
40
|
}
|
|
@@ -56,7 +69,6 @@ export function buildCallGraph(perFileIR) {
|
|
|
56
69
|
// ClassName.method falls back).
|
|
57
70
|
function resolve(name) {
|
|
58
71
|
if (!name || typeof name !== 'string') return null;
|
|
59
|
-
// Direct ident match — search every file's same-file map.
|
|
60
72
|
for (const m of byNameInFile.values()) {
|
|
61
73
|
if (m.has(name)) return m.get(name);
|
|
62
74
|
}
|
|
@@ -67,6 +79,14 @@ export function buildCallGraph(perFileIR) {
|
|
|
67
79
|
if (m.has(tail)) return m.get(tail);
|
|
68
80
|
}
|
|
69
81
|
}
|
|
82
|
+
// Follow re-exports: if name was re-exported from another file, resolve there
|
|
83
|
+
for (const [key, { sourceName }] of reexportMap) {
|
|
84
|
+
if (key.endsWith(`::${name}`) || (sourceName === name)) {
|
|
85
|
+
for (const m of byNameInFile.values()) {
|
|
86
|
+
if (m.has(sourceName)) return m.get(sourceName);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
70
90
|
return null;
|
|
71
91
|
}
|
|
72
92
|
return { functions, edges, callersOf, resolve };
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
import * as fs from 'node:fs';
|
|
45
45
|
import * as path from 'node:path';
|
|
46
46
|
import * as crypto from 'node:crypto';
|
|
47
|
+
import { statePath, ensureStateDir, safeWriteState } from '../posture/state-dir.js';
|
|
47
48
|
|
|
48
49
|
// Bump on every prompt change so the cache invalidates. Exported as a
|
|
49
50
|
// stable public symbol (premortem 4R-15) so the validator-cache GC subcommand
|
|
@@ -98,7 +99,9 @@ function endpointConfig() {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
function ensureCacheDir(scanRoot) {
|
|
101
|
-
const
|
|
102
|
+
const base = ensureStateDir(scanRoot);
|
|
103
|
+
if (!base) return null;
|
|
104
|
+
const dir = path.join(base, 'llm-cache');
|
|
102
105
|
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
103
106
|
return dir;
|
|
104
107
|
}
|
|
@@ -112,15 +115,14 @@ function cacheKey(finding, fileHash, modelId) {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
function readCache(scanRoot, key) {
|
|
115
|
-
const fp =
|
|
118
|
+
const fp = statePath(scanRoot, 'llm-cache', key + '.json');
|
|
116
119
|
if (!fs.existsSync(fp)) return null;
|
|
117
120
|
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
function writeCache(scanRoot, key, value) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
try { fs.writeFileSync(fp, JSON.stringify(value, null, 2)); } catch {}
|
|
124
|
+
const fp = statePath(scanRoot, 'llm-cache', key + '.json');
|
|
125
|
+
safeWriteState(fp, JSON.stringify(value, null, 2));
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
function fileHashOf(fileContents, file) {
|
package/src/mcp/audit.js
CHANGED
|
@@ -82,6 +82,11 @@ async function _postRemote(url, entry) {
|
|
|
82
82
|
export function auditCall({ sessionRoot, tool, args, outcome, reason }) {
|
|
83
83
|
if (!sessionRoot) return;
|
|
84
84
|
try {
|
|
85
|
+
// Safety: only write audit log if sessionRoot looks like a project root
|
|
86
|
+
const MARKERS = ['.git', 'package.json', 'pyproject.toml', 'go.mod', 'Cargo.toml', 'pom.xml', 'composer.json', 'Gemfile'];
|
|
87
|
+
let hasMarker = false;
|
|
88
|
+
for (const m of MARKERS) { try { if (fs.existsSync(path.join(sessionRoot, m))) { hasMarker = true; break; } } catch {} }
|
|
89
|
+
if (!hasMarker) return;
|
|
85
90
|
const dir = path.join(sessionRoot, '.agentic-security');
|
|
86
91
|
fs.mkdirSync(dir, { recursive: true });
|
|
87
92
|
const logFile = path.join(dir, 'mcp-audit.log');
|
|
@@ -28,13 +28,14 @@
|
|
|
28
28
|
|
|
29
29
|
import * as fs from 'node:fs';
|
|
30
30
|
import * as path from 'node:path';
|
|
31
|
+
import { statePath } from './state-dir.js';
|
|
31
32
|
|
|
32
33
|
const DEFAULT_THRESHOLD = 0.15;
|
|
33
34
|
const MIN_SAMPLE_SIZE = 10;
|
|
34
35
|
const WINDOW_DAYS = 30;
|
|
35
36
|
|
|
36
37
|
function loadTriageFeedback(scanRoot) {
|
|
37
|
-
const fp =
|
|
38
|
+
const fp = statePath(scanRoot, 'triage-feedback.json');
|
|
38
39
|
try {
|
|
39
40
|
if (!fs.existsSync(fp)) return [];
|
|
40
41
|
const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import * as fs from 'node:fs';
|
|
29
29
|
import * as path from 'node:path';
|
|
30
|
+
import { statePath } from './state-dir.js';
|
|
30
31
|
|
|
31
32
|
const MIN_SAMPLES_FOR_CALIBRATION = 30;
|
|
32
33
|
|
|
@@ -102,7 +103,7 @@ function _readJsonMaybe(fp) {
|
|
|
102
103
|
// seed file. The bundled seed ships with this release; the customer file
|
|
103
104
|
// overrides per-family when N is higher there.
|
|
104
105
|
export function loadCalibrationHistory(scanRoot) {
|
|
105
|
-
const customer = _readJsonMaybe(
|
|
106
|
+
const customer = _readJsonMaybe(statePath(scanRoot, 'validator-metrics.json')) || {};
|
|
106
107
|
const seedPath = new URL('./calibration-seed.json', import.meta.url);
|
|
107
108
|
let seed = null;
|
|
108
109
|
try { seed = JSON.parse(fs.readFileSync(seedPath, 'utf8')); } catch { seed = null; }
|
|
@@ -122,7 +123,7 @@ export function loadCalibrationHistory(scanRoot) {
|
|
|
122
123
|
if (customer) merge(customer);
|
|
123
124
|
// Merge triage-derived TP/FP counts (auto-feedback loop)
|
|
124
125
|
try {
|
|
125
|
-
const triage = _readJsonMaybe(
|
|
126
|
+
const triage = _readJsonMaybe(statePath(scanRoot, 'triage.json'));
|
|
126
127
|
if (triage && triage.findings) {
|
|
127
128
|
const triageFams = {};
|
|
128
129
|
for (const f of Object.values(triage.findings)) {
|
|
@@ -12,13 +12,19 @@ import * as fs from 'node:fs';
|
|
|
12
12
|
import * as fsp from 'node:fs/promises';
|
|
13
13
|
import * as path from 'node:path';
|
|
14
14
|
import * as crypto from 'node:crypto';
|
|
15
|
+
import { isSafeStateDir, statePath } from './state-dir.js';
|
|
15
16
|
|
|
16
17
|
function historyDir(scanRoot) {
|
|
17
|
-
return
|
|
18
|
+
return statePath(scanRoot, 'fix-history');
|
|
18
19
|
}
|
|
19
20
|
function logPath(scanRoot) { return path.join(historyDir(scanRoot), 'log.json'); }
|
|
20
21
|
|
|
21
|
-
function ensure(scanRoot) {
|
|
22
|
+
function ensure(scanRoot) {
|
|
23
|
+
const dir = historyDir(scanRoot);
|
|
24
|
+
if (!isSafeStateDir(path.dirname(dir))) return false;
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
22
28
|
|
|
23
29
|
export function readLog(scanRoot) {
|
|
24
30
|
const fp = logPath(scanRoot);
|
package/src/posture/profile.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import * as fs from 'node:fs';
|
|
7
7
|
import * as path from 'node:path';
|
|
8
8
|
import * as yaml from 'js-yaml';
|
|
9
|
+
import { statePath, safeWriteState, resolveProjectRoot } from './state-dir.js';
|
|
9
10
|
|
|
10
11
|
export const PROFILES = ['vibecoder', 'pro'];
|
|
11
12
|
|
|
@@ -35,7 +36,7 @@ export const DEFAULTS = {
|
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
function _profilePath(scanRoot) {
|
|
38
|
-
return
|
|
39
|
+
return statePath(scanRoot, 'profile.yml');
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export function loadProfile(scanRoot) {
|
|
@@ -52,10 +53,8 @@ export function loadProfile(scanRoot) {
|
|
|
52
53
|
|
|
53
54
|
export function saveProfile(scanRoot, updates) {
|
|
54
55
|
const fp = _profilePath(scanRoot);
|
|
55
|
-
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
56
56
|
const current = loadProfile(scanRoot);
|
|
57
57
|
const next = { ...current, ...updates };
|
|
58
|
-
// Strip values equal to defaults so the file stays minimal.
|
|
59
58
|
const defaults = DEFAULTS[next.profile];
|
|
60
59
|
const out = {};
|
|
61
60
|
for (const k of Object.keys(next)) {
|
|
@@ -63,7 +62,7 @@ export function saveProfile(scanRoot, updates) {
|
|
|
63
62
|
out[k] = next[k];
|
|
64
63
|
}
|
|
65
64
|
if (!('profile' in out)) out.profile = next.profile;
|
|
66
|
-
|
|
65
|
+
safeWriteState(fp, yaml.dump(out));
|
|
67
66
|
return next;
|
|
68
67
|
}
|
|
69
68
|
|
|
@@ -71,7 +70,7 @@ export function saveProfile(scanRoot, updates) {
|
|
|
71
70
|
// Returns 'pro' if the repo has signals indicating professional security work,
|
|
72
71
|
// otherwise 'vibecoder'. Run only on first scan.
|
|
73
72
|
export function detectProfile(scanRoot) {
|
|
74
|
-
const root = scanRoot
|
|
73
|
+
const root = resolveProjectRoot(scanRoot);
|
|
75
74
|
const signals = ['SECURITY.md', '.github/workflows/security.yml', '.semgrep.yml',
|
|
76
75
|
'.snyk', 'codeql-config.yml', 'compliance/', 'docs/threat-model.md'];
|
|
77
76
|
for (const s of signals) {
|
|
@@ -11,11 +11,10 @@ import * as fs from 'node:fs';
|
|
|
11
11
|
import * as path from 'node:path';
|
|
12
12
|
import * as yaml from 'js-yaml';
|
|
13
13
|
import { verifyLastScan } from './integrity.js';
|
|
14
|
-
|
|
15
|
-
const OVERRIDES_PATH = '.agentic-security/rules.yml';
|
|
14
|
+
import { statePath } from './state-dir.js';
|
|
16
15
|
|
|
17
16
|
function _path(scanRoot) {
|
|
18
|
-
return
|
|
17
|
+
return statePath(scanRoot, 'rules.yml');
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
export function loadOverrides(scanRoot) {
|
|
@@ -30,8 +30,7 @@
|
|
|
30
30
|
import * as fs from 'node:fs';
|
|
31
31
|
import * as path from 'node:path';
|
|
32
32
|
import * as crypto from 'node:crypto';
|
|
33
|
-
|
|
34
|
-
const TRUSTED_KEYS_FILE = '.agentic-security/trusted-keys.json';
|
|
33
|
+
import { statePath } from './state-dir.js';
|
|
35
34
|
|
|
36
35
|
// Built-in trust root. These are the keys the maintainers of agentic-security
|
|
37
36
|
// use to sign official rule packs. Production deployment requires the
|
|
@@ -49,7 +48,7 @@ export const BUNDLED_OFFICIAL_KEYS = [
|
|
|
49
48
|
];
|
|
50
49
|
|
|
51
50
|
function _trustedKeysPath(scanRoot) {
|
|
52
|
-
return
|
|
51
|
+
return statePath(scanRoot, 'trusted-keys.json');
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
// Load the EFFECTIVE trusted-key set. Composition:
|
|
@@ -13,14 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from 'node:fs';
|
|
15
15
|
import * as path from 'node:path';
|
|
16
|
-
|
|
17
|
-
const TRIAGE_PATH = path.join('.agentic-security', 'triage-feedback.json');
|
|
18
|
-
const PROPOSED_DIR = path.join('.agentic-security', 'rules-proposed');
|
|
16
|
+
import { statePath, isSafeStateDir } from './state-dir.js';
|
|
19
17
|
|
|
20
18
|
const DEFAULT_FP_THRESHOLD = 5;
|
|
21
19
|
|
|
22
20
|
function _readTriage(scanRoot) {
|
|
23
|
-
const fp =
|
|
21
|
+
const fp = statePath(scanRoot, 'triage-feedback.json');
|
|
24
22
|
if (!fs.existsSync(fp)) return null;
|
|
25
23
|
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
26
24
|
}
|
|
@@ -87,7 +85,8 @@ export function synthesizeRules(scanRoot, opts = {}) {
|
|
|
87
85
|
groups.get(k).push(e);
|
|
88
86
|
}
|
|
89
87
|
const proposals = [];
|
|
90
|
-
const dir =
|
|
88
|
+
const dir = statePath(scanRoot, 'rules-proposed');
|
|
89
|
+
if (!opts.dryRun && !isSafeStateDir(path.dirname(dir))) return [];
|
|
91
90
|
for (const [, group] of groups) {
|
|
92
91
|
if (group.length < threshold) continue;
|
|
93
92
|
const summary = _summarizeGroup(group);
|
|
@@ -105,4 +104,4 @@ export function synthesizeRules(scanRoot, opts = {}) {
|
|
|
105
104
|
return proposals;
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
export const _internals = { DEFAULT_FP_THRESHOLD
|
|
107
|
+
export const _internals = { DEFAULT_FP_THRESHOLD };
|