fullshield 0.1.3 → 0.2.6

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/dist/index.d.ts CHANGED
@@ -42,7 +42,7 @@ interface Finding {
42
42
  note: string;
43
43
  }>;
44
44
  /** Dependency reachability (Phase 13 / harden-3). */
45
- reachability?: 'reachable' | 'imported-not-called' | 'reachable-via-dependency' | 'not-imported' | 'reachability-unknown';
45
+ reachability?: 'reachable' | 'imported-not-called' | 'reachable-via-dependency' | 'not-imported' | 'reachability-unknown' | 'symbol-reachable' | 'symbol-not-called';
46
46
  /** Lockfile path from an imported direct dependency, e.g. express > body-parser > qs. */
47
47
  dependencyPath?: string[];
48
48
  /** Deterministic (default) vs AI-added. AI findings never fail CI. */
package/dist/index.js CHANGED
@@ -1,14 +1,15 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { readFile, mkdir, writeFile, stat, unlink, readdir, mkdtemp } from 'fs/promises';
3
- import { resolve, join, basename, dirname, relative, posix } from 'path';
3
+ import { resolve, join, basename, dirname, posix, relative } from 'path';
4
4
  import { execFile, spawnSync, spawn, execFileSync } from 'child_process';
5
5
  import { createInterface } from 'readline';
6
6
  import { promisify } from 'util';
7
7
  import { performance } from 'perf_hooks';
8
- import { homedir, tmpdir } from 'os';
8
+ import { homedir, availableParallelism, tmpdir } from 'os';
9
9
  import { existsSync, statSync, readFileSync, readdirSync, cpSync, mkdirSync, rmSync } from 'fs';
10
- import ignoreModule from 'ignore';
10
+ import { Worker } from 'worker_threads';
11
11
  import { fileURLToPath } from 'url';
12
+ import ignoreModule from 'ignore';
12
13
  import pc from 'picocolors';
13
14
 
14
15
  var __defProp = Object.defineProperty;
