@clear-capabilities/agentic-security-scanner 0.137.0 → 0.139.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/CHANGELOG.md +219 -0
- package/dist/113.index.js +2 -2
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +29 -1
- package/dist/526.index.js +2 -2
- package/dist/637.index.js +1 -1
- package/dist/agentic-security.mjs +14 -14
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +10 -6
- package/src/dataflow/CLAUDE.md +30 -0
- package/src/dataflow/catalog.js +512 -14
- package/src/dataflow/engine.js +275 -27
- package/src/dataflow/summaries.js +30 -5
- package/src/engine.js +512 -120
- package/src/ir/CLAUDE.md +20 -5
- package/src/ir/balanced-call.js +11 -1
- package/src/ir/callgraph.js +34 -0
- package/src/ir/parser-cs.js +55 -6
- package/src/ir/parser-go.js +106 -2
- package/src/ir/parser-java.js +111 -10
- package/src/ir/parser-js.js +40 -0
- package/src/ir/parser-kt.js +194 -10
- package/src/ir/parser-php.js +108 -6
- package/src/ir/parser-py.helper.py +199 -10
- package/src/ir/parser-rb.js +405 -31
- package/src/mcp/tools.js +29 -1
- package/src/posture/accuracy-scorecard.js +103 -0
- package/src/runScan.js +5 -2
- package/src/sast/CLAUDE.md +1 -1
- package/src/sast/_auth-signals.js +141 -0
- package/src/sast/_comment-strip.js +80 -13
- package/src/sast/codegen-sink.js +110 -0
- package/src/sast/convention-deviation.js +235 -0
- package/src/sast/fastapi-hardening.js +45 -6
- package/src/sast/file-upload.js +29 -1
- package/src/sast/ownership-authz.js +245 -0
- package/src/sast/php.js +12 -2
- package/src/sast/rate-limit.js +2 -0
- package/src/sast/rbac-consistency.js +1 -1
- package/src/sast/redirect-toctou.js +167 -0
- package/src/sast/resource-exhaustion.js +217 -0
- package/src/sast/sibling-guard.js +176 -0
- package/src/sast/zip-slip.js +53 -2
package/src/engine.js
CHANGED
|
@@ -51,7 +51,9 @@ import { isJavaBarProvablySafe } from './sast/java-constant-fold.js';
|
|
|
51
51
|
import { findTaintedCollections, extractionFromTaintedCollection } from './sast/java-collection-passthrough.js';
|
|
52
52
|
import { deadBranchRanges as _deadBranchRanges, isLineInDeadRange as _isLineInDeadRange } from './sast/java-ast-folding.js';
|
|
53
53
|
import { scanJavaDeserialization } from './sast/java-deserialization.js';
|
|
54
|
+
import { blankComments } from './sast/_comment-strip.js';
|
|
54
55
|
import { scanJwtExp } from './sast/jwt-exp.js';
|
|
56
|
+
import { scanSiblingGuard } from './sast/sibling-guard.js';
|
|
55
57
|
import { scanZipSlip } from './sast/zip-slip.js';
|
|
56
58
|
import { scanFileUpload } from './sast/file-upload.js';
|
|
57
59
|
import { scanHostHeader } from './sast/host-header.js';
|
|
@@ -65,6 +67,11 @@ import { scanRust, extractRustImportMap } from './sast/rust.js';
|
|
|
65
67
|
import { scanGoExtended } from './sast/go-extended.js';
|
|
66
68
|
import { scanDatabaseRLS } from './sast/db-rls.js';
|
|
67
69
|
import { scanRateLimit } from './sast/rate-limit.js';
|
|
70
|
+
import { scanConventionDeviationProject } from './sast/convention-deviation.js';
|
|
71
|
+
import { scanResourceExhaustion } from './sast/resource-exhaustion.js';
|
|
72
|
+
import { scanRedirectToctou } from './sast/redirect-toctou.js';
|
|
73
|
+
import { scanCodegenSink } from './sast/codegen-sink.js';
|
|
74
|
+
import { scanOwnershipAuthz } from './sast/ownership-authz.js';
|
|
68
75
|
import { scanAuthProvider } from './sast/auth-provider.js';
|
|
69
76
|
import { scanEnvHygiene } from './sast/env-hygiene.js';
|
|
70
77
|
import { scanWebhook } from './sast/webhook.js';
|
|
@@ -242,7 +249,59 @@ const localStorage = sessionStorage;
|
|
|
242
249
|
const DATA_CLASSES={PII:{label:"PII",color:"#c792ea",patterns:["fname","lname","first_name","last_name","surname","full_name","dob","date_of_birth","ssn","social_security","tax_id","national_id","address_line","postal_code","zip_code","home_address","phone","mobile","email","drivers_license","passport_no","voter_id","geolocation","fingerprint","voice_print","dna_sequence","student_id","salary"]},PHI:{label:"PHI",color:"#ff6b9d",patterns:["ehr_id","mrn","medical_record_number","patient_id","patient_name","diagnosis","icd_code","cpt_code","prescription","blood_type","pregnancy_status","disability","mental_health","medical_history","treatment_plan","lab_result","medication","immunization","insurance_plan","policy_number","hipaa","phi"]},PCI:{label:"PCI",color:"#ff6b35",patterns:["pan","credit_card","card_number","cc_num","cardholder_name","cvv","cvc","cvv2","security_code","card_verification","exp_date","card_expiry","track_1","track_2","magstripe","pin_block","routing_number","bank_account","iban","swift_code","account_number"]},Confidential:{label:"Confidential",color:"#ffb800",patterns:["top_secret","secret","confidential","classified","cui","controlled","attorney_client","privileged","trade_secret","proprietary","internal_use_only","do_not_distribute","export_controlled","itar","cjis","password","passwd","api_key","secret_key","access_token","auth_token","connection_string","private_key"]}};
|
|
243
250
|
function classifyField(n){const l=n.toLowerCase().replace(/[-\s]/g,"_");const m=[];for(const[c,info]of Object.entries(DATA_CLASSES))for(const p of info.patterns)if(l.includes(p)){m.push(c);break;}return m;}
|
|
244
251
|
function classifyEndpoint(fields){const c=new Set();for(const f of fields)for(const x of classifyField(f))c.add(x);return[...c];}
|
|
245
|
-
|
|
252
|
+
// Which comment syntax a file uses. This has to be language-aware rather than
|
|
253
|
+
// one regex for every file, because the same two characters mean OPPOSITE
|
|
254
|
+
// things across languages: `#` opens a comment in Python/Ruby/shell but is a
|
|
255
|
+
// preprocessor or region directive in C/C++/C# (`#include`, `#region`), and
|
|
256
|
+
// `//` is a comment in the C family but FLOOR DIVISION in Python. Guessing
|
|
257
|
+
// wrong does not merely leak a false positive — it deletes real code and
|
|
258
|
+
// silently destroys every finding on the rest of the line.
|
|
259
|
+
const _HASH_COMMENT_EXTS = new Set([
|
|
260
|
+
'.py', '.pyw', '.pyi', '.sh', '.bash', '.zsh',
|
|
261
|
+
'.yml', '.yaml', '.pl', '.pm', '.ex', '.exs', '.cr', '.nim', '.toml', '.cfg', '.ini',
|
|
262
|
+
]);
|
|
263
|
+
// Ruby: `#` lines plus `=begin`/`=end` blocks.
|
|
264
|
+
const _RUBY_COMMENT_EXTS = new Set(['.rb', '.rake', '.gemspec', '.ru']);
|
|
265
|
+
// PHP and HCL/Terraform accept `//`, `/* */` AND `#` simultaneously.
|
|
266
|
+
const _ALL_COMMENT_EXTS = new Set(['.php', '.php5', '.phtml', '.tf', '.tfvars', '.hcl']);
|
|
267
|
+
function _commentLangFor(fp) {
|
|
268
|
+
const base = (String(fp || '').toLowerCase().split(/[\\/]/).pop()) || '';
|
|
269
|
+
if (base === 'dockerfile' || base.startsWith('dockerfile.') || base === 'makefile') return 'py';
|
|
270
|
+
const dot = base.lastIndexOf('.');
|
|
271
|
+
const ext = dot > 0 ? base.slice(dot) : '';
|
|
272
|
+
if (_ALL_COMMENT_EXTS.has(ext)) return 'php';
|
|
273
|
+
if (_RUBY_COMMENT_EXTS.has(ext)) return 'rb';
|
|
274
|
+
if (_HASH_COMMENT_EXTS.has(ext)) return 'py';
|
|
275
|
+
return undefined; // C-family default: `//` and `/* */` only, `#` left alone.
|
|
276
|
+
}
|
|
277
|
+
// Single-entry memo for the comment-blanked view.
|
|
278
|
+
//
|
|
279
|
+
// `stripNoise` is called ~15 times for the SAME file within one pass
|
|
280
|
+
// (performRegexAnalysis, scanRoutes, scanStructuralVulns, scanLogicVulns,
|
|
281
|
+
// scanJavaSAST, scanCiphers, …), and the per-file dispatch computes `cc` on top
|
|
282
|
+
// of that. The old implementation was two native regex replaces, so recomputing
|
|
283
|
+
// was nearly free; `blankComments` is an interpreted character loop, so it is
|
|
284
|
+
// not. Measured on a 307-file entry: 13.4 s → 16.0 s, a 19.7% regression,
|
|
285
|
+
// before this memo.
|
|
286
|
+
//
|
|
287
|
+
// One slot is enough because the engine processes one file at a time, and
|
|
288
|
+
// `fileContents[p]` hands back the SAME string reference on every call — so the
|
|
289
|
+
// `===` guard is a pointer comparison, not a content comparison, and cannot
|
|
290
|
+
// return a stale result for different content.
|
|
291
|
+
let _blankMemoSrc = null, _blankMemoLang, _blankMemoOut = null;
|
|
292
|
+
function _blankCached(src, lang) {
|
|
293
|
+
if (src === _blankMemoSrc && lang === _blankMemoLang) return _blankMemoOut;
|
|
294
|
+
const out = blankComments(src, lang);
|
|
295
|
+
_blankMemoSrc = src; _blankMemoLang = lang; _blankMemoOut = out;
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// `fp` is optional for backwards compatibility with call sites that have no
|
|
300
|
+
// path in scope. WITH a path this delegates to the shared, string-literal-aware
|
|
301
|
+
// `blankComments`; without one it keeps the original C-family-only behaviour.
|
|
302
|
+
function stripNoise(code, fp){
|
|
303
|
+
if (fp !== undefined && fp !== null) return _blankCached(code, _commentLangFor(fp));
|
|
304
|
+
let c=code.replace(/\/\*[\s\S]*?\*\//g,m=>m.replace(/[^\n]/g,' '));c=c.replace(/\/\/[^\n]*/g,m=>' '.repeat(m.length));return c;}
|
|
246
305
|
// File-context inference. Used to gate rules that only apply in a given runtime
|
|
247
306
|
// context — e.g. "Synchronous Blocking I/O (DoS Risk in Server Context)" should
|
|
248
307
|
// not fire on CLI scripts / hooks / VS Code extensions.
|
|
@@ -605,7 +664,8 @@ function _isIaCFile(p){
|
|
|
605
664
|
if (IAC_FILENAMES.has(base)) return true;
|
|
606
665
|
if (/\.dockerfile$/i.test(base)) return true;
|
|
607
666
|
if (/\.tf$|\.tfvars$/i.test(base)) return true;
|
|
608
|
-
// K8s YAML heuristic: under k8s/
|
|
667
|
+
// K8s YAML heuristic: under k8s/ — the CONTENT half of this rule lives in
|
|
668
|
+
// `isKubernetesManifest` below, because a path predicate cannot see content.
|
|
609
669
|
if (/(?:^|\/)k8s(?:\/|$)/.test(p) && /\.ya?ml$/i.test(base)) return true;
|
|
610
670
|
if (/(?:^|\/)\.github\/workflows\/.*\.ya?ml$/.test(p)) return true;
|
|
611
671
|
if (/(?:^|\/)\.gitlab-ci\.ya?ml$/.test(p)) return true;
|
|
@@ -620,6 +680,34 @@ function _isIaCFile(p){
|
|
|
620
680
|
if (/(?:^|\/)(?:prompts?|templates?\/prompts?)\//i.test(p)) return true;
|
|
621
681
|
return false;
|
|
622
682
|
}
|
|
683
|
+
// PRD F11.4 — the content half of the Kubernetes-manifest rule.
|
|
684
|
+
//
|
|
685
|
+
// `_isIaCFile` admitted YAML as Kubernetes only when the path contained a
|
|
686
|
+
// directory literally named `k8s/`, while its own comment claimed the caller
|
|
687
|
+
// also checked for `kind:`. Nothing ever did. Measured consequence: the
|
|
688
|
+
// k8s-admission detector returned 3 findings when called directly on its own
|
|
689
|
+
// fixture and 0 through a scan, because `shouldScan()` never let the walker
|
|
690
|
+
// keep the file — real manifests under `deploy/`, `manifests/`, `charts/`,
|
|
691
|
+
// `kubernetes/`, `overlays/` or the repository root were invisible, and that is
|
|
692
|
+
// where most projects actually put them.
|
|
693
|
+
//
|
|
694
|
+
// Keyed on CONTENT rather than on more directory names, because the set of
|
|
695
|
+
// directory names people use is unbounded and the file format is not. A
|
|
696
|
+
// Kubernetes object is defined by carrying BOTH `apiVersion:` and `kind:` at
|
|
697
|
+
// the start of a line — requiring both, at line-start, is what keeps this from
|
|
698
|
+
// matching ordinary YAML that merely mentions the word "kind" in a value.
|
|
699
|
+
//
|
|
700
|
+
// Only the head of the file is examined: a manifest declares these in its first
|
|
701
|
+
// few lines, and reading further would put an unbounded regex on the hot path
|
|
702
|
+
// of every YAML file in a repository.
|
|
703
|
+
const _K8S_HEAD_BYTES = 2048;
|
|
704
|
+
const _K8S_API_VERSION_RE = /^\s*apiVersion\s*:/m;
|
|
705
|
+
const _K8S_KIND_RE = /^\s*kind\s*:\s*\S/m;
|
|
706
|
+
export function isKubernetesManifest(relPath, content) {
|
|
707
|
+
if (typeof content !== 'string' || !/\.ya?ml$/i.test(relPath || '')) return false;
|
|
708
|
+
const head = content.length > _K8S_HEAD_BYTES ? content.slice(0, _K8S_HEAD_BYTES) : content;
|
|
709
|
+
return _K8S_API_VERSION_RE.test(head) && _K8S_KIND_RE.test(head);
|
|
710
|
+
}
|
|
623
711
|
function getExt(n){const p=n.split(".");return p.length>1?p.pop().toLowerCase():"";}
|
|
624
712
|
function shouldScan(p){if(/\.(test|spec|mock)\./i.test(p))return false;if(/_test\.go$/i.test(p))return false;if(/_spec\.rb$/i.test(p))return false;if(/Test\.(?:java|cs|kt|scala)$/i.test(p))return false;if(/\.min\.[mc]?js$/i.test(p))return false;for(const x of p.split("/"))if(IGNORE_DIRS.has(x))return false;
|
|
625
713
|
// Mobile + framework manifest files needed by the v4 detectors.
|
|
@@ -1338,7 +1426,14 @@ function _hasSsrfHostGuard(ctx) { return _guardMatchNearSinkIdentifier(ctx, _SSR
|
|
|
1338
1426
|
// A path-traversal containment guard near the file sink: a basename/strip
|
|
1339
1427
|
// helper that removes directory components, a framework safe-join, or a
|
|
1340
1428
|
// canonicalize-then-startsWith containment check.
|
|
1341
|
-
|
|
1429
|
+
// The trailing alternative is a SHAPE, not a name: a project-local throwing
|
|
1430
|
+
// validator invoked on the value that reaches the sink. A closed list of
|
|
1431
|
+
// blessed helpers can never contain a codebase's own validator, so
|
|
1432
|
+
// GHSA-q939-rpr3-3284's fix (`EnsureValidLocalName(filename);` immediately
|
|
1433
|
+
// before `Path.Combine(..., filename)`) left the finding firing on the FIXED
|
|
1434
|
+
// revision. Safe to match on shape because _guardMatchNearSinkIdentifier
|
|
1435
|
+
// already requires the guard to name the same identifier as the sink.
|
|
1436
|
+
const _PATH_GUARD_RE = /\b(?:basename|GetFileName|secure_filename|sanitize_filename|send_from_directory|safe_join)\s*\(|\b(?:startsWith|startswith|StartsWith|HasPrefix)\s*\(|\bgetCanonicalPath\b|\btoRealPath\b|\bfilepath\s*\.\s*(?:Clean|Base|Abs)\b|\b(?:[Ee]nsure|[Vv]alidate|[Aa]ssert|[Rr]equire|[Vv]erify|[Cc]heck)\w{1,40}\s*\(/;
|
|
1342
1437
|
function _hasPathGuard(ctx) { return _guardMatchNearSinkIdentifier(ctx, _PATH_GUARD_RE); }
|
|
1343
1438
|
|
|
1344
1439
|
// Reflected-XSS output-encoding guard: an HTML escaper applied near the sink.
|
|
@@ -1394,6 +1489,20 @@ export function dropGuardedFindings(findings, fileContents) {
|
|
|
1394
1489
|
// sanitizer-dataflow pipeline, which records them in scan.suppressions.
|
|
1395
1490
|
// Don't hard-drop them here or that bookkeeping is lost (smoke FP-3).
|
|
1396
1491
|
if (f.isSanitized) return true;
|
|
1492
|
+
// A finding whose CLAIM IS the guard's presence must survive guard
|
|
1493
|
+
// recognition, or the evidence is read as the refutation.
|
|
1494
|
+
//
|
|
1495
|
+
// `sibling-guard-omission` reports "this file applies <guard> to X and not
|
|
1496
|
+
// to its sibling Y". The window around Y therefore ALWAYS contains a
|
|
1497
|
+
// containment guard — that is the whole point — so the generic check below
|
|
1498
|
+
// dropped 100% of these findings, structurally, and no amount of detector
|
|
1499
|
+
// work could ever have surfaced one. Found while writing the rule: it fired
|
|
1500
|
+
// correctly in isolation and produced nothing through a scan, the same
|
|
1501
|
+
// signature as the rate-limit.js defect.
|
|
1502
|
+
//
|
|
1503
|
+
// Scoped to this family rather than loosening the window heuristic, which
|
|
1504
|
+
// is doing its job for every other CWE-22 emitter.
|
|
1505
|
+
if (f.family === 'sibling-guard-omission') return true;
|
|
1397
1506
|
const cwe = f.cwe || '';
|
|
1398
1507
|
const isXss = cwe === 'CWE-79' && /reflected xss/i.test(f.vuln || '');
|
|
1399
1508
|
if (cwe !== 'CWE-918' && cwe !== 'CWE-22' && !isXss) return true;
|
|
@@ -1567,7 +1676,7 @@ function _detectAllowlistGuard(lines, varName, srcLine, sinkLine) {
|
|
|
1567
1676
|
return null;
|
|
1568
1677
|
}
|
|
1569
1678
|
|
|
1570
|
-
function performRegexAnalysis(fp,raw){if(_INTENTIONAL_VULN_PATH_RE.test(fp.replace(/\\/g,'/')))return{findings:[],sources:[],sinks:[],sanitizers:[]};const cleaned=stripNoise(raw);const cleanedNoStrings=stripNoiseAndStrings(raw);const lines=raw.split("\n");const findings=[],sources=[],sinks=[],sanitizers=[];
|
|
1679
|
+
function performRegexAnalysis(fp,raw){if(_INTENTIONAL_VULN_PATH_RE.test(fp.replace(/\\/g,'/')))return{findings:[],sources:[],sinks:[],sanitizers:[]};const cleaned=stripNoise(raw,fp);const cleanedNoStrings=stripNoiseAndStrings(raw);const lines=raw.split("\n");const findings=[],sources=[],sinks=[],sanitizers=[];
|
|
1571
1680
|
for(const sp of SOURCE_PATTERNS){const re=new RegExp(sp.regex.source,sp.regex.flags);let m;while((m=re.exec(cleaned))){const line=lineAt(cleaned,m.index);const lt=lines[line-1]||"";let _srcVar=null;if(typeof sp.getVar==='function'){try{_srcVar=sp.getVar(m,lt)||null;}catch(_){_srcVar=null;}}if(!_srcVar){const am=lt.match(/(?:const|let|var|)\s*(\w+)\s*=/)||lt.match(/(\w+)\s*=/);_srcVar=am?am[1]:null;}const _itype=sp.inputType(m);if(_srcVar&&(_itype==='cookies'||_itype==='headers')){const _mc=lt.indexOf(m[0]);if(_mc>=0){const _before=lt.substring(0,_mc);if((_before.match(/\(/g)||[]).length>(_before.match(/\)/g)||[]).length)_srcVar=null;}}sources.push({label:sp.getLabel(m),category:sp.category,inputType:_itype,variable:_srcVar,line,file:fp,snippet:lt.trim()});}}
|
|
1572
1681
|
// Feat-1: in-file Python helper-taint propagation. Pushes synthetic sources
|
|
1573
1682
|
// for function parameters that are tainted via call-site argument flow.
|
|
@@ -1647,7 +1756,7 @@ function performAnalysis(fp, raw) {
|
|
|
1647
1756
|
return performRegexAnalysis(fp, raw);
|
|
1648
1757
|
}
|
|
1649
1758
|
|
|
1650
|
-
function scanRoutes(fp,raw){const cleaned=stripNoise(raw);const lines=raw.split("\n");const routes=[];const mwAuth=detectMiddlewareAuth(raw);for(const p of ROUTE_PATTERNS){const re=new RegExp(p.regex.source,p.regex.flags);let m;while((m=re.exec(cleaned))){let method="GET";if(p.mI&&m[p.mI]){const r=m[p.mI].toUpperCase();method=["GET","POST","PUT","PATCH","DELETE","ALL","OPTIONS","HEAD"].includes(r)?r:"GET";}if(p.mtI&&m[p.mtI]){const fm=m[p.mtI].replace(/['"]/g,"").split(",").map(s=>s.trim().toUpperCase());if(fm.length)method=fm[0];}const path=p.pI&&m[p.pI]?m[p.pI]:"(file-based)";const line=lineAt(cleaned,m.index);let hasAuth=false;const nearby=lines.slice(Math.max(0,line-3),line+3).join(" ");if(/(?:authenticate|auth|jwt\.verify|verifyToken|authMiddleware|checkAuth|protect|authorize|isAuthorized|expressJwt|denyAll)\s*[\(,]/i.test(nearby))hasAuth=true;if(!hasAuth)for(const mw of mwAuth)if(line>mw.line&&(mw.scope==="/"||path.startsWith(mw.scope)))hasAuth=true;const rp=[];for(const sp of SOURCE_PATTERNS){const re2=new RegExp(sp.regex.source,sp.regex.flags);const block=lines.slice(Math.max(0,line-2),Math.min(lines.length,line+30)).join("\n");let m2;while((m2=re2.exec(block))){const v=m2[2]||m2[3]||m2[1];if(v)rp.push(v);}}const cls=classifyEndpoint(rp);const classifiedFields={};rp.forEach(field=>{const fc=classifyField(field);if(fc.length)classifiedFields[field]=fc;});const uploadBlock2=lines.slice(Math.max(0,line-5),Math.min(lines.length,line+40)).join("\n");const hasFileUpload=/(?:multer|busboy|formidable|upload\.(?:single|array|fields|any|none)|req\.files?\b|request\.files?\b|request\.FILES|\$_FILES|\$request->(?:file|hasFile)|multipart\/form-data|r\.FormFile|r\.MultipartForm)/i.test(uploadBlock2);routes.push({method,path,framework:p.fw,file:fp,line,hasAuth,hasFileUpload,params:rp,classifications:cls,classifiedFields});}}return routes;}
|
|
1759
|
+
function scanRoutes(fp,raw){const cleaned=stripNoise(raw,fp);const lines=raw.split("\n");const routes=[];const mwAuth=detectMiddlewareAuth(raw);for(const p of ROUTE_PATTERNS){const re=new RegExp(p.regex.source,p.regex.flags);let m;while((m=re.exec(cleaned))){let method="GET";if(p.mI&&m[p.mI]){const r=m[p.mI].toUpperCase();method=["GET","POST","PUT","PATCH","DELETE","ALL","OPTIONS","HEAD"].includes(r)?r:"GET";}if(p.mtI&&m[p.mtI]){const fm=m[p.mtI].replace(/['"]/g,"").split(",").map(s=>s.trim().toUpperCase());if(fm.length)method=fm[0];}const path=p.pI&&m[p.pI]?m[p.pI]:"(file-based)";const line=lineAt(cleaned,m.index);let hasAuth=false;const nearby=lines.slice(Math.max(0,line-3),line+3).join(" ");if(/(?:authenticate|auth|jwt\.verify|verifyToken|authMiddleware|checkAuth|protect|authorize|isAuthorized|expressJwt|denyAll|checkPermission|checkAnyPermission|hasAnyPermission|hasPermission|requirePermission)\s*[\(,]/i.test(nearby))hasAuth=true;if(!hasAuth)for(const mw of mwAuth)if(line>mw.line&&(mw.scope==="/"||path.startsWith(mw.scope)))hasAuth=true;const rp=[];for(const sp of SOURCE_PATTERNS){const re2=new RegExp(sp.regex.source,sp.regex.flags);const block=lines.slice(Math.max(0,line-2),Math.min(lines.length,line+30)).join("\n");let m2;while((m2=re2.exec(block))){const v=m2[2]||m2[3]||m2[1];if(v)rp.push(v);}}const cls=classifyEndpoint(rp);const classifiedFields={};rp.forEach(field=>{const fc=classifyField(field);if(fc.length)classifiedFields[field]=fc;});const uploadBlock2=lines.slice(Math.max(0,line-5),Math.min(lines.length,line+40)).join("\n");const hasFileUpload=/(?:multer|busboy|formidable|upload\.(?:single|array|fields|any|none)|req\.files?\b|request\.files?\b|request\.FILES|\$_FILES|\$request->(?:file|hasFile)|multipart\/form-data|r\.FormFile|r\.MultipartForm)/i.test(uploadBlock2);routes.push({method,path,framework:p.fw,file:fp,line,hasAuth,hasFileUpload,params:rp,classifications:cls,classifiedFields});}}return routes;}
|
|
1651
1760
|
|
|
1652
1761
|
const LOGIC_PATTERNS=[
|
|
1653
1762
|
{regex:/Math\.random\s*\(\s*\)/g,vuln:"Weak Randomness",severity:"medium",cwe:"CWE-330",stride:"Spoofing",fix:"Use crypto.randomBytes or crypto.randomUUID for security-sensitive values.",code:"// BEFORE\nconst token = Math.random().toString(36);\n\n// AFTER\nconst token = crypto.randomBytes(32).toString('hex');"},
|
|
@@ -1678,7 +1787,7 @@ const LOGIC_PATTERNS=[
|
|
|
1678
1787
|
// ── Missing Bounds on Financial/Quantity Fields ──────────────────────────────
|
|
1679
1788
|
{regex:/(?:req|request|ctx)\s*(?:\.\s*body|\[\s*['"]body['"]\s*\])\s*[.[\s]*(?:quantity|amount|price|units|count|qty)\b(?![^;]{0,200}(?:Number\.isInteger|isNaN|Math\.abs|>=\s*1|>\s*0|>0|>=1|max\s*:))/g,vuln:"Missing Positive-Integer Validation on Financial Field",severity:"medium",cwe:"CWE-20",stride:"Tampering",fix:"Validate that financial/quantity fields are positive integers before processing. Negative values can create credit or reverse transactions.",code:"// BEFORE\nawait Order.create({ quantity: req.body.quantity, price: product.price });\n\n// AFTER\nconst qty = req.body.quantity;\nif (!Number.isInteger(qty) || qty < 1 || qty > 10000)\n return res.status(400).json({ error: 'quantity must be 1-10000' });\nawait Order.create({ quantity: qty, price: product.price });"},
|
|
1680
1789
|
// ── #22: Missing timeout on outbound HTTP requests (DoS) ─────────────────────
|
|
1681
|
-
{regex:/(?:await\s+)
|
|
1790
|
+
{regex:/(?:await\s+)?\b(?:fetch|axios\.(?:get|post|put|patch|delete|request)|http\.(?:get|request)|https\.(?:get|request)|got)\s*\(/gi,vuln:"Missing Timeout on Outbound HTTP Request (DoS)",severity:"medium",cwe:"CWE-400",stride:"Denial of Service",appliesTo:["server"],fix:"Set a timeout on all outbound requests to prevent event-loop starvation from stalled upstreams.",code:"// fetch (Node 18+)\nconst resp = await fetch(url, { signal: AbortSignal.timeout(5000) });\n\n// axios\nawait axios.get(url, { timeout: 5000 });\n\n// node http\nconst req = http.get(url, cb);\nreq.setTimeout(5000, () => req.destroy());"},
|
|
1682
1791
|
// ── #24: ORM collection queries without pagination limit (DoS) ───────────────
|
|
1683
1792
|
{regex:/\.\s*(?:findAll|findMany|findAndCountAll)\s*\(\s*\{[^}]{0,500}\}/g,vuln:"ORM Collection Query Without Pagination Limit (DoS)",severity:"medium",cwe:"CWE-400",stride:"Denial of Service",appliesTo:["server"],fix:"Always set limit/take on collection queries to bound memory and DB load.",code:"const items = await Model.findAll({\n where: { userId: req.user.id },\n limit: Math.min(Number(req.query.limit) || 50, 100),\n offset: Number(req.query.offset) || 0,\n});"},
|
|
1684
1793
|
// ── #27: Missing audit log on sensitive mutations (Repudiation) ──────────────
|
|
@@ -2149,7 +2258,7 @@ function scanStructuralVulns(fp, raw) {
|
|
|
2149
2258
|
// Default cleaned view strips strings; if any pattern needs the literal-aware
|
|
2150
2259
|
// view, we fall back to stripNoise for that one pattern.
|
|
2151
2260
|
const cleaned = stripNoiseAndStrings(raw);
|
|
2152
|
-
const cleanedNoise = stripNoise(raw);
|
|
2261
|
+
const cleanedNoise = stripNoise(raw,fp);
|
|
2153
2262
|
const lines = raw.split('\n');
|
|
2154
2263
|
const findings = [];
|
|
2155
2264
|
const ctx = inferFileContext(fp, raw);
|
|
@@ -2332,6 +2441,28 @@ function _isFalsePositiveCredential(fp, snippet, fullMatch){
|
|
|
2332
2441
|
const pathLC = fp.replace(/\\\\/g,'/').toLowerCase();
|
|
2333
2442
|
if (_CRED_PATH_RE.test(pathLC) || _CRED_FILE_RE.test(pathLC)) return {skip:true, reason:'path-filter'};
|
|
2334
2443
|
// Extract LHS identifier from the snippet (handles `const x =`, `x =`, `{ x:`, etc.)
|
|
2444
|
+
// Well-known protocol / spec vocabulary that is public by definition: OAuth
|
|
2445
|
+
// and OIDC grant, token and response types, URN token-type identifiers, HTTP
|
|
2446
|
+
// auth schemes, JWT alg names, and common enum-ish method names. Anchored to
|
|
2447
|
+
// the WHOLE value so a real secret that merely contains one of these words is
|
|
2448
|
+
// unaffected.
|
|
2449
|
+
const _PROTOCOL_CONSTANT_VAL_RE = new RegExp('^(?:'
|
|
2450
|
+
+ 'urn:[\\w.:-]+' // urn:ietf:params:oauth:...
|
|
2451
|
+
+ '|(?:refresh|access|id|device|bearer|mac|dpop)_token'
|
|
2452
|
+
+ '|authorization_code|client_credentials|implicit|password_grant'
|
|
2453
|
+
+ '|urn:ietf:params:oauth:grant-type:[\\w-]+'
|
|
2454
|
+
+ '|(?:client_secret|private_key)_(?:basic|post|jwt)'
|
|
2455
|
+
+ '|(?:Bearer|Basic|Digest|Negotiate|Hawk)'
|
|
2456
|
+
+ '|(?:HS|RS|ES|PS)(?:256|384|512)|none|EdDSA'
|
|
2457
|
+
+ '|(?:openid|profile|email|offline_access|address|phone)'
|
|
2458
|
+
+ '|code|token id_token|id_token token|query|fragment|form_post'
|
|
2459
|
+
// Enum-ish auth vocabulary, NOT all snake_case: the value must end in a
|
|
2460
|
+
// protocol-vocabulary word. A first draft allowed any snake_case string,
|
|
2461
|
+
// which would have suppressed a genuine lowercase secret.
|
|
2462
|
+
+ '|[a-z][a-z0-9_]{0,40}_(?:auth|token|grant|type|method|scope|mode|flow)'
|
|
2463
|
+
+ '|(?:auth|token|grant|scope)_[a-z][a-z0-9_]{0,40}'
|
|
2464
|
+
+ ')$', 'i');
|
|
2465
|
+
|
|
2335
2466
|
const am = snippet.match(/(?:^|\W)(\w{2,})\s*[:=]\s*['"]/);
|
|
2336
2467
|
const varName = am ? am[1] : '';
|
|
2337
2468
|
if (varName && _CRED_VARNAME_RE.test(varName)) return {skip:true, reason:'var-name-placeholder'};
|
|
@@ -2340,6 +2471,19 @@ function _isFalsePositiveCredential(fp, snippet, fullMatch){
|
|
|
2340
2471
|
const val = valM ? valM[1] : '';
|
|
2341
2472
|
if (val.length < 8) return {skip:true, reason:'value-too-short'};
|
|
2342
2473
|
if (_CRED_PLACEHOLDER_VAL_RE.test(val)) return {skip:true, reason:'placeholder-value'};
|
|
2474
|
+
// T2.1 audit — PROTOCOL CONSTANTS are not secrets.
|
|
2475
|
+
//
|
|
2476
|
+
// "Hardcoded Secret" was the 2nd-highest-volume rule over the independent
|
|
2477
|
+
// population (70 findings in a 24-entry sample), and the sampled instances
|
|
2478
|
+
// were all OAuth/OIDC vocabulary declared as named constants:
|
|
2479
|
+
// GrantTypeRefreshToken = "refresh_token"
|
|
2480
|
+
// TokenTypeURNAccessToken = "urn:ietf:params:oauth:token-type:access_token"
|
|
2481
|
+
// AuthRecipeMethodMobileBasicAuth = "mobile_basic_auth"
|
|
2482
|
+
// These are PUBLIC identifiers from a published spec — every conforming
|
|
2483
|
+
// implementation ships the identical string. The rule fired only because the
|
|
2484
|
+
// constant's NAME contains token/auth. Flagging them teaches readers to
|
|
2485
|
+
// ignore the whole class, which is what a noisy critical-severity rule costs.
|
|
2486
|
+
if (_PROTOCOL_CONSTANT_VAL_RE.test(val)) return {skip:true, reason:'protocol-constant'};
|
|
2343
2487
|
// Shannon-entropy + dictionary-word filter (Recommendation #1 from the
|
|
2344
2488
|
// SCA/SAST improvement plan). The Juliet Java hardcoded-secret detector
|
|
2345
2489
|
// was producing 468 FPs / 1 TP because test fixtures use short
|
|
@@ -2671,7 +2815,7 @@ function _buildProjectIndex(fileContents){
|
|
|
2671
2815
|
if (typeof c !== 'string') continue;
|
|
2672
2816
|
// Strip comments before checking — keywords in JSDoc / TODO / docstrings shouldn't
|
|
2673
2817
|
// count as evidence of an e-commerce model. Only real code does.
|
|
2674
|
-
const cleaned = stripNoise(c);
|
|
2818
|
+
const cleaned = stripNoise(c,fp);
|
|
2675
2819
|
if (_ECOMMERCE_MODEL_RE.test(cleaned)) hasEcommerce = true;
|
|
2676
2820
|
const fileConsts = {};
|
|
2677
2821
|
let m;
|
|
@@ -2802,7 +2946,7 @@ function scanLogicVulns(fp,raw){
|
|
|
2802
2946
|
// string-literal route paths and key names, so the comment-stripped (but
|
|
2803
2947
|
// string-preserving) view is the right default. Rules that explicitly only
|
|
2804
2948
|
// describe a code shape can opt in via `stripsStrings: true`.
|
|
2805
|
-
const cleaned=stripNoise(raw);
|
|
2949
|
+
const cleaned=stripNoise(raw,fp);
|
|
2806
2950
|
const cleanedFull=stripNoiseAndStrings(raw);
|
|
2807
2951
|
const lines=raw.split("\n");const results=[];
|
|
2808
2952
|
const ctx = inferFileContext(fp, raw);
|
|
@@ -2868,8 +3012,24 @@ const STORED_TAINT_SINK_PATTERNS=[
|
|
|
2868
3012
|
|
|
2869
3013
|
function buildStoredTaintRegistry(fc){
|
|
2870
3014
|
const registry={};
|
|
3015
|
+
// PRD T3.2 — this used to REQUIRE the field name to be one of a closed list
|
|
3016
|
+
// (bio|description|about|…). That is the same closed-enumeration bug found
|
|
3017
|
+
// four separate times in this engine during the same audit (blessed auth
|
|
3018
|
+
// dependencies, `id`/`userId`-only object ids, snake_case-only guards): real
|
|
3019
|
+
// schemas name their fields whatever they like, and a field called `payload`,
|
|
3020
|
+
// `src` or `mobiledoc` carries stored user input exactly as well as `bio`.
|
|
3021
|
+
// The list is kept only as a CONFIDENCE signal now; admission is decided by
|
|
3022
|
+
// whether the value side actually references a request source.
|
|
2871
3023
|
const STRING_FIELDS=/(?:bio|description|about|message|content|body|note|comment|title|summary|text|review|feedback|username|displayName)/i;
|
|
2872
|
-
|
|
3024
|
+
// Fields that are never user prose — registering them produces noise, not flows.
|
|
3025
|
+
const NON_CONTENT_FIELDS=/^(?:id|_id|uuid|createdAt|updatedAt|deletedAt|version|type|status|role|password|hash|salt|token)$/i;
|
|
3026
|
+
for(const[fp,rawCode] of Object.entries(fc)){
|
|
3027
|
+
// Comments are not code. sast/CLAUDE.md's first gotcha is exactly this, and
|
|
3028
|
+
// skipping it here registered phantom fields from ILLUSTRATIVE EXAMPLES in
|
|
3029
|
+
// detector docblocks (`// Sequelize Model.create({ field: req.body.x })`),
|
|
3030
|
+
// which then matched in unrelated files and produced 8 cross-file findings
|
|
3031
|
+
// between this project's own source files.
|
|
3032
|
+
const code=stripNoise(rawCode,fp); // engine-local, line-preserving
|
|
2873
3033
|
const lines=code.split('\n');
|
|
2874
3034
|
// Find ORM writes that include string-type fields likely to hold user input
|
|
2875
3035
|
const ormWrite=/(?:create|update|save|upsert|findOrCreate)\s*\(\s*\{([^}]{0,800})\}/g;
|
|
@@ -2879,14 +3039,15 @@ function buildStoredTaintRegistry(fc){
|
|
|
2879
3039
|
const fieldMatches=inner.match(/(\w+)\s*:/g)||[];
|
|
2880
3040
|
for(const f of fieldMatches){
|
|
2881
3041
|
const fn=f.replace(/\s*:/,'');
|
|
2882
|
-
if(
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
3042
|
+
if(NON_CONTENT_FIELDS.test(fn))continue;
|
|
3043
|
+
// Admission is by PROVENANCE, not by name: the value side must
|
|
3044
|
+
// reference a request source. A recognised prose-ish name only raises
|
|
3045
|
+
// confidence.
|
|
3046
|
+
const fieldRe=new RegExp(fn+'\\s*:\\s*(?:req\\.|request\\.|ctx\\.|\\$_(?:POST|GET)|body\\[|params\\[|query\\[)','i');
|
|
3047
|
+
if(fieldRe.test(inner)||/req\.|request\.|ctx\./i.test(inner)){
|
|
3048
|
+
const line=code.substring(0,m.index).split('\n').length;
|
|
3049
|
+
if(!registry[fn])registry[fn]=[];
|
|
3050
|
+
registry[fn].push({file:fp,line,snippet:lines[line-1]?.trim()||'',named:STRING_FIELDS.test(fn)});
|
|
2890
3051
|
}
|
|
2891
3052
|
}
|
|
2892
3053
|
}
|
|
@@ -2905,37 +3066,56 @@ function crossStoredTaint(fc, storedRegistry){
|
|
|
2905
3066
|
if(writes.some(w=>w.file===fp))continue;
|
|
2906
3067
|
const lines=code.split('\n');
|
|
2907
3068
|
// Check if this file renders the stored field in a dangerous context
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
)
|
|
3069
|
+
// PRD T3.2 — sink families beyond XSS.
|
|
3070
|
+
//
|
|
3071
|
+
// This used to recognise ONLY render sinks (innerHTML / res.send /
|
|
3072
|
+
// template literal), so a stored value flowing to a network, filesystem
|
|
3073
|
+
// or shell sink was invisible. The evidence entry that forced this is
|
|
3074
|
+
// Ghost's mobiledoc re-render (GHSA-g366-23fw-ggp6): stored post content
|
|
3075
|
+
// is walked on a later render pass and its image `src` handed to an
|
|
3076
|
+
// unguarded fetch — a stored SSRF, not stored XSS. Each family carries
|
|
3077
|
+
// its own CWE so the finding is not mislabelled as XSS.
|
|
3078
|
+
const F = field;
|
|
3079
|
+
const STORED_SINK_FAMILIES = [
|
|
3080
|
+
{ re: `(?:\`[^\`]*\\$\\{[^}]*\\b${F}\\b[^}]*\\}\`|(?:innerHTML|outerHTML)\\s*=[^;]{0,200}\\b${F}\\b|res\\s*\\.\\s*(?:send|write|json)\\s*\\([^;]{0,200}\\b${F}\\b)`,
|
|
3081
|
+
vuln: 'Stored XSS / Second-Order Injection', cwe: 'CWE-79', sanit: /(?:escape|DOMPurify|sanitize|encodeURIComponent|escapeHtml|textContent)\s*\(/ },
|
|
3082
|
+
{ re: `(?:fetch|axios(?:\\.\\w{1,10})?|got|request|urlopen|requests\\.\\w{1,10}|httpx\\.\\w{1,10}|getImageSize\\w{0,20})\\s*\\([^;]{0,200}\\b${F}\\b`,
|
|
3083
|
+
vuln: 'Stored SSRF (second-order)', cwe: 'CWE-918', sanit: /(?:isAllowed|allowlist|allowList|denyList|isPrivate|ssrf|validateUrl|is_?internal)\s*\(/i },
|
|
3084
|
+
{ re: `(?:readFile|readFileSync|createReadStream|open|sendFile|path\\.join)\\s*\\([^;]{0,200}\\b${F}\\b`,
|
|
3085
|
+
vuln: 'Stored path traversal (second-order)', cwe: 'CWE-22', sanit: /(?:basename|resolve|realpath|secure_filename|normalize)\s*\(/i },
|
|
3086
|
+
{ re: `(?:exec|execSync|spawn|system|popen)\\s*\\([^;]{0,200}\\b${F}\\b`,
|
|
3087
|
+
vuln: 'Stored command injection (second-order)', cwe: 'CWE-78', sanit: /(?:shell-?escape|shlex\.quote|execFile)\s*\(/i },
|
|
3088
|
+
];
|
|
3089
|
+
for (const fam of STORED_SINK_FAMILIES) {
|
|
3090
|
+
const sinkRe=new RegExp(fam.re,'g');
|
|
2912
3091
|
let m2;
|
|
2913
3092
|
while((m2=sinkRe.exec(code))!==null){
|
|
2914
3093
|
const sinkLine=code.substring(0,m2.index).split('\n').length;
|
|
2915
3094
|
const snippet=lines[sinkLine-1]?.trim()||'';
|
|
2916
3095
|
// Check for sanitizer in the vicinity
|
|
2917
3096
|
const nearby=lines.slice(Math.max(0,sinkLine-5),sinkLine+2).join(' ');
|
|
2918
|
-
const isSanitized
|
|
3097
|
+
const isSanitized=fam.sanit.test(nearby);
|
|
2919
3098
|
if(!isSanitized){
|
|
2920
3099
|
for(const write of writes){
|
|
2921
3100
|
const id=`stored:${write.file}:${write.line}:${fp}:${sinkLine}:${field}`;
|
|
2922
3101
|
findings.push({
|
|
2923
3102
|
id,
|
|
2924
3103
|
source:{label:`Stored field: ${field} (written at ${write.file.split('/').pop()}:${write.line})`,category:'Stored Taint',inputType:'stored',variable:field,line:write.line,file:write.file,snippet:write.snippet},
|
|
2925
|
-
sink:{type:'Stored Sink',severity:'high',vuln:
|
|
3104
|
+
sink:{type:'Stored Sink',severity:'high',vuln:fam.vuln,cwe:fam.cwe,stride:'Tampering',line:sinkLine,file:fp,snippet,args:snippet},
|
|
2926
3105
|
path:[
|
|
2927
3106
|
{type:'source',label:`ORM write: ${field} = user_input`,line:write.line,snippet:write.snippet},
|
|
2928
3107
|
{type:'propagation',label:`Stored in database (${field})`,line:write.line,snippet:''},
|
|
2929
3108
|
{type:'sink',label:`Rendered unsanitized in ${fp.split('/').pop()}:${sinkLine}`,line:sinkLine,snippet}
|
|
2930
3109
|
],
|
|
2931
3110
|
isSanitized:false,sanitizerType:null,
|
|
2932
|
-
severity:'high',vuln:
|
|
3111
|
+
severity:'high',vuln:fam.vuln,cwe:fam.cwe,stride:'Tampering',
|
|
2933
3112
|
file:`${write.file} -> ${fp}`,isCrossFile:true,parser:'STORED_TAINT'
|
|
2934
3113
|
});
|
|
2935
3114
|
break; // one finding per (field,sinkFile,sinkLine) is enough
|
|
2936
3115
|
}
|
|
2937
3116
|
}
|
|
2938
3117
|
}
|
|
3118
|
+
}
|
|
2939
3119
|
}
|
|
2940
3120
|
}
|
|
2941
3121
|
return findings;
|
|
@@ -3097,6 +3277,19 @@ const JAVA_FAMILY_RULES = [
|
|
|
3097
3277
|
sinkRe: /\bnew\s+(?:java\s*\.\s*util\s*\.\s*)?Random\s*\(|\bMath\s*\.\s*random\s*\(\s*\)|\bThreadLocalRandom\s*\.\s*current\s*\(\s*\)\s*\.\s*next/,
|
|
3098
3278
|
sanitizerRe: /\bSecureRandom\b/,
|
|
3099
3279
|
requiresSource: false,
|
|
3280
|
+
// T2.1 audit — a weak PRNG is only a vulnerability when its output is used
|
|
3281
|
+
// for something that must be UNPREDICTABLE. This rule had no such
|
|
3282
|
+
// requirement, so it fired on netty's leak-detector sampling
|
|
3283
|
+
// (`ThreadLocalRandom.current().nextInt(samplingInterval)`), round-robin
|
|
3284
|
+
// address selection, and a `new Random(0L)` inside a microbenchmark — 36
|
|
3285
|
+
// findings in a 24-entry sample, none of them security-relevant.
|
|
3286
|
+
//
|
|
3287
|
+
// The JS sibling rule (sast/weak-randomness.js) has always required a
|
|
3288
|
+
// security-context keyword; this is the same gate, applied file-wide
|
|
3289
|
+
// because Java's randomness helpers are usually a few lines from what they
|
|
3290
|
+
// seed. Returning truthy SUPPRESSES, so this reads "no security context
|
|
3291
|
+
// anywhere in the file".
|
|
3292
|
+
fileSafePredicate: (cleaned) => !/\b(?:token|secret|password|passwd|nonce|salt|iv\b|session|cookie|csrf|otp|mfa|api_?key|apikey|credential|auth|jwt|sign|cipher|encrypt|decrypt|random_?id|uuid|guid|reset|verif|captcha|challenge|ticket|voucher|coupon)\w*/i.test(cleaned), // NOT `seed`: benchmarks seed RNGs deterministically on purpose
|
|
3100
3293
|
},
|
|
3101
3294
|
{
|
|
3102
3295
|
family: 'sql-injection',
|
|
@@ -3114,7 +3307,16 @@ const JAVA_FAMILY_RULES = [
|
|
|
3114
3307
|
// SqlSession.{selectList,selectOne,selectMap,update,insert,delete} (MyBatis),
|
|
3115
3308
|
// JdbcTemplate.{queryForStream,queryForRowSet}, R2DBC DatabaseClient.sql,
|
|
3116
3309
|
// JdbcOperations / JdbcAggregateOperations.* (Spring Data JDBC).
|
|
3117
|
-
|
|
3310
|
+
// T2.1 audit: bare `execute` was in this list, and in Java that is
|
|
3311
|
+
// overwhelmingly java.util.concurrent.Executor.execute(Runnable) — a
|
|
3312
|
+
// THREAD POOL, not a database. Measured on the independent population
|
|
3313
|
+
// it produced high-severity SQL-injection findings on netty's
|
|
3314
|
+
// DefaultChannelPipeline, NonStickyEventExecutorGroup and
|
|
3315
|
+
// ThreadExecutorMap (`executor.execute(command)`), and on Appium's
|
|
3316
|
+
// AppiumCommandExecutor. Statement.execute(sql) IS real JDBC, so the
|
|
3317
|
+
// verb is kept but now requires a database-shaped receiver. This is the
|
|
3318
|
+
// same correction the comment above records for update/insert/delete.
|
|
3319
|
+
sinkRe: /(?:\.\s*(?:executeQuery|executeUpdate|executeBatch|prepareStatement|prepareCall|createQuery|createNativeQuery|createSQLQuery|createCriteriaQuery|createSqlQuery|createStatement|addBatch|queryForObject|queryForList|queryForMap|queryForLong|queryForInt|queryForRowSet|queryForStream|batchUpdate|find_by_sql|sqlRestriction|selectList|selectOne|selectMap|selectCursor|sql)\s*\(|\b(?:stmt|statement|st|ps|pstmt|preparedStatement|cstmt|conn|connection|jdbc\w{0,20}|jdbcTemplate|template|session|entityManager|em|db|database|dataSource|sqlSession)\s*\.\s*execute\s*\()/i,
|
|
3118
3320
|
sanitizerRe: null,
|
|
3119
3321
|
useTaint: true,
|
|
3120
3322
|
},
|
|
@@ -3804,7 +4006,7 @@ function _buildGlobalJavaTaintedMethodIndex(fileContents) {
|
|
|
3804
4006
|
for (const [path, content] of Object.entries(fileContents)) {
|
|
3805
4007
|
if (!/\.java$/i.test(path) || !content || content.length > 500_000) continue;
|
|
3806
4008
|
if (!_GLOBAL_JAVA_SOURCE_RE_FOR_INDEX.test(content)) continue;
|
|
3807
|
-
const cleaned = stripNoise(content);
|
|
4009
|
+
const cleaned = stripNoise(content,path);
|
|
3808
4010
|
// Same method-extraction pattern as _javaFindSanitizerMethods.
|
|
3809
4011
|
const re = /\b(?:public|private|protected|static|final|\s)+\s*[\w.<>\[\]]+\s+([A-Za-z_]\w*)\s*\(([^)]*)\)\s*(?:throws\s+[\w.,\s]+)?\s*\{/g;
|
|
3810
4012
|
let m;
|
|
@@ -4512,7 +4714,7 @@ function _javaArgWrappedBySanitizer(argStr, family) {
|
|
|
4512
4714
|
|
|
4513
4715
|
function scanJavaSAST(fp, raw) {
|
|
4514
4716
|
if (!/\.java$/i.test(fp)) return [];
|
|
4515
|
-
const cleaned = stripNoise(raw);
|
|
4717
|
+
const cleaned = stripNoise(raw,fp);
|
|
4516
4718
|
const lines = raw.split('\n');
|
|
4517
4719
|
const findings = [];
|
|
4518
4720
|
// hasSource: file has a direct user-input source OR calls a globally-known
|
|
@@ -4968,7 +5170,7 @@ function scanMiddlewareOrdering(fp, raw){
|
|
|
4968
5170
|
if (!ctx.isServer) return [];
|
|
4969
5171
|
// Need literal mount-path strings — keep comments stripped but preserve
|
|
4970
5172
|
// string contents so the path argument is readable.
|
|
4971
|
-
const cleaned = stripNoise(raw);
|
|
5173
|
+
const cleaned = stripNoise(raw,fp);
|
|
4972
5174
|
const findings = [];
|
|
4973
5175
|
const lines = raw.split('\n');
|
|
4974
5176
|
let firstAuthAt = Infinity;
|
|
@@ -5037,7 +5239,7 @@ function scanAliasedSinks(fp, raw){
|
|
|
5037
5239
|
if (!/\.(js|jsx|ts|tsx|mjs|cjs)$/i.test(fp)) return [];
|
|
5038
5240
|
// Comments-only strip; preserve string literals so bracket-access shapes
|
|
5039
5241
|
// like cp['exec'](...) — which need to see the literal 'exec' — match.
|
|
5040
|
-
const cleaned = stripNoise(raw);
|
|
5242
|
+
const cleaned = stripNoise(raw,fp);
|
|
5041
5243
|
const lines = raw.split('\n');
|
|
5042
5244
|
const findings = [];
|
|
5043
5245
|
// 1. Property-assigned aliases: const X = cp.exec
|
|
@@ -5175,10 +5377,129 @@ function scanAliasedSinks(fp, raw){
|
|
|
5175
5377
|
// path on patterns that lack any star/plus quantifier altogether.
|
|
5176
5378
|
let _safeRegex;
|
|
5177
5379
|
try { _safeRegex = _require('safe-regex'); } catch (_) { _safeRegex = null; }
|
|
5380
|
+
/**
|
|
5381
|
+
* Is the pattern anchored at the start (`^`, or `\A`)?
|
|
5382
|
+
*
|
|
5383
|
+
* An UNANCHORED exec() retries from every start offset, so a greedy leading
|
|
5384
|
+
* quantifier that fails late costs O(n) per offset — O(n^2) overall. That
|
|
5385
|
+
* polynomial blow-up, not intrinsic backtracking, is the defect in
|
|
5386
|
+
* GHSA-29g2-3rmr-qm68 (sveltejs/kit), whose entire fix was to prepend `^`.
|
|
5387
|
+
* A start anchor leaves exactly one start position and removes the multiplier.
|
|
5388
|
+
*
|
|
5389
|
+
* Deliberately conservative: `^` inside an alternation (`^a|b`) does NOT
|
|
5390
|
+
* anchor the whole pattern, since the `b` branch is still free-floating.
|
|
5391
|
+
*/
|
|
5392
|
+
function _isAnchoredRegex(body){
|
|
5393
|
+
const s = String(body || '');
|
|
5394
|
+
if (!/^(?:\^|\\A)/.test(s)) return false;
|
|
5395
|
+
// A top-level `|` means some branch is unanchored — not a real anchor.
|
|
5396
|
+
let depth = 0, cls = false;
|
|
5397
|
+
for (let i = 0; i < s.length; i++) {
|
|
5398
|
+
const ch = s[i];
|
|
5399
|
+
if (ch === '\\') { i++; continue; }
|
|
5400
|
+
if (cls) { if (ch === ']') cls = false; continue; }
|
|
5401
|
+
if (ch === '[') { cls = true; continue; }
|
|
5402
|
+
if (ch === '(') depth++;
|
|
5403
|
+
else if (ch === ')') depth--;
|
|
5404
|
+
else if (ch === '|' && depth === 0) return false;
|
|
5405
|
+
}
|
|
5406
|
+
return true;
|
|
5407
|
+
}
|
|
5408
|
+
/**
|
|
5409
|
+
* A quantified group that itself contains a quantifier — `(a+)+`, `(?:\w+\s?)*`.
|
|
5410
|
+
* This is the intrinsic, exponential form of catastrophic backtracking, and it
|
|
5411
|
+
* is NOT cured by anchoring, which is why the anchor exemption below refuses
|
|
5412
|
+
* to apply when this is present.
|
|
5413
|
+
*/
|
|
5414
|
+
function _hasNestedQuantifier(body){
|
|
5415
|
+
const s = String(body || '');
|
|
5416
|
+
for (let i = 0; i < s.length; i++) {
|
|
5417
|
+
if (s[i] === '\\') { i++; continue; }
|
|
5418
|
+
if (s[i] !== '(') continue;
|
|
5419
|
+
let depth = 0, cls = false, j = i;
|
|
5420
|
+
for (; j < s.length; j++) {
|
|
5421
|
+
const ch = s[j];
|
|
5422
|
+
if (ch === '\\') { j++; continue; }
|
|
5423
|
+
if (cls) { if (ch === ']') cls = false; continue; }
|
|
5424
|
+
if (ch === '[') { cls = true; continue; }
|
|
5425
|
+
if (ch === '(') depth++;
|
|
5426
|
+
else if (ch === ')') { depth--; if (depth === 0) break; }
|
|
5427
|
+
}
|
|
5428
|
+
if (j >= s.length) continue;
|
|
5429
|
+
const inner = s.slice(i + 1, j);
|
|
5430
|
+
const after = s.slice(j + 1);
|
|
5431
|
+
const groupQuantified = /^(?:[*+]|\{\d+,\d*\})/.test(after);
|
|
5432
|
+
if (groupQuantified && /[*+]|\{\d+,/.test(inner)) return true;
|
|
5433
|
+
}
|
|
5434
|
+
return false;
|
|
5435
|
+
}
|
|
5436
|
+
/**
|
|
5437
|
+
* Does an UNBOUNDED quantifier appear before the first REQUIRED literal?
|
|
5438
|
+
*
|
|
5439
|
+
* This is the mechanism behind polynomial (scan-and-retry) ReDoS. When a
|
|
5440
|
+
* greedy `+`/`*` runs before the engine has any literal to discriminate on,
|
|
5441
|
+
* an unanchored match must re-run that scan from every start offset — O(n^2).
|
|
5442
|
+
* Once a required literal comes first, the engine can reject most offsets in
|
|
5443
|
+
* O(1) and the blow-up disappears.
|
|
5444
|
+
*
|
|
5445
|
+
* /([^/ \t]+)\/…/ → `+` before the required `/` → true (the
|
|
5446
|
+
* real defect in GHSA-29g2-3rmr-qm68)
|
|
5447
|
+
* /(?:^|[\\/])\.env(?:…)?$/ → required `.` first → false
|
|
5448
|
+
*
|
|
5449
|
+
* Needed because the two analysers available here have opposite blind spots:
|
|
5450
|
+
* redos-nfa.js models exponential backtracking only and clears BOTH revisions
|
|
5451
|
+
* of the sveltejs pattern, while safe-regex's star-height heuristic flags
|
|
5452
|
+
* plain `(?:^|[\\/])\.env…` file-matchers that cannot backtrack at all.
|
|
5453
|
+
*/
|
|
5454
|
+
function _hasLeadingUnboundedQuantifier(body){
|
|
5455
|
+
const s = String(body || '');
|
|
5456
|
+
const unboundedAt = (k) => s[k] === '+' || s[k] === '*'
|
|
5457
|
+
|| (s[k] === '{' && /^\{\d+,\s*\}/.test(s.slice(k)));
|
|
5458
|
+
for (let i = 0; i < s.length; i++){
|
|
5459
|
+
const ch = s[i];
|
|
5460
|
+
if (ch === '\\'){
|
|
5461
|
+
const nxt = s[i + 2];
|
|
5462
|
+
if (unboundedAt(i + 2)) return true;
|
|
5463
|
+
if (nxt !== '?' && nxt !== '*') return false; // required escaped literal
|
|
5464
|
+
i++; continue;
|
|
5465
|
+
}
|
|
5466
|
+
if (ch === '['){
|
|
5467
|
+
let j = i + 1;
|
|
5468
|
+
for (; j < s.length; j++){
|
|
5469
|
+
if (s[j] === '\\'){ j++; continue; }
|
|
5470
|
+
if (s[j] === ']') break;
|
|
5471
|
+
}
|
|
5472
|
+
if (unboundedAt(j + 1)) return true;
|
|
5473
|
+
i = j; continue;
|
|
5474
|
+
}
|
|
5475
|
+
if (ch === '(' || ch === ')' || ch === '|' || ch === '^' || ch === '?'
|
|
5476
|
+
|| ch === ':' || ch === '=' || ch === '!' || ch === '<') continue;
|
|
5477
|
+
if (ch === '.'){ if (unboundedAt(i + 1)) return true; continue; }
|
|
5478
|
+
// A plain literal character: required unless immediately made optional.
|
|
5479
|
+
if (unboundedAt(i + 1)) return true;
|
|
5480
|
+
if (s[i + 1] !== '?') return false;
|
|
5481
|
+
}
|
|
5482
|
+
return false;
|
|
5483
|
+
}
|
|
5178
5484
|
function _isLikelyUnsafeRegex(body){
|
|
5179
5485
|
if (!body || body.length < 3) return false;
|
|
5180
5486
|
// Cheap prefilter: must contain a quantifier *, +, or {n,} for ReDoS to be possible.
|
|
5181
5487
|
if (!/[*+]|\{\d+,/.test(body)) return false;
|
|
5488
|
+
// safe-regex (star-height heuristic) is the only signal below, and it
|
|
5489
|
+
// over-reports on patterns that cannot backtrack — plain file matchers like
|
|
5490
|
+
// /(?:^|[\\/])\.env(?:\.[\w-]+)?$/ were reported across this project's own
|
|
5491
|
+
// source. Require a mechanism to actually be present: either intrinsic
|
|
5492
|
+
// (a nested quantifier) or scan-and-retry (an unbounded quantifier ahead of
|
|
5493
|
+
// the first required literal). redos-nfa.js is consulted as a second
|
|
5494
|
+
// opinion for the intrinsic case only, since it does not model the
|
|
5495
|
+
// polynomial one.
|
|
5496
|
+
if (!_hasNestedQuantifier(body) && !_hasLeadingUnboundedQuantifier(body)) return false;
|
|
5497
|
+
// FIX-DISCRIMINATION (GHSA-29g2-3rmr-qm68): an anchored pattern with no
|
|
5498
|
+
// nested quantifier has neither failure mode — no start-offset multiplier,
|
|
5499
|
+
// no intrinsic catastrophe. safe-regex's star-height heuristic cannot see
|
|
5500
|
+
// anchoring at all and flagged the fixed revision identically to the
|
|
5501
|
+
// vulnerable one, so the finding survived its own fix.
|
|
5502
|
+
if (_isAnchoredRegex(body) && !_hasNestedQuantifier(body)) return false;
|
|
5182
5503
|
if (_safeRegex) {
|
|
5183
5504
|
try { if (!_safeRegex(body)) return true; } catch (_) { /* fall through */ }
|
|
5184
5505
|
}
|
|
@@ -5262,6 +5583,44 @@ function _charSetsIntersect(a, b){
|
|
|
5262
5583
|
function _inClass(ch, cls){
|
|
5263
5584
|
return cls.negate ? !cls.members.has(ch) : cls.members.has(ch);
|
|
5264
5585
|
}
|
|
5586
|
+
/**
|
|
5587
|
+
* Extract regex-literal bodies, honouring character classes.
|
|
5588
|
+
*
|
|
5589
|
+
* The previous extractor was itself a regex:
|
|
5590
|
+
* /\/((?:\\.|[^\/\n])+)\/[gimsuy]{0,5}/g
|
|
5591
|
+
* which has no notion of `[...]`, so an unescaped `/` inside a character
|
|
5592
|
+
* class — legal, and extremely common in path/URL patterns — TERMINATED the
|
|
5593
|
+
* literal early and emitted two phantom fragments in place of one real regex.
|
|
5594
|
+
*
|
|
5595
|
+
* Found via GHSA-29g2-3rmr-qm68, where
|
|
5596
|
+
* /^[ \t]*([^/ \t]+)\/([^; \t]+)[ \t]*(?:;[ \t]*q=([0-9.]+))?/
|
|
5597
|
+
* was split into `^[ \t]*([^` and `([^; \t]+)[ \t]*(?:;[ \t]*q=([0-9.]+))?`.
|
|
5598
|
+
* The second fragment never existed in the source, and — having lost the
|
|
5599
|
+
* leading `^` to the split — was judged unanchored and reported as ReDoS on
|
|
5600
|
+
* the FIXED revision, so the finding survived its own fix.
|
|
5601
|
+
*
|
|
5602
|
+
* Returns [{body, index}] with `index` at the opening slash.
|
|
5603
|
+
*/
|
|
5604
|
+
function _extractRegexLiterals(source){
|
|
5605
|
+
const out = [];
|
|
5606
|
+
const s = String(source || '');
|
|
5607
|
+
for (let i = 0; i < s.length; i++){
|
|
5608
|
+
if (s[i] !== '/') continue;
|
|
5609
|
+
if (s[i + 1] === '/' || s[i + 1] === '*') continue; // comment, not a literal
|
|
5610
|
+
let j = i + 1, cls = false, body = '', closed = false;
|
|
5611
|
+
for (; j < s.length; j++){
|
|
5612
|
+
const ch = s[j];
|
|
5613
|
+
if (ch === '\n') break; // literals never span lines
|
|
5614
|
+
if (ch === '\\'){ body += ch + (s[j + 1] || ''); j++; continue; }
|
|
5615
|
+
if (cls){ if (ch === ']') cls = false; body += ch; continue; }
|
|
5616
|
+
if (ch === '['){ cls = true; body += ch; continue; }
|
|
5617
|
+
if (ch === '/'){ closed = true; break; }
|
|
5618
|
+
body += ch;
|
|
5619
|
+
}
|
|
5620
|
+
if (closed && body.length){ out.push({ body, index: i }); i = j; }
|
|
5621
|
+
}
|
|
5622
|
+
return out;
|
|
5623
|
+
}
|
|
5265
5624
|
function scanReDoS(fp,raw){
|
|
5266
5625
|
// ReDoS only applies to languages with regex-literal or RegExp() syntax.
|
|
5267
5626
|
// Solidity, Java, Go, C/C++, Rust use `/` as the division operator —
|
|
@@ -5274,7 +5633,6 @@ function scanReDoS(fp,raw){
|
|
|
5274
5633
|
// shapes. Otherwise lines like `// hello /world/` are mis-parsed as regexes
|
|
5275
5634
|
// and safe-regex chokes on the text content.
|
|
5276
5635
|
const cleaned = stripNoiseAndStrings(raw);
|
|
5277
|
-
const litRe=/\/((?:\\.|[^\/\n])+)\/[gimsuy]{0,5}/g;
|
|
5278
5636
|
const ctorRe=/new\s+RegExp\s*\(\s*['"`]((?:\\.|[^'"`\n])+)['"`]/g;
|
|
5279
5637
|
function check(re, source){
|
|
5280
5638
|
let m;while((m=re.exec(source))!==null){
|
|
@@ -5291,13 +5649,25 @@ function scanReDoS(fp,raw){
|
|
|
5291
5649
|
}
|
|
5292
5650
|
}
|
|
5293
5651
|
// Regex literals: scan against `cleaned` so comment slashes don't fool us.
|
|
5294
|
-
|
|
5652
|
+
// Extracted with a character-class-aware walker rather than a regex — see
|
|
5653
|
+
// _extractRegexLiterals for why the regex form was actively harmful.
|
|
5654
|
+
for (const { body, index } of _extractRegexLiterals(cleaned)) {
|
|
5655
|
+
if (!_isLikelyUnsafeRegex(body)) continue;
|
|
5656
|
+
const line = cleaned.substring(0, index).split("\n").length;
|
|
5657
|
+
out.push({
|
|
5658
|
+
vuln:"Regex ReDoS — Catastrophic Backtracking",
|
|
5659
|
+
severity:"medium",cwe:"CWE-1333",stride:"Denial of Service",
|
|
5660
|
+
fix:"Rewrite the regex to avoid nested quantifiers and overlapping alternation. Consider the `re2` library for linear-time matching.",
|
|
5661
|
+
code:"// Use the Google RE2 library which guarantees linear-time evaluation:\nconst RE2 = require('re2');\nconst re = new RE2(pattern);",
|
|
5662
|
+
file:fp,line,snippet:lines[line-1]?.trim()||body
|
|
5663
|
+
});
|
|
5664
|
+
}
|
|
5295
5665
|
// `new RegExp("...")` form: also via cleaned (comments out, but the literal
|
|
5296
5666
|
// string pattern is the source we care about — already preserved by
|
|
5297
5667
|
// stripNoiseAndStrings via the regex-literal carve-out... actually no, our
|
|
5298
5668
|
// string-stripper blanks "..." contents. Use the comment-stripped view for
|
|
5299
5669
|
// ctor form so the pattern string survives.).
|
|
5300
|
-
check(ctorRe, stripNoise(raw));
|
|
5670
|
+
check(ctorRe, stripNoise(raw,fp));
|
|
5301
5671
|
return out;
|
|
5302
5672
|
}
|
|
5303
5673
|
|
|
@@ -5405,7 +5775,7 @@ const EXTRA_STRUCTURAL_PATTERNS=[
|
|
|
5405
5775
|
|
|
5406
5776
|
function scanExtraStructural(fp,raw){
|
|
5407
5777
|
const cleaned=stripNoiseAndStrings(raw);
|
|
5408
|
-
const cleanedNoise=stripNoise(raw);
|
|
5778
|
+
const cleanedNoise=stripNoise(raw,fp);
|
|
5409
5779
|
const lines=raw.split('\n');
|
|
5410
5780
|
const findings=[];
|
|
5411
5781
|
const ctx = inferFileContext(fp, raw);
|
|
@@ -6478,7 +6848,7 @@ const CIPHER_TRANSIT_PATTERNS=[
|
|
|
6478
6848
|
{regex:/OpenSSL::SSL::SSLContext\.new/g,getLabel:()=>"Ruby OpenSSL::SSL::SSLContext",ctx:"Ruby ssl"},
|
|
6479
6849
|
];
|
|
6480
6850
|
function classifyCipherStrength(cipher){const c=cipher.toUpperCase();if(/\bRC4\b|\bRC2\b|\bARCFOUR\b|SSLV2|SSLV3|\bNULL\b|\bEXPORT\b|\bANULL\b|\bENULL\b|\bECB\b|\bMD5\b|\bSHA1\b(?![\d_])/.test(c))return"weak";if(/\bDES\b/.test(c)&&!/3DES|EDE|TRIPLE/.test(c))return"weak";if(/3DES|TRIPLE.?DES|DES.EDE|TLS.?1.?1|TLSV1\.1/.test(c))return"weak";if(/BCRYPT|ARGON2|SCRYPT|PBKDF2|FERNET|CHACHA20|CHACHAPOLY|PASSWORD_BCRYPT|PASSWORD_ARGON/.test(c))return"strong";if(/\bAES\b|SHA256|SHA384|SHA512|SHA3|BLAKE2|HMACSHA256|HMACSHA512|\bGCM\b|\bCCM\b|ECDHE|DHE|TLS.?1.?[23]|TLSV1\.[23]|HTTPS.SERVER|TLS.SERVER|TLS.CERTIF|HS256|RS256|ES256/.test(c))return"strong";return"unknown";}
|
|
6481
|
-
function scanCiphers(fp,raw){const cleaned=stripNoise(raw);const lines=raw.split("\n");const atRest=[],inTransit=[];for(const pat of CIPHER_REST_PATTERNS){const re=new RegExp(pat.regex.source,pat.regex.flags);let m;while((m=re.exec(cleaned))){const line=lineAt(cleaned,m.index);const cipher=pat.getLabel(m);atRest.push({cipher,strength:classifyCipherStrength(cipher),ctx:pat.ctx,file:fp,line,snippet:(lines[line-1]||"").trim()});}}for(const pat of CIPHER_TRANSIT_PATTERNS){const re=new RegExp(pat.regex.source,pat.regex.flags);let m;while((m=re.exec(cleaned))){const line=lineAt(cleaned,m.index);const cipher=pat.getLabel(m);inTransit.push({cipher,strength:classifyCipherStrength(cipher),ctx:pat.ctx,file:fp,line,snippet:(lines[line-1]||"").trim()});}}const uniq=(a)=>a.filter((v,i,arr)=>arr.findIndex(x=>x.cipher===v.cipher&&x.file===v.file&&x.line===v.line)===i);return{atRest:uniq(atRest),inTransit:uniq(inTransit)};}
|
|
6851
|
+
function scanCiphers(fp,raw){const cleaned=stripNoise(raw,fp);const lines=raw.split("\n");const atRest=[],inTransit=[];for(const pat of CIPHER_REST_PATTERNS){const re=new RegExp(pat.regex.source,pat.regex.flags);let m;while((m=re.exec(cleaned))){const line=lineAt(cleaned,m.index);const cipher=pat.getLabel(m);atRest.push({cipher,strength:classifyCipherStrength(cipher),ctx:pat.ctx,file:fp,line,snippet:(lines[line-1]||"").trim()});}}for(const pat of CIPHER_TRANSIT_PATTERNS){const re=new RegExp(pat.regex.source,pat.regex.flags);let m;while((m=re.exec(cleaned))){const line=lineAt(cleaned,m.index);const cipher=pat.getLabel(m);inTransit.push({cipher,strength:classifyCipherStrength(cipher),ctx:pat.ctx,file:fp,line,snippet:(lines[line-1]||"").trim()});}}const uniq=(a)=>a.filter((v,i,arr)=>arr.findIndex(x=>x.cipher===v.cipher&&x.file===v.file&&x.line===v.line)===i);return{atRest:uniq(atRest),inTransit:uniq(inTransit)};}
|
|
6482
6852
|
|
|
6483
6853
|
function scanCredentials(fp,raw){
|
|
6484
6854
|
if(!CRED_PREFILTER.test(raw))return[];
|
|
@@ -7651,7 +8021,7 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
7651
8021
|
const _perFileTimeoutMs = parseInt(process.env.AGENTIC_SECURITY_PER_FILE_TIMEOUT_MS || '10000', 10);
|
|
7652
8022
|
const _fileTimings = [];
|
|
7653
8023
|
let _filesSkipped = 0, _filesTimedOut = 0, _filesDenseSkipped = 0;
|
|
7654
|
-
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=[];
|
|
8024
|
+
const files=Object.keys(fileContents).filter(f=>(shouldScan(f) || isKubernetesManifest(f, fileContents[f])) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];
|
|
7655
8025
|
// ---- R8: opt-in per-file checkpointing (AGENTIC_SECURITY_RESUME=1, or
|
|
7656
8026
|
// runScan({resume:true})). Default OFF, so existing behaviour is untouched.
|
|
7657
8027
|
// Only this loop is checkpointed; every cross-file pass below re-runs, so
|
|
@@ -7719,112 +8089,112 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
7719
8089
|
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"});
|
|
7720
8090
|
if(_ckptDone.has(p)&&_ckptReplay(p))continue;
|
|
7721
8091
|
const _mk={aR:aR.length,aF:aF.length,aSrc:aSrc.length,aSink:aSink.length,aSan:aSan.length,aLogic:aLogic.length,aSecrets:aSecrets.length,aCR:aCiphersRest.length,aCT:aCiphersTransit.length,sup:_suppressionLog.length};
|
|
7722
|
-
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){_filesDenseSkipped++;continue;}fc[p]=c;aR.push(...scanRoutes(p,
|
|
8092
|
+
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){_filesDenseSkipped++;continue;}const cc=_blankCached(c,_commentLangFor(p));fc[p]=c;aR.push(...scanRoutes(p,cc));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,cc));aSecrets.push(...scanCredentials(p,c));aF.push(...scanStructuralVulns(p,cc));aF.push(...scanExtraStructural(p,cc));aF.push(...scanAliasedSinks(p,cc));aF.push(...scanJavaSAST(p,cc));aF.push(...scanJavaBenchExtras(p,cc));aLogic.push(...scanMiddlewareOrdering(p,cc));aLogic.push(...scanReDoS(p,cc));if(/\.(?:java|cs|kt|py|php|phtml)$/i.test(p)){try{aLogic.push(...scanRegexReDoS(p,cc));}catch(_){}}aLogic.push(...scanTodosNearSecurity(p,c));aSecrets.push(...scanEntropySecrets(p,c));const cp=scanCiphers(p,cc);aCiphersRest.push(...cp.atRest);aCiphersTransit.push(...cp.inTransit);if(/\.(graphql|gql)$/i.test(p))aF.push(...scanGraphQL(p,cc));aF.push(...scanIaC(p,cc));aF.push(...scanTerraform(p,cc));
|
|
7723
8093
|
aF.push(...scanLLM(p,c));
|
|
7724
8094
|
aF.push(...scanLLMOwasp(p,c));
|
|
7725
8095
|
aF.push(...scanLlmCost(p,c));
|
|
7726
|
-
aLogic.push(...scanBusinessLogic(p,
|
|
7727
|
-
aF.push(...scanPipeline(p,
|
|
7728
|
-
aF.push(...scanContainer(p,
|
|
7729
|
-
aF.push(...scanInstallScripts(p,
|
|
8096
|
+
aLogic.push(...scanBusinessLogic(p,cc));
|
|
8097
|
+
aF.push(...scanPipeline(p,cc));
|
|
8098
|
+
aF.push(...scanContainer(p,cc));
|
|
8099
|
+
aF.push(...scanInstallScripts(p,cc));
|
|
7730
8100
|
aF.push(...scanMCP(p,c));
|
|
7731
8101
|
aF.push(...scanClaudeSettings(p,c));
|
|
7732
8102
|
aF.push(...scanClaudeMdPromptInjection(p,c));
|
|
7733
8103
|
aF.push(...scanClaudeHookInjection(p,c));
|
|
7734
|
-
aF.push(...scanDjangoHardening(p,
|
|
7735
|
-
aF.push(...scanDefiDeep(p,
|
|
7736
|
-
aF.push(...scanSpringbootHardening(p,
|
|
7737
|
-
aF.push(...scanLaravelHardening(p,
|
|
7738
|
-
aF.push(...scanSwift(p,
|
|
7739
|
-
aF.push(...scanDartFlutter(p,
|
|
7740
|
-
aF.push(...scanWeakRandomness(p,
|
|
7741
|
-
aF.push(...scanGraphQLModule(p,
|
|
7742
|
-
aF.push(...scanSensitiveDataLogging(p,
|
|
7743
|
-
aF.push(...scanComparisonSafety(p,
|
|
7744
|
-
aF.push(...scanWeakPasswordHash(p,
|
|
7745
|
-
aF.push(...scanCachePoisoning(p,
|
|
7746
|
-
aF.push(...scanNullByteInjection(p,
|
|
8104
|
+
aF.push(...scanDjangoHardening(p,cc));
|
|
8105
|
+
aF.push(...scanDefiDeep(p,cc));
|
|
8106
|
+
aF.push(...scanSpringbootHardening(p,cc));
|
|
8107
|
+
aF.push(...scanLaravelHardening(p,cc));
|
|
8108
|
+
aF.push(...scanSwift(p,cc));
|
|
8109
|
+
aF.push(...scanDartFlutter(p,cc));
|
|
8110
|
+
aF.push(...scanWeakRandomness(p,cc));
|
|
8111
|
+
aF.push(...scanGraphQLModule(p,cc));
|
|
8112
|
+
aF.push(...scanSensitiveDataLogging(p,cc));
|
|
8113
|
+
aF.push(...scanComparisonSafety(p,cc));
|
|
8114
|
+
aF.push(...scanWeakPasswordHash(p,cc));
|
|
8115
|
+
aF.push(...scanCachePoisoning(p,cc));
|
|
8116
|
+
aF.push(...scanNullByteInjection(p,cc));
|
|
7747
8117
|
aF.push(...scanLlmTradingAgent(p,c));
|
|
7748
|
-
aF.push(...scanMobileManifest(p,
|
|
7749
|
-
aF.push(...scanQuarkusHardening(p,
|
|
7750
|
-
aF.push(...scanFastapiHardening(p,
|
|
7751
|
-
aF.push(...scanAuthZ(p,
|
|
7752
|
-
aF.push(...scanModelLoad(p,
|
|
8118
|
+
aF.push(...scanMobileManifest(p,cc));
|
|
8119
|
+
aF.push(...scanQuarkusHardening(p,cc));
|
|
8120
|
+
aF.push(...scanFastapiHardening(p,cc));
|
|
8121
|
+
aF.push(...scanAuthZ(p,cc));
|
|
8122
|
+
aF.push(...scanModelLoad(p,cc));
|
|
7753
8123
|
aF.push(...scanPromptTemplate(p,c));
|
|
7754
|
-
aF.push(...scanXXE(p,
|
|
7755
|
-
aF.push(...scanJNDI(p,
|
|
7756
|
-
aF.push(...scanJavaDeserialization(p,
|
|
7757
|
-
aF.push(...scanJwtExp(p,
|
|
7758
|
-
aF.push(...scanZipSlip(p,
|
|
7759
|
-
aF.push(...scanFileUpload(p,
|
|
7760
|
-
aF.push(...scanHostHeader(p,
|
|
7761
|
-
aF.push(...scanPythonSinks(p,
|
|
7762
|
-
aF.push(...scanCSharp(p,
|
|
7763
|
-
aF.push(...scanCpp(p,
|
|
7764
|
-
aF.push(...scanSolidity(p,
|
|
7765
|
-
aF.push(...scanRust(p,
|
|
7766
|
-
aF.push(...scanGoExtended(p,
|
|
7767
|
-
aF.push(...scanDatabaseRLS(p,
|
|
7768
|
-
aF.push(...scanRateLimit(p,
|
|
7769
|
-
aF.push(...scanAuthProvider(p,
|
|
7770
|
-
aF.push(...scanEnvHygiene(p,
|
|
7771
|
-
aF.push(...scanWebhook(p,
|
|
7772
|
-
aF.push(...scanClientSide(p,
|
|
8124
|
+
aF.push(...scanXXE(p,cc));
|
|
8125
|
+
aF.push(...scanJNDI(p,cc));
|
|
8126
|
+
aF.push(...scanJavaDeserialization(p,cc));
|
|
8127
|
+
aF.push(...scanJwtExp(p,cc));
|
|
8128
|
+
aF.push(...scanZipSlip(p,cc));aF.push(...scanSiblingGuard(p,cc));
|
|
8129
|
+
aF.push(...scanFileUpload(p,cc));
|
|
8130
|
+
aF.push(...scanHostHeader(p,cc));
|
|
8131
|
+
aF.push(...scanPythonSinks(p,cc));
|
|
8132
|
+
aF.push(...scanCSharp(p,cc));
|
|
8133
|
+
aF.push(...scanCpp(p,cc));
|
|
8134
|
+
aF.push(...scanSolidity(p,cc));
|
|
8135
|
+
aF.push(...scanRust(p,cc));
|
|
8136
|
+
aF.push(...scanGoExtended(p,cc));
|
|
8137
|
+
aF.push(...scanDatabaseRLS(p,cc));
|
|
8138
|
+
aF.push(...scanRateLimit(p,cc));aF.push(...scanResourceExhaustion(p,cc));aF.push(...scanRedirectToctou(p,cc));aF.push(...scanCodegenSink(p,cc));aF.push(...scanOwnershipAuthz(p,cc));
|
|
8139
|
+
aF.push(...scanAuthProvider(p,cc));
|
|
8140
|
+
aF.push(...scanEnvHygiene(p,cc));
|
|
8141
|
+
aF.push(...scanWebhook(p,cc));
|
|
8142
|
+
aF.push(...scanClientSide(p,cc));
|
|
7773
8143
|
aF.push(...scanPromptFirewall(p,c));
|
|
7774
8144
|
aF.push(...scanLlmRedteam(p,c));
|
|
7775
8145
|
aF.push(...scanJulietShape(p,c));
|
|
7776
|
-
aF.push(...scanCppDataflow(p,
|
|
8146
|
+
aF.push(...scanCppDataflow(p,cc));
|
|
7777
8147
|
// Phase 1: new detectors.
|
|
7778
|
-
aF.push(...scanMassAssignment(p,
|
|
7779
|
-
aF.push(...scanPrototypePollution(p,
|
|
7780
|
-
aF.push(...scanCSRF(p,
|
|
7781
|
-
aF.push(...scanTOCTOU(p,
|
|
7782
|
-
aF.push(...scanNoSQLInjection(p,
|
|
7783
|
-
aF.push(...scanLDAPInjection(p,
|
|
7784
|
-
aF.push(...scanXPathInjection(p,
|
|
7785
|
-
aF.push(...scanSSTI(p,
|
|
7786
|
-
aF.push(...scanOpenRedirect(p,
|
|
7787
|
-
aF.push(...scanWrongContextSanitizer(p,
|
|
7788
|
-
aF.push(...scanSanitizerContextMismatch(p,
|
|
7789
|
-
aF.push(...scanFrontendHygiene(p,
|
|
7790
|
-
aF.push(...scanCsvInjection(p,
|
|
8148
|
+
aF.push(...scanMassAssignment(p,cc));
|
|
8149
|
+
aF.push(...scanPrototypePollution(p,cc));
|
|
8150
|
+
aF.push(...scanCSRF(p,cc));
|
|
8151
|
+
aF.push(...scanTOCTOU(p,cc));
|
|
8152
|
+
aF.push(...scanNoSQLInjection(p,cc));
|
|
8153
|
+
aF.push(...scanLDAPInjection(p,cc));
|
|
8154
|
+
aF.push(...scanXPathInjection(p,cc));
|
|
8155
|
+
aF.push(...scanSSTI(p,cc));
|
|
8156
|
+
aF.push(...scanOpenRedirect(p,cc));
|
|
8157
|
+
aF.push(...scanWrongContextSanitizer(p,cc));
|
|
8158
|
+
aF.push(...scanSanitizerContextMismatch(p,cc));
|
|
8159
|
+
aF.push(...scanFrontendHygiene(p,cc));
|
|
8160
|
+
aF.push(...scanCsvInjection(p,cc));
|
|
7791
8161
|
// R16 — specialist crypto-hygiene classes (constant-time comparison,
|
|
7792
8162
|
// secret zeroization). Narrow by design: keyed on the secret-ness of the
|
|
7793
8163
|
// identifier, and silent whenever the correct constant-time or
|
|
7794
8164
|
// guaranteed-wipe API is already present.
|
|
7795
|
-
aF.push(...scanCryptoSpecialist(p,
|
|
7796
|
-
aF.push(...scanStoredTaint(p,
|
|
7797
|
-
aF.push(...scanJavaStructural(p,
|
|
7798
|
-
aF.push(...scanCsharpStructural(p,
|
|
7799
|
-
aF.push(...scanJsFrameworkStructural(p,
|
|
7800
|
-
aF.push(...scanPythonStructural(p,
|
|
7801
|
-
aF.push(...scanGoStructural(p,
|
|
7802
|
-
aF.push(...scanSecretConcat(p,
|
|
7803
|
-
aF.push(...scanXssReflectedMultilang(p,
|
|
7804
|
-
aF.push(...scanCodeInjectionMultilang(p,
|
|
7805
|
-
aF.push(...scanResponseSplitting(p,
|
|
8165
|
+
aF.push(...scanCryptoSpecialist(p,cc));
|
|
8166
|
+
aF.push(...scanStoredTaint(p,cc));
|
|
8167
|
+
aF.push(...scanJavaStructural(p,cc));
|
|
8168
|
+
aF.push(...scanCsharpStructural(p,cc));
|
|
8169
|
+
aF.push(...scanJsFrameworkStructural(p,cc));
|
|
8170
|
+
aF.push(...scanPythonStructural(p,cc));
|
|
8171
|
+
aF.push(...scanGoStructural(p,cc));
|
|
8172
|
+
aF.push(...scanSecretConcat(p,cc));
|
|
8173
|
+
aF.push(...scanXssReflectedMultilang(p,cc));
|
|
8174
|
+
aF.push(...scanCodeInjectionMultilang(p,cc));
|
|
8175
|
+
aF.push(...scanResponseSplitting(p,cc));
|
|
7806
8176
|
aF.push(...scanStoredPromptInjection(p,c));
|
|
7807
8177
|
aF.push(...scanRAGPoisoning(p,c));
|
|
7808
8178
|
aF.push(...scanAgentToolEscalation(p,c));
|
|
7809
8179
|
aF.push(...scanAgentUntrustedFlow(p,c));
|
|
7810
|
-
aF.push(...scanEventEntrypoints(p,
|
|
7811
|
-
aF.push(...scanDbTaint(p,
|
|
7812
|
-
aF.push(...scanSSRFCloudMetadata(p,
|
|
7813
|
-
aF.push(...scanMutationXSS(p,
|
|
7814
|
-
aF.push(...scanKotlin(p,
|
|
7815
|
-
aF.push(...scanRuby(p,
|
|
7816
|
-
aF.push(...scanPhp(p,
|
|
8180
|
+
aF.push(...scanEventEntrypoints(p,cc));
|
|
8181
|
+
aF.push(...scanDbTaint(p,cc));
|
|
8182
|
+
aF.push(...scanSSRFCloudMetadata(p,cc));
|
|
8183
|
+
aF.push(...scanMutationXSS(p,cc));
|
|
8184
|
+
aF.push(...scanKotlin(p,cc));
|
|
8185
|
+
aF.push(...scanRuby(p,cc));
|
|
8186
|
+
aF.push(...scanPhp(p,cc));
|
|
7817
8187
|
// Integration block: scaffolded SAST scanners. Gated by env var.
|
|
7818
8188
|
if (process.env.AGENTIC_SECURITY_NO_INTEGRATION !== '1') {
|
|
7819
8189
|
if (process.env.AGENTIC_SECURITY_NO_LLM_APP !== '1') aF.push(...scanLlmApp(p,c));
|
|
7820
|
-
if (process.env.AGENTIC_SECURITY_NO_MOBILE !== '1') aF.push(...scanMobile(p,
|
|
7821
|
-
if (process.env.AGENTIC_SECURITY_NO_PQC !== '1') aF.push(...scanPqc(p,
|
|
7822
|
-
if (process.env.AGENTIC_SECURITY_NO_WEB3_ADV!== '1') aF.push(...scanWeb3Advanced(p,
|
|
7823
|
-
if (process.env.AGENTIC_SECURITY_NO_DAPP !== '1') aF.push(...scanDappFrontend(p,
|
|
7824
|
-
if (process.env.AGENTIC_SECURITY_NO_CLOUD_IAM!== '1') aF.push(...scanCloudIam(p,
|
|
7825
|
-
if (process.env.AGENTIC_SECURITY_NO_K8S_ADM !== '1') aF.push(...scanK8sAdmission(p,
|
|
7826
|
-
if (process.env.AGENTIC_SECURITY_NO_CRYPTO_PROTO !== '1') aF.push(...scanCryptoProtocol(p,
|
|
7827
|
-
if (process.env.AGENTIC_SECURITY_NO_ML_SUPPLY !== '1') aF.push(...scanMlSupplyChain(p,
|
|
8190
|
+
if (process.env.AGENTIC_SECURITY_NO_MOBILE !== '1') aF.push(...scanMobile(p,cc));
|
|
8191
|
+
if (process.env.AGENTIC_SECURITY_NO_PQC !== '1') aF.push(...scanPqc(p,cc));
|
|
8192
|
+
if (process.env.AGENTIC_SECURITY_NO_WEB3_ADV!== '1') aF.push(...scanWeb3Advanced(p,cc));
|
|
8193
|
+
if (process.env.AGENTIC_SECURITY_NO_DAPP !== '1') aF.push(...scanDappFrontend(p,cc));
|
|
8194
|
+
if (process.env.AGENTIC_SECURITY_NO_CLOUD_IAM!== '1') aF.push(...scanCloudIam(p,cc));
|
|
8195
|
+
if (process.env.AGENTIC_SECURITY_NO_K8S_ADM !== '1') aF.push(...scanK8sAdmission(p,cc));
|
|
8196
|
+
if (process.env.AGENTIC_SECURITY_NO_CRYPTO_PROTO !== '1') aF.push(...scanCryptoProtocol(p,cc));
|
|
8197
|
+
if (process.env.AGENTIC_SECURITY_NO_ML_SUPPLY !== '1') aF.push(...scanMlSupplyChain(p,cc));
|
|
7828
8198
|
}
|
|
7829
8199
|
const _ftElapsed=Date.now()-_ft0;
|
|
7830
8200
|
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++;}
|
|
@@ -7918,7 +8288,7 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
7918
8288
|
continue;
|
|
7919
8289
|
}
|
|
7920
8290
|
// Annotation-based category for OWASP Benchmark: `@WebServlet("/cat-NN/...")`.
|
|
7921
|
-
const cat = _javaWebServletCategory(stripNoise(c));
|
|
8291
|
+
const cat = _javaWebServletCategory(stripNoise(c,p));
|
|
7922
8292
|
if (cat) _benchCategoryByFile.set(p, cat);
|
|
7923
8293
|
}
|
|
7924
8294
|
// Pre-compute fileWide safe shapes per file. Used by _shouldKeep below.
|
|
@@ -7933,7 +8303,7 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
7933
8303
|
const c = fc[p];
|
|
7934
8304
|
if (!c) continue;
|
|
7935
8305
|
const safeSet = new Set();
|
|
7936
|
-
const cleanedP = stripNoise(c);
|
|
8306
|
+
const cleanedP = stripNoise(c,p);
|
|
7937
8307
|
for (const fam of Object.keys(_OWASP_SAFE_SHAPES || {})) {
|
|
7938
8308
|
const cfg = _OWASP_SAFE_SHAPES[fam];
|
|
7939
8309
|
if (cfg && cfg.fileWide && cfg.fileWide(cleanedP)) safeSet.add(fam);
|
|
@@ -7979,6 +8349,9 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
7979
8349
|
setProgress({current:i,total:files.length,file:"Session taint...",phase:"Linking"});const sess=crossSessionTaint(fc);aF.push(...sess);
|
|
7980
8350
|
setProgress({current:i,total:files.length,file:"Call graph...",phase:"Linking"});const callGraph=buildCallGraph(fc);
|
|
7981
8351
|
// R19 (PRD §5): cross-route BOLA/BFLA over the aggregated route inventory.
|
|
8352
|
+
// Convention deviation is PROJECT-scoped by design: the convention is a
|
|
8353
|
+
// property of the codebase, not of one file (see convention-deviation.js).
|
|
8354
|
+
try{aF.push(...scanConventionDeviationProject(fc));}catch(_){}
|
|
7982
8355
|
try{aF.push(...scanApiBrokenAuthz(aR));}catch(_){}
|
|
7983
8356
|
// R22 (PRD §5): cross-service edges inferred from code (client call → matched route).
|
|
7984
8357
|
try{aF.push(...scanCrossService(aR,fc));}catch(_){}
|
|
@@ -7991,6 +8364,20 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null,
|
|
|
7991
8364
|
setProgress({current:i,total:files.length,file:"Config file cross-ref...",phase:"Linking"});aLogic.push(...scanConfigFiles(fc));
|
|
7992
8365
|
setProgress({current:i,total:files.length,file:"OSV vulnerability database...",phase:"SCA"});
|
|
7993
8366
|
const allFileContents={...fc, ...depFileContents};
|
|
8367
|
+
// PRD F11.4 — malicious install hooks, scanned where package.json actually
|
|
8368
|
+
// lives. `scanInstallScripts` was wired into the per-file SAST dispatch, but
|
|
8369
|
+
// that loop iterates `files`, which comes from `fileContents` — and
|
|
8370
|
+
// `package.json` is routed to depFileContents and fails `shouldScan()`, so
|
|
8371
|
+
// the detector was never invoked by a real scan. Measured: 1 finding when
|
|
8372
|
+
// called directly on its fixture, 0 through runScan.
|
|
8373
|
+
//
|
|
8374
|
+
// Fixed HERE rather than by admitting package.json into the SAST loop, which
|
|
8375
|
+
// would put a manifest through all ~117 code detectors to satisfy one rule.
|
|
8376
|
+
// A manifest detector belongs on the manifest path.
|
|
8377
|
+
for (const [mp, mc] of Object.entries(depFileContents)) {
|
|
8378
|
+
if (!/(?:^|\/)package\.json$/i.test(mp)) continue;
|
|
8379
|
+
try { aF.push(...scanInstallScripts(mp, mc)); } catch (_) { /* never fail a scan on one manifest */ }
|
|
8380
|
+
}
|
|
7994
8381
|
const components=parseManifests(allFileContents);
|
|
7995
8382
|
// R8 (PRD §5): OS packages from an extracted container image's package DBs
|
|
7996
8383
|
// (dpkg/apk) — baked-in deps the Dockerfile never names. Feed the OSV/SBOM pipeline.
|
|
@@ -9606,3 +9993,8 @@ export {
|
|
|
9606
9993
|
STORED_TAINT_FIELD_PATTERNS, STORED_TAINT_SINK_PATTERNS,
|
|
9607
9994
|
FIXES, NEW_FIXES,
|
|
9608
9995
|
};
|
|
9996
|
+
|
|
9997
|
+
/** Internals exposed for targeted unit tests (not part of the public API). */
|
|
9998
|
+
export const _internals = {
|
|
9999
|
+
_isLikelyUnsafeRegex, _isAnchoredRegex, _hasNestedQuantifier, _extractRegexLiterals, _hasLeadingUnboundedQuantifier,
|
|
10000
|
+
};
|