@clear-capabilities/agentic-security-scanner 0.77.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 +1907 -0
- package/bin/.agentic-security/last-scan.json +1907 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +166 -0
- package/bin/.agentic-security/streak.json +20 -0
- package/bin/agentic-security.js +55 -9
- 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 +159 -0
- package/dist/824.index.js +126 -0
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +5 -0
- package/dist/agentic-security.mjs +32 -32
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +4 -4
- package/src/dataflow/async-sequencing.js +16 -7
- package/src/dataflow/builtin-summaries.js +131 -0
- package/src/dataflow/catalog.js +107 -0
- package/src/dataflow/cross-repo.js +75 -1
- package/src/dataflow/engine.js +181 -8
- package/src/dataflow/implicit-flow.js +24 -6
- package/src/dataflow/stub-aware-filter.js +69 -11
- package/src/dataflow/summaries.js +28 -3
- package/src/engine-parallel.js +70 -0
- package/src/engine.js +270 -19
- package/src/integrations/index.js +2 -1
- package/src/ir/callgraph.js +27 -7
- package/src/ir/index.js +22 -1
- package/src/ir/parser-go.js +403 -0
- package/src/ir/parser-js.js +2 -0
- package/src/ir/parser-php.js +330 -0
- package/src/ir/parser-py.helper.py +137 -11
- package/src/ir/parser-rb.js +309 -0
- 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 +16 -1
- 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 +16 -5
- package/src/posture/validator-metrics.js +3 -6
- package/src/report/index.js +23 -2
- package/src/sast/cache-poisoning.js +77 -0
- package/src/sast/comparison-safety.js +73 -0
- package/src/sast/db-taint.js +78 -0
- package/src/sast/graphql.js +127 -0
- package/src/sast/llm-stored-prompt.js +57 -0
- package/src/sast/mutation-xss.js +43 -0
- package/src/sast/nosql-injection.js +5 -0
- package/src/sast/null-byte-injection.js +76 -0
- package/src/sast/redos-nfa.js +338 -0
- package/src/sast/rust.js +26 -0
- package/src/sast/sensitive-data-logging.js +73 -0
- package/src/sast/weak-password-hash.js +77 -0
- package/src/sast/weak-randomness.js +100 -0
- package/src/sca/binary-metadata.js +124 -0
- package/src/sca/llm-function-extract.js +107 -0
- package/src/sca/py-package-functions.js +118 -0
- package/src/sca/vendor-detect.js +144 -0
package/src/engine.js
CHANGED
|
@@ -23,6 +23,13 @@ import { scanSpringbootHardening } from './sast/springboot-hardening.js';
|
|
|
23
23
|
import { scanLaravelHardening } from './sast/laravel-hardening.js';
|
|
24
24
|
import { scanSwift } from './sast/swift.js';
|
|
25
25
|
import { scanDartFlutter } from './sast/dart-flutter.js';
|
|
26
|
+
import { scanWeakRandomness } from './sast/weak-randomness.js';
|
|
27
|
+
import { scanGraphQL as scanGraphQLModule } from './sast/graphql.js';
|
|
28
|
+
import { scanSensitiveDataLogging } from './sast/sensitive-data-logging.js';
|
|
29
|
+
import { scanComparisonSafety } from './sast/comparison-safety.js';
|
|
30
|
+
import { scanWeakPasswordHash } from './sast/weak-password-hash.js';
|
|
31
|
+
import { scanCachePoisoning } from './sast/cache-poisoning.js';
|
|
32
|
+
import { scanNullByteInjection } from './sast/null-byte-injection.js';
|
|
26
33
|
import { scanLlmTradingAgent } from './sast/llm-trading-agent.js';
|
|
27
34
|
import { scanMobileManifest } from './sast/mobile-manifest.js';
|
|
28
35
|
import { scanQuarkusHardening } from './sast/quarkus-hardening.js';
|
|
@@ -48,7 +55,7 @@ import { scanCpp } from './sast/cpp.js';
|
|
|
48
55
|
import { scanJulietShape, applyJulietJavaSuppressions, applyJulietCsSuppressions } from './sast/juliet-shape.js';
|
|
49
56
|
import { scanCppDataflow, _parseErrorCount as _cppDataflowParseErrors } from './sast/cpp-dataflow.js';
|
|
50
57
|
import { scanSolidity } from './sast/solidity.js';
|
|
51
|
-
import { scanRust } from './sast/rust.js';
|
|
58
|
+
import { scanRust, extractRustImportMap } from './sast/rust.js';
|
|
52
59
|
import { scanGoExtended } from './sast/go-extended.js';
|
|
53
60
|
import { scanDatabaseRLS } from './sast/db-rls.js';
|
|
54
61
|
import { scanRateLimit } from './sast/rate-limit.js';
|
|
@@ -74,10 +81,10 @@ import { scanXPathInjection } from './sast/xpath-injection.js';
|
|
|
74
81
|
import { scanSSTI } from './sast/ssti.js';
|
|
75
82
|
import { scanOpenRedirect } from './sast/open-redirect.js';
|
|
76
83
|
import { scanResponseSplitting } from './sast/response-splitting.js';
|
|
77
|
-
import { scanStoredPromptInjection } from './sast/llm-stored-prompt.js';
|
|
84
|
+
import { scanStoredPromptInjection, scanStoredPromptInjectionCrossFile } from './sast/llm-stored-prompt.js';
|
|
78
85
|
import { scanRAGPoisoning } from './sast/rag-poisoning.js';
|
|
79
86
|
import { scanAgentToolEscalation } from './sast/agent-tool-escalation.js';
|
|
80
|
-
import { scanDbTaint } from './sast/db-taint.js';
|
|
87
|
+
import { scanDbTaint, scanDbTaintCrossFile } from './sast/db-taint.js';
|
|
81
88
|
import { scanSSRFCloudMetadata } from './sast/ssrf-cloud-metadata.js';
|
|
82
89
|
import { scanMutationXSS } from './sast/mutation-xss.js';
|
|
83
90
|
import { scanDeserializationGadgets, _detectGadgets } from './sast/deserialization-gadgets.js';
|
|
@@ -4556,10 +4563,14 @@ function scanMiddlewareOrdering(fp, raw){
|
|
|
4556
4563
|
if (isAuth && line < firstAuthAt) firstAuthAt = line;
|
|
4557
4564
|
if (mountPath) mounts.push({ line, mountPath, handlerArgs, isAuth });
|
|
4558
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
|
+
}
|
|
4559
4570
|
for (const mt of mounts) {
|
|
4560
4571
|
if (mt.isAuth) continue;
|
|
4561
4572
|
if (!_SENSITIVE_PATH_RE.test(mt.mountPath)) continue;
|
|
4562
|
-
if (mt.line >= firstAuthAt) continue;
|
|
4573
|
+
if (mt.line >= firstAuthAt) continue;
|
|
4563
4574
|
findings.push({
|
|
4564
4575
|
vuln: `Sensitive Route Mounted Before Auth Middleware (${mt.mountPath})`,
|
|
4565
4576
|
severity: 'high', cwe: 'CWE-285', stride: 'Elevation of Privilege',
|
|
@@ -4567,6 +4578,27 @@ function scanMiddlewareOrdering(fp, raw){
|
|
|
4567
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).`,
|
|
4568
4579
|
});
|
|
4569
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
|
+
}
|
|
4570
4602
|
return findings;
|
|
4571
4603
|
}
|
|
4572
4604
|
|
|
@@ -5444,28 +5476,106 @@ function dedupeFindingsWithEvidence(findings){
|
|
|
5444
5476
|
// that export is actually imported or invoked in the codebase.
|
|
5445
5477
|
let _VULN_FUNCTION_HINTS_DATA;
|
|
5446
5478
|
try { _VULN_FUNCTION_HINTS_DATA = _require('./sca/vuln-function-hints.json'); } catch(_) { _VULN_FUNCTION_HINTS_DATA = {}; }
|
|
5479
|
+
let _VULN_FUNCTION_HINTS_GENERATED;
|
|
5480
|
+
try { _VULN_FUNCTION_HINTS_GENERATED = _require('./sca/vuln-function-hints-generated.json'); } catch(_) { _VULN_FUNCTION_HINTS_GENERATED = {}; }
|
|
5447
5481
|
const VULN_FUNCTION_HINTS = {
|
|
5448
|
-
"lodash":["merge","defaultsDeep","set","setWith","zipObjectDeep"],
|
|
5449
|
-
"jsonwebtoken":["decode"],
|
|
5450
|
-
"marked":["parse"],
|
|
5482
|
+
"lodash":["merge","defaultsDeep","set","setWith","zipObjectDeep","template"],
|
|
5483
|
+
"jsonwebtoken":["decode","verify","sign"],
|
|
5484
|
+
"marked":["parse","lexer"],
|
|
5451
5485
|
"ejs":["render","renderFile","compile"],
|
|
5452
|
-
"node-fetch":["default"],
|
|
5453
|
-
"xml2js":["parseString"],
|
|
5454
|
-
"js-yaml":["load"],
|
|
5486
|
+
"node-fetch":["default","fetch"],
|
|
5487
|
+
"xml2js":["parseString","parseStringPromise"],
|
|
5488
|
+
"js-yaml":["load","loadAll"],
|
|
5455
5489
|
"minimist":["parse"],
|
|
5490
|
+
"express":["static","urlencoded","json"],
|
|
5491
|
+
"axios":["get","post","put","patch","delete","request","create"],
|
|
5492
|
+
"pg":["query","connect"],
|
|
5493
|
+
"mysql2":["query","execute","createConnection","createPool"],
|
|
5494
|
+
"mongoose":["connect","model","find","findOne","findById","aggregate"],
|
|
5495
|
+
"sequelize":["query","literal","define"],
|
|
5496
|
+
"handlebars":["compile","precompile","registerHelper"],
|
|
5497
|
+
"pug":["compile","render","renderFile"],
|
|
5498
|
+
"sharp":["resize","toBuffer","toFile"],
|
|
5499
|
+
"tar":["extract","create","list"],
|
|
5500
|
+
"glob":["sync","glob"],
|
|
5501
|
+
"cookie":["parse","serialize"],
|
|
5502
|
+
"cookie-parser":["default"],
|
|
5503
|
+
"cors":["default"],
|
|
5504
|
+
"helmet":["default","contentSecurityPolicy"],
|
|
5505
|
+
"passport":["authenticate","initialize","session"],
|
|
5506
|
+
"bcrypt":["hash","compare","genSalt"],
|
|
5507
|
+
"bcryptjs":["hash","compare","genSalt"],
|
|
5508
|
+
"crypto-js":["AES","DES","TripleDES","MD5","SHA1","SHA256","HmacSHA256"],
|
|
5509
|
+
"serialize-javascript":["default"],
|
|
5510
|
+
"shelljs":["exec","which","cat","sed"],
|
|
5511
|
+
"child_process":["exec","execSync","spawn","fork"],
|
|
5512
|
+
"vm2":["VM","NodeVM"],
|
|
5513
|
+
"yaml":["parse","parseDocument"],
|
|
5514
|
+
"dotenv":["config","parse"],
|
|
5515
|
+
"jsonwebtoken":["decode","verify","sign"],
|
|
5516
|
+
"jose":["jwtVerify","SignJWT","compactDecrypt"],
|
|
5517
|
+
"cheerio":["load"],
|
|
5518
|
+
"puppeteer":["launch","connect"],
|
|
5519
|
+
"nodemailer":["createTransport"],
|
|
5520
|
+
"redis":["createClient","get","set","del"],
|
|
5521
|
+
"ioredis":["get","set","del","eval"],
|
|
5522
|
+
"knex":["raw","select","where","insert","update","del"],
|
|
5523
|
+
"prisma":["findUnique","findFirst","findMany","create","update","delete","queryRaw"],
|
|
5524
|
+
"typeorm":["query","createQueryBuilder","getRepository"],
|
|
5525
|
+
"sqlite3":["run","get","all","exec"],
|
|
5526
|
+
"better-sqlite3":["prepare","exec","pragma"],
|
|
5527
|
+
"ws":["on","send","close"],
|
|
5528
|
+
"socket.io":["on","emit","to","broadcast"],
|
|
5529
|
+
"formidable":["parse"],
|
|
5530
|
+
"multer":["single","array","fields"],
|
|
5531
|
+
"path-to-regexp":["compile","parse"],
|
|
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('_'))) : {}),
|
|
5456
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('_'))) : {}),
|
|
5457
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
|
+
}
|
|
5458
5557
|
function markUsedVulnFunctions(supplyChain,fc){
|
|
5459
5558
|
const used={};
|
|
5460
5559
|
const perFile={};
|
|
5461
5560
|
for(const[fp,content] of Object.entries(fc)){
|
|
5462
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(_){}}
|
|
5463
5565
|
for(const[pkg,fns] of Object.entries(VULN_FUNCTION_HINTS)){
|
|
5464
5566
|
if(!perFile[pkg])perFile[pkg]=[];
|
|
5465
5567
|
for(const fn of fns){
|
|
5466
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;
|
|
5467
5571
|
for(let li=0;li<lines.length;li++){
|
|
5468
|
-
|
|
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){
|
|
5469
5579
|
perFile[pkg].push({pkg,fn,file:fp,line:li+1});
|
|
5470
5580
|
if(!used[pkg])used[pkg]=new Set();
|
|
5471
5581
|
used[pkg].add(fn);
|
|
@@ -5477,7 +5587,41 @@ function markUsedVulnFunctions(supplyChain,fc){
|
|
|
5477
5587
|
}
|
|
5478
5588
|
for(const sc of supplyChain||[]){
|
|
5479
5589
|
if(sc.type!=='vulnerable_dep')continue;
|
|
5480
|
-
|
|
5590
|
+
// Merge hints: versioned → hardcoded → OSV ecosystem_specific → skip if none
|
|
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
|
+
}
|
|
5602
|
+
const osvFns=Array.isArray(sc.osvVulnFunctions)?sc.osvVulnFunctions.map(f=>{const d=f.lastIndexOf('.');return d>0?f.slice(d+1):f;}):[];
|
|
5603
|
+
const allFns=[...new Set([...versionedFns,...hardcoded,...osvFns])];
|
|
5604
|
+
if(!allFns.length){sc.functionReachable='unknown';sc.noKnownCallSite=true;sc._hintSource='none';continue;}
|
|
5605
|
+
sc._hintSource=osvFns.length?(hardcoded.length?'hardcoded+osv':'osv'):'hardcoded';
|
|
5606
|
+
// Search codebase for these functions (if not already searched via VULN_FUNCTION_HINTS)
|
|
5607
|
+
if(osvFns.length&&!hardcoded.length){
|
|
5608
|
+
for(const[fp,content] of Object.entries(fc)){
|
|
5609
|
+
const lines=content.split('\n');
|
|
5610
|
+
for(const fn of osvFns){
|
|
5611
|
+
const shortFn=fn.lastIndexOf('.')>0?fn.slice(fn.lastIndexOf('.')+1):fn;
|
|
5612
|
+
const re=new RegExp(`\\b${shortFn.replace(/\W/g,'\\$&')}\\b`,'g');
|
|
5613
|
+
for(let li=0;li<lines.length;li++){
|
|
5614
|
+
if(re.test(lines[li])){
|
|
5615
|
+
if(!perFile[sc.name])perFile[sc.name]=[];
|
|
5616
|
+
perFile[sc.name].push({pkg:sc.name,fn:shortFn,file:fp,line:li+1});
|
|
5617
|
+
if(!used[sc.name])used[sc.name]=new Set();
|
|
5618
|
+
used[sc.name].add(shortFn);
|
|
5619
|
+
}
|
|
5620
|
+
re.lastIndex=0;
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
}
|
|
5624
|
+
}
|
|
5481
5625
|
sc.usedVulnerableFunctions=[...(used[sc.name]||[])];
|
|
5482
5626
|
const sites=(perFile[sc.name]||[]);
|
|
5483
5627
|
const seen=new Set();
|
|
@@ -5971,7 +6115,10 @@ function _parsePackageLockJson(text,filePath){
|
|
|
5971
6115
|
const parts=scoped?name.slice(1).split('/'):['',name];
|
|
5972
6116
|
const group=scoped?`@${parts[0]}`:'';
|
|
5973
6117
|
const pkgName=scoped?parts[1]:name;
|
|
6118
|
+
const depChain=path.split('node_modules/').filter(Boolean);
|
|
6119
|
+
const isDirect=depChain.length<=1;
|
|
5974
6120
|
out.push({name,version:ver,group,scope:info.dev?'optional':'required',
|
|
6121
|
+
depChain,isDirect,
|
|
5975
6122
|
purl:_makePurl('npm',pkgName,ver,group),ecosystem:'npm',filePath,isUnpinned:false});
|
|
5976
6123
|
}
|
|
5977
6124
|
}catch(_){}return out;
|
|
@@ -6260,8 +6407,33 @@ function _parsePubspecLock(text,filePath){
|
|
|
6260
6407
|
}return out;
|
|
6261
6408
|
}
|
|
6262
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
|
+
}
|
|
6263
6435
|
function parseManifests(allFileContents){
|
|
6264
|
-
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};
|
|
6265
6437
|
const out=[],seen=new Set();
|
|
6266
6438
|
for(const[fp,content]of Object.entries(allFileContents)){
|
|
6267
6439
|
const base=fp.split('/').pop();
|
|
@@ -6393,7 +6565,13 @@ async function queryOSV(components,allFileContents){
|
|
|
6393
6565
|
const resp=await fetch(`https://api.osv.dev/v1/vulns/${vid}`);
|
|
6394
6566
|
const d=await resp.json();
|
|
6395
6567
|
const fixedVersions=new Set();
|
|
6396
|
-
|
|
6568
|
+
const osvVulnFunctions=[];
|
|
6569
|
+
for(const aff of(d.affected||[])){
|
|
6570
|
+
for(const rng of(aff.ranges||[]))for(const ev of(rng.events||[]))if(ev.fixed)fixedVersions.add(ev.fixed);
|
|
6571
|
+
const es=aff.ecosystem_specific||aff.database_specific||{};
|
|
6572
|
+
if(Array.isArray(es.vulnerable_functions))osvVulnFunctions.push(...es.vulnerable_functions);
|
|
6573
|
+
if(Array.isArray(es.imports))for(const imp of es.imports)if(Array.isArray(imp.symbols))osvVulnFunctions.push(...imp.symbols);
|
|
6574
|
+
}
|
|
6397
6575
|
let severity='medium';
|
|
6398
6576
|
const db=d.database_specific||{};
|
|
6399
6577
|
if(db.severity)severity=db.severity.toLowerCase()==='moderate'?'medium':db.severity.toLowerCase();
|
|
@@ -6409,6 +6587,7 @@ async function queryOSV(components,allFileContents){
|
|
|
6409
6587
|
vuln={id:vid,description:(d.summary||d.details||'No description.').slice(0,300),
|
|
6410
6588
|
fixedVersions:[...fixedVersions].sort(),
|
|
6411
6589
|
aliases:(d.aliases||[]).filter(a=>a.startsWith('CVE-')),
|
|
6590
|
+
osvVulnFunctions:[...new Set(osvVulnFunctions)],
|
|
6412
6591
|
severity,cvssVector,hasKnownAttackRef};
|
|
6413
6592
|
_osvCacheSet('vuln:'+vid,vuln);
|
|
6414
6593
|
}catch(_){continue;}
|
|
@@ -6419,7 +6598,7 @@ async function queryOSV(components,allFileContents){
|
|
|
6419
6598
|
results.push({type:'vulnerable_dep',name:comp.name,version:comp.version,ecosystem:comp.ecosystem,
|
|
6420
6599
|
purl:comp.purl,osvId:vid,cveAliases:vuln.aliases,description:vuln.description,
|
|
6421
6600
|
fixedVersions:vuln.fixedVersions,severity:vuln.severity,cvssVector:vuln.cvssVector,
|
|
6422
|
-
hasKnownAttackRef:vuln.hasKnownAttackRef,reachable:comp.reachable,scope:comp.scope,
|
|
6601
|
+
hasKnownAttackRef:vuln.hasKnownAttackRef,osvVulnFunctions:vuln.osvVulnFunctions||[],reachable:comp.reachable,scope:comp.scope,
|
|
6423
6602
|
file:comp.filePath,
|
|
6424
6603
|
// kept for generateRecs() compat
|
|
6425
6604
|
advisory:`${vid}${cveStr}, ${vuln.description}`,
|
|
@@ -6605,7 +6784,10 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6605
6784
|
// variants, OWASP Benchmark's helpers package). Roadmap item #5.
|
|
6606
6785
|
try { _GLOBAL_JAVA_TAINTED_METHODS = _buildGlobalJavaTaintedMethodIndex(fileContents); }
|
|
6607
6786
|
catch { _GLOBAL_JAVA_TAINTED_METHODS = new Set(); }
|
|
6608
|
-
const
|
|
6787
|
+
const _perFileTimeoutMs = parseInt(process.env.AGENTIC_SECURITY_PER_FILE_TIMEOUT_MS || '10000', 10);
|
|
6788
|
+
const _fileTimings = [];
|
|
6789
|
+
let _filesSkipped = 0, _filesTimedOut = 0;
|
|
6790
|
+
const files=Object.keys(fileContents).filter(f=>shouldScan(f) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];let i=0;for(const p of files){i++;const _ft0=Date.now();setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});try{const c=fileContents[p];if(!c||c.length>500000){_filesSkipped++;continue;}const _avgLine=c.length/Math.max(c.split('\n').length,1);if(_avgLine>400&&c.length>10000)continue;fc[p]=c;aR.push(...scanRoutes(p,c));const ta=performAnalysis(p,c);pfr[p]=ta;aF.push(...ta.findings);aSrc.push(...ta.sources);aSink.push(...ta.sinks);aSan.push(...ta.sanitizers);aLogic.push(...scanLogicVulns(p,c));aSecrets.push(...scanCredentials(p,c));aF.push(...scanStructuralVulns(p,c));aF.push(...scanExtraStructural(p,c));aF.push(...scanAliasedSinks(p,c));aF.push(...scanJavaSAST(p,c));aF.push(...scanJavaBenchExtras(p,c));aLogic.push(...scanMiddlewareOrdering(p,c));aLogic.push(...scanReDoS(p,c));aLogic.push(...scanTodosNearSecurity(p,c));aSecrets.push(...scanEntropySecrets(p,c));const cp=scanCiphers(p,c);aCiphersRest.push(...cp.atRest);aCiphersTransit.push(...cp.inTransit);if(/\.(graphql|gql)$/i.test(p))aF.push(...scanGraphQL(p,c));aF.push(...scanIaC(p,c));
|
|
6609
6791
|
aF.push(...scanLLM(p,c));
|
|
6610
6792
|
aF.push(...scanLLMOwasp(p,c));
|
|
6611
6793
|
aLogic.push(...scanBusinessLogic(p,c));
|
|
@@ -6621,6 +6803,13 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6621
6803
|
aF.push(...scanLaravelHardening(p,c));
|
|
6622
6804
|
aF.push(...scanSwift(p,c));
|
|
6623
6805
|
aF.push(...scanDartFlutter(p,c));
|
|
6806
|
+
aF.push(...scanWeakRandomness(p,c));
|
|
6807
|
+
aF.push(...scanGraphQLModule(p,c));
|
|
6808
|
+
aF.push(...scanSensitiveDataLogging(p,c));
|
|
6809
|
+
aF.push(...scanComparisonSafety(p,c));
|
|
6810
|
+
aF.push(...scanWeakPasswordHash(p,c));
|
|
6811
|
+
aF.push(...scanCachePoisoning(p,c));
|
|
6812
|
+
aF.push(...scanNullByteInjection(p,c));
|
|
6624
6813
|
aF.push(...scanLlmTradingAgent(p,c));
|
|
6625
6814
|
aF.push(...scanMobileManifest(p,c));
|
|
6626
6815
|
aF.push(...scanQuarkusHardening(p,c));
|
|
@@ -6669,7 +6858,11 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6669
6858
|
aF.push(...scanMutationXSS(p,c));
|
|
6670
6859
|
aF.push(...scanKotlin(p,c));
|
|
6671
6860
|
aF.push(...scanRuby(p,c));
|
|
6672
|
-
aF.push(...scanPhp(p,c));
|
|
6861
|
+
aF.push(...scanPhp(p,c));
|
|
6862
|
+
const _ftElapsed=Date.now()-_ft0;
|
|
6863
|
+
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++;}
|
|
6864
|
+
_fileTimings.push({file:p,ms:_ftElapsed});
|
|
6865
|
+
}catch(_){_fileTimings.push({file:p,ms:Date.now()-_ft0,error:true});}if(i%5===0)await new Promise(r=>setTimeout(r,0));}
|
|
6673
6866
|
// Deserialization-gadget detector runs once with full-tree context (it needs
|
|
6674
6867
|
// manifest contents to know which gadget libs are on the classpath).
|
|
6675
6868
|
try {
|
|
@@ -6817,26 +7010,50 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
6817
7010
|
setProgress({current:i,total:files.length,file:"OSV vulnerability database...",phase:"SCA"});
|
|
6818
7011
|
const allFileContents={...fc, ...depFileContents};
|
|
6819
7012
|
const components=parseManifests(allFileContents);
|
|
7013
|
+
try{const{detectVendoredLibraries}=await import('./sca/vendor-detect.js');const vendored=detectVendoredLibraries(fc);for(const v of vendored){const key=`${v.ecosystem}:${v.name}:${v.version}`;if(!components.some(c=>`${c.ecosystem}:${c.name}:${c.version}`===key))components.push({...v,group:'',purl:`pkg:${v.ecosystem}/${v.name}@${v.version}`,filePath:v.file,isUnpinned:false,reachable:true});}}catch(_){}
|
|
6820
7014
|
const reach=buildReachabilitySet(fc);
|
|
6821
7015
|
const reachabilitySet=reach.imported;
|
|
6822
|
-
components.forEach(c=>{
|
|
7016
|
+
components.forEach(c=>{
|
|
7017
|
+
c.reachable=reachabilitySet.has(c.name.toLowerCase())||(c.ecosystem==='pypi'&&reachabilitySet.has(c.name.replace(/-/g,'_').toLowerCase()));
|
|
7018
|
+
c.isBuildOnly=(c.scope==='optional'&&!c.reachable);
|
|
7019
|
+
if(!c.isDirect&&c.isDirect!==false)c.isDirect=!c.depChain||!c.depChain.length||c.depChain.length<=1;
|
|
7020
|
+
});
|
|
6823
7021
|
let supplyChain=[];try{supplyChain=await queryOSV(components,allFileContents);}catch(_){supplyChain=[];}
|
|
6824
7022
|
// Feat-9: enrich SCA findings with EPSS abuse-probability scores
|
|
6825
7023
|
try{supplyChain=await _enrichWithEPSS(supplyChain);}catch(_){}
|
|
6826
7024
|
// 0.10.0: enrich SCA findings with CISA KEV (CISA KEV catalog)
|
|
6827
7025
|
try{supplyChain=await _enrichWithKEV(supplyChain);}catch(_){}
|
|
6828
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(_){}}
|
|
7029
|
+
// LLM-assisted function extraction for CVEs without hints (opt-in: AGENTIC_SECURITY_LLM_SCA=1)
|
|
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(_){}}
|
|
6829
7031
|
setProgress({current:i,total:files.length,file:"Registry metadata...",phase:"SCA"});
|
|
6830
7032
|
let registryInfo=new Map();try{registryInfo=await queryRegistries(components);}catch(_){}
|
|
6831
7033
|
const dd=(a,k)=>[...new Map(a.map(x=>[k(x),x])).values()];
|
|
6832
7034
|
// 0.6.0 Feat-1: annotate function-level reachability on SCA findings
|
|
6833
7035
|
try { _annotateFunctionReachability(supplyChain,dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`).map(r=>({...r})),callGraph,fc); } catch(_) {}
|
|
7036
|
+
// Reachability tier classification for SCA findings
|
|
7037
|
+
for(const sc of supplyChain||[]){
|
|
7038
|
+
if(sc.type!=='vulnerable_dep')continue;
|
|
7039
|
+
if(sc.functionReachable==='reachable')sc.reachabilityTier='function-reachable';
|
|
7040
|
+
else if(sc.functionReachable==='unreachable')sc.reachabilityTier='unreachable';
|
|
7041
|
+
else if(sc.reachable)sc.reachabilityTier='import-reachable';
|
|
7042
|
+
else if(sc.isBuildOnly||sc.scope==='optional'&&!sc.reachable){sc.reachabilityTier='build-only';sc.isBuildOnly=true;if(sc.severity==='critical')sc.severity='medium';else if(sc.severity==='high')sc.severity='low';}
|
|
7043
|
+
else if(sc.scope==='required')sc.reachabilityTier='manifest-only';
|
|
7044
|
+
else sc.reachabilityTier='transitive-only';
|
|
7045
|
+
if(sc.reachabilityTier==='transitive-only'){sc.unreachable=true;sc._reachabilityDemoted=true;}
|
|
7046
|
+
}
|
|
7047
|
+
// Early dedup: collapse duplicates BEFORE the annotation pipeline to reduce work.
|
|
7048
|
+
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(_){}
|
|
6834
7049
|
// Sort findings: critical first, then structural patterns last within same severity
|
|
6835
7050
|
aF.sort((a,b)=>({critical:0,high:1,medium:2,low:3}[a.severity]??4)-({critical:0,high:1,medium:2,low:3}[b.severity]??4));
|
|
6836
7051
|
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);}
|
|
6837
7052
|
const attackResult=computeAttackPathComponents(aF,components,reach.byFile);
|
|
6838
7053
|
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});}}
|
|
6839
7054
|
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};});
|
|
7055
|
+
try{aF.push(...scanDbTaintCrossFile(fc));}catch(_){}
|
|
7056
|
+
try{aF.push(...scanStoredPromptInjectionCrossFile(fc));}catch(_){}
|
|
6840
7057
|
let finalFindings;try{finalFindings=dedupeFindingsWithEvidence(aF);}catch(_){finalFindings=dd(aF,f=>f.id);}
|
|
6841
7058
|
// 0.34.6: filter out Java FPs where a sanitizer pattern (argv-form ProcessBuilder,
|
|
6842
7059
|
// parameterized prepareStatement, constant-folded dead-branch) is present.
|
|
@@ -7115,6 +7332,33 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7115
7332
|
f.validator_verdict = 'unvalidated';
|
|
7116
7333
|
}
|
|
7117
7334
|
finalFindings.push(...irFindings);
|
|
7335
|
+
// Java SCA enrichment: use deep-mode IR call graph to improve Java function reachability
|
|
7336
|
+
try {
|
|
7337
|
+
for (const sc of supplyChain) {
|
|
7338
|
+
if (sc.type !== 'vulnerable_dep' || sc.ecosystem !== 'maven') continue;
|
|
7339
|
+
if (sc.functionReachable === 'reachable') continue;
|
|
7340
|
+
const allFns = [...(sc.osvVulnFunctions || []), ...(VULN_FUNCTION_HINTS[sc.name] || [])];
|
|
7341
|
+
if (!allFns.length) continue;
|
|
7342
|
+
for (const fn of callGraph.functions ? callGraph.functions.values() : []) {
|
|
7343
|
+
if (!fn.cfg || !fn.cfg.nodes) continue;
|
|
7344
|
+
for (const node of Object.values(fn.cfg.nodes)) {
|
|
7345
|
+
if (node.kind !== 'call') continue;
|
|
7346
|
+
const callee = typeof node.callee === 'string' ? node.callee : null;
|
|
7347
|
+
if (!callee) continue;
|
|
7348
|
+
const shortCallee = callee.includes('.') ? callee.split('.').pop() : callee;
|
|
7349
|
+
if (allFns.some(f => f === shortCallee || f === callee)) {
|
|
7350
|
+
sc.functionReachable = 'reachable';
|
|
7351
|
+
sc.reachabilityTier = 'function-reachable';
|
|
7352
|
+
if (!sc.vulnerableFunctionCallSites) sc.vulnerableFunctionCallSites = [];
|
|
7353
|
+
sc.vulnerableFunctionCallSites.push({ pkg: sc.name, fn: shortCallee, file: fn.file, line: node.line });
|
|
7354
|
+
sc._javaIrEnriched = true;
|
|
7355
|
+
break;
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
if (sc.functionReachable === 'reachable') break;
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
} catch { /* Java SCA enrichment is best-effort */ }
|
|
7118
7362
|
} catch (e) {
|
|
7119
7363
|
// Deep mode is best-effort. A parser blowup in one file shouldn't kill
|
|
7120
7364
|
// the scan — fall back to the pattern-only result.
|
|
@@ -7308,7 +7552,14 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
|
|
|
7308
7552
|
// v3 next-gen: why-fired provenance is captured LAST so it reflects the
|
|
7309
7553
|
// final state of each finding after every other annotator has run.
|
|
7310
7554
|
_runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, {}); });
|
|
7311
|
-
|
|
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(_){}
|
|
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}};
|
|
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};}
|
|
7312
7563
|
|
|
7313
7564
|
// Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
|
|
7314
7565
|
// Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.
|
|
@@ -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 };
|
package/src/ir/index.js
CHANGED
|
@@ -13,6 +13,9 @@ import {
|
|
|
13
13
|
probePythonAvailable,
|
|
14
14
|
} from './parser-py-cst.js';
|
|
15
15
|
import { parseJavaFile } from './parser-java.js';
|
|
16
|
+
import { parseGoFile } from './parser-go.js';
|
|
17
|
+
import { parsePhpFile } from './parser-php.js';
|
|
18
|
+
import { parseRubyFile } from './parser-rb.js';
|
|
16
19
|
import { buildCallGraph } from './callgraph.js';
|
|
17
20
|
import { buildClassHierarchy } from './class-hierarchy.js';
|
|
18
21
|
import { computeSSA, isSSAEnabled } from './ssa.js';
|
|
@@ -76,6 +79,15 @@ export function buildProjectIR(fileContents) {
|
|
|
76
79
|
} else if (/\.kt$/i.test(file)) {
|
|
77
80
|
const ir = parseKotlinFile(file, code);
|
|
78
81
|
if (ir) perFile[file] = ir;
|
|
82
|
+
} else if (/\.go$/i.test(file)) {
|
|
83
|
+
const ir = parseGoFile(file, code);
|
|
84
|
+
if (ir) perFile[file] = ir;
|
|
85
|
+
} else if (/\.(?:php|phtml)$/i.test(file)) {
|
|
86
|
+
const ir = parsePhpFile(file, code);
|
|
87
|
+
if (ir) perFile[file] = ir;
|
|
88
|
+
} else if (/\.rb$/i.test(file)) {
|
|
89
|
+
const ir = parseRubyFile(file, code);
|
|
90
|
+
if (ir) perFile[file] = ir;
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
93
|
if (pyBatch.length) {
|
|
@@ -116,6 +128,15 @@ export async function buildProjectIRAsync(fileContents) {
|
|
|
116
128
|
const ir = await parseJavaFile(file, code);
|
|
117
129
|
if (ir) perFile[file] = ir;
|
|
118
130
|
} catch { /* skip */ }
|
|
131
|
+
} else if (/\.go$/i.test(file)) {
|
|
132
|
+
const ir = parseGoFile(file, code);
|
|
133
|
+
if (ir) perFile[file] = ir;
|
|
134
|
+
} else if (/\.(?:php|phtml)$/i.test(file)) {
|
|
135
|
+
const ir = parsePhpFile(file, code);
|
|
136
|
+
if (ir) perFile[file] = ir;
|
|
137
|
+
} else if (/\.rb$/i.test(file)) {
|
|
138
|
+
const ir = parseRubyFile(file, code);
|
|
139
|
+
if (ir) perFile[file] = ir;
|
|
119
140
|
}
|
|
120
141
|
}
|
|
121
142
|
if (pyBatch.length) {
|
|
@@ -149,4 +170,4 @@ export function parsePythonFile(file, code) {
|
|
|
149
170
|
return parsePythonFileRegex(file, code);
|
|
150
171
|
}
|
|
151
172
|
|
|
152
|
-
export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable };
|
|
173
|
+
export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, parseGoFile, parsePhpFile, parseRubyFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable };
|