fullshield 0.3.0 → 0.4.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/dist/mcp.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createHash } from 'crypto';
2
- import { readFile, mkdir, writeFile, stat, readdir } from 'fs/promises';
2
+ import { readFile, mkdir, writeFile, readdir, stat } from 'fs/promises';
3
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
- import { existsSync, statSync, readFileSync, readdirSync, mkdtempSync, cpSync, writeFileSync, rmSync } from 'fs';
7
+ import { existsSync, mkdirSync, writeFileSync, statSync, readFileSync, rmSync, readdirSync, mkdtempSync, cpSync } from 'fs';
8
8
  import { homedir, availableParallelism, tmpdir } from 'os';
9
9
  import { performance } from 'perf_hooks';
10
10
  import { Worker } from 'worker_threads';
@@ -453,7 +453,7 @@ function scanTextForSecretHits(path, text) {
453
453
  return hits;
454
454
  }
455
455
  function scanTextForSecrets(path, text) {
456
- return scanTextForSecretHits(path, text).map((hit) => hit.finding);
456
+ return scanTextForSecretHits(path, text).map((hit2) => hit2.finding);
457
457
  }
458
458
  var secretsRules, REMEDIATION, MAX_FILE_SIZE, secretsModule;
459
459
  var init_secrets = __esm({
@@ -591,13 +591,13 @@ async function readBlob(cwd, hash) {
591
591
  return void 0;
592
592
  }
593
593
  }
594
- function remapHit(hit, file) {
595
- const snippet = hit.finding.snippet;
596
- const line = hit.finding.line ?? 0;
597
- const id = makeFindingId(hit.finding.ruleId, file, `${line}|${snippet ?? "file"}`);
594
+ function remapHit(hit2, file) {
595
+ const snippet = hit2.finding.snippet;
596
+ const line = hit2.finding.line ?? 0;
597
+ const id = makeFindingId(hit2.finding.ruleId, file, `${line}|${snippet ?? "file"}`);
598
598
  return {
599
- finding: { ...hit.finding, id, file },
600
- secret: hit.secret
599
+ finding: { ...hit2.finding, id, file },
600
+ secret: hit2.secret
601
601
  };
602
602
  }
603
603
  function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
@@ -687,8 +687,8 @@ async function headSecretKeys(cwd) {
687
687
  hits = scanTextForSecretHits(path, text);
688
688
  seen.set(hash, hits);
689
689
  }
690
- for (const hit of hits) {
691
- keys.add(`${hit.finding.ruleId}\0${hit.finding.snippet ?? ""}`);
690
+ for (const hit2 of hits) {
691
+ keys.add(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
692
692
  }
693
693
  }
694
694
  return keys;
@@ -721,8 +721,8 @@ async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
721
721
  hits2 = rawHits.filter((h) => allowed.has(h.finding.id));
722
722
  blobHits.set(change.hash, hits2);
723
723
  }
724
- for (const hit of hits2) {
725
- const mapped = remapHit(hit, change.path);
724
+ for (const hit2 of hits2) {
725
+ const mapped = remapHit(hit2, change.path);
726
726
  if (!first.has(mapped.finding.id)) {
727
727
  first.set(mapped.finding.id, { hit: mapped, commit: change.commit });
728
728
  }
@@ -730,11 +730,11 @@ async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
730
730
  }
731
731
  const atHead = await headSecretKeys(cwd);
732
732
  const hits = [];
733
- for (const { hit, commit } of first.values()) {
734
- const stillAtHead = atHead.has(`${hit.finding.ruleId}\0${hit.finding.snippet ?? ""}`);
733
+ for (const { hit: hit2, commit } of first.values()) {
734
+ const stillAtHead = atHead.has(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
735
735
  hits.push({
736
- finding: { ...hit.finding, introducedIn: commit, stillAtHead },
737
- secret: hit.secret
736
+ finding: { ...hit2.finding, introducedIn: commit, stillAtHead },
737
+ secret: hit2.secret
738
738
  });
739
739
  }
740
740
  const scanned = Math.min(maxCommits, total);
@@ -976,15 +976,15 @@ function providerFor(ruleId, secret) {
976
976
  async function verifySecretHits(hits, options = {}) {
977
977
  const fetchImpl = options.fetchImpl ?? fetch;
978
978
  const out = [];
979
- for (const hit of hits) {
980
- const secret = hit.secret;
979
+ for (const hit2 of hits) {
980
+ const secret = hit2.secret;
981
981
  if (secret === void 0 || secret.length === 0) {
982
- out.push({ finding: { ...hit.finding, verification: "unknown" }, secret: hit.secret });
982
+ out.push({ finding: { ...hit2.finding, verification: "unknown" }, secret: hit2.secret });
983
983
  continue;
984
984
  }
985
- const run = providerFor(hit.finding.ruleId, secret);
985
+ const run = providerFor(hit2.finding.ruleId, secret);
986
986
  const verification = run === void 0 ? "unknown" : await run(secret, fetchImpl);
987
- out.push({ finding: { ...hit.finding, verification }, secret: hit.secret });
987
+ out.push({ finding: { ...hit2.finding, verification }, secret: hit2.secret });
988
988
  }
989
989
  return out;
990
990
  }
@@ -1544,6 +1544,169 @@ var configModule = {
1544
1544
  // src/modules/deps/index.ts
1545
1545
  init_finding();
1546
1546
 
1547
+ // src/modules/deps/lockfile-modern.ts
1548
+ function addEdge(edges, from, to) {
1549
+ if (from.length === 0 || to.length === 0 || from === to) return;
1550
+ const list = edges.get(from) ?? /* @__PURE__ */ new Set();
1551
+ list.add(to);
1552
+ edges.set(from, list);
1553
+ }
1554
+ function graph(packages, edges, directProd, directDev, edgesKnown) {
1555
+ return { packages, edges, directProd, directDev, edgesKnown };
1556
+ }
1557
+ function parseYarnBerry(contents) {
1558
+ const packages = [];
1559
+ const edges = /* @__PURE__ */ new Map();
1560
+ const directProd = /* @__PURE__ */ new Set();
1561
+ const directDev = /* @__PURE__ */ new Set();
1562
+ let current;
1563
+ let inDeps = false;
1564
+ let devBlock = false;
1565
+ for (const raw of contents.split(/\r?\n/)) {
1566
+ if (!raw.startsWith(" ") && raw.trimEnd().endsWith(":")) {
1567
+ const key = raw.slice(0, -1).trim().replace(/^"|"$/g, "");
1568
+ current = key.includes("@workspace:.") ? "" : nameFromBerryKey(key);
1569
+ inDeps = false;
1570
+ devBlock = false;
1571
+ continue;
1572
+ }
1573
+ const version = /^\s+version:\s+(.+)$/.exec(raw)?.[1]?.replace(/["']/g, "").trim();
1574
+ if (version !== void 0 && current !== void 0 && current.length > 0 && !inDeps) {
1575
+ packages.push({ name: current, version: versionFromProtocol(version), path: `yarn:${current}` });
1576
+ continue;
1577
+ }
1578
+ if (/^\s+dependencies:\s*$/.test(raw)) {
1579
+ inDeps = true;
1580
+ devBlock = false;
1581
+ continue;
1582
+ }
1583
+ if (/^\s+devDependencies:\s*$/.test(raw)) {
1584
+ inDeps = true;
1585
+ devBlock = true;
1586
+ continue;
1587
+ }
1588
+ if (!inDeps || current === void 0) continue;
1589
+ const dep = /^\s{4}"?([^":]+)"?:\s+(.+)$/.exec(raw);
1590
+ if (dep?.[1] === void 0) continue;
1591
+ const depName = dep[1];
1592
+ if (current.length === 0) {
1593
+ if (devBlock) directDev.add(depName);
1594
+ else directProd.add(depName);
1595
+ } else {
1596
+ addEdge(edges, current, depName);
1597
+ }
1598
+ }
1599
+ return graph(packages, edges, directProd, directDev, true);
1600
+ }
1601
+ function nameFromBerryKey(key) {
1602
+ const first = key.split(",")[0]?.trim() ?? key;
1603
+ if (first.startsWith("@")) {
1604
+ const match = /^(@[^@/]+\/[^@/]+)@/.exec(first);
1605
+ return match?.[1];
1606
+ }
1607
+ const at = first.indexOf("@");
1608
+ return at <= 0 ? first : first.slice(0, at);
1609
+ }
1610
+ function versionFromProtocol(raw) {
1611
+ const patch = /^patch:(.+)#/.exec(raw);
1612
+ const body = patch?.[1] ?? raw;
1613
+ const npm = /^(?:npm:)?(@?[^@]+)@(.+)$/.exec(body);
1614
+ if (npm?.[2] !== void 0) return npm[2];
1615
+ return body;
1616
+ }
1617
+ function parsePnpmV9(contents) {
1618
+ const packages = [];
1619
+ const edges = /* @__PURE__ */ new Map();
1620
+ const directProd = /* @__PURE__ */ new Set();
1621
+ const directDev = /* @__PURE__ */ new Set();
1622
+ let block = "none";
1623
+ let snapshot;
1624
+ let inSnapshotDeps = false;
1625
+ for (const raw of contents.split(/\r?\n/)) {
1626
+ if (raw === "packages:") {
1627
+ block = "packages";
1628
+ snapshot = void 0;
1629
+ continue;
1630
+ }
1631
+ if (raw === "snapshots:") {
1632
+ block = "snapshots";
1633
+ snapshot = void 0;
1634
+ continue;
1635
+ }
1636
+ if (raw === "importers:" || raw.startsWith("lockfileVersion:")) {
1637
+ block = "none";
1638
+ continue;
1639
+ }
1640
+ if (block !== "snapshots" && /^\s{4}dependencies:\s*$/.test(raw)) {
1641
+ block = "importer-deps";
1642
+ continue;
1643
+ }
1644
+ if (/^\s{4}devDependencies:\s*$/.test(raw)) {
1645
+ block = "importer-dev";
1646
+ continue;
1647
+ }
1648
+ if (block === "packages") {
1649
+ const header = /^\s{2}['"]?(@?[^@'\s]+)@([^:'"]+)['"]?:/.exec(raw);
1650
+ if (header?.[1] !== void 0 && header[2] !== void 0) {
1651
+ packages.push({ name: header[1], version: header[2], path: `pnpm:${header[1]}` });
1652
+ }
1653
+ continue;
1654
+ }
1655
+ if (block === "snapshots") {
1656
+ const header = /^\s{2}['"]?(@?[^@'\s]+)@([^:'"]+)['"]?:/.exec(raw);
1657
+ if (header?.[1] !== void 0) {
1658
+ snapshot = header[1];
1659
+ inSnapshotDeps = false;
1660
+ continue;
1661
+ }
1662
+ if (/^\s+dependencies:\s*$/.test(raw)) {
1663
+ inSnapshotDeps = true;
1664
+ continue;
1665
+ }
1666
+ if (inSnapshotDeps && snapshot !== void 0) {
1667
+ const dep = /^\s{6}['"]?([^'":]+)['"]?:/.exec(raw);
1668
+ if (dep?.[1] !== void 0) addEdge(edges, snapshot, dep[1]);
1669
+ }
1670
+ continue;
1671
+ }
1672
+ if (block === "importer-deps" || block === "importer-dev") {
1673
+ const dep = /^\s{6}['"]?([^'":]+)['"]?:/.exec(raw);
1674
+ if (dep?.[1] !== void 0 && !raw.trim().startsWith("specifier") && !raw.trim().startsWith("version")) {
1675
+ if (block === "importer-dev") directDev.add(dep[1]);
1676
+ else directProd.add(dep[1]);
1677
+ }
1678
+ }
1679
+ }
1680
+ return graph(packages, edges, directProd, directDev, true);
1681
+ }
1682
+ function parseBunLock(contents) {
1683
+ const stripped = contents.replace(/^\s*\/\/.*$/gm, "");
1684
+ const packages = [];
1685
+ const edges = /* @__PURE__ */ new Map();
1686
+ const directProd = /* @__PURE__ */ new Set();
1687
+ const directDev = /* @__PURE__ */ new Set();
1688
+ try {
1689
+ const json = JSON.parse(stripped);
1690
+ for (const [name, value] of Object.entries(json.packages ?? {})) {
1691
+ if (!Array.isArray(value)) continue;
1692
+ const entry = Array.from(value);
1693
+ const spec = typeof entry[0] === "string" ? entry[0] : "";
1694
+ const version = spec.includes("@") ? spec.slice(spec.lastIndexOf("@") + 1) : spec;
1695
+ packages.push({ name, version, path: `bun:${name}` });
1696
+ const meta = entry[1];
1697
+ if (meta !== null && typeof meta === "object" && "dependencies" in meta) {
1698
+ const deps = meta.dependencies;
1699
+ if (deps !== null && typeof deps === "object") {
1700
+ for (const dep of Object.keys(deps)) addEdge(edges, name, dep);
1701
+ }
1702
+ }
1703
+ }
1704
+ } catch {
1705
+ return graph([], /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), false);
1706
+ }
1707
+ return graph(packages, edges, directProd, directDev, packages.length > 0);
1708
+ }
1709
+
1547
1710
  // src/modules/deps/lockfile.ts
1548
1711
  function basename2(filename) {
1549
1712
  const parts = filename.replace(/\\/g, "/").split("/");
@@ -1573,7 +1736,7 @@ function objectKeys(value) {
1573
1736
  }
1574
1737
  return Object.keys(value);
1575
1738
  }
1576
- function addEdge(edges, from, to) {
1739
+ function addEdge2(edges, from, to) {
1577
1740
  const set = edges.get(from) ?? /* @__PURE__ */ new Set();
1578
1741
  set.add(to);
1579
1742
  edges.set(from, set);
@@ -1653,7 +1816,7 @@ function parseYarnGraph(contents) {
1653
1816
  const depBerry = /^\s{4}(?:"([^"]+)"|(@?[\w./-]+)):/.exec(raw);
1654
1817
  const depName = depClassic?.[1] ?? depClassic?.[2] ?? depBerry?.[1] ?? depBerry?.[2];
1655
1818
  if (depName !== void 0) {
1656
- addEdge(edges, currentName, depName);
1819
+ addEdge2(edges, currentName, depName);
1657
1820
  continue;
1658
1821
  }
1659
1822
  if (/^\s{2}\S/.test(raw)) inDeps = false;
@@ -1748,7 +1911,7 @@ function parsePnpmGraph(contents) {
1748
1911
  const dep = /^ (?:'([^']+)'|"([^"]+)"|([^:]+)):/.exec(raw);
1749
1912
  const depName = dep?.[1] ?? dep?.[2] ?? dep?.[3];
1750
1913
  if (depName !== void 0) {
1751
- addEdge(edges, currentPkg, depName);
1914
+ addEdge2(edges, currentPkg, depName);
1752
1915
  continue;
1753
1916
  }
1754
1917
  if (/^ \S/.test(raw)) inPkgDeps = false;
@@ -1780,7 +1943,7 @@ function graphFromPackagesTable(packages) {
1780
1943
  const name = key === "" ? "" : nameFromPackagesKey(key);
1781
1944
  if (name === void 0 || name.length === 0) continue;
1782
1945
  for (const dep of [...objectKeys(value.dependencies), ...objectKeys(value.optionalDependencies)]) {
1783
- addEdge(edges, name, dep);
1946
+ addEdge2(edges, name, dep);
1784
1947
  }
1785
1948
  }
1786
1949
  return { edges, directProd, directDev };
@@ -1790,7 +1953,7 @@ function graphFromDependenciesTree(deps, edges) {
1790
1953
  const child = value.dependencies;
1791
1954
  if (child === void 0 || typeof child !== "object" || child === null) continue;
1792
1955
  const nested = child;
1793
- for (const childName of Object.keys(nested)) addEdge(edges, name, childName);
1956
+ for (const childName of Object.keys(nested)) addEdge2(edges, name, childName);
1794
1957
  graphFromDependenciesTree(nested, edges);
1795
1958
  }
1796
1959
  }
@@ -1803,8 +1966,16 @@ function parseLockfileGraph(filename, contents) {
1803
1966
  directDev: /* @__PURE__ */ new Set(),
1804
1967
  edgesKnown: false
1805
1968
  };
1806
- if (name === "yarn.lock") return parseYarnGraph(contents);
1807
- if (name === "pnpm-lock.yaml") return parsePnpmGraph(contents);
1969
+ const modern = process.env.FULLSHIELD_EXPERIMENTAL_LOCKFILES === "1";
1970
+ if (name === "yarn.lock") {
1971
+ if (modern && contents.includes("__metadata")) return parseYarnBerry(contents);
1972
+ return parseYarnGraph(contents);
1973
+ }
1974
+ if (name === "pnpm-lock.yaml") {
1975
+ if (modern && /lockfileVersion:\s*['"]?9/.test(contents)) return parsePnpmV9(contents);
1976
+ return parsePnpmGraph(contents);
1977
+ }
1978
+ if (modern && name === "bun.lock") return parseBunLock(contents);
1808
1979
  if (name !== "package-lock.json") return empty;
1809
1980
  try {
1810
1981
  const json = JSON.parse(contents);
@@ -1973,6 +2144,86 @@ function exactPinsFromManifest(pkg) {
1973
2144
  return out;
1974
2145
  }
1975
2146
 
2147
+ // src/modules/deps/evidence.ts
2148
+ var IMPORT_RE = /(?:require\(\s*['"]([^'"]+)['"]\s*\)|from\s+['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\))/g;
2149
+ function packageNameFromSpec(spec) {
2150
+ if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
2151
+ if (spec.startsWith("@")) {
2152
+ const slash2 = spec.indexOf("/");
2153
+ if (slash2 < 0) return void 0;
2154
+ const rest = spec.slice(slash2 + 1);
2155
+ const cut = rest.indexOf("/");
2156
+ return cut === -1 ? spec : spec.slice(0, slash2 + 1 + cut);
2157
+ }
2158
+ const slash = spec.indexOf("/");
2159
+ return slash === -1 ? spec : spec.slice(0, slash);
2160
+ }
2161
+ function collectImportSites(sources) {
2162
+ const byPkg = /* @__PURE__ */ new Map();
2163
+ const files = [...sources.keys()].sort();
2164
+ for (const file of files) {
2165
+ const text = sources.get(file) ?? "";
2166
+ const lines = text.split(/\r?\n/);
2167
+ for (let i = 0; i < lines.length; i += 1) {
2168
+ const line = lines[i] ?? "";
2169
+ IMPORT_RE.lastIndex = 0;
2170
+ let match;
2171
+ while ((match = IMPORT_RE.exec(line)) !== null) {
2172
+ const spec = match[1] ?? match[2] ?? match[3];
2173
+ if (spec === void 0) continue;
2174
+ const pkg = packageNameFromSpec(spec);
2175
+ if (pkg === void 0) continue;
2176
+ const list = byPkg.get(pkg) ?? [];
2177
+ list.push({ file, line: i + 1, specifier: spec, pkg });
2178
+ byPkg.set(pkg, list);
2179
+ }
2180
+ }
2181
+ }
2182
+ for (const [pkg, list] of byPkg) {
2183
+ list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
2184
+ byPkg.set(pkg, list.slice(0, 5));
2185
+ }
2186
+ return byPkg;
2187
+ }
2188
+ function evidenceForFinding(finding4, sites, productionFiles, edgesKnown) {
2189
+ if (finding4.ruleId !== "DEPS-VULN-001" && finding4.ruleId !== "DEPS-MALICIOUS-001") return void 0;
2190
+ const snippet = finding4.snippet ?? "";
2191
+ const at = snippet.startsWith("@") ? snippet.indexOf("@", 1) : snippet.indexOf("@");
2192
+ const pkg = at <= 0 ? snippet : snippet.slice(0, at);
2193
+ if (pkg.length === 0) return void 0;
2194
+ const imports = (sites.get(pkg) ?? []).map((site) => ({
2195
+ file: site.file,
2196
+ line: site.line,
2197
+ specifier: site.specifier
2198
+ }));
2199
+ const first = imports[0];
2200
+ const path = finding4.dependencyPath;
2201
+ let reason;
2202
+ switch (finding4.reachability) {
2203
+ case "not-imported":
2204
+ reason = `No production file imports ${pkg}. Checked ${productionFiles.length} production file(s)${productionFiles.length > 0 ? `: ${productionFiles.slice(0, 8).join(", ")}` : ""}.`;
2205
+ return { imports: [], reason };
2206
+ case "reachability-unknown":
2207
+ reason = edgesKnown ? `Reachability for ${pkg} is unknown.` : `Lockfile edges are missing, so ${pkg} is reachability-unknown.`;
2208
+ return { imports, ...first !== void 0 ? {} : {}, reason };
2209
+ case "reachable-via-dependency":
2210
+ reason = path !== void 0 && path.length > 0 ? `Required through ${path.join(" > ")}.` : `Required by an imported package.`;
2211
+ return { imports, ...path !== void 0 ? { dependencyPath: path } : {}, reason };
2212
+ case "imported-not-called":
2213
+ reason = first !== void 0 ? `Imported from ${first.file}:${first.line} and not called.` : `Imported and not called.`;
2214
+ return { imports, reason };
2215
+ case "symbol-reachable":
2216
+ reason = first !== void 0 ? `A named vulnerable export is called from ${first.file}:${first.line}.` : `A named vulnerable export is called.`;
2217
+ return { imports, reason };
2218
+ case "symbol-not-called":
2219
+ reason = `A named vulnerable export is not called.`;
2220
+ return { imports, reason };
2221
+ default:
2222
+ reason = first !== void 0 ? `Imported from ${first.file}:${first.line}.` : `Imported by production code.`;
2223
+ return { imports, ...path !== void 0 ? { dependencyPath: path } : {}, reason };
2224
+ }
2225
+ }
2226
+
1976
2227
  // src/modules/deps/reachability.ts
1977
2228
  init_finding();
1978
2229
 
@@ -2080,7 +2331,7 @@ function packageNameFromSnippet(snippet) {
2080
2331
  const at = snippet.indexOf("@");
2081
2332
  return at <= 0 ? snippet : snippet.slice(0, at);
2082
2333
  }
2083
- function packageNameFromSpec(spec) {
2334
+ function packageNameFromSpec2(spec) {
2084
2335
  if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
2085
2336
  if (spec.startsWith("@")) {
2086
2337
  const parts = spec.split("/");
@@ -2116,7 +2367,7 @@ function indexPackageUsage(sources) {
2116
2367
  const local = m[1];
2117
2368
  const spec = m[2];
2118
2369
  if (local === void 0 || spec === void 0) continue;
2119
- const pkg = packageNameFromSpec(spec);
2370
+ const pkg = packageNameFromSpec2(spec);
2120
2371
  if (pkg === void 0) continue;
2121
2372
  bump(pkg, localIsCalled(source, local));
2122
2373
  }
@@ -2126,7 +2377,7 @@ function indexPackageUsage(sources) {
2126
2377
  while ((named = namedRe.exec(source)) !== null) {
2127
2378
  const spec = named[2];
2128
2379
  if (spec === void 0) continue;
2129
- const pkg = packageNameFromSpec(spec);
2380
+ const pkg = packageNameFromSpec2(spec);
2130
2381
  if (pkg === void 0) continue;
2131
2382
  const locals = (named[1] ?? "").split(",").map((s) => s.trim().split(/\s+as\s+/).pop() ?? "");
2132
2383
  bump(
@@ -2185,34 +2436,55 @@ function reachableFrom(target, roots, edges) {
2185
2436
  if (roots.has(target)) return true;
2186
2437
  return shortestDependencyPath(target, roots, edges) !== void 0;
2187
2438
  }
2188
- function importedDirectNames(usage, graph) {
2189
- const directs = /* @__PURE__ */ new Set([...graph.directProd, ...graph.directDev]);
2439
+ function importedDirectNames(usage, graph2) {
2440
+ const directs = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
2190
2441
  const out = /* @__PURE__ */ new Set();
2191
- for (const [name, hit] of usage) {
2192
- if (!hit.imported) continue;
2442
+ for (const [name, hit2] of usage) {
2443
+ if (!hit2.imported) continue;
2193
2444
  if (directs.size === 0 || directs.has(name)) out.add(name);
2194
2445
  }
2195
2446
  return out;
2196
2447
  }
2197
- function classifyReachability(pkgName, productionUsage, graph) {
2198
- const hit = productionUsage.get(pkgName);
2199
- if (hit?.imported === true) {
2200
- return { reachability: hit.called ? "reachable" : "imported-not-called" };
2448
+ function classifyReachability(pkgName, productionUsage, graph2) {
2449
+ const hit2 = productionUsage.get(pkgName);
2450
+ if (hit2?.imported === true) {
2451
+ return { reachability: hit2.called ? "reachable" : "imported-not-called" };
2201
2452
  }
2202
- if (graph !== void 0 && graph.edgesKnown) {
2203
- const roots = importedDirectNames(productionUsage, graph);
2204
- const paths = collectDependencyPaths(pkgName, roots, graph.edges);
2453
+ if (graph2 !== void 0 && graph2.edgesKnown) {
2454
+ const roots = importedDirectNames(productionUsage, graph2);
2455
+ const paths = collectDependencyPaths(pkgName, roots, graph2.edges);
2205
2456
  const path = paths[0];
2206
2457
  if (path !== void 0 && path.length >= 2) {
2207
2458
  return { reachability: "reachable-via-dependency", path, paths };
2208
2459
  }
2209
2460
  return { reachability: "not-imported" };
2210
2461
  }
2211
- if (graph !== void 0 && !graph.edgesKnown) {
2462
+ if (graph2 !== void 0 && !graph2.edgesKnown) {
2212
2463
  return { reachability: "reachability-unknown" };
2213
2464
  }
2214
2465
  return { reachability: "not-imported" };
2215
2466
  }
2467
+ function unusedDirectPackages(ctx) {
2468
+ if (!ctx.graph.edgesKnown) return [];
2469
+ const imported = importedDirectNames(ctx.productionUsage, ctx.graph);
2470
+ return [...ctx.graph.directProd].filter((name) => {
2471
+ if (ctx.productionUsage.get(name)?.imported === true) return false;
2472
+ return !reachableFrom(name, imported, ctx.graph.edges);
2473
+ }).sort();
2474
+ }
2475
+ function unusedDirectPlan(findings, ctx) {
2476
+ const packages = unusedDirectPackages(ctx);
2477
+ if (packages.length === 0) return void 0;
2478
+ const names = new Set(packages);
2479
+ let advisories = 0;
2480
+ for (const finding4 of findings) {
2481
+ if (finding4.ruleId !== "DEPS-VULN-001" && finding4.ruleId !== "DEPS-MALICIOUS-001") continue;
2482
+ if (finding4.snippet === void 0) continue;
2483
+ const pkg = packageNameFromSnippet(finding4.snippet);
2484
+ if (pkg !== void 0 && names.has(pkg)) advisories += 1;
2485
+ }
2486
+ return { packages, advisories };
2487
+ }
2216
2488
  function reachabilitySteps(reachability) {
2217
2489
  if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
2218
2490
  return 0;
@@ -2435,6 +2707,176 @@ function osvSeverity(vuln) {
2435
2707
  return "high";
2436
2708
  }
2437
2709
 
2710
+ // src/modules/deps/upgrade-plan.ts
2711
+ function sentenceFor(bumps, unresolved) {
2712
+ const parts = [];
2713
+ if (bumps.length === 1) {
2714
+ const bump = bumps[0];
2715
+ if (bump !== void 0) {
2716
+ const noun = bump.findings === 1 ? "finding" : "findings";
2717
+ parts.push(`1 bump (${bump.name} ${bump.from} \u2192 ${bump.to}) resolves ${bump.findings} ${noun}.`);
2718
+ }
2719
+ } else if (bumps.length > 1) {
2720
+ const listed = bumps.map((bump) => `${bump.name} ${bump.from} \u2192 ${bump.to}`).join(", ");
2721
+ const total = bumps.reduce((sum, bump) => sum + bump.findings, 0);
2722
+ const noun = total === 1 ? "finding" : "findings";
2723
+ parts.push(`${bumps.length} bumps (${listed}) resolve ${total} ${noun}.`);
2724
+ }
2725
+ if (unresolved.length > 0) {
2726
+ parts.push(`No fixed version in the OSV cache for ${unresolved.join(", ")}.`);
2727
+ }
2728
+ return parts.join(" ");
2729
+ }
2730
+ function chooseUpgradePlan(advisories) {
2731
+ const byName = /* @__PURE__ */ new Map();
2732
+ for (const advisory of advisories) {
2733
+ const list = byName.get(advisory.name);
2734
+ if (list === void 0) byName.set(advisory.name, [advisory]);
2735
+ else list.push(advisory);
2736
+ }
2737
+ const unresolved = [];
2738
+ const candidates = [];
2739
+ for (const [name, list] of byName) {
2740
+ const from = list[0]?.from ?? "0.0.0";
2741
+ if (list.some((advisory) => advisory.fixed === void 0)) {
2742
+ unresolved.push(name);
2743
+ continue;
2744
+ }
2745
+ let to = list[0]?.fixed;
2746
+ for (const advisory of list) {
2747
+ if (advisory.fixed !== void 0 && (to === void 0 || compareSemver(advisory.fixed, to) > 0)) {
2748
+ to = advisory.fixed;
2749
+ }
2750
+ }
2751
+ if (to === void 0 || compareSemver(to, from) <= 0) continue;
2752
+ candidates.push({
2753
+ name,
2754
+ from,
2755
+ to,
2756
+ ids: list.map((advisory) => advisory.id)
2757
+ });
2758
+ }
2759
+ unresolved.sort();
2760
+ const chosen = smallestMaxCover(candidates);
2761
+ if (chosen.length === 0 && unresolved.length === 0) return void 0;
2762
+ const bumps = chosen.map((candidate) => ({
2763
+ name: candidate.name,
2764
+ from: candidate.from,
2765
+ to: candidate.to,
2766
+ safe: sameMajor(candidate.from, candidate.to),
2767
+ findings: candidate.ids.length
2768
+ }));
2769
+ return { bumps, unresolved, sentence: sentenceFor(bumps, unresolved) };
2770
+ }
2771
+ function smallestMaxCover(candidates) {
2772
+ const limit = Math.min(candidates.length, 12);
2773
+ let best = [];
2774
+ let bestCount = -1;
2775
+ const walk3 = (index, picked, covered) => {
2776
+ if (index === limit) {
2777
+ if (covered.size > bestCount || covered.size === bestCount && betterTie(picked, best)) {
2778
+ best = picked;
2779
+ bestCount = covered.size;
2780
+ }
2781
+ return;
2782
+ }
2783
+ const next = candidates[index];
2784
+ if (next === void 0) return;
2785
+ walk3(index + 1, picked, covered);
2786
+ const grown = new Set(covered);
2787
+ for (const id of next.ids) grown.add(id);
2788
+ walk3(index + 1, [...picked, next], grown);
2789
+ };
2790
+ walk3(0, [], /* @__PURE__ */ new Set());
2791
+ return [...best].sort((a, b) => a.name.localeCompare(b.name));
2792
+ }
2793
+ function betterTie(picked, best) {
2794
+ if (picked.length !== best.length) return picked.length < best.length;
2795
+ const left = picked.map((item) => item.name).sort().join("\0");
2796
+ const right = best.map((item) => item.name).sort().join("\0");
2797
+ return left < right;
2798
+ }
2799
+
2800
+ // src/supply/campaign.ts
2801
+ init_finding();
2802
+ var RPC_RE = /getSignaturesForAddress|eth_getLogs/;
2803
+ var NET_RE = /https\.request|http\.request|fetch\s*\(|axios\./;
2804
+ var EVAL_RE = /\beval\s*\(/;
2805
+ var STEAL_RE = /Login Data|leveldb|discord[\\/]Local Storage|wallet-extension|Ethereum\/keystore/i;
2806
+ var PLATFORM_RE = /process\.platform\s*===\s*['"]win32['"]|os\.platform\s*\(\s*\)/;
2807
+ var DOWNLOAD_RE = /https?:\/\/example\.invalid|createWriteStream|https\.get/;
2808
+ var SPAWN_RE = /child_process|spawn\s*\(|execFile\s*\(|\.exe['"]/;
2809
+ function hit(ruleId, title, message, file, severity) {
2810
+ return {
2811
+ id: makeFindingId(ruleId, file, message),
2812
+ ruleId,
2813
+ module: "deps",
2814
+ title,
2815
+ severity,
2816
+ confidence: "high",
2817
+ file,
2818
+ message,
2819
+ remediation: "Do not install this package. Remove it, delete node_modules, and reinstall from a clean lockfile."
2820
+ };
2821
+ }
2822
+ function campaignFindings(files) {
2823
+ const findings = [];
2824
+ for (const source of files) {
2825
+ const hasChain = RPC_RE.test(source.text);
2826
+ const hasNet = NET_RE.test(source.text) || EVAL_RE.test(source.text);
2827
+ if (hasChain && hasNet) {
2828
+ findings.push(
2829
+ hit(
2830
+ "SUPPLY-CHAINC2-001",
2831
+ "Package reads a blockchain RPC and then fetches or evaluates the result",
2832
+ "static: RPC method combined with fetch or eval",
2833
+ source.file,
2834
+ "high"
2835
+ )
2836
+ );
2837
+ }
2838
+ const steals = STEAL_RE.test(source.text);
2839
+ const callsNet = NET_RE.test(source.text);
2840
+ if (steals && callsNet) {
2841
+ findings.push(
2842
+ hit(
2843
+ "SUPPLY-STEALER-001",
2844
+ "Package reads a local credential store and makes a network call",
2845
+ "static: browser, chat, or wallet path combined with a network call",
2846
+ source.file,
2847
+ "critical"
2848
+ )
2849
+ );
2850
+ }
2851
+ const gated2 = PLATFORM_RE.test(source.text);
2852
+ const downloads = DOWNLOAD_RE.test(source.text);
2853
+ const spawns = SPAWN_RE.test(source.text);
2854
+ if (gated2 && downloads && spawns) {
2855
+ findings.push(
2856
+ hit(
2857
+ "SUPPLY-PLATFORMGATE-001",
2858
+ "Package downloads and launches a file only on one platform",
2859
+ "static: platform check combined with a download and a process spawn",
2860
+ source.file,
2861
+ "high"
2862
+ )
2863
+ );
2864
+ }
2865
+ }
2866
+ return findings;
2867
+ }
2868
+ function securityPlaceholderFinding(name, locked, latest) {
2869
+ if (latest !== "0.0.1-security") return void 0;
2870
+ if (locked === "0.0.1-security") return void 0;
2871
+ return hit(
2872
+ "SUPPLY-SECHOLDER-001",
2873
+ `Locked ${name}@${locked} was replaced on the registry by the security placeholder`,
2874
+ `${name} latest is 0.0.1-security while the lockfile pins ${locked}`,
2875
+ "package-lock.json",
2876
+ "critical"
2877
+ );
2878
+ }
2879
+
2438
2880
  // src/modules/deps/popular.ts
2439
2881
  var POPULAR_PACKAGES = [
2440
2882
  "express",
@@ -4466,7 +4908,7 @@ ${scripts}`;
4466
4908
  if (!TOKEN_RE.test(text)) return [];
4467
4909
  const replicates = PUBLISH_RE.test(text) || REGISTRY_PUT_RE.test(text) || GITHUB_CREATE_RE.test(text) || WORKFLOW_PATH_RE.test(text);
4468
4910
  if (!replicates) return [];
4469
- const hit = sources.find(
4911
+ const hit2 = sources.find(
4470
4912
  (s) => TOKEN_RE.test(s.text) && (PUBLISH_RE.test(s.text) || REGISTRY_PUT_RE.test(s.text) || GITHUB_CREATE_RE.test(s.text) || WORKFLOW_PATH_RE.test(s.text))
4471
4913
  ) ?? sources.find((s) => TOKEN_RE.test(s.text));
4472
4914
  return [
@@ -4474,7 +4916,7 @@ ${scripts}`;
4474
4916
  "SUPPLY-WORM-001",
4475
4917
  "Self-replicating worm indicators in dependency code",
4476
4918
  "static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",
4477
- hit?.file,
4919
+ hit2?.file,
4478
4920
  "critical"
4479
4921
  )
4480
4922
  ];
@@ -4484,13 +4926,13 @@ function analyzeWorkflowTampering(dir) {
4484
4926
  const text = `${sources.map((s) => s.text).join("\n")}
4485
4927
  ${scripts}`;
4486
4928
  if (!CI_WRITE_RE.test(text)) return [];
4487
- const hit = sources.find((s) => CI_WRITE_RE.test(s.text));
4929
+ const hit2 = sources.find((s) => CI_WRITE_RE.test(s.text));
4488
4930
  return [
4489
4931
  finding(
4490
4932
  "SUPPLY-WORKFLOW-001",
4491
4933
  "Dependency writes GitHub workflow, git hooks, or CI config",
4492
4934
  "static: install script or package code writes .github/, .git/hooks/, or CI configuration",
4493
- hit?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
4935
+ hit2?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
4494
4936
  "critical"
4495
4937
  )
4496
4938
  ];
@@ -4512,8 +4954,8 @@ function parentsOf(name, edges) {
4512
4954
  }
4513
4955
  return out;
4514
4956
  }
4515
- function isDirect(name, graph) {
4516
- return graph.directProd.has(name) || graph.directDev.has(name);
4957
+ function isDirect(name, graph2) {
4958
+ return graph2.directProd.has(name) || graph2.directDev.has(name);
4517
4959
  }
4518
4960
  function packagesWithAFixedRelease(findings) {
4519
4961
  const names = /* @__PURE__ */ new Set();
@@ -4531,17 +4973,17 @@ function targetVersion(finding4) {
4531
4973
  function kindFromLockfile(lockFile) {
4532
4974
  return lockfileKind(lockFile) ?? "npm";
4533
4975
  }
4534
- function attachOverrideProposals(findings, graph, lockFile, manifestRaw) {
4535
- if (!graph.edgesKnown || manifestRaw === void 0) return findings;
4976
+ function attachOverrideProposals(findings, graph2, lockFile, manifestRaw) {
4977
+ if (!graph2.edgesKnown || manifestRaw === void 0) return findings;
4536
4978
  const withFix = packagesWithAFixedRelease(findings);
4537
4979
  const kind = kindFromLockfile(lockFile);
4538
4980
  return findings.map((finding4) => {
4539
4981
  if (finding4.ruleId !== "DEPS-VULN-001" || finding4.snippet === void 0) return finding4;
4540
4982
  const name = packageNameFromSnippet2(finding4.snippet);
4541
- if (name.length === 0 || isDirect(name, graph)) return finding4;
4983
+ if (name.length === 0 || isDirect(name, graph2)) return finding4;
4542
4984
  const version = targetVersion(finding4);
4543
4985
  if (version === void 0) return finding4;
4544
- const parentFixed = parentsOf(name, graph.edges).some((parent) => withFix.has(parent));
4986
+ const parentFixed = parentsOf(name, graph2.edges).some((parent) => withFix.has(parent));
4545
4987
  if (parentFixed) return finding4;
4546
4988
  const after = withManifestOverride(manifestRaw, kind, name, version);
4547
4989
  const diff = unifiedDiff("package.json", manifestRaw, after);
@@ -4562,10 +5004,10 @@ function scanInstalledPackages(cwd, packages) {
4562
5004
  const dir = packageDir(cwd, pkg);
4563
5005
  if (dir === void 0) continue;
4564
5006
  const prefix = pkg.path.startsWith("node_modules/") ? pkg.path : `node_modules/${pkg.name}`;
4565
- for (const hit of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
5007
+ for (const hit2 of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
4566
5008
  out.push({
4567
- ...hit,
4568
- file: hit.file !== void 0 && hit.file.length > 0 ? `${prefix}/${hit.file}` : prefix
5009
+ ...hit2,
5010
+ file: hit2.file !== void 0 && hit2.file.length > 0 ? `${prefix}/${hit2.file}` : prefix
4569
5011
  });
4570
5012
  }
4571
5013
  }
@@ -4644,19 +5086,19 @@ function maliciousFinding(lockFile, pkg, vuln) {
4644
5086
  references: [vuln.id, ...vuln.aliases ?? []]
4645
5087
  };
4646
5088
  }
4647
- function typosquatFinding(lockFile, hit) {
4648
- const snippet = `${hit.name}@${hit.version}`;
5089
+ function typosquatFinding(lockFile, hit2) {
5090
+ const snippet = `${hit2.name}@${hit2.version}`;
4649
5091
  return {
4650
5092
  id: makeFindingId("DEPS-TYPOSQUAT-001", lockFile, snippet),
4651
5093
  ruleId: "DEPS-TYPOSQUAT-001",
4652
5094
  module: "deps",
4653
- title: `Possible typosquat: ${hit.name}`,
5095
+ title: `Possible typosquat: ${hit2.name}`,
4654
5096
  severity: "medium",
4655
5097
  confidence: "low",
4656
5098
  file: lockFile,
4657
5099
  snippet,
4658
- message: `Package "${hit.name}" is 1 character from popular package "${hit.lookalike}" (possible typosquat).`,
4659
- remediation: `Confirm you intended to install "${hit.name}". If not, remove it and depend on "${hit.lookalike}" instead.`
5100
+ message: `Package "${hit2.name}" is 1 character from popular package "${hit2.lookalike}" (possible typosquat).`,
5101
+ remediation: `Confirm you intended to install "${hit2.name}". If not, remove it and depend on "${hit2.lookalike}" instead.`
4660
5102
  };
4661
5103
  }
4662
5104
  function noLockFinding() {
@@ -4690,6 +5132,8 @@ function deprecatedFinding(lockFile, pkg) {
4690
5132
  var depsModule = {
4691
5133
  name: "deps",
4692
5134
  async run(ctx) {
5135
+ depsModule.unusedDirect = void 0;
5136
+ depsModule.upgradePlan = void 0;
4693
5137
  const lockFile = LOCKFILE_NAMES.find((name) => ctx.files.includes(name));
4694
5138
  const cacheDir = ctx.config.deps.cacheDir !== void 0 ? resolve(ctx.cwd, ctx.config.deps.cacheDir) : defaultCacheDir();
4695
5139
  const osvOpts = { cacheDir, offline: ctx.config.deps.offline };
@@ -4715,21 +5159,21 @@ var depsModule = {
4715
5159
  }
4716
5160
  const pins = exactPinsFromManifest(manifest);
4717
5161
  const directNames2 = await readDirectDependencyNames(ctx.cwd);
4718
- for (const hit of findTyposquats(pins, { directNames: directNames2 })) {
4719
- findings2.push(typosquatFinding("package.json", hit));
5162
+ for (const hit2 of findTyposquats(pins, { directNames: directNames2 })) {
5163
+ findings2.push(typosquatFinding("package.json", hit2));
4720
5164
  }
4721
5165
  const osvHits2 = await queryOsv(pins, osvOpts);
4722
5166
  const symbols2 = /* @__PURE__ */ new Map();
4723
- for (const hit of osvHits2) {
4724
- for (const vuln of hit.vulns) {
4725
- const finding4 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit.pkg, vuln) : vulnFinding("package.json", hit.pkg, vuln, depLoc(hit.pkg.name));
5167
+ for (const hit2 of osvHits2) {
5168
+ for (const vuln of hit2.vulns) {
5169
+ const finding4 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit2.pkg, vuln) : vulnFinding("package.json", hit2.pkg, vuln, depLoc(hit2.pkg.name));
4726
5170
  const names = affectedFunctionNames(vuln);
4727
5171
  if (names.length > 0 && finding4.ruleId === "DEPS-VULN-001") symbols2.set(finding4.id, names);
4728
5172
  findings2.push(finding4);
4729
5173
  }
4730
5174
  }
4731
5175
  const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
4732
- return applyReachability(findings2, noLockCtx, symbols2);
5176
+ return withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, applyReachability(findings2, noLockCtx, symbols2));
4733
5177
  }
4734
5178
  let contents;
4735
5179
  try {
@@ -4737,12 +5181,12 @@ var depsModule = {
4737
5181
  } catch {
4738
5182
  return [];
4739
5183
  }
4740
- const graph = parseLockfileGraph(lockFile, contents);
4741
- const packages = graph.packages;
5184
+ const graph2 = parseLockfileGraph(lockFile, contents);
5185
+ const packages = graph2.packages;
4742
5186
  const findings = [];
4743
5187
  const directNames = await readDirectDependencyNames(ctx.cwd);
4744
- for (const hit of findTyposquats(packages, { directNames })) {
4745
- findings.push(typosquatFinding(lockFile, hit));
5188
+ for (const hit2 of findTyposquats(packages, { directNames })) {
5189
+ findings.push(typosquatFinding(lockFile, hit2));
4746
5190
  }
4747
5191
  for (const pkg of packages) {
4748
5192
  if (pkg.deprecated !== void 0) {
@@ -4751,23 +5195,129 @@ var depsModule = {
4751
5195
  }
4752
5196
  const osvHits = await queryOsv(packages, osvOpts);
4753
5197
  const symbols = /* @__PURE__ */ new Map();
4754
- for (const hit of osvHits) {
4755
- for (const vuln of hit.vulns) {
4756
- const finding4 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit.pkg, vuln) : vulnFinding(lockFile, hit.pkg, vuln, depLoc(hit.pkg.name));
5198
+ for (const hit2 of osvHits) {
5199
+ for (const vuln of hit2.vulns) {
5200
+ const finding4 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit2.pkg, vuln) : vulnFinding(lockFile, hit2.pkg, vuln, depLoc(hit2.pkg.name));
4757
5201
  const names = affectedFunctionNames(vuln);
4758
5202
  if (names.length > 0 && finding4.ruleId === "DEPS-VULN-001") symbols.set(finding4.id, names);
4759
5203
  findings.push(finding4);
4760
5204
  }
4761
5205
  }
4762
- let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph), symbols);
5206
+ const reachCtx = await reachabilityCtx(ctx.cwd, ctx.files, graph2);
5207
+ let reached = applyReachability(findings, reachCtx, symbols);
4763
5208
  if (experimentalVex()) {
4764
- reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
5209
+ reached = attachOverrideProposals(reached, graph2, lockFile, manifestRaw);
4765
5210
  reached.push(...scanInstalledPackages(ctx.cwd, packages));
4766
5211
  }
4767
5212
  if (licenseEnabled()) reached.push(...await scanCopyleftLicenses(ctx.cwd));
5213
+ depsModule.unusedDirect = unusedDirectPlan(reached, reachCtx);
5214
+ const direct = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
5215
+ const advisories = [];
5216
+ for (const hit2 of osvHits) {
5217
+ if (!direct.has(hit2.pkg.name)) continue;
5218
+ for (const vuln of hit2.vulns) {
5219
+ if (isMaliciousAdvisory(vuln)) continue;
5220
+ advisories.push({
5221
+ name: hit2.pkg.name,
5222
+ from: hit2.pkg.version,
5223
+ id: vuln.id,
5224
+ fixed: minFixedVersion(vuln.affected ?? [])
5225
+ });
5226
+ }
5227
+ }
5228
+ depsModule.upgradePlan = chooseUpgradePlan(advisories);
5229
+ if (process.env.FULLSHIELD_EXPERIMENTAL_REACH_EVIDENCE === "1") {
5230
+ const sites = collectImportSites(reachCtx.sources);
5231
+ const files = [...reachCtx.sources.keys()].sort();
5232
+ reached = reached.map((finding4) => {
5233
+ const evidence = evidenceForFinding(finding4, sites, files, graph2.edgesKnown);
5234
+ return evidence === void 0 ? finding4 : { ...finding4, reachabilityEvidence: evidence };
5235
+ });
5236
+ }
5237
+ reached = await withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, reached);
5238
+ if (process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY === "1") {
5239
+ reached.push(...campaignFindings([...reachCtx.sources.entries()].map(([file, text]) => ({ file, text }))));
5240
+ for (const name of [...direct].sort()) {
5241
+ const locked = packages.find((pkg) => pkg.name === name)?.version;
5242
+ if (locked === void 0) continue;
5243
+ const latest = await readPlaceholderLatest(join(cacheDir, "registry", `${name.replaceAll("/", "__")}.json`));
5244
+ const placeholder = securityPlaceholderFinding(name, locked, latest);
5245
+ if (placeholder !== void 0) reached.push(placeholder);
5246
+ }
5247
+ }
4768
5248
  return reached;
4769
5249
  }
4770
5250
  };
5251
+ function workspacesEnabled() {
5252
+ return process.env.FULLSHIELD_EXPERIMENTAL_WORKSPACES === "1";
5253
+ }
5254
+ function workspacePatterns(manifestRaw) {
5255
+ try {
5256
+ const manifest = JSON.parse(manifestRaw);
5257
+ const field = manifest.workspaces;
5258
+ if (Array.isArray(field)) return field.filter((item) => typeof item === "string");
5259
+ if (field !== null && typeof field === "object" && Array.isArray(field.packages)) {
5260
+ return field.packages.filter((item) => typeof item === "string");
5261
+ }
5262
+ } catch {
5263
+ return [];
5264
+ }
5265
+ return [];
5266
+ }
5267
+ async function expandWorkspace(cwd, pattern) {
5268
+ const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
5269
+ if (!normalized.includes("*")) return [normalized];
5270
+ const slash = normalized.indexOf("/*");
5271
+ if (slash < 0) return [];
5272
+ const parent = normalized.slice(0, slash);
5273
+ try {
5274
+ const entries = await readdir(join(cwd, parent), { withFileTypes: true });
5275
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => `${parent}/${entry.name}`).sort();
5276
+ } catch {
5277
+ return [];
5278
+ }
5279
+ }
5280
+ async function workspaceAdvisoryFindings(cwd, manifestRaw, osvOpts) {
5281
+ const dirs = /* @__PURE__ */ new Set();
5282
+ for (const pattern of workspacePatterns(manifestRaw)) {
5283
+ for (const dir of await expandWorkspace(cwd, pattern)) dirs.add(dir);
5284
+ }
5285
+ const findings = [];
5286
+ for (const dir of [...dirs].sort()) {
5287
+ if (dir === "" || dir === ".") continue;
5288
+ const lockName = LOCKFILE_NAMES.find((name) => existsSync(join(cwd, dir, name)));
5289
+ if (lockName === void 0) continue;
5290
+ let text;
5291
+ try {
5292
+ text = await readFile(join(cwd, dir, lockName), "utf8");
5293
+ } catch {
5294
+ continue;
5295
+ }
5296
+ const graph2 = parseLockfileGraph(lockName, text);
5297
+ const hits = await queryOsv(graph2.packages, osvOpts);
5298
+ const file = `${dir}/${lockName}`;
5299
+ for (const hit2 of hits) {
5300
+ for (const vuln of hit2.vulns) {
5301
+ findings.push(
5302
+ isMaliciousAdvisory(vuln) ? maliciousFinding(file, hit2.pkg, vuln) : vulnFinding(file, hit2.pkg, vuln)
5303
+ );
5304
+ }
5305
+ }
5306
+ }
5307
+ return findings;
5308
+ }
5309
+ async function withWorkspaceAdvisories(cwd, manifestRaw, osvOpts, findings) {
5310
+ if (!workspacesEnabled() || manifestRaw === void 0) return findings;
5311
+ return [...findings, ...await workspaceAdvisoryFindings(cwd, manifestRaw, osvOpts)];
5312
+ }
5313
+ async function readPlaceholderLatest(file) {
5314
+ try {
5315
+ const raw = JSON.parse(await readFile(file, "utf8"));
5316
+ return typeof raw.latest === "string" ? raw.latest : void 0;
5317
+ } catch {
5318
+ return void 0;
5319
+ }
5320
+ }
4771
5321
  function graphFromManifest(manifest) {
4772
5322
  const keys = (field) => {
4773
5323
  const block = manifest[field];
@@ -4784,7 +5334,7 @@ function graphFromManifest(manifest) {
4784
5334
  edgesKnown: false
4785
5335
  };
4786
5336
  }
4787
- async function reachabilityCtx(cwd, files, graph) {
5337
+ async function reachabilityCtx(cwd, files, graph2) {
4788
5338
  const prodSources = /* @__PURE__ */ new Map();
4789
5339
  for (const rel of files) {
4790
5340
  if (!/\.[cm]?[jt]sx?$/.test(rel)) continue;
@@ -4794,7 +5344,7 @@ async function reachabilityCtx(cwd, files, graph) {
4794
5344
  } catch {
4795
5345
  }
4796
5346
  }
4797
- return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
5347
+ return { productionUsage: indexPackageUsage(prodSources), graph: graph2, sources: prodSources };
4798
5348
  }
4799
5349
 
4800
5350
  // src/modules/mcp/index.ts
@@ -5146,67 +5696,200 @@ init_finding();
5146
5696
  // src/modules/sast/interproc.ts
5147
5697
  init_finding();
5148
5698
 
5149
- // src/modules/sast/frameworks.ts
5150
- var NEXT_HANDLERS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5151
- var NEST_HTTP = /^(Get|Post|Put|Patch|Delete|All|Head|Options)$/;
5152
- var NEST_INPUT = /^(Body|Query|Param|Headers)$/;
5153
- var REQ_READERS = /* @__PURE__ */ new Set(["json", "formData", "text", "arrayBuffer"]);
5154
- var HONO_READERS = /* @__PURE__ */ new Set(["query", "param", "header", "json", "parseBody", "text"]);
5155
- function decoratorExpr(dec) {
5156
- if (dec.type !== "Decorator") return void 0;
5157
- return dec.expression;
5158
- }
5159
- function decoratorName(dec) {
5160
- const expr = decoratorExpr(dec);
5161
- if (expr === void 0) return void 0;
5162
- if (expr.type === "Identifier") return expr.name;
5163
- if (expr.type === "CallExpression") return calleeName(expr);
5164
- return void 0;
5165
- }
5166
- function nodeDecorators(node) {
5167
- const decs = node.decorators;
5168
- return Array.isArray(decs) ? decs : [];
5169
- }
5170
- function nestInputDecorator(param) {
5171
- if (!frameworksEnabled()) return void 0;
5172
- for (const dec of nodeDecorators(param)) {
5173
- const name = decoratorName(dec);
5174
- if (name !== void 0 && NEST_INPUT.test(name)) return name;
5699
+ // src/runtime/lru.ts
5700
+ var LruMap = class {
5701
+ constructor(max) {
5702
+ this.max = max;
5703
+ if (max < 1) throw new Error("LruMap max must be >= 1");
5175
5704
  }
5176
- return void 0;
5177
- }
5178
- function nestDecoratorArgCount(param, name) {
5179
- for (const dec of nodeDecorators(param)) {
5180
- if (decoratorName(dec) !== name) continue;
5181
- const expr = decoratorExpr(dec);
5182
- if (expr?.type === "CallExpression") return expr.arguments.length;
5183
- return 0;
5705
+ max;
5706
+ map = /* @__PURE__ */ new Map();
5707
+ get size() {
5708
+ return this.map.size;
5184
5709
  }
5185
- return 0;
5186
- }
5187
- function isCtxIdent(node) {
5188
- return node.type === "Identifier" && node.name === "ctx";
5189
- }
5190
- function isCReq(node) {
5191
- return node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "c" && propName(node) === "req";
5192
- }
5193
- function isCtxRequest(node) {
5194
- return node.type === "MemberExpression" && isCtxIdent(node.object) && propName(node) === "request";
5195
- }
5196
- function isFrameworkMemberTaint(node) {
5197
- if (!frameworksEnabled() || node.type !== "MemberExpression") return false;
5198
- const name = propName(node);
5199
- if (name === void 0) return false;
5200
- if (isCtxIdent(node.object) && (REQUEST_SOURCE_PROPS.has(name) || name === "query")) return true;
5201
- if (isCtxRequest(node.object) && REQUEST_SOURCE_PROPS.has(name)) return true;
5202
- return false;
5203
- }
5204
- function isFrameworkTaintCall(node) {
5205
- if (!frameworksEnabled() || node.type !== "CallExpression") return false;
5206
- const name = calleeName(node);
5207
- if (name === void 0 || node.callee.type !== "MemberExpression") return false;
5208
- const obj = node.callee.object;
5209
- if (obj.type === "Identifier" && obj.name === "request" && REQ_READERS.has(name)) return true;
5710
+ has(key) {
5711
+ return this.map.has(key);
5712
+ }
5713
+ get(key) {
5714
+ if (!this.map.has(key)) return void 0;
5715
+ const value = this.map.get(key);
5716
+ this.map.delete(key);
5717
+ this.map.set(key, value);
5718
+ return value;
5719
+ }
5720
+ set(key, value) {
5721
+ if (this.map.has(key)) this.map.delete(key);
5722
+ this.map.set(key, value);
5723
+ while (this.map.size > this.max) {
5724
+ const oldest = this.map.keys().next().value;
5725
+ if (oldest === void 0) break;
5726
+ this.map.delete(oldest);
5727
+ }
5728
+ return this;
5729
+ }
5730
+ };
5731
+ function packageVersion() {
5732
+ let dir = dirname(fileURLToPath(import.meta.url));
5733
+ for (let depth = 0; depth < 6; depth += 1) {
5734
+ try {
5735
+ const raw = readFileSync(join(dir, "package.json"), "utf8");
5736
+ const pkg = JSON.parse(raw);
5737
+ if (pkg.name === "fullshield" && typeof pkg.version === "string") {
5738
+ return pkg.version;
5739
+ }
5740
+ } catch {
5741
+ }
5742
+ const parent = dirname(dir);
5743
+ if (parent === dir) break;
5744
+ dir = parent;
5745
+ }
5746
+ throw new Error("fullshield package.json not found");
5747
+ }
5748
+
5749
+ // src/core/parse-pool.ts
5750
+ var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
5751
+ var cache = new LruMap(64);
5752
+ function parsePoolEnabled() {
5753
+ return process.env.FULLSHIELD_PARSE_POOL === "1";
5754
+ }
5755
+ function parsePoolSize() {
5756
+ const raw = process.env.FULLSHIELD_PARSE_WORKERS;
5757
+ if (raw !== void 0 && raw !== "") {
5758
+ const parsed = Number(raw);
5759
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
5760
+ }
5761
+ return Math.min(2, Math.max(1, availableParallelism() - 1));
5762
+ }
5763
+ function workerMemoryMb() {
5764
+ const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
5765
+ const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
5766
+ if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
5767
+ return 512;
5768
+ }
5769
+ function parseCacheDir() {
5770
+ return join(homedir(), ".cache", "fullshield", "parse");
5771
+ }
5772
+ function parseCacheKey(code) {
5773
+ return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
5774
+ }
5775
+ function parseOne(code) {
5776
+ const key = parseCacheKey(code);
5777
+ const hit2 = cache.get(key);
5778
+ if (hit2 !== void 0) return Promise.resolve(hit2);
5779
+ return new Promise((resolve5, reject) => {
5780
+ const worker = new Worker(fileURLToPath(workerUrl), {
5781
+ workerData: { code },
5782
+ resourceLimits: { maxOldGenerationSizeMb: workerMemoryMb() }
5783
+ });
5784
+ let settled = false;
5785
+ const finish = () => {
5786
+ void worker.terminate();
5787
+ };
5788
+ const succeed = (ast) => {
5789
+ if (settled) return;
5790
+ settled = true;
5791
+ cache.set(key, ast);
5792
+ finish();
5793
+ resolve5(ast);
5794
+ };
5795
+ const fail = (error) => {
5796
+ if (settled) return;
5797
+ settled = true;
5798
+ finish();
5799
+ reject(error);
5800
+ };
5801
+ worker.once("message", (ast) => {
5802
+ succeed(ast);
5803
+ });
5804
+ worker.once("error", (error) => {
5805
+ fail(error);
5806
+ });
5807
+ worker.once("exit", (code2) => {
5808
+ if (settled) return;
5809
+ fail(new Error(`parse worker exited ${code2 ?? "null"}`));
5810
+ });
5811
+ });
5812
+ }
5813
+ async function parseWithPool(codes) {
5814
+ const size = parsePoolSize();
5815
+ const out = new Array(codes.length);
5816
+ let next = 0;
5817
+ async function run() {
5818
+ for (; ; ) {
5819
+ const index = next;
5820
+ next += 1;
5821
+ if (index >= codes.length) return;
5822
+ const code = codes[index];
5823
+ if (code === void 0) return;
5824
+ out[index] = await parseOne(code);
5825
+ }
5826
+ }
5827
+ const workers = Math.min(size, Math.max(1, codes.length));
5828
+ await Promise.all(Array.from({ length: workers }, () => run()));
5829
+ return out;
5830
+ }
5831
+
5832
+ // src/modules/sast/frameworks.ts
5833
+ var NEXT_HANDLERS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5834
+ var NEST_HTTP = /^(Get|Post|Put|Patch|Delete|All|Head|Options)$/;
5835
+ var NEST_INPUT = /^(Body|Query|Param|Headers)$/;
5836
+ var REQ_READERS = /* @__PURE__ */ new Set(["json", "formData", "text", "arrayBuffer"]);
5837
+ var HONO_READERS = /* @__PURE__ */ new Set(["query", "param", "header", "json", "parseBody", "text"]);
5838
+ function decoratorExpr(dec) {
5839
+ if (dec.type !== "Decorator") return void 0;
5840
+ return dec.expression;
5841
+ }
5842
+ function decoratorName(dec) {
5843
+ const expr = decoratorExpr(dec);
5844
+ if (expr === void 0) return void 0;
5845
+ if (expr.type === "Identifier") return expr.name;
5846
+ if (expr.type === "CallExpression") return calleeName(expr);
5847
+ return void 0;
5848
+ }
5849
+ function nodeDecorators(node) {
5850
+ const decs = node.decorators;
5851
+ return Array.isArray(decs) ? decs : [];
5852
+ }
5853
+ function nestInputDecorator(param) {
5854
+ if (!frameworksEnabled()) return void 0;
5855
+ for (const dec of nodeDecorators(param)) {
5856
+ const name = decoratorName(dec);
5857
+ if (name !== void 0 && NEST_INPUT.test(name)) return name;
5858
+ }
5859
+ return void 0;
5860
+ }
5861
+ function nestDecoratorArgCount(param, name) {
5862
+ for (const dec of nodeDecorators(param)) {
5863
+ if (decoratorName(dec) !== name) continue;
5864
+ const expr = decoratorExpr(dec);
5865
+ if (expr?.type === "CallExpression") return expr.arguments.length;
5866
+ return 0;
5867
+ }
5868
+ return 0;
5869
+ }
5870
+ function isCtxIdent(node) {
5871
+ return node.type === "Identifier" && node.name === "ctx";
5872
+ }
5873
+ function isCReq(node) {
5874
+ return node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "c" && propName(node) === "req";
5875
+ }
5876
+ function isCtxRequest(node) {
5877
+ return node.type === "MemberExpression" && isCtxIdent(node.object) && propName(node) === "request";
5878
+ }
5879
+ function isFrameworkMemberTaint(node) {
5880
+ if (!frameworksEnabled() || node.type !== "MemberExpression") return false;
5881
+ const name = propName(node);
5882
+ if (name === void 0) return false;
5883
+ if (isCtxIdent(node.object) && (REQUEST_SOURCE_PROPS.has(name) || name === "query")) return true;
5884
+ if (isCtxRequest(node.object) && REQUEST_SOURCE_PROPS.has(name)) return true;
5885
+ return false;
5886
+ }
5887
+ function isFrameworkTaintCall(node) {
5888
+ if (!frameworksEnabled() || node.type !== "CallExpression") return false;
5889
+ const name = calleeName(node);
5890
+ if (name === void 0 || node.callee.type !== "MemberExpression") return false;
5891
+ const obj = node.callee.object;
5892
+ if (obj.type === "Identifier" && obj.name === "request" && REQ_READERS.has(name)) return true;
5210
5893
  if (obj.type === "Identifier" && (obj.name === "formData" || obj.name === "searchParams") && name === "get") {
5211
5894
  return true;
5212
5895
  }
@@ -5281,6 +5964,10 @@ function isMcpArgsTaintSource(node) {
5281
5964
  }
5282
5965
 
5283
5966
  // src/modules/sast/taint.ts
5967
+ var customTaint;
5968
+ function setCustomTaint(config) {
5969
+ customTaint = config;
5970
+ }
5284
5971
  var SCHEMA_ROOT = /^(z|zod|joi|yup|schema)$/i;
5285
5972
  function objectChainMentions(node, pattern) {
5286
5973
  let current = node;
@@ -5426,6 +6113,61 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
5426
6113
  return false;
5427
6114
  }
5428
6115
  }
6116
+ function valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners) {
6117
+ if (node === void 0) return false;
6118
+ if (isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) return false;
6119
+ if (node.type === "Identifier") {
6120
+ if (nosqlSanitized.has(node.name)) return false;
6121
+ return built.has(node.name) || tainted.has(node.name);
6122
+ }
6123
+ if (isRequestObjectNode(node)) return true;
6124
+ if (node.type === "AwaitExpression") {
6125
+ return valueFeedsFilter(node.argument, tainted, returners, nosqlSanitized, built, filterReturners);
6126
+ }
6127
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && filterReturners.has(node.callee.name)) {
6128
+ return true;
6129
+ }
6130
+ if (node.type === "ObjectExpression") {
6131
+ return node.properties.some((prop) => {
6132
+ if (prop.type === "SpreadElement") {
6133
+ return valueFeedsFilter(prop.argument, tainted, returners, nosqlSanitized, built, filterReturners);
6134
+ }
6135
+ if (prop.type !== "Property") return false;
6136
+ return valueFeedsFilter(prop.value, tainted, returners, nosqlSanitized, built, filterReturners);
6137
+ });
6138
+ }
6139
+ if (node.type === "ArrayExpression") {
6140
+ return node.elements.some((el) => {
6141
+ if (el === null) return false;
6142
+ if (el.type === "SpreadElement") {
6143
+ return valueFeedsFilter(el.argument, tainted, returners, nosqlSanitized, built, filterReturners);
6144
+ }
6145
+ return valueFeedsFilter(el, tainted, returners, nosqlSanitized, built, filterReturners);
6146
+ });
6147
+ }
6148
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
6149
+ return node.arguments.some(
6150
+ (arg) => arg.type !== "SpreadElement" && valueFeedsFilter(arg, tainted, returners, nosqlSanitized, built, filterReturners)
6151
+ );
6152
+ }
6153
+ if (node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "MemberExpression") {
6154
+ return exprIsTainted(node, tainted, returners);
6155
+ }
6156
+ return false;
6157
+ }
6158
+ function initIsBuiltFilter(node, tainted, returners, nosqlSanitized, built, filterReturners) {
6159
+ if (node.type === "AwaitExpression") {
6160
+ return initIsBuiltFilter(node.argument, tainted, returners, nosqlSanitized, built, filterReturners);
6161
+ }
6162
+ if (node.type === "Identifier") return built.has(node.name) && !nosqlSanitized.has(node.name);
6163
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && filterReturners.has(node.callee.name)) {
6164
+ return true;
6165
+ }
6166
+ if (node.type === "ObjectExpression" || node.type === "ArrayExpression") {
6167
+ return valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners);
6168
+ }
6169
+ return node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign" && valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners);
6170
+ }
5429
6171
  function exprIsSanitized(node, tainted, names, isCall, returners = NO_RETURNERS) {
5430
6172
  if (node === void 0) return true;
5431
6173
  if (isCall(node)) return true;
@@ -5489,7 +6231,7 @@ function seedMcpToolParams(scope, tainted) {
5489
6231
  for (const name of boundNames(param)) tainted.add(name);
5490
6232
  }
5491
6233
  }
5492
- function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
6234
+ function analyzeScope(scope, seed = [], returners = NO_RETURNERS, filterReturners = NO_RETURNERS, initialBuilt = NO_RETURNERS) {
5493
6235
  const tainted = new Set(seed);
5494
6236
  seedDestructuredRequestParams(scope, tainted);
5495
6237
  seedFrameworkParams(scope, tainted);
@@ -5497,6 +6239,8 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5497
6239
  const urlSanitized = /* @__PURE__ */ new Set();
5498
6240
  const xssSanitized = /* @__PURE__ */ new Set();
5499
6241
  const nosqlSanitized = /* @__PURE__ */ new Set();
6242
+ const sqlCleared = /* @__PURE__ */ new Set();
6243
+ const customPath = /* @__PURE__ */ new Set();
5500
6244
  let changed = true;
5501
6245
  while (changed) {
5502
6246
  changed = false;
@@ -5507,6 +6251,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5507
6251
  for (const name of boundNames(node.id)) nosqlSanitized.add(name);
5508
6252
  }
5509
6253
  if (isSchemaValidatorCall(init)) return;
6254
+ applyCustomCall(init, boundNames(node.id), tainted, sqlCleared, customPath);
5510
6255
  if (isUrlSanitizerCall(init)) {
5511
6256
  for (const name of boundNames(node.id)) urlSanitized.add(name);
5512
6257
  }
@@ -5527,6 +6272,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5527
6272
  for (const name of boundNames(node.left)) nosqlSanitized.add(name);
5528
6273
  }
5529
6274
  if (isSchemaValidatorCall(node.right)) return;
6275
+ applyCustomCall(node.right, boundNames(node.left), tainted, sqlCleared, customPath);
5530
6276
  if (isUrlSanitizerCall(node.right)) {
5531
6277
  for (const name of boundNames(node.left)) urlSanitized.add(name);
5532
6278
  }
@@ -5553,7 +6299,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5553
6299
  for (const name of boundNames(node.left)) resolved.add(name);
5554
6300
  }
5555
6301
  });
5556
- const pathSanitized = /* @__PURE__ */ new Set();
6302
+ const pathSanitized = new Set(customPath);
5557
6303
  walkScope(scope, (node) => {
5558
6304
  if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
5559
6305
  if (propName(node.callee) !== "startsWith") return;
@@ -5595,17 +6341,92 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5595
6341
  for (const name of boundNames(node.left)) requestObjects.add(name);
5596
6342
  }
5597
6343
  });
6344
+ const builtFilters = new Set(initialBuilt);
6345
+ let grew = true;
6346
+ while (grew) {
6347
+ grew = false;
6348
+ const add = (names) => {
6349
+ for (const name of names) {
6350
+ if (nosqlSanitized.has(name) || builtFilters.has(name)) continue;
6351
+ builtFilters.add(name);
6352
+ grew = true;
6353
+ }
6354
+ };
6355
+ walkScope(scope, (node) => {
6356
+ if (node.type === "VariableDeclarator" && node.init !== null) {
6357
+ if (initIsBuiltFilter(node.init, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
6358
+ add(boundNames(node.id));
6359
+ }
6360
+ }
6361
+ if (node.type !== "AssignmentExpression") return;
6362
+ if (node.left.type === "Identifier" && initIsBuiltFilter(node.right, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
6363
+ add([node.left.name]);
6364
+ }
6365
+ if (node.left.type === "MemberExpression" && node.left.object.type === "Identifier" && valueFeedsFilter(node.right, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
6366
+ add([node.left.object.name]);
6367
+ }
6368
+ });
6369
+ }
5598
6370
  return {
5599
6371
  tainted,
5600
6372
  pathSanitized,
6373
+ sqlCleared,
5601
6374
  urlSanitized,
5602
6375
  xssSanitized,
5603
6376
  nosqlSanitized,
5604
6377
  hasPasswordName,
5605
6378
  requestObjects,
6379
+ builtFilters,
6380
+ filterReturners: new Set(filterReturners),
5606
6381
  returners: new Set(returners)
5607
6382
  };
5608
6383
  }
6384
+ function locallyDeclared(scope) {
6385
+ const names = /* @__PURE__ */ new Set();
6386
+ if (isFunctionNode(scope)) {
6387
+ for (const param of scope.params) {
6388
+ for (const name of boundNames(param)) names.add(name);
6389
+ }
6390
+ }
6391
+ walkScope(scope, (node) => {
6392
+ if (node.type === "VariableDeclarator") {
6393
+ for (const name of boundNames(node.id)) names.add(name);
6394
+ }
6395
+ if (node.type === "FunctionDeclaration" && node.id !== null) names.add(node.id.name);
6396
+ });
6397
+ return names;
6398
+ }
6399
+ function inheritedBuiltFilters(fn, facts) {
6400
+ const local = locallyDeclared(fn);
6401
+ const inherited = /* @__PURE__ */ new Set();
6402
+ let parent = fn.parent;
6403
+ while (parent !== void 0 && !isFunctionNode(parent)) {
6404
+ parent = parent.parent;
6405
+ }
6406
+ const outer = parent === void 0 ? void 0 : facts.get(parent);
6407
+ if (outer === void 0) return inherited;
6408
+ for (const name of outer.builtFilters) {
6409
+ if (!local.has(name)) inherited.add(name);
6410
+ }
6411
+ return inherited;
6412
+ }
6413
+ function scopeReturnsBuiltFilter(scope, facts) {
6414
+ let yes = false;
6415
+ walkScope(scope, (node) => {
6416
+ if (node.type !== "ReturnStatement" || node.argument === null) return;
6417
+ if (initIsBuiltFilter(
6418
+ node.argument,
6419
+ facts.tainted,
6420
+ facts.returners,
6421
+ facts.nosqlSanitized,
6422
+ facts.builtFilters,
6423
+ facts.filterReturners
6424
+ )) {
6425
+ yes = true;
6426
+ }
6427
+ });
6428
+ return yes;
6429
+ }
5609
6430
  function functionName(fn) {
5610
6431
  if (fn.type === "FunctionDeclaration" && fn.id !== null) return fn.id.name;
5611
6432
  if (isFunctionNode(fn) && fn.id !== null && fn.id.type === "Identifier") return fn.id.name;
@@ -5695,11 +6516,12 @@ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
5695
6516
  const seeds = /* @__PURE__ */ new Map();
5696
6517
  for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
5697
6518
  const returners = new Set(extraReturners2);
6519
+ const filterReturners = /* @__PURE__ */ new Set();
5698
6520
  for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
5699
6521
  let changed = false;
5700
- facts.set(ast, analyzeScope(ast, [], returners));
6522
+ facts.set(ast, analyzeScope(ast, [], returners, filterReturners));
5701
6523
  for (const fn of functions) {
5702
- facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners));
6524
+ facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners, filterReturners, inheritedBuiltFilters(fn, facts)));
5703
6525
  }
5704
6526
  for (const fn of functions) {
5705
6527
  const name = functionName(fn);
@@ -5709,6 +6531,10 @@ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
5709
6531
  returners.add(name);
5710
6532
  changed = true;
5711
6533
  }
6534
+ if (scopeReturnsBuiltFilter(fn, f) && !filterReturners.has(name)) {
6535
+ filterReturners.add(name);
6536
+ changed = true;
6537
+ }
5712
6538
  }
5713
6539
  for (const fn of functions) {
5714
6540
  const f = facts.get(fn);
@@ -5741,6 +6567,54 @@ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
5741
6567
  }
5742
6568
  return facts;
5743
6569
  }
6570
+ var SQL_SINK_NAMES = /* @__PURE__ */ new Set(["query", "execute"]);
6571
+ var CMD_SINK_NAMES = /* @__PURE__ */ new Set(["exec", "execSync", "spawn", "spawnSync"]);
6572
+ var PATH_SINK_NAMES = /* @__PURE__ */ new Set(["readFile", "readFileSync", "writeFile", "writeFileSync"]);
6573
+ function sinkClasses(scope, tainted, returners) {
6574
+ const found = /* @__PURE__ */ new Set();
6575
+ walkScope(scope, (node) => {
6576
+ if (node.type !== "CallExpression") return;
6577
+ const name = calleeName(node);
6578
+ const first = node.arguments[0];
6579
+ if (name === void 0 || first === void 0 || first.type === "SpreadElement") return;
6580
+ if (!exprIsTainted(first, tainted, returners)) return;
6581
+ if (SQL_SINK_NAMES.has(name)) found.add("sqli");
6582
+ if (CMD_SINK_NAMES.has(name)) found.add("cmdi");
6583
+ if (PATH_SINK_NAMES.has(name)) found.add("path");
6584
+ });
6585
+ return [...found].sort();
6586
+ }
6587
+ function fileTaintSummary(ast) {
6588
+ attachParents(ast);
6589
+ const facts = analyzeFile(ast);
6590
+ const returnsTaint = [];
6591
+ const functions = [];
6592
+ walk(ast, (node) => {
6593
+ if (!isFunctionNode(node)) return;
6594
+ const name = functionName(node);
6595
+ const own = facts.get(node);
6596
+ if (name === void 0 || own === void 0) return;
6597
+ if (scopeReturnsTaint(node, own.tainted, own.returners)) returnsTaint.push(name);
6598
+ const paramsToReturn = [];
6599
+ const sinks = [];
6600
+ node.params.forEach((param, index) => {
6601
+ const pname = boundNames(param)[0];
6602
+ if (pname === void 0) return;
6603
+ const seeded = analyzeScope(node, [pname]);
6604
+ if (scopeReturnsTaint(node, seeded.tainted, seeded.returners)) paramsToReturn.push(index);
6605
+ for (const cls of sinkClasses(node, seeded.tainted, seeded.returners)) sinks.push({ param: index, class: cls });
6606
+ });
6607
+ functions.push({
6608
+ name,
6609
+ line: node.loc?.start.line ?? 0,
6610
+ paramsToReturn,
6611
+ sinks
6612
+ });
6613
+ });
6614
+ returnsTaint.sort();
6615
+ functions.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
6616
+ return { returnsTaint, functions };
6617
+ }
5744
6618
  function functionsThatReturnTaint(ast) {
5745
6619
  attachParents(ast);
5746
6620
  const names = /* @__PURE__ */ new Set();
@@ -5762,18 +6636,50 @@ function factsFor(node, map) {
5762
6636
  return map.get(enclosingScope(node)) ?? {
5763
6637
  tainted: /* @__PURE__ */ new Set(),
5764
6638
  pathSanitized: /* @__PURE__ */ new Set(),
6639
+ sqlCleared: /* @__PURE__ */ new Set(),
5765
6640
  urlSanitized: /* @__PURE__ */ new Set(),
5766
6641
  xssSanitized: /* @__PURE__ */ new Set(),
5767
6642
  nosqlSanitized: /* @__PURE__ */ new Set(),
5768
6643
  hasPasswordName: false,
5769
6644
  requestObjects: /* @__PURE__ */ new Set(),
6645
+ builtFilters: /* @__PURE__ */ new Set(),
6646
+ filterReturners: /* @__PURE__ */ new Set(),
5770
6647
  returners: /* @__PURE__ */ new Set()
5771
6648
  };
5772
6649
  }
6650
+ function nodeHoldsBuiltFilter(node, map) {
6651
+ const facts = factsFor(node, map);
6652
+ if (node.type === "Identifier") return facts.builtFilters.has(node.name);
6653
+ if (node.type === "AwaitExpression") return nodeHoldsBuiltFilter(node.argument, map);
6654
+ if (node.type === "CallExpression" && node.callee.type === "Identifier") {
6655
+ return facts.filterReturners.has(node.callee.name);
6656
+ }
6657
+ return false;
6658
+ }
5773
6659
  function nodeIsTainted(node, map) {
5774
6660
  const facts = factsFor(node, map);
5775
6661
  return exprIsTainted(node, facts.tainted, facts.returners);
5776
6662
  }
6663
+ function nodeIsSqlCleared(node, map) {
6664
+ if (node.type === "Identifier") return factsFor(node, map).sqlCleared.has(node.name);
6665
+ return false;
6666
+ }
6667
+ function applyCustomCall(init, names, tainted, sqlCleared, customPath) {
6668
+ if (customTaint === void 0 || init === void 0 || init.type !== "CallExpression") return;
6669
+ const called2 = calleeName(init);
6670
+ if (called2 === void 0) return;
6671
+ if (customTaint.sources.has(called2)) {
6672
+ for (const name of names) tainted.add(name);
6673
+ }
6674
+ const clears = customTaint.sanitizers.get(called2);
6675
+ if (clears === void 0) return;
6676
+ if (clears.has("sqli")) {
6677
+ for (const name of names) sqlCleared.add(name);
6678
+ }
6679
+ if (clears.has("path")) {
6680
+ for (const name of names) customPath.add(name);
6681
+ }
6682
+ }
5777
6683
  function nodeIsPathSanitized(node, map) {
5778
6684
  if (node.type !== "Identifier") return false;
5779
6685
  return factsFor(node, map).pathSanitized.has(node.name);
@@ -5785,15 +6691,156 @@ function nodeIsSanitizedFor(node, map, kind) {
5785
6691
  }
5786
6692
  return exprIsSanitized(node, facts.tainted, facts.xssSanitized, isXssSanitizerCall, facts.returners);
5787
6693
  }
5788
- function nodeIsRequestObject(node, map) {
5789
- if (isRequestObjectNode(node) || isFrameworkRequestObject(node)) return true;
5790
- if (node.type === "Identifier") return factsFor(node, map).requestObjects.has(node.name);
5791
- return false;
6694
+ function nodeIsRequestObject(node, map) {
6695
+ if (isRequestObjectNode(node) || isFrameworkRequestObject(node)) return true;
6696
+ if (node.type === "Identifier") return factsFor(node, map).requestObjects.has(node.name);
6697
+ return false;
6698
+ }
6699
+ function nodeIsNosqlSanitized(node, map) {
6700
+ if (isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) return true;
6701
+ if (node.type === "Identifier") return factsFor(node, map).nosqlSanitized.has(node.name);
6702
+ return false;
6703
+ }
6704
+
6705
+ // src/modules/sast/summaries.ts
6706
+ function taintSummariesEnabled() {
6707
+ return process.env.FULLSHIELD_TAINT_SUMMARIES === "1";
6708
+ }
6709
+ var configHash = "";
6710
+ function setTaintSummaryConfig(config) {
6711
+ fileMemo.clear();
6712
+ if (config === void 0) {
6713
+ configHash = "";
6714
+ return;
6715
+ }
6716
+ configHash = createHash("sha256").update(
6717
+ JSON.stringify({
6718
+ maxFileBytes: config.sast.maxFileBytes,
6719
+ sources: config.sast.sources ?? [],
6720
+ sanitizers: config.sast.sanitizers ?? [],
6721
+ taintV2: taintV2Enabled(),
6722
+ frameworks: frameworksEnabled(),
6723
+ gaps: sastGapsEnabled(),
6724
+ classes: classesEnabled(),
6725
+ custom: process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1"
6726
+ })
6727
+ ).digest("hex");
6728
+ }
6729
+ function summaryDir() {
6730
+ return process.env.FULLSHIELD_TAINT_SUMMARY_DIR ?? join(parseCacheDir(), "taint");
6731
+ }
6732
+ function taintSummaryKey(source, extra = "") {
6733
+ return createHash("sha256").update(packageVersion()).update("\0").update(configHash).update("\0").update(extra).update("\0").update(source).digest("hex");
6734
+ }
6735
+ function summaryPath(key) {
6736
+ return join(summaryDir(), `${key}.json`);
6737
+ }
6738
+ function isStringArray(value) {
6739
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
6740
+ }
6741
+ function readJson3(key) {
6742
+ let text;
6743
+ try {
6744
+ text = readFileSync(summaryPath(key), "utf8");
6745
+ } catch {
6746
+ return void 0;
6747
+ }
6748
+ try {
6749
+ return JSON.parse(text);
6750
+ } catch {
6751
+ discard(key);
6752
+ return void 0;
6753
+ }
6754
+ }
6755
+ function discard(key) {
6756
+ rmSync(summaryPath(key), { force: true });
6757
+ }
6758
+ function writeJson(key, value) {
6759
+ const dir = summaryDir();
6760
+ mkdirSync(dir, { recursive: true });
6761
+ writeFileSync(summaryPath(key), JSON.stringify(value));
6762
+ }
6763
+ function readFileSummary(key) {
6764
+ const raw = readJson3(key);
6765
+ if (typeof raw !== "object" || raw === null) {
6766
+ if (raw !== void 0) discard(key);
6767
+ return void 0;
6768
+ }
6769
+ const row = raw;
6770
+ if (row.v !== 1 || !isStringArray(row.returnsTaint) || !Array.isArray(row.functions)) {
6771
+ discard(key);
6772
+ return void 0;
6773
+ }
6774
+ return row;
6775
+ }
6776
+ var fileMemo = /* @__PURE__ */ new Map();
6777
+ function cachedReturners(source, ast) {
6778
+ const key = taintSummaryKey(source, "file");
6779
+ const memo = fileMemo.get(key);
6780
+ if (memo !== void 0) return new Set(memo.returnsTaint);
6781
+ const cached = readFileSummary(key);
6782
+ if (cached !== void 0) {
6783
+ fileMemo.set(key, cached);
6784
+ return new Set(cached.returnsTaint);
6785
+ }
6786
+ const summary = fileTaintSummary(ast);
6787
+ const stored = { v: 1, returnsTaint: summary.returnsTaint, functions: summary.functions };
6788
+ fileMemo.set(key, stored);
6789
+ writeJson(key, stored);
6790
+ return new Set(stored.returnsTaint);
6791
+ }
6792
+ function lists(facts) {
6793
+ const names = (set) => [...set].sort();
6794
+ return {
6795
+ v: 1,
6796
+ tainted: names(facts.tainted),
6797
+ pathSanitized: names(facts.pathSanitized),
6798
+ sqlCleared: names(facts.sqlCleared),
6799
+ urlSanitized: names(facts.urlSanitized),
6800
+ xssSanitized: names(facts.xssSanitized),
6801
+ nosqlSanitized: names(facts.nosqlSanitized),
6802
+ requestObjects: names(facts.requestObjects),
6803
+ builtFilters: names(facts.builtFilters),
6804
+ filterReturners: names(facts.filterReturners),
6805
+ returners: names(facts.returners),
6806
+ hasPasswordName: facts.hasPasswordName
6807
+ };
6808
+ }
6809
+ function factsFrom(row) {
6810
+ return {
6811
+ tainted: new Set(row.tainted),
6812
+ pathSanitized: new Set(row.pathSanitized),
6813
+ sqlCleared: new Set(row.sqlCleared),
6814
+ urlSanitized: new Set(row.urlSanitized),
6815
+ xssSanitized: new Set(row.xssSanitized),
6816
+ nosqlSanitized: new Set(row.nosqlSanitized),
6817
+ requestObjects: new Set(row.requestObjects),
6818
+ builtFilters: new Set(row.builtFilters),
6819
+ filterReturners: new Set(row.filterReturners),
6820
+ returners: new Set(row.returners),
6821
+ hasPasswordName: row.hasPasswordName
6822
+ };
5792
6823
  }
5793
- function nodeIsNosqlSanitized(node, map) {
5794
- if (isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) return true;
5795
- if (node.type === "Identifier") return factsFor(node, map).nosqlSanitized.has(node.name);
5796
- return false;
6824
+ function readFacts(key) {
6825
+ const raw = readJson3(key);
6826
+ if (typeof raw !== "object" || raw === null) {
6827
+ if (raw !== void 0) discard(key);
6828
+ return void 0;
6829
+ }
6830
+ const row = raw;
6831
+ if (row.v !== 1 || !isStringArray(row.tainted) || !isStringArray(row.pathSanitized) || !isStringArray(row.sqlCleared) || !isStringArray(row.urlSanitized) || !isStringArray(row.xssSanitized) || !isStringArray(row.nosqlSanitized) || !isStringArray(row.requestObjects) || !isStringArray(row.builtFilters) || !isStringArray(row.filterReturners) || !isStringArray(row.returners) || typeof row.hasPasswordName !== "boolean") {
6832
+ discard(key);
6833
+ return void 0;
6834
+ }
6835
+ return factsFrom(row);
6836
+ }
6837
+ function cachedFunctionFacts(fileSource, fn, seed) {
6838
+ const key = taintSummaryKey(fileSource, `seed:${fn.loc?.start.line ?? 0}:${seed.join(",")}`);
6839
+ const cached = readFacts(key);
6840
+ if (cached !== void 0) return cached;
6841
+ const facts = analyzeFunctionWithSeed(fn, seed);
6842
+ writeJson(key, lists(facts));
6843
+ return facts;
5797
6844
  }
5798
6845
 
5799
6846
  // src/modules/sast/interproc.ts
@@ -5906,22 +6953,22 @@ function collectInstances(ast, ns) {
5906
6953
  if (node.type !== "VariableDeclarator" || node.init === null) return;
5907
6954
  if (node.id.type !== "Identifier" || node.init.type !== "NewExpression") return;
5908
6955
  if (node.init.callee.type !== "Identifier") return;
5909
- const hit = ns.get(node.init.callee.name);
5910
- if (hit === void 0) return;
5911
- instances.set(node.id.name, hit.includes("#") ? hit.split("#")[0] ?? hit : hit);
6956
+ const hit2 = ns.get(node.init.callee.name);
6957
+ if (hit2 === void 0) return;
6958
+ instances.set(node.id.name, hit2.includes("#") ? hit2.split("#")[0] ?? hit2 : hit2);
5912
6959
  });
5913
6960
  return instances;
5914
6961
  }
5915
6962
  function resolveCallee(node, ns, instances, currentFile, functions) {
5916
6963
  if (node.type !== "CallExpression") return void 0;
5917
6964
  if (node.callee.type === "Identifier") {
5918
- const hit = ns.get(node.callee.name);
5919
- if (hit !== void 0) {
5920
- if (hit.includes("#")) {
5921
- const [file, name] = hit.split("#");
6965
+ const hit2 = ns.get(node.callee.name);
6966
+ if (hit2 !== void 0) {
6967
+ if (hit2.includes("#")) {
6968
+ const [file, name] = hit2.split("#");
5922
6969
  if (file !== void 0 && name !== void 0) return { file, name };
5923
6970
  }
5924
- return { file: hit, name: node.callee.name };
6971
+ return { file: hit2, name: node.callee.name };
5925
6972
  }
5926
6973
  if (functions.has(`${currentFile}#${node.callee.name}`)) {
5927
6974
  return { file: currentFile, name: node.callee.name };
@@ -6028,9 +7075,9 @@ function runInterproc(parsed) {
6028
7075
  if (fn === void 0) continue;
6029
7076
  const param = fn.params[seed.paramIndex];
6030
7077
  if (param === void 0) continue;
6031
- const facts = analyzeFunctionWithSeed(fn.node, [param]);
6032
7078
  const p = byFile.get(fn.file);
6033
7079
  if (p === void 0) continue;
7080
+ const facts = taintSummariesEnabled() ? cachedFunctionFacts(p.source, fn.node, [param]) : analyzeFunctionWithSeed(fn.node, [param]);
6034
7081
  const ns = requires.get(fn.file) ?? /* @__PURE__ */ new Map();
6035
7082
  const inst = instances.get(fn.file) ?? /* @__PURE__ */ new Map();
6036
7083
  walk(fn.node, (node) => {
@@ -6453,6 +7500,178 @@ var jwtRule = {
6453
7500
  }
6454
7501
  };
6455
7502
 
7503
+ // src/modules/sast/validation.ts
7504
+ var PRIMITIVE = /* @__PURE__ */ new Set(["string", "number", "boolean", "IsString", "IsNumber", "IsInt", "IsBoolean"]);
7505
+ var OPEN = /* @__PURE__ */ new Set(["any", "unknown", "object", "array", "record", "lazy", "union", "tuple"]);
7506
+ var SCHEMA_ROOT2 = /* @__PURE__ */ new Set(["z", "zod", "Joi", "joi"]);
7507
+ var ROUTE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
7508
+ function validationEnabled() {
7509
+ return process.env.FULLSHIELD_EXPERIMENTAL_VALIDATION === "1";
7510
+ }
7511
+ function parentOf(node) {
7512
+ return node.parent;
7513
+ }
7514
+ function mentions(node, names) {
7515
+ let current = node;
7516
+ for (let i = 0; i < 12 && current !== void 0; i += 1) {
7517
+ if (current.type === "CallExpression") {
7518
+ const name = calleeName(current);
7519
+ if (name !== void 0 && names.has(name)) return true;
7520
+ current = current.callee.type === "MemberExpression" ? current.callee.object : void 0;
7521
+ continue;
7522
+ }
7523
+ if (current.type === "MemberExpression") {
7524
+ const name = propName(current);
7525
+ if (name !== void 0 && names.has(name)) return true;
7526
+ current = current.object;
7527
+ continue;
7528
+ }
7529
+ if (current.type === "Identifier" && names.has(current.name)) return true;
7530
+ break;
7531
+ }
7532
+ return false;
7533
+ }
7534
+ function rootName(node) {
7535
+ let current = node;
7536
+ for (let i = 0; i < 8 && current !== void 0; i += 1) {
7537
+ if (current.type === "Identifier") return current.name;
7538
+ if (current.type === "CallExpression") {
7539
+ current = current.callee;
7540
+ continue;
7541
+ }
7542
+ if (current.type === "MemberExpression") {
7543
+ current = current.object;
7544
+ continue;
7545
+ }
7546
+ break;
7547
+ }
7548
+ return void 0;
7549
+ }
7550
+ function propertyName(prop) {
7551
+ if (prop.type !== "Property") return void 0;
7552
+ const key = prop.key;
7553
+ if (key.type === "Identifier") return key.name;
7554
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
7555
+ return void 0;
7556
+ }
7557
+ function fieldsOfObjectSchema(node) {
7558
+ if (node.type !== "CallExpression" || calleeName(node) !== "object") return /* @__PURE__ */ new Set();
7559
+ const root = rootName(node.callee);
7560
+ if (root === void 0 || !SCHEMA_ROOT2.has(root)) return /* @__PURE__ */ new Set();
7561
+ const arg = node.arguments[0];
7562
+ if (arg === void 0 || arg.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
7563
+ const fields = /* @__PURE__ */ new Set();
7564
+ for (const prop of arg.properties) {
7565
+ if (prop.type !== "Property") continue;
7566
+ const name = propertyName(prop);
7567
+ if (name === void 0) continue;
7568
+ if (mentions(prop.value, OPEN)) continue;
7569
+ if (mentions(prop.value, PRIMITIVE)) fields.add(name);
7570
+ }
7571
+ return fields;
7572
+ }
7573
+ function decoratorName2(decorator) {
7574
+ const expr = decorator.type === "Decorator" ? decorator.expression : decorator;
7575
+ if (expr === void 0) return void 0;
7576
+ if (expr.type === "CallExpression") return calleeName(expr);
7577
+ if (expr.type === "Identifier") return expr.name;
7578
+ return void 0;
7579
+ }
7580
+ function fieldsOfClass(node) {
7581
+ if (node.type !== "ClassDeclaration" && node.type !== "ClassExpression") return /* @__PURE__ */ new Set();
7582
+ if (node.body.type !== "ClassBody") return /* @__PURE__ */ new Set();
7583
+ const fields = /* @__PURE__ */ new Set();
7584
+ for (const element of node.body.body) {
7585
+ if (element.type !== "PropertyDefinition") continue;
7586
+ const key = element.key;
7587
+ const name = key.type === "Identifier" ? key.name : void 0;
7588
+ if (name === void 0) continue;
7589
+ const decorators = element.decorators ?? [];
7590
+ if (decorators.some((decorator) => PRIMITIVE.has(decoratorName2(decorator) ?? ""))) fields.add(name);
7591
+ }
7592
+ return fields;
7593
+ }
7594
+ function programOf(node) {
7595
+ let current = node;
7596
+ while (current !== void 0 && current.type !== "Program") current = parentOf(current);
7597
+ return current;
7598
+ }
7599
+ function bindings(program) {
7600
+ const map = /* @__PURE__ */ new Map();
7601
+ walk(program, (node) => {
7602
+ if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.init !== null && node.init !== void 0) {
7603
+ const fields = fieldsOfObjectSchema(node.init);
7604
+ if (fields.size > 0) map.set(node.id.name, fields);
7605
+ }
7606
+ if (node.type === "ClassDeclaration" && node.id !== null) {
7607
+ const fields = fieldsOfClass(node);
7608
+ if (fields.size > 0) map.set(node.id.name, fields);
7609
+ }
7610
+ });
7611
+ return map;
7612
+ }
7613
+ var bindingCache = /* @__PURE__ */ new WeakMap();
7614
+ function fieldsOfExpr(expr, known) {
7615
+ if (expr.type === "Identifier") return new Set(known.get(expr.name) ?? []);
7616
+ if (expr.type === "ClassExpression" || expr.type === "ClassDeclaration") return fieldsOfClass(expr);
7617
+ const direct = fieldsOfObjectSchema(expr);
7618
+ if (expr.type !== "CallExpression") return direct;
7619
+ for (const arg of expr.arguments) {
7620
+ if (arg.type === "SpreadElement") continue;
7621
+ for (const name of fieldsOfExpr(arg, known)) direct.add(name);
7622
+ }
7623
+ return direct;
7624
+ }
7625
+ function isRouteCall2(node) {
7626
+ if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return false;
7627
+ const method = propName(node.callee);
7628
+ if (method === void 0 || !ROUTE_METHODS.has(method)) return false;
7629
+ return node.callee.object.type === "Identifier" && /^(app|router)$/i.test(node.callee.object.name);
7630
+ }
7631
+ function routeOf(node) {
7632
+ let current = node;
7633
+ while (current !== void 0) {
7634
+ const parent = parentOf(current);
7635
+ if (isFunctionNode(current) && parent !== void 0 && isRouteCall2(parent)) return parent;
7636
+ current = parent;
7637
+ }
7638
+ return void 0;
7639
+ }
7640
+ function requestField(node) {
7641
+ if (node.type !== "MemberExpression" || node.computed === true) return void 0;
7642
+ const field = node.property.type === "Identifier" ? node.property.name : void 0;
7643
+ if (field === void 0 || node.object.type !== "MemberExpression" || node.object.computed === true) return void 0;
7644
+ const bag = node.object.property.type === "Identifier" ? node.object.property.name : void 0;
7645
+ if (bag !== "body" && bag !== "query" && bag !== "params") return void 0;
7646
+ if (node.object.object.type !== "Identifier" || node.object.object.name !== "req") return void 0;
7647
+ return field;
7648
+ }
7649
+ var routeCache = /* @__PURE__ */ new WeakMap();
7650
+ function fieldsForRoute(route) {
7651
+ const cached = routeCache.get(route);
7652
+ if (cached !== void 0) return cached;
7653
+ const program = programOf(route);
7654
+ const known = program === void 0 ? /* @__PURE__ */ new Map() : bindingCache.get(program) ?? bindings(program);
7655
+ if (program !== void 0) bindingCache.set(program, known);
7656
+ const fields = /* @__PURE__ */ new Set();
7657
+ if (route.type === "CallExpression") {
7658
+ for (const arg of route.arguments) {
7659
+ if (arg.type === "SpreadElement" || isFunctionNode(arg)) continue;
7660
+ for (const name of fieldsOfExpr(arg, known)) fields.add(name);
7661
+ }
7662
+ }
7663
+ routeCache.set(route, fields);
7664
+ return fields;
7665
+ }
7666
+ function isValidatedRouteField(node) {
7667
+ if (!validationEnabled()) return false;
7668
+ const field = requestField(node);
7669
+ if (field === void 0) return false;
7670
+ const route = routeOf(node);
7671
+ if (route === void 0) return false;
7672
+ return fieldsForRoute(route).has(field);
7673
+ }
7674
+
6456
7675
  // src/modules/sast/rules/nosqli.ts
6457
7676
  var MONGO_SINKS = /* @__PURE__ */ new Set([
6458
7677
  "find",
@@ -6537,6 +7756,7 @@ function isNosqlSafeValue(node, source, isNosqlSanitized) {
6537
7756
  if (isNosqlSanitized(node) || isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) {
6538
7757
  return true;
6539
7758
  }
7759
+ if (isValidatedRouteField(node)) return true;
6540
7760
  return isSanitizeWrap(node, source);
6541
7761
  }
6542
7762
  function objectHasUnsafeTaintedValue(node, isTainted, isSafe) {
@@ -6600,6 +7820,14 @@ var nosqliRule = {
6600
7820
  return;
6601
7821
  }
6602
7822
  if (middleware) return;
7823
+ if (ctx.holdsBuiltFilter(filter)) {
7824
+ ctx.report({
7825
+ node,
7826
+ message: "A Mongo filter built from request input is passed to the query.",
7827
+ confidence: "medium"
7828
+ });
7829
+ return;
7830
+ }
6603
7831
  if (objectHasUnsafeTaintedValue(filter, (n) => ctx.isTainted(n), isSafe)) {
6604
7832
  ctx.report({
6605
7833
  node,
@@ -6776,7 +8004,7 @@ var sqliRule = {
6776
8004
  CallExpression(node) {
6777
8005
  if (node.type !== "CallExpression" || !isSqlSink2(node)) return;
6778
8006
  const sql = node.arguments[0];
6779
- if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql)) {
8007
+ if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql) && !ctx.isSqlCleared(sql)) {
6780
8008
  ctx.report({
6781
8009
  node,
6782
8010
  message: "Tainted input is concatenated into a SQL string passed to query/execute."
@@ -6926,27 +8154,27 @@ function numberLiteral(node) {
6926
8154
  return node.value;
6927
8155
  }
6928
8156
  function containsIdentMember(node, objectName) {
6929
- let hit = false;
8157
+ let hit2 = false;
6930
8158
  walk(node, (child) => {
6931
8159
  if (child.type === "MemberExpression" && child.object.type === "Identifier" && child.object.name === objectName) {
6932
- hit = true;
8160
+ hit2 = true;
6933
8161
  }
6934
8162
  });
6935
- return hit;
8163
+ return hit2;
6936
8164
  }
6937
8165
  function containsCallNamed(node, name) {
6938
- let hit = false;
8166
+ let hit2 = false;
6939
8167
  walk(node, (child) => {
6940
- if (child.type === "CallExpression" && calleeName(child) === name) hit = true;
8168
+ if (child.type === "CallExpression" && calleeName(child) === name) hit2 = true;
6941
8169
  });
6942
- return hit;
8170
+ return hit2;
6943
8171
  }
6944
8172
  function containsOriginalName(node) {
6945
- let hit = false;
8173
+ let hit2 = false;
6946
8174
  walk(node, (child) => {
6947
- if (child.type === "MemberExpression" && propName(child) === "originalname") hit = true;
8175
+ if (child.type === "MemberExpression" && propName(child) === "originalname") hit2 = true;
6948
8176
  });
6949
- return hit;
8177
+ return hit2;
6950
8178
  }
6951
8179
  function schemaLooksSafe(schema) {
6952
8180
  if (schema === void 0) return false;
@@ -7567,136 +8795,6 @@ function scanPromptInjection(file, source) {
7567
8795
  return findings;
7568
8796
  }
7569
8797
 
7570
- // src/runtime/lru.ts
7571
- var LruMap = class {
7572
- constructor(max) {
7573
- this.max = max;
7574
- if (max < 1) throw new Error("LruMap max must be >= 1");
7575
- }
7576
- max;
7577
- map = /* @__PURE__ */ new Map();
7578
- get size() {
7579
- return this.map.size;
7580
- }
7581
- has(key) {
7582
- return this.map.has(key);
7583
- }
7584
- get(key) {
7585
- if (!this.map.has(key)) return void 0;
7586
- const value = this.map.get(key);
7587
- this.map.delete(key);
7588
- this.map.set(key, value);
7589
- return value;
7590
- }
7591
- set(key, value) {
7592
- if (this.map.has(key)) this.map.delete(key);
7593
- this.map.set(key, value);
7594
- while (this.map.size > this.max) {
7595
- const oldest = this.map.keys().next().value;
7596
- if (oldest === void 0) break;
7597
- this.map.delete(oldest);
7598
- }
7599
- return this;
7600
- }
7601
- };
7602
- function packageVersion() {
7603
- let dir = dirname(fileURLToPath(import.meta.url));
7604
- for (let depth = 0; depth < 6; depth += 1) {
7605
- try {
7606
- const raw = readFileSync(join(dir, "package.json"), "utf8");
7607
- const pkg = JSON.parse(raw);
7608
- if (pkg.name === "fullshield" && typeof pkg.version === "string") {
7609
- return pkg.version;
7610
- }
7611
- } catch {
7612
- }
7613
- const parent = dirname(dir);
7614
- if (parent === dir) break;
7615
- dir = parent;
7616
- }
7617
- throw new Error("fullshield package.json not found");
7618
- }
7619
-
7620
- // src/core/parse-pool.ts
7621
- var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
7622
- var cache = new LruMap(64);
7623
- function parsePoolEnabled() {
7624
- return process.env.FULLSHIELD_PARSE_POOL === "1";
7625
- }
7626
- function parsePoolSize() {
7627
- const raw = process.env.FULLSHIELD_PARSE_WORKERS;
7628
- if (raw !== void 0 && raw !== "") {
7629
- const parsed = Number(raw);
7630
- if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
7631
- }
7632
- return Math.min(2, Math.max(1, availableParallelism() - 1));
7633
- }
7634
- function workerMemoryMb() {
7635
- const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
7636
- const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
7637
- if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
7638
- return 512;
7639
- }
7640
- function parseCacheKey(code) {
7641
- return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
7642
- }
7643
- function parseOne(code) {
7644
- const key = parseCacheKey(code);
7645
- const hit = cache.get(key);
7646
- if (hit !== void 0) return Promise.resolve(hit);
7647
- return new Promise((resolve5, reject) => {
7648
- const worker = new Worker(fileURLToPath(workerUrl), {
7649
- workerData: { code },
7650
- resourceLimits: { maxOldGenerationSizeMb: workerMemoryMb() }
7651
- });
7652
- let settled = false;
7653
- const finish = () => {
7654
- void worker.terminate();
7655
- };
7656
- const succeed = (ast) => {
7657
- if (settled) return;
7658
- settled = true;
7659
- cache.set(key, ast);
7660
- finish();
7661
- resolve5(ast);
7662
- };
7663
- const fail = (error) => {
7664
- if (settled) return;
7665
- settled = true;
7666
- finish();
7667
- reject(error);
7668
- };
7669
- worker.once("message", (ast) => {
7670
- succeed(ast);
7671
- });
7672
- worker.once("error", (error) => {
7673
- fail(error);
7674
- });
7675
- worker.once("exit", (code2) => {
7676
- if (settled) return;
7677
- fail(new Error(`parse worker exited ${code2 ?? "null"}`));
7678
- });
7679
- });
7680
- }
7681
- async function parseWithPool(codes) {
7682
- const size = parsePoolSize();
7683
- const out = new Array(codes.length);
7684
- let next = 0;
7685
- async function run() {
7686
- for (; ; ) {
7687
- const index = next;
7688
- next += 1;
7689
- if (index >= codes.length) return;
7690
- const code = codes[index];
7691
- if (code === void 0) return;
7692
- out[index] = await parseOne(code);
7693
- }
7694
- }
7695
- const workers = Math.min(size, Math.max(1, codes.length));
7696
- await Promise.all(Array.from({ length: workers }, () => run()));
7697
- return out;
7698
- }
7699
-
7700
8798
  // src/modules/sast/engine.ts
7701
8799
  var sastRules = [
7702
8800
  sqliRule,
@@ -7756,10 +8854,12 @@ function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ *
7756
8854
  });
7757
8855
  },
7758
8856
  isTainted: (node) => nodeIsTainted(node, facts),
8857
+ isSqlCleared: (node) => nodeIsSqlCleared(node, facts),
7759
8858
  isPathSanitized: (node) => nodeIsPathSanitized(node, facts),
7760
8859
  isSanitizedFor: (node, kind) => nodeIsSanitizedFor(node, facts, kind),
7761
8860
  isNosqlSanitized: (node) => nodeIsNosqlSanitized(node, facts),
7762
8861
  isRequestObject: (node) => nodeIsRequestObject(node, facts),
8862
+ holdsBuiltFilter: (node) => nodeHoldsBuiltFilter(node, facts),
7763
8863
  functionHasPasswordName: (node) => factsFor(node, facts).hasPasswordName,
7764
8864
  assignedName: (node) => assignedNameOf(node),
7765
8865
  projectCsrfProtected
@@ -7797,10 +8897,12 @@ function findingsFromTemplate(filename, source) {
7797
8897
  });
7798
8898
  },
7799
8899
  isTainted: () => false,
8900
+ isSqlCleared: () => false,
7800
8901
  isPathSanitized: () => false,
7801
8902
  isSanitizedFor: () => false,
7802
8903
  isNosqlSanitized: () => false,
7803
8904
  isRequestObject: () => false,
8905
+ holdsBuiltFilter: () => false,
7804
8906
  functionHasPasswordName: () => false,
7805
8907
  assignedName: () => void 0,
7806
8908
  projectCsrfProtected: false
@@ -7932,7 +9034,7 @@ async function runSastStreaming(ctx) {
7932
9034
  }
7933
9035
  const ast = await parseCode(source);
7934
9036
  if (ast === void 0) continue;
7935
- returning.set(rel, functionsThatReturnTaint(ast));
9037
+ returning.set(rel, taintSummariesEnabled() ? cachedReturners(source, ast) : functionsThatReturnTaint(ast));
7936
9038
  parseable.push(rel);
7937
9039
  fileSet.add(rel);
7938
9040
  }
@@ -7963,48 +9065,69 @@ async function runSastStreaming(ctx) {
7963
9065
  var sastModule = {
7964
9066
  name: "sast",
7965
9067
  async run(ctx) {
7966
- const candidates = ctx.files.filter((rel) => SOURCE_EXT.test(rel) || TEMPLATE_EXT.test(rel));
7967
- if (candidates.length > sastStreamThreshold()) return runSastStreaming(ctx);
7968
- const maxBytes = ctx.config.sast.maxFileBytes;
7969
- const findings = [];
7970
- const parsed = [];
7971
- for (const rel of candidates) {
7972
- const source = await readScanSource(ctx.cwd, rel);
7973
- if (source === void 0) continue;
7974
- const why = skipReason(source, maxBytes);
7975
- if (why !== void 0) {
7976
- findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
7977
- continue;
7978
- }
7979
- if (TEMPLATE_EXT.test(rel)) {
7980
- findings.push(...findingsFromTemplate(rel, source));
7981
- continue;
9068
+ if (process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1") {
9069
+ const sources = new Set((ctx.config.sast.sources ?? []).map((row) => row.callee));
9070
+ const sanitizers = /* @__PURE__ */ new Map();
9071
+ for (const row of ctx.config.sast.sanitizers ?? []) {
9072
+ const classes = sanitizers.get(row.callee) ?? /* @__PURE__ */ new Set();
9073
+ for (const name of row.clears) classes.add(name);
9074
+ sanitizers.set(row.callee, classes);
7982
9075
  }
7983
- const ast = await parseCode(source);
7984
- if (ast === void 0) continue;
7985
- parsed.push({ file: rel, source, ast });
9076
+ setCustomTaint({ sources, sanitizers });
9077
+ } else {
9078
+ setCustomTaint(void 0);
7986
9079
  }
7987
- const files = new Set(parsed.map((p) => p.file));
7988
- const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
7989
- const returning = /* @__PURE__ */ new Map();
7990
- for (const entry of parsed) {
7991
- returning.set(entry.file, functionsThatReturnTaint(entry.ast));
7992
- }
7993
- for (const entry of parsed) {
7994
- findings.push(
7995
- ...findingsFromAst(
9080
+ setTaintSummaryConfig(taintSummariesEnabled() ? ctx.config : void 0);
9081
+ try {
9082
+ const candidates = ctx.files.filter((rel) => SOURCE_EXT.test(rel) || TEMPLATE_EXT.test(rel));
9083
+ if (candidates.length > sastStreamThreshold()) return await runSastStreaming(ctx);
9084
+ const maxBytes = ctx.config.sast.maxFileBytes;
9085
+ const findings = [];
9086
+ const parsed = [];
9087
+ for (const rel of candidates) {
9088
+ const source = await readScanSource(ctx.cwd, rel);
9089
+ if (source === void 0) continue;
9090
+ const why = skipReason(source, maxBytes);
9091
+ if (why !== void 0) {
9092
+ findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
9093
+ continue;
9094
+ }
9095
+ if (TEMPLATE_EXT.test(rel)) {
9096
+ findings.push(...findingsFromTemplate(rel, source));
9097
+ continue;
9098
+ }
9099
+ const ast = await parseCode(source);
9100
+ if (ast === void 0) continue;
9101
+ parsed.push({ file: rel, source, ast });
9102
+ }
9103
+ const files = new Set(parsed.map((p) => p.file));
9104
+ const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
9105
+ const returning = /* @__PURE__ */ new Map();
9106
+ for (const entry of parsed) {
9107
+ returning.set(
7996
9108
  entry.file,
7997
- entry.source,
7998
- entry.ast,
7999
- extraReturners(entry.file, entry.ast, files, returning),
8000
- projectCsrf
8001
- )
8002
- );
9109
+ taintSummariesEnabled() ? cachedReturners(entry.source, entry.ast) : functionsThatReturnTaint(entry.ast)
9110
+ );
9111
+ }
9112
+ for (const entry of parsed) {
9113
+ findings.push(
9114
+ ...findingsFromAst(
9115
+ entry.file,
9116
+ entry.source,
9117
+ entry.ast,
9118
+ extraReturners(entry.file, entry.ast, files, returning),
9119
+ projectCsrf
9120
+ )
9121
+ );
9122
+ }
9123
+ findings.push(...runInterproc(parsed));
9124
+ const out = dedupeSastSinks(findings);
9125
+ releaseParsed(parsed);
9126
+ return out;
9127
+ } finally {
9128
+ setCustomTaint(void 0);
9129
+ setTaintSummaryConfig(void 0);
8003
9130
  }
8004
- findings.push(...runInterproc(parsed));
8005
- const out = dedupeSastSinks(findings);
8006
- releaseParsed(parsed);
8007
- return out;
8008
9131
  }
8009
9132
  };
8010
9133
 
@@ -8281,12 +9404,12 @@ ${text}`);
8281
9404
  const seeds = fixtureLogic(prompt);
8282
9405
  const findings = [];
8283
9406
  for (const seed of seeds) {
8284
- const hit = fileTexts.find((f) => f.file.includes(seed.fileHint) && f.text.includes(seed.fileHint === "user" ? "findById" : "Object.assign")) ?? fileTexts.find(
9407
+ const hit2 = fileTexts.find((f) => f.file.includes(seed.fileHint) && f.text.includes(seed.fileHint === "user" ? "findById" : "Object.assign")) ?? fileTexts.find(
8285
9408
  (f) => f.text.includes(seed.ruleId === "AI-IDOR-001" ? "findById(req.params.id)" : "Object.assign(user, req.body)")
8286
9409
  );
8287
- if (hit === void 0) continue;
8288
- const line = hit.text.split(/\r?\n/).findIndex((l) => l.includes(seed.ruleId === "AI-IDOR-001" ? "findById" : "Object.assign")) + 1;
8289
- findings.push(asAiFinding(seed, hit.file, line > 0 ? line : 1));
9410
+ if (hit2 === void 0) continue;
9411
+ const line = hit2.text.split(/\r?\n/).findIndex((l) => l.includes(seed.ruleId === "AI-IDOR-001" ? "findById" : "Object.assign")) + 1;
9412
+ findings.push(asAiFinding(seed, hit2.file, line > 0 ? line : 1));
8290
9413
  }
8291
9414
  return findings;
8292
9415
  }
@@ -8550,6 +9673,56 @@ var KNOWN_KEYS = /* @__PURE__ */ new Set([
8550
9673
  "ai",
8551
9674
  "sast"
8552
9675
  ]);
9676
+ var SAST_CLASSES = /* @__PURE__ */ new Set([
9677
+ "sqli",
9678
+ "nosqli",
9679
+ "cmdi",
9680
+ "path",
9681
+ "xss",
9682
+ "ssrf",
9683
+ "redirect",
9684
+ "eval",
9685
+ "jwt",
9686
+ "crypto",
9687
+ "proto",
9688
+ "secrets",
9689
+ "deps",
9690
+ "config"
9691
+ ]);
9692
+ function parseSastSources(value, source) {
9693
+ if (value === void 0) return void 0;
9694
+ if (!Array.isArray(value)) throw new ConfigError(`${source}: "sast.sources" must be an array`);
9695
+ return value.map((item) => {
9696
+ if (typeof item !== "object" || item === null) {
9697
+ throw new ConfigError(`${source}: "sast.sources" entries must be objects`);
9698
+ }
9699
+ const row = item;
9700
+ if (typeof row.callee !== "string" || row.callee.length === 0) {
9701
+ throw new ConfigError(`${source}: "sast.sources[].callee" must be a string`);
9702
+ }
9703
+ if (row.returns !== "tainted") {
9704
+ throw new ConfigError(`${source}: "sast.sources[].returns" must be "tainted"`);
9705
+ }
9706
+ return { callee: row.callee, returns: "tainted" };
9707
+ });
9708
+ }
9709
+ function parseSastSanitizers(value, source) {
9710
+ if (value === void 0) return void 0;
9711
+ if (!Array.isArray(value)) throw new ConfigError(`${source}: "sast.sanitizers" must be an array`);
9712
+ return value.map((item) => {
9713
+ if (typeof item !== "object" || item === null) {
9714
+ throw new ConfigError(`${source}: "sast.sanitizers" entries must be objects`);
9715
+ }
9716
+ const row = item;
9717
+ if (typeof row.callee !== "string" || row.callee.length === 0) {
9718
+ throw new ConfigError(`${source}: "sast.sanitizers[].callee" must be a string`);
9719
+ }
9720
+ if (!Array.isArray(row.clears) || row.clears.some((name) => typeof name !== "string" || !SAST_CLASSES.has(name))) {
9721
+ throw new ConfigError(`${source}: "sast.sanitizers[].clears" must be B4 class names`);
9722
+ }
9723
+ return { callee: row.callee, clears: row.clears.filter((name) => name !== "secrets" && name !== "deps") };
9724
+ });
9725
+ }
8553
9726
  function validate(raw, source) {
8554
9727
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
8555
9728
  throw new ConfigError(`${source}: config must be a JSON object`);
@@ -8675,6 +9848,16 @@ function validate(raw, source) {
8675
9848
  }
8676
9849
  sast.maxFileBytes = rawSast.maxFileBytes;
8677
9850
  }
9851
+ const custom = process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1";
9852
+ if (!custom && (rawSast.sources !== void 0 || rawSast.sanitizers !== void 0)) {
9853
+ throw new ConfigError(
9854
+ `${source}: unknown key "sast.sources" (FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS)`
9855
+ );
9856
+ }
9857
+ if (custom) {
9858
+ sast.sources = parseSastSources(rawSast.sources, source);
9859
+ sast.sanitizers = parseSastSanitizers(rawSast.sanitizers, source);
9860
+ }
8678
9861
  out.sast = sast;
8679
9862
  }
8680
9863
  return out;
@@ -8974,6 +10157,8 @@ function dedupe2(findings) {
8974
10157
  async function runScan(options = {}, modules = builtinModules) {
8975
10158
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8976
10159
  const t0 = performance.now();
10160
+ depsModule.unusedDirect = void 0;
10161
+ depsModule.upgradePlan = void 0;
8977
10162
  const cwd = resolve(options.cwd ?? process.cwd());
8978
10163
  const stats = await stat(cwd).catch(() => null);
8979
10164
  if (stats === null || !stats.isDirectory()) {
@@ -9072,8 +10257,8 @@ async function runScan(options = {}, modules = builtinModules) {
9072
10257
  const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
9073
10258
  const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
9074
10259
  const secretById = /* @__PURE__ */ new Map();
9075
- for (const hit of extraHits) {
9076
- if (hit.secret !== void 0) secretById.set(hit.finding.id, hit.secret);
10260
+ for (const hit2 of extraHits) {
10261
+ if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
9077
10262
  }
9078
10263
  for (const finding4 of findings) {
9079
10264
  if (finding4.module !== "secrets" || finding4.file === void 0 || secretById.has(finding4.id)) {
@@ -9087,8 +10272,8 @@ async function runScan(options = {}, modules = builtinModules) {
9087
10272
  continue;
9088
10273
  }
9089
10274
  }
9090
- for (const hit of scanTextForSecretHits2(finding4.file, text)) {
9091
- if (hit.secret !== void 0) secretById.set(hit.finding.id, hit.secret);
10275
+ for (const hit2 of scanTextForSecretHits2(finding4.file, text)) {
10276
+ if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
9092
10277
  }
9093
10278
  }
9094
10279
  findings = sortFindings(
@@ -9140,7 +10325,9 @@ async function runScan(options = {}, modules = builtinModules) {
9140
10325
  filesScanned: files.length,
9141
10326
  modulesRequested: [...modulesRequested],
9142
10327
  modulesRun,
9143
- ...historyNotice !== void 0 ? { history: historyNotice } : {}
10328
+ ...historyNotice !== void 0 ? { history: historyNotice } : {},
10329
+ ...depsModule.unusedDirect !== void 0 ? { unusedDirect: depsModule.unusedDirect } : {},
10330
+ ...depsModule.upgradePlan !== void 0 ? { upgradePlan: depsModule.upgradePlan } : {}
9144
10331
  },
9145
10332
  config,
9146
10333
  policy: policyResult,
@@ -9368,9 +10555,9 @@ async function callMcpTool(session, name, args = {}) {
9368
10555
  return { findings: session.findings };
9369
10556
  }
9370
10557
  if (name === "explain_finding") {
9371
- const hit = session.findings.find((f) => f.ruleId === args.ruleId);
9372
- if (!hit) return { text: `no finding ${args.ruleId}` };
9373
- return { text: `${hit.title}: ${hit.message} ${hit.remediation}` };
10558
+ const hit2 = session.findings.find((f) => f.ruleId === args.ruleId);
10559
+ if (!hit2) return { text: `no finding ${args.ruleId}` };
10560
+ return { text: `${hit2.title}: ${hit2.message} ${hit2.remediation}` };
9374
10561
  }
9375
10562
  if (name === "propose_fix") {
9376
10563
  return proposeOnly(session, args.ruleId);
@@ -9381,25 +10568,25 @@ async function callMcpTool(session, name, args = {}) {
9381
10568
  return { error: `unknown tool ${name}` };
9382
10569
  }
9383
10570
  function proposeOnly(session, ruleId) {
9384
- const hit = session.findings.find((f) => f.ruleId === ruleId);
9385
- if (!hit?.file || !session.cwd) return { diff: "", wrote: false };
9386
- const abs = join(session.cwd, hit.file);
10571
+ const hit2 = session.findings.find((f) => f.ruleId === ruleId);
10572
+ if (!hit2?.file || !session.cwd) return { diff: "", wrote: false };
10573
+ const abs = join(session.cwd, hit2.file);
9387
10574
  const source = readFileSync(abs, "utf8");
9388
- const proposal = proposeFix(hit, source);
10575
+ const proposal = proposeFix(hit2, source);
9389
10576
  return { diff: proposal?.diff ?? "", wrote: false };
9390
10577
  }
9391
10578
  function verifyInTemp(session, ruleId) {
9392
- const hit = session.findings.find((f) => f.ruleId === ruleId);
9393
- if (!hit?.file || !session.cwd) return { originalUntouched: true, tempApplied: false };
9394
- const original = join(session.cwd, hit.file);
10579
+ const hit2 = session.findings.find((f) => f.ruleId === ruleId);
10580
+ if (!hit2?.file || !session.cwd) return { originalUntouched: true, tempApplied: false };
10581
+ const original = join(session.cwd, hit2.file);
9395
10582
  const before = readFileSync(original, "utf8");
9396
10583
  const mtime = statSync(original).mtimeMs;
9397
- const proposal = proposeFix(hit, before);
10584
+ const proposal = proposeFix(hit2, before);
9398
10585
  if (!proposal?.diff) return { originalUntouched: true, tempApplied: false };
9399
10586
  const tmp = mkdtempSync(join(tmpdir(), "fullshield-mcp-verify-"));
9400
10587
  try {
9401
10588
  cpSync(session.cwd, tmp, { recursive: true });
9402
- const copy = join(tmp, hit.file);
10589
+ const copy = join(tmp, hit2.file);
9403
10590
  const next = applyUnifiedDiff(readFileSync(copy, "utf8"), proposal.diff);
9404
10591
  writeFileSync(copy, next);
9405
10592
  const untouched = readFileSync(original, "utf8") === before && statSync(original).mtimeMs === mtime;