@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.
Files changed (69) hide show
  1. package/bin/.agentic-security/findings.json +1907 -0
  2. package/bin/.agentic-security/last-scan.json +1907 -0
  3. package/bin/.agentic-security/last-scan.json.sig +1 -0
  4. package/bin/.agentic-security/scan-history.json +166 -0
  5. package/bin/.agentic-security/streak.json +20 -0
  6. package/bin/agentic-security.js +55 -9
  7. package/dist/178.index.js +1 -1
  8. package/dist/384.index.js +1 -1
  9. package/dist/476.index.js +5 -5
  10. package/dist/637.index.js +1 -1
  11. package/dist/700.index.js +138 -0
  12. package/dist/718.index.js +159 -0
  13. package/dist/824.index.js +126 -0
  14. package/dist/838.index.js +1 -1
  15. package/dist/985.index.js +5 -0
  16. package/dist/agentic-security.mjs +32 -32
  17. package/dist/agentic-security.mjs.sha256 +1 -1
  18. package/package.json +4 -4
  19. package/src/dataflow/async-sequencing.js +16 -7
  20. package/src/dataflow/builtin-summaries.js +131 -0
  21. package/src/dataflow/catalog.js +107 -0
  22. package/src/dataflow/cross-repo.js +75 -1
  23. package/src/dataflow/engine.js +181 -8
  24. package/src/dataflow/implicit-flow.js +24 -6
  25. package/src/dataflow/stub-aware-filter.js +69 -11
  26. package/src/dataflow/summaries.js +28 -3
  27. package/src/engine-parallel.js +70 -0
  28. package/src/engine.js +270 -19
  29. package/src/integrations/index.js +2 -1
  30. package/src/ir/callgraph.js +27 -7
  31. package/src/ir/index.js +22 -1
  32. package/src/ir/parser-go.js +403 -0
  33. package/src/ir/parser-js.js +2 -0
  34. package/src/ir/parser-php.js +330 -0
  35. package/src/ir/parser-py.helper.py +137 -11
  36. package/src/ir/parser-rb.js +309 -0
  37. package/src/llm-validator/index.js +7 -5
  38. package/src/mcp/audit.js +5 -0
  39. package/src/posture/calibration-drift.js +2 -1
  40. package/src/posture/calibration.js +16 -1
  41. package/src/posture/fix-history.js +8 -2
  42. package/src/posture/profile.js +4 -5
  43. package/src/posture/rule-overrides.js +2 -3
  44. package/src/posture/rule-pack-signing.js +2 -3
  45. package/src/posture/rule-synthesis.js +5 -6
  46. package/src/posture/security-trend.js +4 -7
  47. package/src/posture/state-dir.js +124 -0
  48. package/src/posture/streak.js +3 -0
  49. package/src/posture/suppressions.js +5 -8
  50. package/src/posture/triage.js +16 -5
  51. package/src/posture/validator-metrics.js +3 -6
  52. package/src/report/index.js +23 -2
  53. package/src/sast/cache-poisoning.js +77 -0
  54. package/src/sast/comparison-safety.js +73 -0
  55. package/src/sast/db-taint.js +78 -0
  56. package/src/sast/graphql.js +127 -0
  57. package/src/sast/llm-stored-prompt.js +57 -0
  58. package/src/sast/mutation-xss.js +43 -0
  59. package/src/sast/nosql-injection.js +5 -0
  60. package/src/sast/null-byte-injection.js +76 -0
  61. package/src/sast/redos-nfa.js +338 -0
  62. package/src/sast/rust.js +26 -0
  63. package/src/sast/sensitive-data-logging.js +73 -0
  64. package/src/sast/weak-password-hash.js +77 -0
  65. package/src/sast/weak-randomness.js +100 -0
  66. package/src/sca/binary-metadata.js +124 -0
  67. package/src/sca/llm-function-extract.js +107 -0
  68. package/src/sca/py-package-functions.js +118 -0
  69. 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; // mounted after auth — fine
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
- if(re.test(lines[li])){
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
- const hints=VULN_FUNCTION_HINTS[sc.name];if(!hints)continue;
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
- for(const aff of(d.affected||[]))for(const rng of(aff.ranges||[]))for(const ev of(rng.events||[]))if(ev.fixed)fixedVersions.add(ev.fixed);
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 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++;setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});try{const c=fileContents[p];if(!c||c.length>500000)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));
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));}catch(_){}if(i%5===0)await new Promise(r=>setTimeout(r,0));}
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=>{c.reachable=reachabilitySet.has(c.name.toLowerCase())||(c.ecosystem==='pypi'&&reachabilitySet.has(c.name.replace(/-/g,'_').toLowerCase()));});
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
- 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,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors};}
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 path.join(scanRoot || process.cwd(), '.agentic-security', 'integrations.yml');
19
+ return statePath(scanRoot, 'integrations.yml');
19
20
  }
20
21
 
21
22
  export function loadIntegrationConfig(scanRoot) {
@@ -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
- // perFileIR is { [file]: parseJsFile output }
13
- const functions = new Map(); // qid → FunctionIR
14
- const byNameInFile = new Map(); // file → Map<name, qid>
15
- const classMethods = new Map(); // 'ClassName.method' qid
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 };