@@ -1147,6 +1148,9 @@ function classesEnabled() {
1147
1148
  function mcpCfgEnabled() {
1148
1149
  return process.env.FULLSHIELD_EXPERIMENTAL_MCPCFG === "1";
1149
1150
  }
1151
+ function taintV2Enabled() {
1152
+ return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
1153
+ }
1150
1154
 
1151
1155
  // src/modules/config/finding.ts
1152
1156
  init_finding();
@@ -1956,6 +1960,103 @@ function exactPinsFromManifest(pkg) {
1956
1960
 
1957
1961
  // src/modules/deps/reachability.ts
1958
1962
  init_finding();
1963
+
1964
+ // src/modules/deps/symbol-reachability.ts
1965
+ function affectedFunctionNames(vuln) {
1966
+ const names = /* @__PURE__ */ new Set();
1967
+ for (const affected of vuln.affected ?? []) {
1968
+ const list = affected.ecosystem_specific?.affected_functions;
1969
+ if (!Array.isArray(list)) continue;
1970
+ for (const item of list) {
1971
+ if (typeof item === "string" && item.length > 0) names.add(item);
1972
+ }
1973
+ }
1974
+ return [...names];
1975
+ }
1976
+ function escapeRegExp(value) {
1977
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1978
+ }
1979
+ function called(source, local) {
1980
+ return new RegExp(`\\b${escapeRegExp(local)}\\s*\\(`).test(source);
1981
+ }
1982
+ function memberCalled(source, object, member) {
1983
+ return new RegExp(`\\b${escapeRegExp(object)}\\s*\\.\\s*${escapeRegExp(member)}\\s*\\(`).test(source);
1984
+ }
1985
+ function sourceCallsAffectedSymbol(source, symbol) {
1986
+ const esc2 = escapeRegExp(symbol);
1987
+ const named = new RegExp(
1988
+ `import\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`,
1989
+ "g"
1990
+ );
1991
+ let match;
1992
+ while ((match = named.exec(source)) !== null) {
1993
+ const parts = (match[1] ?? "").split(",");
1994
+ for (const part of parts) {
1995
+ const bits = part.trim().split(/\s+as\s+/);
1996
+ const imported = bits[0]?.trim();
1997
+ const local = (bits[1] ?? bits[0])?.trim();
1998
+ if (imported === symbol && local !== void 0 && called(source, local)) return true;
1999
+ }
2000
+ }
2001
+ const star = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"][^'"]+['"]/g;
2002
+ while ((match = star.exec(source)) !== null) {
2003
+ const local = match[1];
2004
+ if (local !== void 0 && memberCalled(source, local, symbol)) return true;
2005
+ }
2006
+ const subDefault = new RegExp(
2007
+ `import\\s+([A-Za-z_$][\\w$]*)\\s+from\\s+['"][^'"]+/${esc2}['"]`
2008
+ );
2009
+ const sub = subDefault.exec(source);
2010
+ if (sub?.[1] !== void 0 && called(source, sub[1])) return true;
2011
+ const reqMember = new RegExp(
2012
+ `(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+['"]\\s*\\)\\s*\\.\\s*${esc2}`
2013
+ );
2014
+ const reqHit = reqMember.exec(source);
2015
+ if (reqHit?.[1] !== void 0 && called(source, reqHit[1])) return true;
2016
+ const reqNs = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
2017
+ while ((match = reqNs.exec(source)) !== null) {
2018
+ const local = match[1];
2019
+ if (local !== void 0 && memberCalled(source, local, symbol)) return true;
2020
+ }
2021
+ const reqNamed = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
2022
+ while ((match = reqNamed.exec(source)) !== null) {
2023
+ const parts = (match[1] ?? "").split(",");
2024
+ for (const part of parts) {
2025
+ const bits = part.trim().split(/\s*:\s*/);
2026
+ const imported = bits[0]?.trim();
2027
+ const local = (bits[1] ?? bits[0])?.trim();
2028
+ if (imported === symbol && local !== void 0 && called(source, local)) return true;
2029
+ }
2030
+ }
2031
+ const reqSub = new RegExp(
2032
+ `(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+/${esc2}['"]\\s*\\)`
2033
+ );
2034
+ const subReq = reqSub.exec(source);
2035
+ if (subReq?.[1] !== void 0 && called(source, subReq[1])) return true;
2036
+ const barrel = new RegExp(`export\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`, "g");
2037
+ while ((match = barrel.exec(source)) !== null) {
2038
+ const parts = (match[1] ?? "").split(",");
2039
+ for (const part of parts) {
2040
+ const bits = part.trim().split(/\s+as\s+/);
2041
+ const exported = (bits[1] ?? bits[0])?.trim();
2042
+ if (exported === symbol) {
2043
+ const local = bits[0]?.trim();
2044
+ if (local !== void 0 && called(source, local)) return true;
2045
+ }
2046
+ }
2047
+ }
2048
+ return false;
2049
+ }
2050
+ function classifySymbolUse(symbols, sources) {
2051
+ for (const symbol of symbols) {
2052
+ for (const source of sources.values()) {
2053
+ if (sourceCallsAffectedSymbol(source, symbol)) return "symbol-reachable";
2054
+ }
2055
+ }
2056
+ return "symbol-not-called";
2057
+ }
2058
+
2059
+ // src/modules/deps/reachability.ts
1959
2060
  function packageNameFromSnippet(snippet) {
1960
2061
  if (snippet.startsWith("@")) {
1961
2062
  const second = snippet.indexOf("@", 1);
@@ -1984,9 +2085,9 @@ function isProductionSource(rel) {
1984
2085
  }
1985
2086
  function indexPackageUsage(sources) {
1986
2087
  const usage = /* @__PURE__ */ new Map();
1987
- const bump = (pkg, called) => {
2088
+ const bump = (pkg, called2) => {
1988
2089
  const prev = usage.get(pkg) ?? { called: false };
1989
- usage.set(pkg, { imported: true, called: prev.called || called });
2090
+ usage.set(pkg, { imported: true, called: prev.called || called2 });
1990
2091
  };
1991
2092
  const nsRe = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
1992
2093
  const namedRe = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
@@ -2074,10 +2175,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
2074
2175
  return { reachability: "not-imported" };
2075
2176
  }
2076
2177
  function reachabilitySteps(reachability) {
2077
- if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown") {
2178
+ if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
2078
2179
  return 0;
2079
2180
  }
2080
- if (reachability === "imported-not-called") return 1;
2181
+ if (reachability === "imported-not-called" || reachability === "symbol-not-called") return 1;
2081
2182
  return 2;
2082
2183
  }
2083
2184
  function downgradeSeverity(severity, steps) {
@@ -2094,7 +2195,7 @@ function isDevOnly(pkgName, ctx) {
2094
2195
  if (inProd) return false;
2095
2196
  return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
2096
2197
  }
2097
- function applyReachability(findings, ctx) {
2198
+ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2098
2199
  return findings.map((finding3) => {
2099
2200
  if (finding3.ruleId === "DEPS-MALICIOUS-001") {
2100
2201
  if (finding3.snippet === void 0) return finding3;
@@ -2111,7 +2212,12 @@ function applyReachability(findings, ctx) {
2111
2212
  if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
2112
2213
  const pkg = packageNameFromSnippet(finding3.snippet);
2113
2214
  if (pkg === void 0 || pkg.length === 0) return finding3;
2114
- const classified = classifyReachability(pkg, ctx.productionUsage, ctx.graph);
2215
+ let classified = classifyReachability(pkg, ctx.productionUsage, ctx.graph);
2216
+ const named = symbols.get(finding3.id) ?? [];
2217
+ if (named.length > 0 && ctx.sources !== void 0) {
2218
+ const symbolClass = classifySymbolUse(named, ctx.sources);
2219
+ classified = { reachability: symbolClass, path: classified.path };
2220
+ }
2115
2221
  const steps = reachabilitySteps(classified.reachability);
2116
2222
  let severity = steps === 0 ? finding3.severity : downgradeSeverity(finding3.severity, steps);
2117
2223
  let message = finding3.message;
@@ -4451,14 +4557,17 @@ var depsModule = {
4451
4557
  findings2.push(typosquatFinding("package.json", hit));
4452
4558
  }
4453
4559
  const osvHits2 = await queryOsv(pins, osvOpts);
4560
+ const symbols2 = /* @__PURE__ */ new Map();
4454
4561
  for (const hit of osvHits2) {
4455
4562
  for (const vuln of hit.vulns) {
4456
- findings2.push(
4457
- isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit.pkg, vuln) : vulnFinding("package.json", hit.pkg, vuln, depLoc(hit.pkg.name))
4458
- );
4563
+ const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit.pkg, vuln) : vulnFinding("package.json", hit.pkg, vuln, depLoc(hit.pkg.name));
4564
+ const names = affectedFunctionNames(vuln);
4565
+ if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols2.set(finding3.id, names);
4566
+ findings2.push(finding3);
4459
4567
  }
4460
4568
  }
4461
- return applyReachability(findings2, await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest)));
4569
+ const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
4570
+ return applyReachability(findings2, noLockCtx, symbols2);
4462
4571
  }
4463
4572
  let contents;
4464
4573
  try {
@@ -4479,14 +4588,16 @@ var depsModule = {
4479
4588
  }
4480
4589
  }
4481
4590
  const osvHits = await queryOsv(packages, osvOpts);
4591
+ const symbols = /* @__PURE__ */ new Map();
4482
4592
  for (const hit of osvHits) {
4483
4593
  for (const vuln of hit.vulns) {
4484
- findings.push(
4485
- isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit.pkg, vuln) : vulnFinding(lockFile, hit.pkg, vuln, depLoc(hit.pkg.name))
4486
- );
4594
+ const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit.pkg, vuln) : vulnFinding(lockFile, hit.pkg, vuln, depLoc(hit.pkg.name));
4595
+ const names = affectedFunctionNames(vuln);
4596
+ if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols.set(finding3.id, names);
4597
+ findings.push(finding3);
4487
4598
  }
4488
4599
  }
4489
- let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph));
4600
+ let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph), symbols);
4490
4601
  if (experimentalVex()) {
4491
4602
  reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
4492
4603
  reached.push(...scanInstalledPackages(ctx.cwd, packages));
@@ -4520,7 +4631,7 @@ async function reachabilityCtx(cwd, files, graph) {
4520
4631
  } catch {
4521
4632
  }
4522
4633
  }
