@clear-capabilities/agentic-security-scanner 0.78.0 → 0.80.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/333.index.js +283 -0
- 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 +95 -1
- package/dist/agentic-security.mjs +83 -83
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -4
- package/src/.agentic-security/findings.json +29799 -7803
- package/src/.agentic-security/last-scan.json +29799 -7803
- package/src/.agentic-security/last-scan.json.sig +1 -1
- package/src/.agentic-security/scan-history.json +5119 -2611
- package/src/.agentic-security/streak.json +6 -6
- package/src/dataflow/.agentic-security/findings.json +2879 -308
- package/src/dataflow/.agentic-security/last-scan.json +2879 -308
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
- package/src/dataflow/.agentic-security/scan-history.json +68 -520
- package/src/dataflow/.agentic-security/streak.json +6 -7
- package/src/dataflow/cross-service-taint.js +201 -0
- package/src/dataflow/engine.js +52 -8
- 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 +890 -132
- package/src/integrations/index.js +2 -1
- package/src/ir/.agentic-security/findings.json +240 -6
- package/src/ir/.agentic-security/last-scan.json +240 -6
- package/src/ir/.agentic-security/last-scan.json.sig +1 -1
- package/src/ir/.agentic-security/scan-history.json +16 -594
- package/src/ir/.agentic-security/streak.json +8 -9
- package/src/ir/callgraph.js +27 -7
- 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/llm-validator/index.js +7 -5
- 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 +143 -0
- package/src/mcp/.agentic-security/streak.json +20 -0
- package/src/mcp/audit.js +5 -0
- package/src/mcp/tools.js +90 -1
- package/src/posture/.agentic-security/findings.json +16809 -4367
- package/src/posture/.agentic-security/last-scan.json +16809 -4367
- package/src/posture/.agentic-security/last-scan.json.sig +1 -1
- package/src/posture/.agentic-security/scan-history.json +6689 -177
- package/src/posture/.agentic-security/streak.json +8 -7
- package/src/posture/api-contract.js +193 -0
- package/src/posture/attack-taxonomy.js +227 -0
- package/src/posture/calibration-drift.js +2 -1
- package/src/posture/calibration.js +3 -2
- package/src/posture/compliance-policy.js +218 -0
- package/src/posture/composite-risk.js +122 -0
- package/src/posture/csharp-analysis.js +330 -0
- package/src/posture/exploit-bundle.js +210 -0
- package/src/posture/federated-learning.js +172 -0
- package/src/posture/fix-history.js +8 -2
- package/src/posture/license-attributions.js +94 -0
- package/src/posture/license-graph.js +238 -0
- package/src/posture/pqc-migration-plan.js +158 -0
- package/src/posture/profile.js +4 -5
- package/src/posture/reachability-filter.js +33 -2
- package/src/posture/realtime-cve-monitor.js +214 -0
- 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/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/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/threat-model-auto.js +268 -0
- package/src/posture/triage-learning.js +170 -0
- package/src/posture/triage.js +29 -6
- package/src/posture/validator-metrics.js +3 -6
- package/src/sast/.agentic-security/findings.json +996 -32
- package/src/sast/.agentic-security/last-scan.json +996 -32
- package/src/sast/.agentic-security/last-scan.json.sig +1 -1
- package/src/sast/.agentic-security/scan-history.json +565 -32
- package/src/sast/.agentic-security/streak.json +10 -8
- 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/db-taint.js +24 -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/rust.js +26 -0
- package/src/sast/web3-advanced.js +375 -0
- package/src/sca/.agentic-security/findings.json +6044 -171
- package/src/sca/.agentic-security/last-scan.json +6044 -171
- package/src/sca/.agentic-security/last-scan.json.sig +1 -1
- package/src/sca/.agentic-security/scan-history.json +83 -6
- package/src/sca/.agentic-security/streak.json +9 -9
- package/src/sca/CLAUDE.md +161 -0
- package/src/sca/binary-metadata.js +146 -0
- package/src/sca/py-package-functions.js +118 -0
- package/src/sca/sigstore-verify.js +215 -0
- package/src/sca/vendor-detect.js +53 -0
- 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/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';
|
|
@@ -68,6 +68,7 @@ import { scanLlmRedteam } from './posture/llm-redteam.js';
|
|
|
68
68
|
import { scanContainer } from './sca/container.js';
|
|
69
69
|
import { detectDepConfusion } from './sca/dep-confusion.js';
|
|
70
70
|
import { loadLicensePolicy, evaluateLicensePolicy } from './posture/license-policy.js';
|
|
71
|
+
import { loadScaPolicy, applyScaPolicy } from './posture/sca-policy.js';
|
|
71
72
|
import { scanDeployPlatform } from './posture/deploy-platform.js';
|
|
72
73
|
import { runStackPlaybook } from './posture/stack-playbook.js';
|
|
73
74
|
// Phase 1 (Sentinel-parity PRD) — new detection modules.
|
|
@@ -92,6 +93,7 @@ import { scanDeserializationGadgets, _detectGadgets } from './sast/deserializati
|
|
|
92
93
|
import { scanKotlin } from './sast/kotlin.js';
|
|
93
94
|
import { scanRuby } from './sast/ruby.js';
|
|
94
95
|
import { scanPhp } from './sast/php.js';
|
|
96
|
+
import { classifySecretCandidate as _entropyClassifySecret } from './sast/_secret-entropy.js';
|
|
95
97
|
// Phase 1 — precision-engineering posture modules.
|
|
96
98
|
import { annotateConfidence } from './posture/confidence.js';
|
|
97
99
|
import { backfillFindingDefaults } from './posture/finding-defaults.js';
|
|
@@ -126,6 +128,37 @@ import { annotateCrownJewelScores } from './posture/crown-jewels.js';
|
|
|
126
128
|
import { annotateFeatureFlagGating } from './posture/feature-flags.js';
|
|
127
129
|
import { annotatePersonaScores } from './posture/persona-prioritization.js';
|
|
128
130
|
import { annotateMitigationComposite } from './posture/mitigation-composite.js';
|
|
131
|
+
import { annotateCompositeRisk } from './posture/composite-risk.js';
|
|
132
|
+
|
|
133
|
+
// ── Integration block: world-class scaffolded modules ──────────────────────
|
|
134
|
+
// Each module is opt-in via its own env var so partial adoption is safe.
|
|
135
|
+
// AGENTIC_SECURITY_NO_INTEGRATION=1 disables the entire block (for CI bench
|
|
136
|
+
// runs that need bit-identical baselines).
|
|
137
|
+
import { scanLlmApp } from './sast/llm-app.js';
|
|
138
|
+
import { scanMobile } from './sast/mobile.js';
|
|
139
|
+
import { scanPqc } from './sast/post-quantum-crypto.js';
|
|
140
|
+
import { scanWeb3Advanced } from './sast/web3-advanced.js';
|
|
141
|
+
import { scanDappFrontend } from './sast/dapp-frontend.js';
|
|
142
|
+
import { scanCloudIam } from './sast/cloud-iam.js';
|
|
143
|
+
import { scanK8sAdmission } from './sast/k8s-admission.js';
|
|
144
|
+
import { scanCryptoProtocol } from './sast/crypto-protocol.js';
|
|
145
|
+
import { scanMlSupplyChain } from './sast/ml-supply-chain.js';
|
|
146
|
+
import { runCrossServiceTaint } from './dataflow/cross-service-taint.js';
|
|
147
|
+
import { annotateRuntimeCorrelation } from './posture/runtime-correlation.js';
|
|
148
|
+
import { applyLearnedCalibration } from './posture/triage-learning.js';
|
|
149
|
+
import { annotateFormalVerification } from './dataflow/formal-verify.js';
|
|
150
|
+
import { annotatePathFeasibility } from './dataflow/smt-feasibility.js';
|
|
151
|
+
import { annotatePrivacyTaint, emitDpiaArtifact } from './dataflow/privacy-taint.js';
|
|
152
|
+
import { buildThreatModel as buildAutoThreatModel, persistThreatModel as persistAutoThreatModel } from './posture/threat-model-auto.js';
|
|
153
|
+
import { runApiContractScan } from './posture/api-contract.js';
|
|
154
|
+
import { annotateProvenance } from './sca/sigstore-verify.js';
|
|
155
|
+
import { runSbomDiff } from './posture/sbom-diff.js';
|
|
156
|
+
import { loadPolicy as loadCompliancePolicy, verifyPolicy as verifyCompliancePolicy, emitEvidenceJsonLd as emitComplianceJsonLd, emitEvidenceMarkdown as emitComplianceMarkdown } from './posture/compliance-policy.js';
|
|
157
|
+
import { generateBundles as generateExploitBundles } from './posture/exploit-bundle.js';
|
|
158
|
+
import { buildMigrationPlan as buildPqcPlan, persistMigrationPlan as persistPqcPlan } from './posture/pqc-migration-plan.js';
|
|
159
|
+
import { analyzeLicenseGraph, loadLicenseGraphPolicy } from './posture/license-graph.js';
|
|
160
|
+
import { generateAttributions, persistAttributions } from './posture/license-attributions.js';
|
|
161
|
+
import { annotateAttackTaxonomy, summarizeTaxonomy } from './posture/attack-taxonomy.js';
|
|
129
162
|
import { annotateTypeNarrowing } from './posture/type-narrowing.js';
|
|
130
163
|
import { annotateWhyFired } from './posture/why-fired.js';
|
|
131
164
|
import { scanSpecificationDrift } from './posture/specification-mining.js';
|
|
@@ -2049,6 +2082,13 @@ function _isFalsePositiveCredential(fp, snippet, fullMatch){
|
|
|
2049
2082
|
const val = valM ? valM[1] : '';
|
|
2050
2083
|
if (val.length < 8) return {skip:true, reason:'value-too-short'};
|
|
2051
2084
|
if (_CRED_PLACEHOLDER_VAL_RE.test(val)) return {skip:true, reason:'placeholder-value'};
|
|
2085
|
+
// Shannon-entropy + dictionary-word filter (Recommendation #1 from the
|
|
2086
|
+
// SCA/SAST improvement plan). The Juliet Java hardcoded-secret detector
|
|
2087
|
+
// was producing 468 FPs / 1 TP because test fixtures use short
|
|
2088
|
+
// dictionary words ("hello", "password", "todo") as fake credentials.
|
|
2089
|
+
// Real secrets score ≥3.5 bits/char and aren't dictionary words.
|
|
2090
|
+
const entropyVerdict = _entropyClassifySecret(val);
|
|
2091
|
+
if (entropyVerdict.skip) return { skip: true, reason: `entropy-filter:${entropyVerdict.reason}` };
|
|
2052
2092
|
// Non-ASCII content with i18n-shaped variable name → translation string
|
|
2053
2093
|
if (_CRED_I18N_VAL_RE.test(val) && /(?:label|message|text|title|description|placeholder)/i.test(varName)) {
|
|
2054
2094
|
return {skip:true, reason:'i18n-text'};
|
|
@@ -2713,7 +2753,16 @@ const JAVA_FAMILY_RULES = [
|
|
|
2713
2753
|
// Generic verbs (update / insert / delete / count / query) removed — they
|
|
2714
2754
|
// misfire on hash.update, list.insert/delete/count, etc. JdbcTemplate's
|
|
2715
2755
|
// verb-based methods still match via batchUpdate / queryForObject etc.
|
|
2716
|
-
|
|
2756
|
+
//
|
|
2757
|
+
// Recommendation #4 of the SCA/SAST improvement plan: expanded sink list.
|
|
2758
|
+
// Added: NamedParameterJdbcTemplate.{query,queryForObject,…} (Spring),
|
|
2759
|
+
// EntityManager.createQuery+createNativeQuery (JPA),
|
|
2760
|
+
// Session.createNativeQuery + createSQLQuery (Hibernate native),
|
|
2761
|
+
// Criteria.add(Restrictions.sqlRestriction(…)) (Hibernate criteria),
|
|
2762
|
+
// SqlSession.{selectList,selectOne,selectMap,update,insert,delete} (MyBatis),
|
|
2763
|
+
// JdbcTemplate.{queryForStream,queryForRowSet}, R2DBC DatabaseClient.sql,
|
|
2764
|
+
// JdbcOperations / JdbcAggregateOperations.* (Spring Data JDBC).
|
|
2765
|
+
sinkRe: /\.\s*(?:executeQuery|executeUpdate|execute|executeBatch|prepareStatement|prepareCall|createQuery|createNativeQuery|createSQLQuery|createCriteriaQuery|createSqlQuery|createStatement|addBatch|queryForObject|queryForList|queryForMap|queryForLong|queryForInt|queryForRowSet|queryForStream|batchUpdate|find_by_sql|sqlRestriction|selectList|selectOne|selectMap|selectCursor|sql)\s*\(/,
|
|
2717
2766
|
sanitizerRe: null,
|
|
2718
2767
|
useTaint: true,
|
|
2719
2768
|
},
|
|
@@ -4563,10 +4612,14 @@ function scanMiddlewareOrdering(fp, raw){
|
|
|
4563
4612
|
if (isAuth && line < firstAuthAt) firstAuthAt = line;
|
|
4564
4613
|
if (mountPath) mounts.push({ line, mountPath, handlerArgs, isAuth });
|
|
4565
4614
|
}
|
|
4615
|
+
let firstRateLimitAt = Infinity;
|
|
4616
|
+
for (const mt of mounts) {
|
|
4617
|
+
if (/rateLimit|rate.?limit|throttle|slowDown/i.test(mt.handlerArgs) && mt.line < firstRateLimitAt) firstRateLimitAt = mt.line;
|
|
4618
|
+
}
|
|
4566
4619
|
for (const mt of mounts) {
|
|
4567
4620
|
if (mt.isAuth) continue;
|
|
4568
4621
|
if (!_SENSITIVE_PATH_RE.test(mt.mountPath)) continue;
|
|
4569
|
-
if (mt.line >= firstAuthAt) continue;
|
|
4622
|
+
if (mt.line >= firstAuthAt) continue;
|
|
4570
4623
|
findings.push({
|
|
4571
4624
|
vuln: `Sensitive Route Mounted Before Auth Middleware (${mt.mountPath})`,
|
|
4572
4625
|
severity: 'high', cwe: 'CWE-285', stride: 'Elevation of Privilege',
|
|
@@ -4574,6 +4627,27 @@ function scanMiddlewareOrdering(fp, raw){
|
|
|
4574
4627
|
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
4628
|
});
|
|
4576
4629
|
}
|
|
4630
|
+
// Check rate-limiting before auth (auth endpoints should be rate-limited)
|
|
4631
|
+
if (firstAuthAt < Infinity && firstRateLimitAt > firstAuthAt) {
|
|
4632
|
+
findings.push({
|
|
4633
|
+
vuln: 'Rate Limiting After Auth Middleware — brute-force attacks bypass rate limits',
|
|
4634
|
+
severity: 'medium', cwe: 'CWE-307', stride: 'Denial of Service',
|
|
4635
|
+
file: fp, line: firstAuthAt, snippet: lines[firstAuthAt - 1]?.trim() || '',
|
|
4636
|
+
fix: 'Register rate-limiting middleware BEFORE auth middleware so brute-force login attempts are throttled: app.use(rateLimit({...})); app.use(authMiddleware);',
|
|
4637
|
+
});
|
|
4638
|
+
}
|
|
4639
|
+
// Check for method-level auth on sensitive routes
|
|
4640
|
+
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;
|
|
4641
|
+
for (const rm of cleaned.matchAll(routeRe)) {
|
|
4642
|
+
const routeLine = lineAt(cleaned, rm.index);
|
|
4643
|
+
if (routeLine >= firstAuthAt) continue;
|
|
4644
|
+
findings.push({
|
|
4645
|
+
vuln: `Sensitive Route Without Auth — ${rm[1].toUpperCase()} ${rm[2]}`,
|
|
4646
|
+
severity: 'high', cwe: 'CWE-306', stride: 'Elevation of Privilege',
|
|
4647
|
+
file: fp, line: routeLine, snippet: lines[routeLine - 1]?.trim() || '',
|
|
4648
|
+
fix: `Add auth middleware to this route: router.${rm[1]}('${rm[2]}', authMiddleware, handler). Or ensure app.use(authMiddleware) appears before this route definition.`,
|
|
4649
|
+
});
|
|
4650
|
+
}
|
|
4577
4651
|
return findings;
|
|
4578
4652
|
}
|
|
4579
4653
|
|
|
@@ -5507,17 +5581,50 @@ const VULN_FUNCTION_HINTS = {
|
|
|
5507
5581
|
...(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
5582
|
...(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
5583
|
};
|
|
5584
|
+
function _semverSatisfies(ver,range){
|
|
5585
|
+
if(!ver||!range)return false;
|
|
5586
|
+
const parse=v=>{const m=(v||'').match(/^[=v]*(\d+)\.(\d+)\.(\d+)/);return m?[+m[1],+m[2],+m[3]]:null;};
|
|
5587
|
+
const cmp=(a,b)=>{for(let i=0;i<3;i++){if(a[i]!==b[i])return a[i]-b[i];}return 0;};
|
|
5588
|
+
const v=parse(ver);if(!v)return false;
|
|
5589
|
+
for(const part of range.split(/\s*\|\|\s*/)){
|
|
5590
|
+
let ok=true;
|
|
5591
|
+
for(const cond of part.split(/\s+/).filter(Boolean)){
|
|
5592
|
+
const m=cond.match(/^([<>=!]+)(.+)$/);if(!m)continue;
|
|
5593
|
+
const t=parse(m[2]);if(!t){ok=false;break;}
|
|
5594
|
+
const c=cmp(v,t);const op=m[1];
|
|
5595
|
+
if(op==='>='&&c<0){ok=false;break;}
|
|
5596
|
+
if(op==='>'&&c<=0){ok=false;break;}
|
|
5597
|
+
if(op==='<='&&c>0){ok=false;break;}
|
|
5598
|
+
if(op==='<'&&c>=0){ok=false;break;}
|
|
5599
|
+
if(op==='='&&c!==0){ok=false;break;}
|
|
5600
|
+
if(op==='!='&&c===0){ok=false;break;}
|
|
5601
|
+
}
|
|
5602
|
+
if(ok)return true;
|
|
5603
|
+
}
|
|
5604
|
+
return false;
|
|
5605
|
+
}
|
|
5510
5606
|
function markUsedVulnFunctions(supplyChain,fc){
|
|
5511
5607
|
const used={};
|
|
5512
5608
|
const perFile={};
|
|
5513
5609
|
for(const[fp,content] of Object.entries(fc)){
|
|
5514
5610
|
const lines=content.split('\n');
|
|
5611
|
+
// Rust import-aware matching: build import map for .rs files
|
|
5612
|
+
let _rustImports=null;
|
|
5613
|
+
if(/\.rs$/i.test(fp)){try{_rustImports=extractRustImportMap(content);}catch(_){}}
|
|
5515
5614
|
for(const[pkg,fns] of Object.entries(VULN_FUNCTION_HINTS)){
|
|
5516
5615
|
if(!perFile[pkg])perFile[pkg]=[];
|
|
5517
5616
|
for(const fn of fns){
|
|
5518
5617
|
const re=new RegExp(`\\b(?:${pkg.replace(/\W/g,'\\$&')}|_)\\.${fn}\\b`,'g');
|
|
5618
|
+
// Rust: also match bare function calls if import map traces them to this package
|
|
5619
|
+
const rustBareRe=_rustImports?new RegExp(`\\b${fn.replace(/\W/g,'\\$&')}\\s*[(<]`,'g'):null;
|
|
5519
5620
|
for(let li=0;li<lines.length;li++){
|
|
5520
|
-
|
|
5621
|
+
let matched=re.test(lines[li]);
|
|
5622
|
+
re.lastIndex=0;
|
|
5623
|
+
if(!matched&&rustBareRe){
|
|
5624
|
+
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;}
|
|
5625
|
+
rustBareRe.lastIndex=0;
|
|
5626
|
+
}
|
|
5627
|
+
if(matched){
|
|
5521
5628
|
perFile[pkg].push({pkg,fn,file:fp,line:li+1});
|
|
5522
5629
|
if(!used[pkg])used[pkg]=new Set();
|
|
5523
5630
|
used[pkg].add(fn);
|
|
@@ -5529,10 +5636,20 @@ function markUsedVulnFunctions(supplyChain,fc){
|
|
|
5529
5636
|
}
|
|
5530
5637
|
for(const sc of supplyChain||[]){
|
|
5531
5638
|
if(sc.type!=='vulnerable_dep')continue;
|
|
5532
|
-
// Merge hints: hardcoded → OSV ecosystem_specific → skip if none
|
|
5639
|
+
// Merge hints: versioned → hardcoded → OSV ecosystem_specific → skip if none
|
|
5533
5640
|
const hardcoded=VULN_FUNCTION_HINTS[sc.name]||[];
|
|
5641
|
+
// Version-scoped hints: check pkg@range keys
|
|
5642
|
+
const versionedFns=[];
|
|
5643
|
+
for(const[hk,hv] of Object.entries(VULN_FUNCTION_HINTS)){
|
|
5644
|
+
if(!hk.includes('@'))continue;
|
|
5645
|
+
const atIdx=hk.indexOf('@');
|
|
5646
|
+
const hPkg=hk.slice(0,atIdx);
|
|
5647
|
+
const hRange=hk.slice(atIdx+1);
|
|
5648
|
+
if(hPkg!==sc.name)continue;
|
|
5649
|
+
try{if(_semverSatisfies(sc.version,hRange))versionedFns.push(...hv);}catch(_){}
|
|
5650
|
+
}
|
|
5534
5651
|
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])];
|
|
5652
|
+
const allFns=[...new Set([...versionedFns,...hardcoded,...osvFns])];
|
|
5536
5653
|
if(!allFns.length){sc.functionReachable='unknown';sc.noKnownCallSite=true;sc._hintSource='none';continue;}
|
|
5537
5654
|
sc._hintSource=osvFns.length?(hardcoded.length?'hardcoded+osv':'osv'):'hardcoded';
|
|
5538
5655
|
// Search codebase for these functions (if not already searched via VULN_FUNCTION_HINTS)
|
|
@@ -5564,41 +5681,125 @@ function markUsedVulnFunctions(supplyChain,fc){
|
|
|
5564
5681
|
}
|
|
5565
5682
|
|
|
5566
5683
|
// Annotate each supplyChain finding with `functionReachable` ∈ {'reachable','unreachable','unknown'}.
|
|
5567
|
-
// The CVE only matters if the developer's code actually calls the vulnerable
|
|
5568
|
-
//
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5684
|
+
// The CVE only matters if the developer's code actually calls the vulnerable
|
|
5685
|
+
// function. The question is HOW reachable: is the call inline in a route
|
|
5686
|
+
// handler (definitely user-input-reachable), called by some caller eventually
|
|
5687
|
+
// reached from a route (transitively reachable), or called by code that no
|
|
5688
|
+
// route ever touches (function-reachable but not route-reachable)?
|
|
5689
|
+
//
|
|
5690
|
+
// Phase 2 / Item 4 of the SCA improvement plan: distinguish these tiers.
|
|
5691
|
+
// Sets two fields:
|
|
5692
|
+
// sc.functionReachable: 'reachable' | 'unreachable' | 'unknown'
|
|
5693
|
+
// sc.routeReachable: true | false (only meaningful when functionReachable === 'reachable')
|
|
5694
|
+
//
|
|
5695
|
+
// Used by the tier-assignment block downstream to label
|
|
5696
|
+
// `reachabilityTier: 'route-reachable-via-function'` (highest urgency) vs
|
|
5697
|
+
// `'function-reachable'` (called but not chained to a route).
|
|
5698
|
+
|
|
5699
|
+
// Set of every (file, fnName) that *is* a route handler. The enclosing
|
|
5700
|
+
// function at a route's declaration line, mapped from the (file, line) pairs
|
|
5701
|
+
// the route scanner emitted.
|
|
5702
|
+
function _buildRouteHandlerSet(routes, fc){
|
|
5703
|
+
const handlers = new Set(); // 'file::fnName'
|
|
5704
|
+
for (const r of (routes || [])) {
|
|
5705
|
+
const content = fc[r.file];
|
|
5706
|
+
if (typeof content !== 'string') continue;
|
|
5707
|
+
const fn = _enclosingFn(content, r.line);
|
|
5708
|
+
if (fn) handlers.add(r.file + '::' + fn);
|
|
5709
|
+
// Capture an anonymous handler too — many JS routes are
|
|
5710
|
+
// `app.get('/x', (req, res) => { … })` where the route line itself IS
|
|
5711
|
+
// the function body. The body's identifier is unstable but we can mark
|
|
5712
|
+
// the file:line for the inline check.
|
|
5713
|
+
}
|
|
5714
|
+
return handlers;
|
|
5715
|
+
}
|
|
5716
|
+
|
|
5717
|
+
// Reverse-BFS through the call graph: starting from `enclosingFn` in `file`,
|
|
5718
|
+
// find every function (anywhere in the program) that transitively calls it,
|
|
5719
|
+
// up to `maxDepth`. Returns the set of caller fnNames.
|
|
5720
|
+
function _reverseCallGraphReachable(callGraph, startFn, maxDepth){
|
|
5721
|
+
if (!callGraph || typeof callGraph !== 'object') return new Set();
|
|
5722
|
+
const visited = new Set([startFn]);
|
|
5723
|
+
let frontier = new Set([startFn]);
|
|
5724
|
+
for (let depth = 0; depth < maxDepth && frontier.size; depth++) {
|
|
5725
|
+
const next = new Set();
|
|
5726
|
+
for (const [file, fileFns] of Object.entries(callGraph)) {
|
|
5727
|
+
if (!fileFns || typeof fileFns !== 'object') continue;
|
|
5728
|
+
for (const [fn, info] of Object.entries(fileFns)) {
|
|
5729
|
+
if (visited.has(file + '::' + fn) || visited.has(fn)) continue;
|
|
5730
|
+
const calls = info && info.calls;
|
|
5731
|
+
if (!calls) continue;
|
|
5732
|
+
for (const target of frontier) {
|
|
5733
|
+
const hit = calls.has?.(target) || (Array.isArray(calls) && calls.includes(target));
|
|
5734
|
+
if (hit) {
|
|
5735
|
+
next.add(fn);
|
|
5736
|
+
// Track both qualified and unqualified — the route-handler set
|
|
5737
|
+
// uses 'file::fn' but the call graph carries bare fn names.
|
|
5738
|
+
visited.add(fn);
|
|
5739
|
+
visited.add(file + '::' + fn);
|
|
5740
|
+
break;
|
|
5741
|
+
}
|
|
5584
5742
|
}
|
|
5585
5743
|
}
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5744
|
+
}
|
|
5745
|
+
frontier = next;
|
|
5746
|
+
}
|
|
5747
|
+
return visited;
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
function _annotateFunctionReachability(supplyChain, routes, callGraph, fc){
|
|
5751
|
+
const routeHandlers = _buildRouteHandlerSet(routes, fc);
|
|
5752
|
+
// Pre-extract just the unqualified names too — the call-graph traversal
|
|
5753
|
+
// matches on bare fnName when the qualified form isn't available.
|
|
5754
|
+
const routeHandlerNames = new Set();
|
|
5755
|
+
for (const k of routeHandlers) {
|
|
5756
|
+
const fn = k.split('::')[1];
|
|
5757
|
+
if (fn) routeHandlerNames.add(fn);
|
|
5758
|
+
}
|
|
5759
|
+
|
|
5760
|
+
for (const sc of (supplyChain || [])) {
|
|
5761
|
+
if (sc.type !== 'vulnerable_dep') continue;
|
|
5762
|
+
const sites = sc.vulnerableFunctionCallSites || [];
|
|
5763
|
+
if (!sites.length) { sc.functionReachable = 'unknown'; sc.routeReachable = false; continue; }
|
|
5764
|
+
let functionReachable = false;
|
|
5765
|
+
let routeReachable = false;
|
|
5766
|
+
for (const site of sites) {
|
|
5767
|
+
// Classifier 1: site is inline inside a route handler (within 25 lines
|
|
5768
|
+
// of the route def, no intervening function declaration). This is the
|
|
5769
|
+
// strongest signal — the vulnerable function is called directly from
|
|
5770
|
+
// user-input handling code.
|
|
5771
|
+
const fileRoutes = (routes || []).filter(r => r.file === site.file);
|
|
5772
|
+
for (const route of fileRoutes) {
|
|
5773
|
+
if (site.line >= route.line && site.line <= route.line + 25) {
|
|
5774
|
+
const fileLines = (fc[site.file] || '').split('\n');
|
|
5775
|
+
const between = fileLines.slice(route.line, site.line - 1).join('\n');
|
|
5776
|
+
if (!/function\s+\w+\s*\(/.test(between)) {
|
|
5777
|
+
functionReachable = true;
|
|
5778
|
+
routeReachable = true;
|
|
5779
|
+
break;
|
|
5596
5780
|
}
|
|
5597
5781
|
}
|
|
5598
5782
|
}
|
|
5599
|
-
if(
|
|
5783
|
+
if (routeReachable) break;
|
|
5784
|
+
// Classifier 2: walk reverse call graph from the enclosing function.
|
|
5785
|
+
// If any caller-chain hits a known route-handler function, the site
|
|
5786
|
+
// is route-reachable-via-function. If no caller at all, we keep
|
|
5787
|
+
// functionReachable=false for this site.
|
|
5788
|
+
const enclosing = _enclosingFn(fc[site.file] || '', site.line);
|
|
5789
|
+
if (!enclosing) continue;
|
|
5790
|
+
const callers = _reverseCallGraphReachable(callGraph, enclosing, 4);
|
|
5791
|
+
if (callers.size > 1) functionReachable = true; // at least one caller exists
|
|
5792
|
+
for (const callerFn of callers) {
|
|
5793
|
+
if (routeHandlerNames.has(callerFn) || routeHandlers.has(site.file + '::' + callerFn)) {
|
|
5794
|
+
routeReachable = true;
|
|
5795
|
+
functionReachable = true;
|
|
5796
|
+
break;
|
|
5797
|
+
}
|
|
5798
|
+
}
|
|
5799
|
+
if (routeReachable) break;
|
|
5600
5800
|
}
|
|
5601
|
-
sc.functionReachable=
|
|
5801
|
+
sc.functionReachable = functionReachable ? 'reachable' : 'unreachable';
|
|
5802
|
+
sc.routeReachable = routeReachable;
|
|
5602
5803
|
}
|
|
5603
5804
|
}
|
|
5604
5805
|
function _enclosingFn(content,line){
|
|
@@ -5896,39 +6097,72 @@ function _osvCacheGet(key){try{const r=sessionStorage.getItem('osv_'+key);return
|
|
|
5896
6097
|
function _osvCacheSet(key,val){try{sessionStorage.setItem('osv_'+key,JSON.stringify(val));}catch(_){}}
|
|
5897
6098
|
|
|
5898
6099
|
// Feat-9: EPSS (community abuse-probability index) overlay. Fetches probability
|
|
5899
|
-
// of abuse in the next 30 days per
|
|
6100
|
+
// of abuse in the next 30 days; caches per-CVE on disk. When offline or
|
|
5900
6101
|
// the API errors, falls back to null fields — never blocks the scan.
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
const
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
6102
|
+
//
|
|
6103
|
+
// Batched: api.first.org accepts up to ~100 CVEs per request via ?cve=A,B,C…
|
|
6104
|
+
// One HTTP round trip per 100 CVEs instead of one per CVE. Cache lookups
|
|
6105
|
+
// remain per-CVE so a partial cache-hit still benefits.
|
|
6106
|
+
const _EPSS_BATCH = 100;
|
|
6107
|
+
async function _fetchEPSSBatch(cveIds){
|
|
6108
|
+
if (!cveIds || !cveIds.length) return new Map();
|
|
6109
|
+
if (process.env.AGENTIC_SECURITY_OFFLINE === '1') return new Map();
|
|
6110
|
+
const out = new Map();
|
|
6111
|
+
// Filter to well-formed CVE ids only.
|
|
6112
|
+
const fresh = cveIds.filter(c => /^CVE-\d{4}-\d+$/i.test(c));
|
|
6113
|
+
for (let i = 0; i < fresh.length; i += _EPSS_BATCH){
|
|
6114
|
+
const batch = fresh.slice(i, i + _EPSS_BATCH);
|
|
6115
|
+
const url = `https://api.first.org/data/v1/epss?cve=${encodeURIComponent(batch.join(','))}`;
|
|
6116
|
+
try {
|
|
6117
|
+
const res = await fetch(url, { headers: { 'User-Agent': 'agentic-security/0.1' } });
|
|
6118
|
+
if (!res.ok) {
|
|
6119
|
+
// Mark every CVE in the batch as "tried and failed" so we don't refetch this scan.
|
|
6120
|
+
for (const c of batch) _osvCacheSet('epss:'+c, false);
|
|
6121
|
+
continue;
|
|
6122
|
+
}
|
|
6123
|
+
const j = await res.json();
|
|
6124
|
+
const seen = new Set();
|
|
6125
|
+
for (const row of (j.data || [])) {
|
|
6126
|
+
const cve = (row.cve || '').toUpperCase();
|
|
6127
|
+
if (!cve) continue;
|
|
6128
|
+
const score = parseFloat(row.epss);
|
|
6129
|
+
const percentile = parseFloat(row.percentile);
|
|
6130
|
+
if (Number.isFinite(score) && Number.isFinite(percentile)) {
|
|
6131
|
+
const v = { score, percentile };
|
|
6132
|
+
out.set(cve, v);
|
|
6133
|
+
_osvCacheSet('epss:'+cve, v);
|
|
6134
|
+
seen.add(cve);
|
|
6135
|
+
}
|
|
6136
|
+
}
|
|
6137
|
+
// CVEs in the batch that EPSS does not know — cache the negative so we
|
|
6138
|
+
// don't retry within this scan run.
|
|
6139
|
+
for (const c of batch) if (!seen.has(c.toUpperCase())) _osvCacheSet('epss:'+c, false);
|
|
6140
|
+
} catch { /* network error → caller continues without enrichment */ }
|
|
6141
|
+
}
|
|
6142
|
+
return out;
|
|
5917
6143
|
}
|
|
5918
6144
|
|
|
5919
6145
|
async function _enrichWithEPSS(supplyChainResults){
|
|
5920
6146
|
const out = supplyChainResults;
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
6147
|
+
// Collect every unique CVE referenced by any SCA finding.
|
|
6148
|
+
const allCves = new Set();
|
|
6149
|
+
for (const r of out) for (const a of (r.cveAliases || [])) if (/^CVE-/.test(a)) allCves.add(a.toUpperCase());
|
|
6150
|
+
if (!allCves.size) return out;
|
|
6151
|
+
// Cache lookup pass: keep CVEs we already have, defer the rest to one batched fetch.
|
|
5924
6152
|
const epssByCve = new Map();
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
6153
|
+
const uncached = [];
|
|
6154
|
+
for (const c of allCves) {
|
|
6155
|
+
const hit = _osvCacheGet('epss:'+c);
|
|
6156
|
+
if (hit === null) uncached.push(c);
|
|
6157
|
+
else if (hit) epssByCve.set(c, hit); // hit === false means "tried, no data" — leave unset
|
|
6158
|
+
}
|
|
6159
|
+
if (uncached.length) {
|
|
6160
|
+
const fetched = await _fetchEPSSBatch(uncached);
|
|
6161
|
+
for (const [cve, v] of fetched) epssByCve.set(cve, v);
|
|
6162
|
+
}
|
|
5929
6163
|
for (const r of out) {
|
|
5930
6164
|
const cve = (r.cveAliases || []).find(a => /^CVE-/.test(a));
|
|
5931
|
-
const epss = cve ? epssByCve.get(cve) : null;
|
|
6165
|
+
const epss = cve ? epssByCve.get(cve.toUpperCase()) : null;
|
|
5932
6166
|
if (epss) {
|
|
5933
6167
|
r.epssScore = epss.score;
|
|
5934
6168
|
r.epssPercentile = epss.percentile;
|
|
@@ -6180,22 +6414,131 @@ function _parseCargoLock(text,filePath){
|
|
|
6180
6414
|
return out;
|
|
6181
6415
|
}
|
|
6182
6416
|
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6417
|
+
// Strip XML comments before tag-extraction so the regex doesn't accidentally
|
|
6418
|
+
// match a commented-out <dependency> block.
|
|
6419
|
+
function _stripPomXmlComments(text){
|
|
6420
|
+
return text.replace(/<!--[\s\S]*?-->/g, '');
|
|
6421
|
+
}
|
|
6422
|
+
|
|
6423
|
+
// Build a property table from <properties>...</properties> AND from common
|
|
6424
|
+
// child elements (<project.version>, etc.). Used to substitute ${propName}
|
|
6425
|
+
// version references. We do TWO passes so a property that references
|
|
6426
|
+
// another property (rare but legal — `${spring.boot.version}`) resolves.
|
|
6427
|
+
function _pomPropertyMap(text){
|
|
6428
|
+
const props = new Map();
|
|
6429
|
+
// <properties> block — the canonical place for user-defined properties.
|
|
6430
|
+
const propsBlock = text.match(/<properties>([\s\S]*?)<\/properties>/);
|
|
6431
|
+
if (propsBlock) {
|
|
6432
|
+
for (const m of propsBlock[1].matchAll(/<([\w.-]+)>([^<]+)<\/\1>/g)) {
|
|
6433
|
+
props.set(m[1].trim(), m[2].trim());
|
|
6434
|
+
}
|
|
6435
|
+
}
|
|
6436
|
+
// Resolve cross-property references (one pass is sufficient for typical
|
|
6437
|
+
// pom.xml shapes — chains deeper than 1 are vanishingly rare).
|
|
6438
|
+
for (const [k, v] of props) {
|
|
6439
|
+
if (v.includes('${')) {
|
|
6440
|
+
const resolved = v.replace(/\$\{([^}]+)\}/g, (_, name) => props.get(name) ?? '');
|
|
6441
|
+
props.set(k, resolved);
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
return props;
|
|
6445
|
+
}
|
|
6446
|
+
|
|
6447
|
+
function _resolvePomVersion(raw, props){
|
|
6448
|
+
if (!raw) return '0.0.0';
|
|
6449
|
+
let v = raw.trim();
|
|
6450
|
+
// Strip Maven's range/qualifier wrappers that aren't useful for SCA matching.
|
|
6451
|
+
// `[1.0,2.0)` → take the lower bound.
|
|
6452
|
+
const rangeMatch = v.match(/^[\[\(]\s*([^,\]\)]+)/);
|
|
6453
|
+
if (rangeMatch) v = rangeMatch[1].trim();
|
|
6454
|
+
// Substitute ${propName} references — return '0.0.0' so OSV knows it's
|
|
6455
|
+
// unresolvable rather than silently dropping the dep.
|
|
6456
|
+
v = v.replace(/\$\{([^}]+)\}/g, (_, name) => props.get(name) || '');
|
|
6457
|
+
if (!v || v.startsWith('$')) return '0.0.0';
|
|
6458
|
+
return v;
|
|
6459
|
+
}
|
|
6460
|
+
|
|
6461
|
+
// Parse a single <dependency>…</dependency> block to a component shape.
|
|
6462
|
+
// Used by both the top-level <dependencies> walk and the
|
|
6463
|
+
// <dependencyManagement>/<dependencies>/<dependency> BOM-import walk.
|
|
6464
|
+
function _parsePomDependencyBlock(inner, filePath, props, source){
|
|
6465
|
+
const gM = inner.match(/<groupId>([^<]+)<\/groupId>/);
|
|
6466
|
+
const aM = inner.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
6467
|
+
const vM = inner.match(/<version>([^<]+)<\/version>/);
|
|
6468
|
+
const sM = inner.match(/<scope>([^<]+)<\/scope>/);
|
|
6469
|
+
const tM = inner.match(/<type>([^<]+)<\/type>/);
|
|
6470
|
+
if (!gM || !aM) return null;
|
|
6471
|
+
const group = _resolvePomVersion(gM[1], props) || gM[1].trim();
|
|
6472
|
+
const artifact = _resolvePomVersion(aM[1], props) || aM[1].trim();
|
|
6473
|
+
const ver = _resolvePomVersion(vM ? vM[1] : '', props);
|
|
6474
|
+
const scopeRaw = sM ? sM[1].trim() : '';
|
|
6475
|
+
const scope = (scopeRaw === 'test' || scopeRaw === 'provided') ? 'optional' : 'required';
|
|
6476
|
+
return {
|
|
6477
|
+
name: `${group}:${artifact}`, version: ver, group, scope,
|
|
6478
|
+
purl: _makePurl('maven', artifact, ver, group), ecosystem: 'maven', filePath,
|
|
6479
|
+
isUnpinned: ver === '0.0.0' || ver.startsWith('$'),
|
|
6480
|
+
isTransitive: false,
|
|
6481
|
+
pomSource: source, // 'direct' | 'managed'
|
|
6482
|
+
pomType: tM ? tM[1].trim() : null, // 'jar' (default) | 'pom' (BOM import) | etc.
|
|
6483
|
+
};
|
|
6484
|
+
}
|
|
6485
|
+
|
|
6486
|
+
function _parsePomXml(text, filePath){
|
|
6487
|
+
const cleanText = _stripPomXmlComments(text);
|
|
6488
|
+
const props = _pomPropertyMap(cleanText);
|
|
6489
|
+
const out = [];
|
|
6490
|
+
// Identify the <dependencyManagement> block once so we can label its
|
|
6491
|
+
// contents as `pomSource: 'managed'` (BOM-imported) without doubly-emitting.
|
|
6492
|
+
const mgmtBlock = cleanText.match(/<dependencyManagement>([\s\S]*?)<\/dependencyManagement>/);
|
|
6493
|
+
const mgmtStart = mgmtBlock ? mgmtBlock.index : -1;
|
|
6494
|
+
const mgmtEnd = mgmtBlock ? mgmtBlock.index + mgmtBlock[0].length : -1;
|
|
6495
|
+
for (const block of cleanText.matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)) {
|
|
6496
|
+
const inBomImport = mgmtStart >= 0 && block.index >= mgmtStart && block.index < mgmtEnd;
|
|
6497
|
+
const comp = _parsePomDependencyBlock(block[1], filePath, props, inBomImport ? 'managed' : 'direct');
|
|
6498
|
+
if (comp) out.push(comp);
|
|
6499
|
+
}
|
|
6500
|
+
return out;
|
|
6501
|
+
}
|
|
6502
|
+
|
|
6503
|
+
// Maven's `mvn dependency:tree -DoutputFile=target/dependency-tree.txt`
|
|
6504
|
+
// emits the FULL resolved transitive graph. When that file is present, we
|
|
6505
|
+
// ingest it for transitive coverage — pom.xml itself only declares direct
|
|
6506
|
+
// deps. Format (per dep, one line, with tree-drawing prefixes we strip):
|
|
6507
|
+
// groupId:artifactId:type:version:scope
|
|
6508
|
+
// +- groupId:artifactId:type:version:scope
|
|
6509
|
+
// | \- groupId:artifactId:type:version:scope
|
|
6510
|
+
// The first line is the project itself; we skip it.
|
|
6511
|
+
function _parseMavenDependencyTree(text, filePath){
|
|
6512
|
+
const out = [];
|
|
6513
|
+
let isFirstLine = true;
|
|
6514
|
+
for (const rawLine of text.split('\n')) {
|
|
6515
|
+
if (!rawLine.trim()) continue;
|
|
6516
|
+
// Strip the tree-drawing prefix: spaces, pipes, dashes, plus signs, backslashes.
|
|
6517
|
+
const stripped = rawLine.replace(/^[\s|+\\-]+/, '').trim();
|
|
6518
|
+
if (!stripped) continue;
|
|
6519
|
+
if (isFirstLine) { isFirstLine = false; continue; }
|
|
6520
|
+
// Format: groupId:artifactId:type:version[:scope]
|
|
6521
|
+
// type is typically `jar` but can be `pom`, `war`, `aar`. Some POMs include
|
|
6522
|
+
// a classifier: groupId:artifactId:type:classifier:version[:scope] (5–6 parts).
|
|
6523
|
+
const parts = stripped.split(':');
|
|
6524
|
+
if (parts.length < 4) continue;
|
|
6525
|
+
const group = parts[0];
|
|
6526
|
+
const artifact = parts[1];
|
|
6527
|
+
// type at [2]; classifier (optional) at [3] when length >= 6.
|
|
6528
|
+
const hasClassifier = parts.length >= 6;
|
|
6529
|
+
const ver = hasClassifier ? parts[4] : parts[3];
|
|
6530
|
+
const scopeRaw = hasClassifier ? parts[5] : parts[4];
|
|
6531
|
+
if (!group || !artifact || !ver) continue;
|
|
6532
|
+
const scope = (scopeRaw === 'test' || scopeRaw === 'provided') ? 'optional' : 'required';
|
|
6533
|
+
out.push({
|
|
6534
|
+
name: `${group}:${artifact}`, version: ver, group, scope,
|
|
6535
|
+
purl: _makePurl('maven', artifact, ver, group), ecosystem: 'maven', filePath,
|
|
6536
|
+
isUnpinned: false,
|
|
6537
|
+
isTransitive: rawLine.match(/^[\s|+\\-]/) ? true : false,
|
|
6538
|
+
pomSource: 'dependency-tree',
|
|
6539
|
+
});
|
|
6540
|
+
}
|
|
6541
|
+
return out;
|
|
6199
6542
|
}
|
|
6200
6543
|
|
|
6201
6544
|
function _parseBuildGradle(text,filePath){
|
|
@@ -6339,8 +6682,144 @@ function _parsePubspecLock(text,filePath){
|
|
|
6339
6682
|
}return out;
|
|
6340
6683
|
}
|
|
6341
6684
|
|
|
6685
|
+
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'};
|
|
6686
|
+
function _parseCMakeLists(text,filePath){
|
|
6687
|
+
const out=[];
|
|
6688
|
+
for(const m of text.matchAll(/find_package\s*\(\s*(\w+)(?:\s+(\d+[\d.]*))?\s*(?:REQUIRED|COMPONENTS)?/gi)){
|
|
6689
|
+
const name=m[1].toLowerCase();const ver=m[2]||'0.0.0';
|
|
6690
|
+
out.push({name,version:ver,group:'',scope:'required',purl:`pkg:generic/${name}@${ver}`,ecosystem:'system',filePath,isUnpinned:!m[2]});
|
|
6691
|
+
}
|
|
6692
|
+
return out;
|
|
6693
|
+
}
|
|
6694
|
+
function _parseConanfile(text,filePath){
|
|
6695
|
+
const out=[];
|
|
6696
|
+
for(const m of text.matchAll(/(?:requires|build_requires)\s*[=(]\s*["']([^/]+)\/([^"'@]+)/g)){
|
|
6697
|
+
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});
|
|
6698
|
+
}
|
|
6699
|
+
return out;
|
|
6700
|
+
}
|
|
6701
|
+
function _parseVcpkgJson(text,filePath){
|
|
6702
|
+
const out=[];try{const d=JSON.parse(text);
|
|
6703
|
+
for(const dep of(d.dependencies||[])){
|
|
6704
|
+
const name=typeof dep==='string'?dep:dep.name;
|
|
6705
|
+
const ver=(typeof dep==='object'&&dep['version>='])?dep['version>=']:'0.0.0';
|
|
6706
|
+
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'});
|
|
6707
|
+
}
|
|
6708
|
+
}catch(_){}return out;
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6711
|
+
// go.sum: the full resolved transitive dependency graph for a Go module.
|
|
6712
|
+
// go.mod only declares direct deps; go.sum is what `go mod download` resolved.
|
|
6713
|
+
// Format (two lines per module — one for the module, one for go.mod):
|
|
6714
|
+
// github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
6715
|
+
// github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
6716
|
+
// We extract the module-line variant (no `/go.mod` suffix) so each module
|
|
6717
|
+
// gets exactly one component. Empty go.sum (no deps) gracefully returns [].
|
|
6718
|
+
function _parseGoSum(text, filePath){
|
|
6719
|
+
const out = [];
|
|
6720
|
+
const seen = new Set();
|
|
6721
|
+
for (const line of text.split('\n')) {
|
|
6722
|
+
const t = line.trim();
|
|
6723
|
+
if (!t || t.startsWith('//')) continue;
|
|
6724
|
+
// Skip the /go.mod hash line — it would double-count each module.
|
|
6725
|
+
if (t.includes('/go.mod ')) continue;
|
|
6726
|
+
const m = t.match(/^(\S+)\s+v([^\s]+)\s+h1:/);
|
|
6727
|
+
if (!m) continue;
|
|
6728
|
+
const name = m[1];
|
|
6729
|
+
// Strip +incompatible / -timestamp-sha suffixes that aren't useful for OSV matching.
|
|
6730
|
+
const ver = m[2].replace(/\+incompatible$/, '').replace(/^v?/, '');
|
|
6731
|
+
const dedupKey = `${name}@${ver}`;
|
|
6732
|
+
if (seen.has(dedupKey)) continue;
|
|
6733
|
+
seen.add(dedupKey);
|
|
6734
|
+
out.push({
|
|
6735
|
+
name, version: ver, group: '', scope: 'required',
|
|
6736
|
+
purl: _makePurl('golang', name, ver, ''), ecosystem: 'golang', filePath,
|
|
6737
|
+
isUnpinned: false,
|
|
6738
|
+
isTransitive: true, // every go.sum entry is a resolved (transitive or direct) dep
|
|
6739
|
+
});
|
|
6740
|
+
}
|
|
6741
|
+
return out;
|
|
6742
|
+
}
|
|
6743
|
+
|
|
6744
|
+
// conan.lock: the resolved Conan dependency lockfile. JSON format:
|
|
6745
|
+
// { "version": "0.5", "graph_lock": { "nodes": { "0": {"ref": "openssl/3.0.0@..." }, "1": {...} } } }
|
|
6746
|
+
// or the newer Conan 2.x format with a flat list. Both supported here.
|
|
6747
|
+
function _parseConanLock(text, filePath){
|
|
6748
|
+
const out = [];
|
|
6749
|
+
let d;
|
|
6750
|
+
try { d = JSON.parse(text); } catch { return out; }
|
|
6751
|
+
const refs = new Set();
|
|
6752
|
+
// Conan 1.x graph_lock format.
|
|
6753
|
+
const nodes = d?.graph_lock?.nodes;
|
|
6754
|
+
if (nodes && typeof nodes === 'object') {
|
|
6755
|
+
for (const node of Object.values(nodes)) {
|
|
6756
|
+
if (node && typeof node.ref === 'string') refs.add(node.ref);
|
|
6757
|
+
}
|
|
6758
|
+
}
|
|
6759
|
+
// Conan 2.x lock format: arrays under requires / build_requires / python_requires.
|
|
6760
|
+
for (const k of ['requires', 'build_requires', 'python_requires']) {
|
|
6761
|
+
if (Array.isArray(d?.[k])) for (const r of d[k]) if (typeof r === 'string') refs.add(r);
|
|
6762
|
+
}
|
|
6763
|
+
for (const ref of refs) {
|
|
6764
|
+
// Format: name/version[@user/channel][#revision]
|
|
6765
|
+
const m = ref.match(/^([^/]+)\/([^@#\s]+)/);
|
|
6766
|
+
if (!m) continue;
|
|
6767
|
+
const name = m[1].toLowerCase();
|
|
6768
|
+
const version = m[2];
|
|
6769
|
+
out.push({
|
|
6770
|
+
name, version, group: '', scope: 'required',
|
|
6771
|
+
purl: `pkg:generic/${name}@${version}`, ecosystem: 'system', filePath,
|
|
6772
|
+
isUnpinned: false,
|
|
6773
|
+
isTransitive: true,
|
|
6774
|
+
});
|
|
6775
|
+
}
|
|
6776
|
+
return out;
|
|
6777
|
+
}
|
|
6778
|
+
|
|
6779
|
+
// vcpkg-configuration.json: overlay-ports config for vcpkg. Schema:
|
|
6780
|
+
// { "default-registry": {...}, "registries": [{ "kind": "git", "packages": [...] }] }
|
|
6781
|
+
// We don't have version info here (vcpkg pins via baseline commit), so emit
|
|
6782
|
+
// each named package with version 0.0.0 so OSV at least sees the package
|
|
6783
|
+
// name. Useful for "are you using an EOL/known-malicious overlay registry?"
|
|
6784
|
+
function _parseVcpkgConfiguration(text, filePath){
|
|
6785
|
+
const out = [];
|
|
6786
|
+
let d;
|
|
6787
|
+
try { d = JSON.parse(text); } catch { return out; }
|
|
6788
|
+
const names = new Set();
|
|
6789
|
+
if (Array.isArray(d?.registries)) {
|
|
6790
|
+
for (const reg of d.registries) {
|
|
6791
|
+
if (Array.isArray(reg?.packages)) for (const p of reg.packages) if (typeof p === 'string') names.add(p);
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
for (const name of names) {
|
|
6795
|
+
out.push({
|
|
6796
|
+
name: name.toLowerCase(), version: '0.0.0', group: '', scope: 'required',
|
|
6797
|
+
purl: `pkg:generic/${name.toLowerCase()}@0.0.0`, ecosystem: 'system', filePath,
|
|
6798
|
+
isUnpinned: true,
|
|
6799
|
+
});
|
|
6800
|
+
}
|
|
6801
|
+
return out;
|
|
6802
|
+
}
|
|
6342
6803
|
function parseManifests(allFileContents){
|
|
6343
|
-
const PARSERS={
|
|
6804
|
+
const PARSERS={
|
|
6805
|
+
'package.json':_parsePackageJson,'package-lock.json':_parsePackageLockJson,
|
|
6806
|
+
'yarn.lock':_parseYarnLock,'pnpm-lock.yaml':_parsePnpmLock,
|
|
6807
|
+
'requirements.txt':_parseRequirementsTxt,'pyproject.toml':_parsePyprojectToml,
|
|
6808
|
+
'poetry.lock':_parsePoetryLock,'Pipfile.lock':_parsePipfileLock,
|
|
6809
|
+
'composer.json':_parseComposerJson,'composer.lock':_parseComposerLock,
|
|
6810
|
+
'Gemfile':_parseGemfile,'Gemfile.lock':_parseGemfileLock,
|
|
6811
|
+
'go.mod':_parseGoMod,'go.sum':_parseGoSum,
|
|
6812
|
+
'Cargo.toml':_parseCargoToml,'Cargo.lock':_parseCargoLock,
|
|
6813
|
+
'pom.xml':_parsePomXml,'build.gradle':_parseBuildGradle,'build.gradle.kts':_parseBuildGradle,
|
|
6814
|
+
'pubspec.yaml':_parsePubspecYaml,'pubspec.lock':_parsePubspecLock,
|
|
6815
|
+
'CMakeLists.txt':_parseCMakeLists,
|
|
6816
|
+
'conanfile.txt':_parseConanfile,'conan.lock':_parseConanLock,
|
|
6817
|
+
'vcpkg.json':_parseVcpkgJson,'vcpkg-configuration.json':_parseVcpkgConfiguration,
|
|
6818
|
+
// Maven dependency-tree output. The canonical path is
|
|
6819
|
+
// `target/dependency-tree.txt` (per `mvn dependency:tree -DoutputFile=...`)
|
|
6820
|
+
// but users sometimes commit it as `dependency-tree.txt` at repo root.
|
|
6821
|
+
'dependency-tree.txt':_parseMavenDependencyTree,
|
|
6822
|
+
};
|
|
6344
6823
|
const out=[],seen=new Set();
|
|
6345
6824
|
for(const[fp,content]of Object.entries(allFileContents)){
|
|
6346
6825
|
const base=fp.split('/').pop();
|
|
@@ -6465,51 +6944,74 @@ async function queryOSV(components,allFileContents){
|
|
|
6465
6944
|
}
|
|
6466
6945
|
}
|
|
6467
6946
|
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
const
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6947
|
+
// External-identifier exception (TOS compliance note):
|
|
6948
|
+
// The domain fragments below are third-party PoC tracker domain names.
|
|
6949
|
+
// They are inputs we match against reference URLs returned by OSV — not
|
|
6950
|
+
// output text we generate. Renaming them loses SCA detection of
|
|
6951
|
+
// PoC-published CVEs.
|
|
6952
|
+
const _KNOWN_PUBLIC_POC_DOMAINS = ['exploit-db','packetstorm','/poc','/0day'];
|
|
6953
|
+
// Fetch missing vuln details in parallel with a concurrency cap so we don't
|
|
6954
|
+
// open hundreds of sockets on a large dep tree. OSV has no batch endpoint
|
|
6955
|
+
// for /v1/vulns/{id}, so this is the best we can do without API changes.
|
|
6956
|
+
const _VULN_CONCURRENCY = 20;
|
|
6957
|
+
const vulnEntries = Object.entries(vulnAffects);
|
|
6958
|
+
const vulnDetails = new Map(); // vid → vuln object (from cache or fresh)
|
|
6959
|
+
// First, drain cache hits with no network involvement.
|
|
6960
|
+
const fetchQueue = [];
|
|
6961
|
+
for (const [vid, _comps] of vulnEntries) {
|
|
6962
|
+
const cached = _osvCacheGet('vuln:' + vid);
|
|
6963
|
+
if (cached) vulnDetails.set(vid, cached);
|
|
6964
|
+
else fetchQueue.push(vid);
|
|
6965
|
+
}
|
|
6966
|
+
// Then fetch uncached vulns with bounded concurrency.
|
|
6967
|
+
async function _fetchOneVuln(vid){
|
|
6968
|
+
try {
|
|
6969
|
+
const resp = await fetch(`https://api.osv.dev/v1/vulns/${vid}`);
|
|
6970
|
+
const d = await resp.json();
|
|
6971
|
+
const fixedVersions = new Set();
|
|
6972
|
+
const osvVulnFunctions = [];
|
|
6973
|
+
for (const aff of (d.affected || [])) {
|
|
6974
|
+
for (const rng of (aff.ranges || [])) for (const ev of (rng.events || [])) if (ev.fixed) fixedVersions.add(ev.fixed);
|
|
6975
|
+
const es = aff.ecosystem_specific || aff.database_specific || {};
|
|
6976
|
+
if (Array.isArray(es.vulnerable_functions)) osvVulnFunctions.push(...es.vulnerable_functions);
|
|
6977
|
+
if (Array.isArray(es.imports)) for (const imp of es.imports) if (Array.isArray(imp.symbols)) osvVulnFunctions.push(...imp.symbols);
|
|
6978
|
+
}
|
|
6979
|
+
let severity = 'medium';
|
|
6980
|
+
const db = d.database_specific || {};
|
|
6981
|
+
if (db.severity) severity = db.severity.toLowerCase() === 'moderate' ? 'medium' : db.severity.toLowerCase();
|
|
6982
|
+
let cvssVector = null;
|
|
6983
|
+
for (const s of (d.severity || [])) if (s.type === 'CVSS_V3' || s.type === 'CVSS_V4') { cvssVector = s.score; break; }
|
|
6984
|
+
const hasKnownAttackRef = (d.references || []).some(r => _KNOWN_PUBLIC_POC_DOMAINS.some(x => (r.url || '').toLowerCase().includes(x)));
|
|
6985
|
+
const vuln = { id: vid,
|
|
6986
|
+
description: (d.summary || d.details || 'No description.').slice(0, 300),
|
|
6987
|
+
fixedVersions: [...fixedVersions].sort(),
|
|
6988
|
+
aliases: (d.aliases || []).filter(a => a.startsWith('CVE-')),
|
|
6989
|
+
osvVulnFunctions: [...new Set(osvVulnFunctions)],
|
|
6990
|
+
severity, cvssVector, hasKnownAttackRef };
|
|
6991
|
+
_osvCacheSet('vuln:' + vid, vuln);
|
|
6992
|
+
return [vid, vuln];
|
|
6993
|
+
} catch (_) { return null; }
|
|
6994
|
+
}
|
|
6995
|
+
for (let i = 0; i < fetchQueue.length; i += _VULN_CONCURRENCY) {
|
|
6996
|
+
const batch = fetchQueue.slice(i, i + _VULN_CONCURRENCY);
|
|
6997
|
+
const settled = await Promise.all(batch.map(_fetchOneVuln));
|
|
6998
|
+
for (const r of settled) if (r) vulnDetails.set(r[0], r[1]);
|
|
6999
|
+
}
|
|
7000
|
+
// Materialize results in the original vulnAffects iteration order.
|
|
7001
|
+
for (const [vid, affectedComps] of vulnEntries) {
|
|
7002
|
+
const vuln = vulnDetails.get(vid);
|
|
7003
|
+
if (!vuln) continue;
|
|
7004
|
+
for (const comp of affectedComps) {
|
|
7005
|
+
const cveStr = vuln.aliases.length ? ` (${vuln.aliases[0]})` : '';
|
|
7006
|
+
const fixStr = vuln.fixedVersions.length ? vuln.fixedVersions[0] : null;
|
|
7007
|
+
results.push({ type: 'vulnerable_dep', name: comp.name, version: comp.version, ecosystem: comp.ecosystem,
|
|
7008
|
+
purl: comp.purl, osvId: vid, cveAliases: vuln.aliases, description: vuln.description,
|
|
7009
|
+
fixedVersions: vuln.fixedVersions, severity: vuln.severity, cvssVector: vuln.cvssVector,
|
|
7010
|
+
hasKnownAttackRef: vuln.hasKnownAttackRef, osvVulnFunctions: vuln.osvVulnFunctions || [], reachable: comp.reachable, scope: comp.scope,
|
|
7011
|
+
file: comp.filePath,
|
|
6510
7012
|
// kept for generateRecs() compat
|
|
6511
|
-
advisory
|
|
6512
|
-
range:fixStr
|
|
7013
|
+
advisory: `${vid}${cveStr}, ${vuln.description}`,
|
|
7014
|
+
range: fixStr ? `< ${fixStr}` : 'see advisory' });
|
|
6513
7015
|
}
|
|
6514
7016
|
}
|
|
6515
7017
|
|
|
@@ -6766,6 +7268,18 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6766
7268
|
aF.push(...scanKotlin(p,c));
|
|
6767
7269
|
aF.push(...scanRuby(p,c));
|
|
6768
7270
|
aF.push(...scanPhp(p,c));
|
|
7271
|
+
// Integration block: scaffolded SAST scanners. Gated by env var.
|
|
7272
|
+
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
7273
|
+
if (process.env.AGENTIC_SECURITY_NO_LLM_APP !== '1') aF.push(...scanLlmApp(p,c));
|
|
7274
|
+
if (process.env.AGENTIC_SECURITY_NO_MOBILE !== '1') aF.push(...scanMobile(p,c));
|
|
7275
|
+
if (process.env.AGENTIC_SECURITY_NO_PQC !== '1') aF.push(...scanPqc(p,c));
|
|
7276
|
+
if (process.env.AGENTIC_SECURITY_NO_WEB3_ADV!== '1') aF.push(...scanWeb3Advanced(p,c));
|
|
7277
|
+
if (process.env.AGENTIC_SECURITY_NO_DAPP !== '1') aF.push(...scanDappFrontend(p,c));
|
|
7278
|
+
if (process.env.AGENTIC_SECURITY_NO_CLOUD_IAM!== '1') aF.push(...scanCloudIam(p,c));
|
|
7279
|
+
if (process.env.AGENTIC_SECURITY_NO_K8S_ADM !== '1') aF.push(...scanK8sAdmission(p,c));
|
|
7280
|
+
if (process.env.AGENTIC_SECURITY_NO_CRYPTO_PROTO !== '1') aF.push(...scanCryptoProtocol(p,c));
|
|
7281
|
+
if (process.env.AGENTIC_SECURITY_NO_ML_SUPPLY !== '1') aF.push(...scanMlSupplyChain(p,c));
|
|
7282
|
+
}
|
|
6769
7283
|
const _ftElapsed=Date.now()-_ft0;
|
|
6770
7284
|
if(_ftElapsed>_perFileTimeoutMs){aF.push({id:`file-timeout:${p}`,file:p,line:0,vuln:`File analysis exceeded ${_perFileTimeoutMs}ms (${_ftElapsed}ms)`,severity:'info',parser:'ENGINE',confidence:0.5,_timeout:true});_filesTimedOut++;}
|
|
6771
7285
|
_fileTimings.push({file:p,ms:_ftElapsed});
|
|
@@ -6931,6 +7445,8 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6931
7445
|
// 0.10.0: enrich SCA findings with CISA KEV (CISA KEV catalog)
|
|
6932
7446
|
try{supplyChain=await _enrichWithKEV(supplyChain);}catch(_){}
|
|
6933
7447
|
try{markUsedVulnFunctions(supplyChain,fc);}catch(_){}
|
|
7448
|
+
// Python AST-based function validation (deep mode only)
|
|
7449
|
+
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
7450
|
// LLM-assisted function extraction for CVEs without hints (opt-in: AGENTIC_SECURITY_LLM_SCA=1)
|
|
6935
7451
|
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
7452
|
setProgress({current:i,total:files.length,file:"Registry metadata...",phase:"SCA"});
|
|
@@ -6938,16 +7454,48 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6938
7454
|
const dd=(a,k)=>[...new Map(a.map(x=>[k(x),x])).values()];
|
|
6939
7455
|
// 0.6.0 Feat-1: annotate function-level reachability on SCA findings
|
|
6940
7456
|
try { _annotateFunctionReachability(supplyChain,dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`).map(r=>({...r})),callGraph,fc); } catch(_) {}
|
|
6941
|
-
// Reachability tier classification for SCA findings
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
7457
|
+
// Reachability tier classification for SCA findings. Order matters —
|
|
7458
|
+
// each tier represents a narrower (more certain) form of reachability,
|
|
7459
|
+
// so we check from most-specific to least-specific.
|
|
7460
|
+
//
|
|
7461
|
+
// route-reachable-via-function (Phase 2 / Item 4): the vulnerable
|
|
7462
|
+
// function is called AND that call site is rooted in (or
|
|
7463
|
+
// transitively-called from) a route handler. Highest urgency.
|
|
7464
|
+
// function-reachable: the vulnerable function is called, but the call
|
|
7465
|
+
// chain does not reach a route handler in <=4 hops. Real but lower
|
|
7466
|
+
// priority than route-reachable.
|
|
7467
|
+
// unreachable: vulnerable function never called from the project.
|
|
7468
|
+
// import-reachable: package is imported but no vulnerable function call
|
|
7469
|
+
// was identified (OSV may not list function-level data).
|
|
7470
|
+
// build-only / manifest-only / transitive-only: progressively weaker
|
|
7471
|
+
// signals; the dep exists but we can't confirm any use of it.
|
|
7472
|
+
for (const sc of supplyChain || []) {
|
|
7473
|
+
if (sc.type !== 'vulnerable_dep') continue;
|
|
7474
|
+
if (sc.functionReachable === 'reachable' && sc.routeReachable === true) {
|
|
7475
|
+
sc.reachabilityTier = 'route-reachable-via-function';
|
|
7476
|
+
} else if (sc.functionReachable === 'reachable') {
|
|
7477
|
+
sc.reachabilityTier = 'function-reachable';
|
|
7478
|
+
} else if (sc.functionReachable === 'unreachable') {
|
|
7479
|
+
sc.reachabilityTier = 'unreachable';
|
|
7480
|
+
} else if (sc.reachable) {
|
|
7481
|
+
sc.reachabilityTier = 'import-reachable';
|
|
7482
|
+
} else if (sc.isBuildOnly || (sc.scope === 'optional' && !sc.reachable)) {
|
|
7483
|
+
sc.reachabilityTier = 'build-only';
|
|
7484
|
+
sc.isBuildOnly = true;
|
|
7485
|
+
if (sc.severity === 'critical') sc.severity = 'medium';
|
|
7486
|
+
else if (sc.severity === 'high') sc.severity = 'low';
|
|
7487
|
+
} else if (sc.scope === 'required') {
|
|
7488
|
+
sc.reachabilityTier = 'manifest-only';
|
|
7489
|
+
} else {
|
|
7490
|
+
sc.reachabilityTier = 'transitive-only';
|
|
7491
|
+
}
|
|
7492
|
+
if (sc.reachabilityTier === 'transitive-only') { sc.unreachable = true; sc._reachabilityDemoted = true; }
|
|
7493
|
+
// Mark as the strongest "vulnerable function actually invoked" signal so
|
|
7494
|
+
// posture/reachability-filter can preserve full severity on this tier
|
|
7495
|
+
// while demoting lower tiers.
|
|
7496
|
+
if (sc.reachabilityTier === 'route-reachable-via-function') {
|
|
7497
|
+
sc.routeReachableViaFunction = true;
|
|
7498
|
+
}
|
|
6951
7499
|
}
|
|
6952
7500
|
// Early dedup: collapse duplicates BEFORE the annotation pipeline to reduce work.
|
|
6953
7501
|
try{const _earlyMap=new Map();for(const f of aF){const k=`${f.file||''}:${f.line||0}:${f.vuln||''}`;if(!_earlyMap.has(k))_earlyMap.set(k,f);}aF.length=0;aF.push(..._earlyMap.values());}catch(_){}
|
|
@@ -6956,6 +7504,44 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6956
7504
|
const vulnsByKey={};for(const sc of supplyChain.filter(s=>s.type==='vulnerable_dep')){const k=`${sc.ecosystem}:${sc.name}:${sc.version}`;if(!vulnsByKey[k])vulnsByKey[k]=[];vulnsByKey[k].push(sc);}
|
|
6957
7505
|
const attackResult=computeAttackPathComponents(aF,components,reach.byFile);
|
|
6958
7506
|
for(const[key,paths]of attackResult.pathsByKey){const[eco,name,...vp]=key.split(':');const ver=vp.join(':');for(const f of paths){if(!f.linkedComponents)f.linkedComponents=[];if(!f.linkedComponents.some(c=>c.name===name&&c.ecosystem===eco))f.linkedComponents.push({ecosystem:eco,name,version:ver});}}
|
|
7507
|
+
// Phase 4 / Item 8 of the SCA improvement plan: reverse pointer.
|
|
7508
|
+
// For each (component-key, sastFindings[]) pair from attackResult, find
|
|
7509
|
+
// the matching supplyChain finding(s) and stamp linkedFindings[] on them
|
|
7510
|
+
// — each entry pointing at the SAST finding that taint-flows to that
|
|
7511
|
+
// dep. Also persist a one-line chain narrative so /show-findings
|
|
7512
|
+
// --chains can consume it without re-invoking the synthesizer agent.
|
|
7513
|
+
// Size-capped to MAX_LINKED_FINDINGS per SCA finding (premortem 3R-7
|
|
7514
|
+
// — don't unbounded-grow last-scan.json on a noisy attack graph).
|
|
7515
|
+
const MAX_LINKED_FINDINGS = 10;
|
|
7516
|
+
for (const [key, paths] of attackResult.pathsByKey) {
|
|
7517
|
+
const scaFindings = vulnsByKey[key] || [];
|
|
7518
|
+
if (!scaFindings.length || !paths.length) continue;
|
|
7519
|
+
for (const sc of scaFindings) {
|
|
7520
|
+
if (!Array.isArray(sc.linkedFindings)) sc.linkedFindings = [];
|
|
7521
|
+
if (!Array.isArray(sc.chainNarratives)) sc.chainNarratives = [];
|
|
7522
|
+
for (const sastF of paths) {
|
|
7523
|
+
if (sc.linkedFindings.length >= MAX_LINKED_FINDINGS) break;
|
|
7524
|
+
const linkEntry = {
|
|
7525
|
+
findingId: sastF.id || null,
|
|
7526
|
+
vuln: sastF.vuln || sastF.title || null,
|
|
7527
|
+
severity: sastF.severity || null,
|
|
7528
|
+
file: sastF.file || sastF.sink?.file || null,
|
|
7529
|
+
line: sastF.sink?.line ?? sastF.line ?? null,
|
|
7530
|
+
};
|
|
7531
|
+
// Dedupe by (findingId, file:line, vuln) so re-runs of this block
|
|
7532
|
+
// (we hit it once per attack-graph traversal) don't grow the array.
|
|
7533
|
+
const dedupeKey = `${linkEntry.findingId}|${linkEntry.file}:${linkEntry.line}|${linkEntry.vuln}`;
|
|
7534
|
+
if (sc.linkedFindings.some(e => `${e.findingId}|${e.file}:${e.line}|${e.vuln}` === dedupeKey)) continue;
|
|
7535
|
+
sc.linkedFindings.push(linkEntry);
|
|
7536
|
+
// Narrative: short, human-readable. Avoids re-invoking the
|
|
7537
|
+
// synthesizer agent for the common case where the chain is just
|
|
7538
|
+
// "SAST X reaches vulnerable dep Y".
|
|
7539
|
+
const narrative = `${linkEntry.vuln || 'sink'} on ${linkEntry.file || '?'}:${linkEntry.line || 0} → ${sc.name}@${sc.version} (${sc.osvId || (sc.cveAliases && sc.cveAliases[0]) || 'unknown CVE'})`;
|
|
7540
|
+
if (!sc.chainNarratives.includes(narrative)) sc.chainNarratives.push(narrative);
|
|
7541
|
+
}
|
|
7542
|
+
sc.hasLinkedSastFindings = sc.linkedFindings.length > 0;
|
|
7543
|
+
}
|
|
7544
|
+
}
|
|
6959
7545
|
const annotatedComponents=components.map(c=>{const key=`${c.ecosystem}:${c.name}:${c.version}`;const vulns=vulnsByKey[key]||[];const riKey=c.ecosystem==='maven'&&c.group?`maven:${c.group}/${c.name}`:`${c.ecosystem}:${c.name}`;const ri=registryInfo.get(riKey)||{};const latestVersion=ri.latestVersion||'';const vd=(ri.versions||{})[c.version]||{};const isDeprecated=typeof vd.deprecated==='string'&&vd.deprecated.length>0;const deprecationMessage=isDeprecated?vd.deprecated:'';const isOutdated=!isDeprecated&&typeof vd.outdated==='string'&&vd.outdated.length>0;const outdatedMessage=isOutdated?vd.outdated:'';const license=ri.license||vd.license||'';return{...c,vulns,hasVulns:vulns.length>0,hasAttackPath:attackResult.flagged.has(key),attackPaths:attackResult.pathsByKey.get(key)||[],latestVersion,isDeprecated,deprecationMessage,isOutdated,outdatedMessage,license};});
|
|
6960
7546
|
try{aF.push(...scanDbTaintCrossFile(fc));}catch(_){}
|
|
6961
7547
|
try{aF.push(...scanStoredPromptInjectionCrossFile(fc));}catch(_){}
|
|
@@ -7087,6 +7673,70 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7087
7673
|
_runAnnotator("annotateFeatureFlagGating", () => { annotateFeatureFlagGating(finalFindings, fc, { scanRoot }); });
|
|
7088
7674
|
// v3 next-gen: composite mitigation verdict consumes every prod signal above.
|
|
7089
7675
|
_runAnnotator("annotateMitigationComposite", () => { annotateMitigationComposite(finalFindings); });
|
|
7676
|
+
// Composite risk score (0..100 derived ordinal). Must run AFTER mitigation
|
|
7677
|
+
// composite + exploitability + toxicityScore so it sees the final values.
|
|
7678
|
+
// Used by agents and UI as the canonical sort key for "which finding first."
|
|
7679
|
+
_runAnnotator("annotateCompositeRisk", () => { annotateCompositeRisk(finalFindings); });
|
|
7680
|
+
|
|
7681
|
+
// ── World-class integration block ─────────────────────────────────────
|
|
7682
|
+
// Each annotator is opt-in via env var and try/catch wrapped. They run
|
|
7683
|
+
// AFTER composite risk so they can read the canonical risk ordinal but
|
|
7684
|
+
// BEFORE crown-jewel / persona scoring so demotions are honored.
|
|
7685
|
+
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
7686
|
+
// Cross-service taint annotation reads .agentic-security/services.yml
|
|
7687
|
+
// and bumps severity on cross-service-reachable findings.
|
|
7688
|
+
if (process.env.AGENTIC_SECURITY_NO_CROSS_SERVICE !== '1') {
|
|
7689
|
+
_runAnnotator("runCrossServiceTaint", () => { runCrossServiceTaint(scanRoot, finalFindings); });
|
|
7690
|
+
}
|
|
7691
|
+
// Runtime correlation: demotes findings whose paths were unobserved
|
|
7692
|
+
// in production eBPF traces (when a trace file is present).
|
|
7693
|
+
if (process.env.AGENTIC_SECURITY_NO_RUNTIME_CORRELATION !== '1') {
|
|
7694
|
+
_runAnnotator("annotateRuntimeCorrelation", async () => { await annotateRuntimeCorrelation(scanRoot, finalFindings); });
|
|
7695
|
+
}
|
|
7696
|
+
// Triage learning: applies per-(project, family, file-glob) calibration
|
|
7697
|
+
// from prior wont-fix / false-positive decisions.
|
|
7698
|
+
if (process.env.AGENTIC_SECURITY_NO_TRIAGE_LEARNING !== '1') {
|
|
7699
|
+
_runAnnotator("applyLearnedCalibration", () => { applyLearnedCalibration(scanRoot, finalFindings); });
|
|
7700
|
+
}
|
|
7701
|
+
// Formal verification: CBMC for C/C++, MIRI for Rust. Opt-in via
|
|
7702
|
+
// AGENTIC_SECURITY_FORMAL=1 (off by default — requires external tools).
|
|
7703
|
+
if (process.env.AGENTIC_SECURITY_FORMAL === '1') {
|
|
7704
|
+
_runAnnotator("annotateFormalVerification", async () => { await annotateFormalVerification(finalFindings, fc, {}); });
|
|
7705
|
+
}
|
|
7706
|
+
// SMT path feasibility: Z3-backed proof of reachability. Opt-in via
|
|
7707
|
+
// AGENTIC_SECURITY_SMT_FEASIBILITY=1.
|
|
7708
|
+
if (process.env.AGENTIC_SECURITY_SMT_FEASIBILITY === '1') {
|
|
7709
|
+
_runAnnotator("annotatePathFeasibility", async () => { await annotatePathFeasibility(finalFindings, {}); });
|
|
7710
|
+
}
|
|
7711
|
+
// Privacy / PII taint: emits pii-exposure findings + DPIA artifact.
|
|
7712
|
+
if (process.env.AGENTIC_SECURITY_NO_PRIVACY !== '1') {
|
|
7713
|
+
_runAnnotator("annotatePrivacyTaint", () => {
|
|
7714
|
+
// Build a minimal IR map from fc; the privacy taint module needs
|
|
7715
|
+
// per-file content + a coarse decls/calls list. We construct it
|
|
7716
|
+
// from existing finding sources rather than re-parsing every file.
|
|
7717
|
+
const minimalIR = new Map();
|
|
7718
|
+
for (const [fp, content] of Object.entries(fc || {})) {
|
|
7719
|
+
if (typeof content !== 'string') continue;
|
|
7720
|
+
minimalIR.set(fp, { _content: content, decls: [], calls: [] });
|
|
7721
|
+
}
|
|
7722
|
+
const r = annotatePrivacyTaint(minimalIR);
|
|
7723
|
+
if (r && Array.isArray(r.findings)) finalFindings.push(...r.findings);
|
|
7724
|
+
// Persist the DPIA scaffold for compliance review.
|
|
7725
|
+
if (r && r.piiFields) {
|
|
7726
|
+
try {
|
|
7727
|
+
const dpia = emitDpiaArtifact(r.piiFields, r.findings || []);
|
|
7728
|
+
fs.writeFileSync(path.join(scanRoot, '.agentic-security', 'dpia.md'), dpia);
|
|
7729
|
+
} catch (_) {}
|
|
7730
|
+
}
|
|
7731
|
+
});
|
|
7732
|
+
}
|
|
7733
|
+
// Attack-taxonomy annotation: stamps each finding with MITRE ATT&CK,
|
|
7734
|
+
// ATLAS, D3FEND, kill-chain stage, and CAPEC IDs so downstream SIEM /
|
|
7735
|
+
// SOAR systems can correlate with existing detection rules.
|
|
7736
|
+
if (process.env.AGENTIC_SECURITY_NO_ATTACK_TAX !== '1') {
|
|
7737
|
+
_runAnnotator("annotateAttackTaxonomy", () => { annotateAttackTaxonomy(finalFindings); });
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7090
7740
|
// v3 next-gen: crown-jewel mapping (FR-PROD-5) — score each file/finding by
|
|
7091
7741
|
// business impact. Must run before persona prioritization (which uses it).
|
|
7092
7742
|
_runAnnotator("annotateCrownJewelScores", () => { annotateCrownJewelScores(finalFindings, fc); });
|
|
@@ -7315,11 +7965,25 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7315
7965
|
const _toxCtx={routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`).map(r=>({...r})),supplyChain,hasCloudCreds:_hasCloudCreds};
|
|
7316
7966
|
try{finalFindings.forEach(f=>scoreToxicity(f,_toxCtx));}catch(_){}
|
|
7317
7967
|
for(const sc of supplyChain||[]){try{scoreToxicity(sc,_toxCtx);}catch(_){}}
|
|
7968
|
+
// Composite risk for the supplyChain bucket too. The SAST pass at
|
|
7969
|
+
// annotateCompositeRisk above runs before scoreToxicity (which only
|
|
7970
|
+
// touches finalFindings in the same line), so this is the catch-up call
|
|
7971
|
+
// for the SCA bucket — must happen after KEV / EPSS / scoreToxicity on
|
|
7972
|
+
// supplyChain so it sees the populated signals.
|
|
7973
|
+
try { annotateCompositeRisk(supplyChain); } catch (_) {}
|
|
7974
|
+
// And re-annotate finalFindings here so they pick up the toxicityScore
|
|
7975
|
+
// that was just set. Idempotent: composite-risk derives fresh from the
|
|
7976
|
+
// current field values on each call.
|
|
7977
|
+
try { annotateCompositeRisk(finalFindings); } catch (_) {}
|
|
7318
7978
|
// 0.9.0 Feat-18: OSSF Scorecard enrichment (opt-in via AGENTIC_SECURITY_SCORECARD=1)
|
|
7319
7979
|
try { await _enrichWithScorecard(annotatedComponents); }
|
|
7320
7980
|
catch (e) { _annotatorErrors.push({ phase: '_enrichWithScorecard', err: String((e && e.message) || e) }); }
|
|
7321
7981
|
// 0.8.0 Feat-10: license policy
|
|
7322
7982
|
try{const lp=loadLicensePolicy(scanRoot);if(lp){const lv=evaluateLicensePolicy(annotatedComponents,lp);aLogic.push(...lv);}}catch(_){}
|
|
7983
|
+
// Phase 4 / Item 7 of the SCA improvement plan: load sca-policy.yml and
|
|
7984
|
+
// apply accept-risk / SLA / major-version-freeze rules. supplyChain
|
|
7985
|
+
// findings get suppressed/tagged in place.
|
|
7986
|
+
try { const sp = loadScaPolicy(scanRoot); if (sp && !sp._error) applyScaPolicy(supplyChain, sp); } catch (_) {}
|
|
7323
7987
|
// 0.9.0 Feat-15: dep confusion
|
|
7324
7988
|
try{const dc=detectDepConfusion(annotatedComponents,scanRoot);aF.push(...dc);}catch(_){}
|
|
7325
7989
|
// Deployment-platform security checklist
|
|
@@ -7457,8 +8121,101 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7457
8121
|
// v3 next-gen: why-fired provenance is captured LAST so it reflects the
|
|
7458
8122
|
// final state of each finding after every other annotator has run.
|
|
7459
8123
|
_runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, {}); });
|
|
8124
|
+
// SCA-SAST correlation: link SAST findings to SCA vulnerable packages
|
|
8125
|
+
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(_){}
|
|
8126
|
+
// Multi-sink chain detection: group findings by source variable
|
|
8127
|
+
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(_){}
|
|
8128
|
+
// SCA transitive dedup: collapse duplicate CVEs across dep chains
|
|
8129
|
+
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(_){}
|
|
8130
|
+
// ── World-class post-scan artifact emitters ──────────────────────────
|
|
8131
|
+
// Each is opt-in via env var. They produce machine-readable artifacts
|
|
8132
|
+
// (threat-model.json/.md, dpia.md, compliance-evidence.json/.md,
|
|
8133
|
+
// sbom-history/<sha>.json, exploit-bundles/) under .agentic-security/.
|
|
8134
|
+
let _threatModel = null, _apiContractFindings = [], _sbomDiff = null,
|
|
8135
|
+
_complianceReport = null, _exploitBundles = null, _pqcPlan = null,
|
|
8136
|
+
_licenseGraph = null, _attributions = null, _taxonomySummary = null;
|
|
8137
|
+
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
8138
|
+
// Threat model — STRIDE + entities + attack trees rooted in findings.
|
|
8139
|
+
if (process.env.AGENTIC_SECURITY_NO_THREAT_MODEL !== '1') {
|
|
8140
|
+
try {
|
|
8141
|
+
_threatModel = buildAutoThreatModel({
|
|
8142
|
+
findings: finalFindings, routes: aR, supplyChain,
|
|
8143
|
+
});
|
|
8144
|
+
persistAutoThreatModel(scanRoot, _threatModel);
|
|
8145
|
+
} catch (_) {}
|
|
8146
|
+
}
|
|
8147
|
+
// API contract — undocumented endpoints / missing-auth flags.
|
|
8148
|
+
if (process.env.AGENTIC_SECURITY_NO_API_CONTRACT !== '1') {
|
|
8149
|
+
try { _apiContractFindings = runApiContractScan(scanRoot, aR) || []; finalFindings.push(..._apiContractFindings); } catch (_) {}
|
|
8150
|
+
}
|
|
8151
|
+
// SBOM diff — drift detection across releases.
|
|
8152
|
+
if (process.env.AGENTIC_SECURITY_NO_SBOM_DIFF !== '1') {
|
|
8153
|
+
try {
|
|
8154
|
+
_sbomDiff = runSbomDiff(scanRoot, annotatedComponents || []);
|
|
8155
|
+
if (_sbomDiff && Array.isArray(_sbomDiff.findings)) finalFindings.push(..._sbomDiff.findings);
|
|
8156
|
+
} catch (_) {}
|
|
8157
|
+
}
|
|
8158
|
+
// Sigstore provenance — opt-in (requires network + flag).
|
|
8159
|
+
if (process.env.AGENTIC_SECURITY_SIGSTORE === '1') {
|
|
8160
|
+
try { /* fire-and-forget async — don't block scan completion */ annotateProvenance(supplyChain, annotatedComponents).catch(()=>{}); } catch (_) {}
|
|
8161
|
+
}
|
|
8162
|
+
// Compliance evidence — auto-generate from policy file if present.
|
|
8163
|
+
if (process.env.AGENTIC_SECURITY_NO_COMPLIANCE !== '1') {
|
|
8164
|
+
try {
|
|
8165
|
+
const policy = loadCompliancePolicy(scanRoot);
|
|
8166
|
+
if (policy && !policy._error) {
|
|
8167
|
+
_complianceReport = verifyCompliancePolicy(policy, { scanRoot, findings: finalFindings });
|
|
8168
|
+
emitComplianceJsonLd(_complianceReport, scanRoot);
|
|
8169
|
+
emitComplianceMarkdown(_complianceReport, scanRoot);
|
|
8170
|
+
}
|
|
8171
|
+
} catch (_) {}
|
|
8172
|
+
}
|
|
8173
|
+
// License graph — transitive copyleft + distribution-mode + relicense.
|
|
8174
|
+
if (process.env.AGENTIC_SECURITY_NO_LICENSE_GRAPH !== '1') {
|
|
8175
|
+
try {
|
|
8176
|
+
const lgPolicy = loadLicenseGraphPolicy(scanRoot);
|
|
8177
|
+
_licenseGraph = analyzeLicenseGraph(annotatedComponents || [], lgPolicy);
|
|
8178
|
+
if (_licenseGraph && Array.isArray(_licenseGraph.findings)) finalFindings.push(..._licenseGraph.findings);
|
|
8179
|
+
} catch (_) {}
|
|
8180
|
+
}
|
|
8181
|
+
// Attributions: emit ATTRIBUTIONS.md (and NOTICE if Apache deps present).
|
|
8182
|
+
if (process.env.AGENTIC_SECURITY_NO_ATTRIBUTIONS !== '1') {
|
|
8183
|
+
try {
|
|
8184
|
+
_attributions = generateAttributions(annotatedComponents || []);
|
|
8185
|
+
if (_attributions && _attributions.componentCount) persistAttributions(scanRoot, _attributions);
|
|
8186
|
+
} catch (_) {}
|
|
8187
|
+
}
|
|
8188
|
+
// Attack taxonomy summary — aggregates ATT&CK / ATLAS / kill-chain
|
|
8189
|
+
// distribution over all findings for the report layer.
|
|
8190
|
+
if (process.env.AGENTIC_SECURITY_NO_ATTACK_TAX !== '1') {
|
|
8191
|
+
try { _taxonomySummary = summarizeTaxonomy(finalFindings); } catch (_) {}
|
|
8192
|
+
}
|
|
8193
|
+
// PQC migration plan — aggregates pqc-migration findings into a
|
|
8194
|
+
// structured plan (.agentic-security/pqc-migration-plan.{json,md}).
|
|
8195
|
+
if (process.env.AGENTIC_SECURITY_NO_PQC_PLAN !== '1') {
|
|
8196
|
+
try {
|
|
8197
|
+
_pqcPlan = buildPqcPlan(finalFindings);
|
|
8198
|
+
if (_pqcPlan) persistPqcPlan(scanRoot, _pqcPlan);
|
|
8199
|
+
} catch (_) {}
|
|
8200
|
+
}
|
|
8201
|
+
// Exploit bundles — per-family PoC + Jest + pytest + remediation for
|
|
8202
|
+
// top-N critical/high findings.
|
|
8203
|
+
if (process.env.AGENTIC_SECURITY_NO_EXPLOIT_BUNDLES !== '1') {
|
|
8204
|
+
try {
|
|
8205
|
+
const bundles = generateExploitBundles(finalFindings, { maxBundles: 25 });
|
|
8206
|
+
if (bundles.size) {
|
|
8207
|
+
_exploitBundles = {};
|
|
8208
|
+
for (const [id, b] of bundles) _exploitBundles[id] = b;
|
|
8209
|
+
const bundlePath = path.join(scanRoot, '.agentic-security', 'exploit-bundles.json');
|
|
8210
|
+
try { fs.mkdirSync(path.dirname(bundlePath), { recursive: true }); } catch {}
|
|
8211
|
+
try { fs.writeFileSync(bundlePath, JSON.stringify(_exploitBundles, null, 2)); } catch {}
|
|
8212
|
+
}
|
|
8213
|
+
} catch (_) {}
|
|
8214
|
+
}
|
|
8215
|
+
}
|
|
8216
|
+
|
|
7460
8217
|
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
|
-
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};}
|
|
8218
|
+
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,threatModel:_threatModel,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary};}
|
|
7462
8219
|
|
|
7463
8220
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
7464
8221
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|
|
@@ -7897,6 +8654,7 @@ export {
|
|
|
7897
8654
|
queryOSV, queryRegistries, computeAttackPathComponents,
|
|
7898
8655
|
markUsedVulnFunctions, dedupeFindingsWithEvidence, scoreTriage,
|
|
7899
8656
|
_enrichWithScorecard, scoreToxicity, _enrichWithKEV, _loadKEVCatalog,
|
|
8657
|
+
_enrichWithEPSS, _fetchEPSSBatch,
|
|
7900
8658
|
classifyOrphans, classifyField, classifyEndpoint, shouldScan,
|
|
7901
8659
|
_isFalsePositiveCredential, _detectSafeSinkShape,
|
|
7902
8660
|
_loadCustomRules, _isCustomSuppressed, _isPathIgnored,
|