4523
- return { productionUsage: indexPackageUsage(prodSources), graph };
4634
+ return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
4524
4635
  }
4525
4636
 
4526
4637
  // src/modules/mcp/index.ts
@@ -5081,6 +5192,26 @@ function isXssSanitizerCall(node) {
5081
5192
  return false;
5082
5193
  }
5083
5194
  var NO_RETURNERS = /* @__PURE__ */ new Set();
5195
+ var CALLBACK_METHODS = /* @__PURE__ */ new Set([
5196
+ "then",
5197
+ "catch",
5198
+ "finally",
5199
+ "map",
5200
+ "forEach",
5201
+ "filter",
5202
+ "flatMap",
5203
+ "find",
5204
+ "reduce"
5205
+ ]);
5206
+ var taintedThisFields = /* @__PURE__ */ new WeakMap();
5207
+ function enclosingClass(node) {
5208
+ let current = node.parent;
5209
+ while (current !== void 0) {
5210
+ if (current.type === "ClassDeclaration" || current.type === "ClassExpression") return current;
5211
+ current = current.parent;
5212
+ }
5213
+ return void 0;
5214
+ }
5084
5215
  function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
5085
5216
  if (node === void 0) return false;
5086
5217
  if (isTaintSource(node) || isFrameworkMemberTaint(node) || isFrameworkTaintCall(node) || isMcpArgsTaintSource(node)) {
@@ -5092,8 +5223,34 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
5092
5223
  case "Identifier":
5093
5224
  return tainted.has(node.name);
5094
5225
  case "MemberExpression":
5226
+ if (taintV2Enabled() && node.object.type === "ThisExpression") {
5227
+ const cls = enclosingClass(node);
5228
+ const name = propName(node);
5229
+ if (cls !== void 0 && name !== void 0 && taintedThisFields.get(cls)?.has(name) === true) {
5230
+ return true;
5231
+ }
5232
+ }
5095
5233
  return exprIsTainted(node.object, tainted, returners);
5234
+ case "ArrayExpression":
5235
+ if (!taintV2Enabled()) return false;
5236
+ return node.elements.some((el) => {
5237
+ if (el === null) return false;
5238
+ if (el.type === "SpreadElement") return exprIsTainted(el.argument, tainted, returners);
5239
+ return exprIsTainted(el, tainted, returners);
5240
+ });
5241
+ case "ObjectExpression":
5242
+ if (!taintV2Enabled()) return false;
5243
+ return node.properties.some((prop) => {
5244
+ if (prop.type === "SpreadElement") return exprIsTainted(prop.argument, tainted, returners);
5245
+ if (prop.type !== "Property") return false;
5246
+ return exprIsTainted(prop.value, tainted, returners);
5247
+ });
5096
5248
  case "CallExpression":
5249
+ if (taintV2Enabled() && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
5250
+ return node.arguments.some(
5251
+ (arg) => arg.type !== "SpreadElement" && exprIsTainted(arg, tainted, returners)
5252
+ );
5253
+ }
5097
5254
  if (isSchemaValidatorCall(node)) return false;
5098
5255
  if (node.callee.type === "Identifier" && returners.has(node.callee.name)) return true;
5099
5256
  if (node.callee.type === "MemberExpression" && exprIsTainted(node.callee.object, tainted, returners)) {
@@ -5324,6 +5481,65 @@ function scopeReturnsTaint(scope, tainted, returners) {
5324
5481
  return yes;
5325
5482
  }
5326
5483
  var MAX_TAINT_HOPS = 5;
5484
+ function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
5485
+ let changed = false;
5486
+ const mark = (fn, name) => {
5487
+ if (name === void 0) return;
5488
+ const set = seeds.get(fn) ?? /* @__PURE__ */ new Set();
5489
+ if (set.has(name)) return;
5490
+ set.add(name);
5491
+ seeds.set(fn, set);
5492
+ changed = true;
5493
+ };
5494
+ for (const fn of functions) {
5495
+ const f = facts.get(fn);
5496
+ if (f === void 0) continue;
5497
+ let parent = fn.parent;
5498
+ while (parent !== void 0 && !isFunctionNode(parent)) {
5499
+ parent = parent.parent;
5500
+ }
5501
+ if (parent !== void 0 && isFunctionNode(parent)) {
5502
+ const outer = facts.get(parent);
5503
+ if (outer !== void 0) {
5504
+ for (const name of outer.tainted) mark(fn, name);
5505
+ }
5506
+ }
5507
+ walkScope(fn, (node) => {
5508
+ if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") {
5509
+ if (node.left.object.type === "ThisExpression" && exprIsTainted(node.right, f.tainted, returners)) {
5510
+ const cls = enclosingClass(node);
5511
+ const field = propName(node.left);
5512
+ if (cls !== void 0 && field !== void 0) {
5513
+ const fields = taintedThisFields.get(cls) ?? /* @__PURE__ */ new Set();
5514
+ if (!fields.has(field)) {
5515
+ fields.add(field);
5516
+ taintedThisFields.set(cls, fields);
5517
+ changed = true;
5518
+ }
5519
+ }
5520
+ }
5521
+ }
5522
+ if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
5523
+ const method = propName(node.callee);
5524
+ if (method === void 0) return;
5525
+ if (CALLBACK_METHODS.has(method) && exprIsTainted(node.callee.object, f.tainted, returners)) {
5526
+ const callback = node.arguments[0];
5527
+ if (callback !== void 0 && isFunctionNode(callback)) {
5528
+ mark(callback, boundNames(callback.params[0])[0]);
5529
+ }
5530
+ }
5531
+ if ((method === "call" || method === "apply") && node.callee.object.type === "Identifier") {
5532
+ const target = nameToFn.get(node.callee.object.name);
5533
+ if (target === void 0 || !isFunctionNode(target)) return;
5534
+ const arg = node.arguments[1];
5535
+ if (arg !== void 0 && arg.type !== "SpreadElement" && exprIsTainted(arg, f.tainted, returners)) {
5536
+ mark(target, boundNames(target.params[0])[0]);
5537
+ }
5538
+ }
5539
+ });
5540
+ }
5541
+ return changed;
5542
+ }
5327
5543
  function analyzeFile(ast, extraReturners = NO_RETURNERS) {
5328
5544
  attachParents(ast);
5329
5545
  const facts = /* @__PURE__ */ new WeakMap();
@@ -5378,6 +5594,9 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
5378
5594
  });
5379
5595
  });
5380
5596
  }
5597
+ if (taintV2Enabled() && seedTaintV2(functions, facts, seeds, returners, nameToFn)) {
5598
+ changed = true;
5599
+ }
5381
5600
  if (!changed) break;
5382
5601
  }
5383
5602
  return facts;
@@ -5520,6 +5739,21 @@ function requireCall(node) {
5520
5739
  }
5521
5740
  return void 0;
5522
5741
  }
5742
+ function reexportsOf(ast) {
5743
+ const out = /* @__PURE__ */ new Map();
5744
+ walk(ast, (node) => {
5745
+ if (node.type !== "ExportNamedDeclaration" || node.source === null || node.source === void 0) return;
5746
+ const spec = literalString(node.source);
5747
+ if (spec === void 0) return;
5748
+ for (const specNode of node.specifiers) {
5749
+ if (specNode.type !== "ExportSpecifier") continue;
5750
+ const exported = specNode.exported.type === "Identifier" ? specNode.exported.name : void 0;
5751
+ const local = specNode.local.type === "Identifier" ? specNode.local.name : exported;
5752
+ if (exported !== void 0 && local !== void 0) out.set(exported, { spec, name: local });
5753
+ }
5754
+ });
5755
+ return out;
5756
+ }
5523
5757
  function resolveRelative(fromFile, spec, files) {
5524
5758
  if (!spec.startsWith(".")) return void 0;
5525
5759
  const base = posix.normalize(posix.join(posix.dirname(fromFile), spec));
@@ -5597,6 +5831,22 @@ function runInterproc(parsed) {
5597
5831
  functions.set(`${fn.file}#${fn.name}`, fn);
5598
5832
  }
5599
5833
  }
5834
+ if (taintV2Enabled()) {
5835
+ const barrels = /* @__PURE__ */ new Map();
5836
+ for (const p of parsed) barrels.set(p.file, reexportsOf(p.ast));
5837
+ for (const ns of requires.values()) {
5838
+ for (const [local, target] of ns) {
5839
+ const hash = target.indexOf("#");
5840
+ if (hash < 0) continue;
5841
+ const file = target.slice(0, hash);
5842
+ const name = target.slice(hash + 1);
5843
+ const re = barrels.get(file)?.get(name);
5844
+ if (re === void 0) continue;
5845
+ const resolved = resolveRelative(file, re.spec, files);
5846
+ if (resolved !== void 0) ns.set(local, `${resolved}#${re.name}`);
5847
+ }
5848
+ }
5849
+ }
5600
5850
  const queue = [];
5601
5851
  for (const p of parsed) {
5602
5852
  const facts = analyzeFile(p.ast);
@@ -6112,7 +6362,10 @@ function isMongoReceiver(node, source) {
6112
6362
  const obj = node.callee.object;
6113
6363
  if (obj.type === "Identifier") {
6114
6364
  if (isCollectionName(obj.name)) return true;
6115
- if (isPascalModel(obj.name)) return !fileHasSqlOrm;
6365
+ if (isPascalModel(obj.name)) {
6366
+ if (/extends\s+BaseEntity\b/.test(source)) return false;
6367
+ return !fileHasSqlOrm;
6368
+ }
6116
6369
  return fileHasMongo;
6117
6370
  }
6118
6371
  if (obj.type === "MemberExpression") {
@@ -7173,6 +7426,72 @@ function scanPromptInjection(file, source) {
7173
7426
  });
7174
7427
  return findings;
7175
7428
  }
7429
+ function packageVersion() {
7430
+ let dir = dirname(fileURLToPath(import.meta.url));
7431
+ for (let depth = 0; depth < 6; depth += 1) {
7432
+ try {
7433
+ const raw = readFileSync(join(dir, "package.json"), "utf8");
7434
+ const pkg = JSON.parse(raw);
7435
+ if (pkg.name === "fullshield" && typeof pkg.version === "string") {
7436
+ return pkg.version;
7437
+ }
7438
+ } catch {
7439
+ }
7440
+ const parent = dirname(dir);
7441
+ if (parent === dir) break;
7442
+ dir = parent;
7443
+ }
7444
+ throw new Error("fullshield package.json not found");
7445
+ }
7446
+
7447
+ // src/core/parse-pool.ts
7448
+ var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
7449
+ var cache = /* @__PURE__ */ new Map();
7450
+ function parsePoolEnabled() {
7451
+ return process.env.FULLSHIELD_PARSE_POOL === "1";
7452
+ }
7453
+ function parsePoolSize() {
7454
+ const raw = process.env.FULLSHIELD_PARSE_WORKERS;
7455
+ if (raw !== void 0 && raw !== "") {
7456
+ const parsed = Number(raw);
7457
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
7458
+ }
7459
+ return Math.max(1, availableParallelism() - 1);
7460
+ }
7461
+ function parseCacheKey(code) {
7462
+ return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
7463
+ }
7464
+ function parseOne(code) {
7465
+ const key = parseCacheKey(code);
7466
+ const hit = cache.get(key);
7467
+ if (hit !== void 0) return Promise.resolve(hit);
7468
+ return new Promise((resolve5, reject) => {
7469
+ const worker = new Worker(fileURLToPath(workerUrl), { workerData: { code } });
7470
+ worker.once("message", (ast) => {
7471
+ cache.set(key, ast);
7472
+ resolve5(ast);
7473
+ });
7474
+ worker.once("error", reject);
7475
+ });
7476
+ }
7477
+ async function parseWithPool(codes) {
7478
+ const size = parsePoolSize();
7479
+ const out = new Array(codes.length);
7480
+ let next = 0;
7481
+ async function run() {
7482
+ for (; ; ) {
7483
+ const index = next;
7484
+ next += 1;
7485
+ if (index >= codes.length) return;
7486
+ const code = codes[index];
7487
+ if (code === void 0) return;
7488
+ out[index] = await parseOne(code);
7489
+ }
7490
+ }
7491
+ const workers = Math.min(size, Math.max(1, codes.length));
7492
+ await Promise.all(Array.from({ length: workers }, () => run()));
7493
+ return out;
7494
+ }
7176
7495
 
7177
7496
  // src/modules/sast/engine.ts
7178
7497
  var sastRules = [
@@ -7306,7 +7625,8 @@ var sastModule = {
7306
7625
  continue;
7307
7626
  }
7308
7627
  try {
7309
- const ast = await parseSource(source);
7628
+ const ast = parsePoolEnabled() ? (await parseWithPool([source]))[0] : await parseSource(source);
7629
+ if (ast === void 0) continue;
7310
7630
  parsed.push({ file: rel, source, ast });
7311
7631
  } catch {
7312
7632
  }
@@ -7701,7 +8021,7 @@ function createAiClient(options) {
7701
8021
  const useFixtures = options.useFixtures ?? !isLiveAi();
7702
8022
  const model = options.model ?? process.env.FULLSHIELD_AI_MODEL ?? (useFixtures ? "recorded-fixture" : "qwen2.5-coder:7b");
7703
8023
  const provider = options.provider ?? process.env.FULLSHIELD_AI_PROVIDER ?? (useFixtures ? "fixture" : "ollama");
7704
- const cache = /* @__PURE__ */ new Map();
8024
+ const cache2 = /* @__PURE__ */ new Map();
7705
8025
  let spentUsd = 0;
7706
8026
  let networkCalls = 0;
7707
8027
  const liveCost = provider === "ollama" ? 0 : FIXTURE_COST;
@@ -7721,7 +8041,7 @@ function createAiClient(options) {
7721
8041
  const redactedPrompt = `${modeInstruction(req.mode)}
7722
8042
  ${wrapUntrustedCode(req.prompt)}`;
7723
8043
  const key = cacheKey(req.model ?? model, redactedPrompt);
7724
- const cached = cache.get(key);
8044
+ const cached = cache2.get(key);
7725
8045
  if (cached !== void 0) {
7726
8046
  return { ...cached, cached: true, costUsd: 0 };
7727
8047
  }
@@ -7735,7 +8055,7 @@ ${wrapUntrustedCode(req.prompt)}`;
7735
8055
  cached: false,
7736
8056
  redactedPrompt
7737
8057
  };
7738
- cache.set(key, response2);
8058
+ cache2.set(key, response2);
7739
8059
  return response2;
7740
8060
  }
7741
8061
  const fetchImpl = options.fetchImpl ?? fetch;
@@ -7785,7 +8105,7 @@ ${wrapUntrustedCode(req.prompt)}`;
7785
8105
  cached: false,
7786
8106
  redactedPrompt
7787
8107
  };
7788
- cache.set(key, response);
8108
+ cache2.set(key, response);
7789
8109
  return response;
7790
8110
  }
7791
8111
  };
@@ -8012,23 +8332,6 @@ async function listStagedFiles(cwd) {
8012
8332
  // src/core/engine.ts
8013
8333
  init_suppression();
8014
8334
  init_finding();
8015
- function packageVersion() {
8016
- let dir = dirname(fileURLToPath(import.meta.url));
8017
- for (let depth = 0; depth < 6; depth += 1) {
8018
- try {
8019
- const raw = readFileSync(join(dir, "package.json"), "utf8");
8020
- const pkg = JSON.parse(raw);
8021
- if (pkg.name === "fullshield" && typeof pkg.version === "string") {
8022
- return pkg.version;
8023
- }
8024
- } catch {
8025
- }
8026
- const parent = dirname(dir);
8027
- if (parent === dir) break;
8028
- dir = parent;
8029
- }
8030
- throw new Error("fullshield package.json not found");
8031
- }
8032
8335
 
8033
8336
  // src/policy/index.ts
8034
8337
  init_finding();
@@ -8815,15 +9118,23 @@ function renderConsoleReport(result) {
8815
9118
  );
8816
9119
  }
8817
9120
  const bySeverityDesc = [...SEVERITIES].reverse();
9121
+ const ordered = bySeverityDesc.flatMap((severity) => findings.filter((f) => f.severity === severity));
9122
+ const shown = ordered.slice(0, 10);
9123
+ const hidden = ordered.length - shown.length;
8818
9124
  for (const severity of bySeverityDesc) {
8819
- const group = findings.filter((f) => f.severity === severity);
9125
+ const group = shown.filter((f) => f.severity === severity);
8820
9126
  if (group.length === 0) continue;
8821
9127
  lines.push("");
8822
- lines.push(SEVERITY_COLOR[severity](`${severity.toUpperCase()} (${group.length})`));
9128
+ lines.push(SEVERITY_COLOR[severity](`${severity.toUpperCase()} (${findings.filter((f) => f.severity === severity).length})`));
8823
9129
  for (const finding3 of group) {
8824
9130
  lines.push(...renderFinding(finding3));
9131
+ lines.push(` docs: rules/${finding3.ruleId}.md`);
8825
9132
  }
8826
9133
  }
9134
+ if (hidden > 0) {
9135
+ lines.push("");
9136
+ lines.push(`${hidden} more \u2014 use --json`);
9137
+ }
8827
9138
  lines.push("");
8828
9139
  if (summary.total === 0) {
8829
9140
  lines.push(pc.green("\u2714 No findings."));