fullshield 0.2.6 → 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/agent.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, writeFileSync, unlinkSync } from 'fs';
7
+ import { existsSync, mkdirSync, writeFileSync, statSync, readFileSync, rmSync, readdirSync, unlinkSync } from 'fs';
8
8
  import { performance } from 'perf_hooks';
9
9
  import { homedir, availableParallelism } from 'os';
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({
@@ -557,6 +557,7 @@ var init_incremental = __esm({
557
557
  // src/modules/secrets/history.ts
558
558
  var history_exports = {};
559
559
  __export(history_exports, {
560
+ historyCommitLimit: () => historyCommitLimit,
560
561
  scanGitHistory: () => scanGitHistory
561
562
  });
562
563
  function gitError(error) {
@@ -590,16 +591,22 @@ async function readBlob(cwd, hash) {
590
591
  return void 0;
591
592
  }
592
593
  }
593
- function remapHit(hit, file) {
594
- const snippet = hit.finding.snippet;
595
- const line = hit.finding.line ?? 0;
596
- 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"}`);
597
598
  return {
598
- finding: { ...hit.finding, id, file },
599
- secret: hit.secret
599
+ finding: { ...hit2.finding, id, file },
600
+ secret: hit2.secret
600
601
  };
601
602
  }
602
- async function streamLogChanges(cwd) {
603
+ function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
604
+ if (raw === void 0 || raw.trim() === "") return 500;
605
+ const parsed = Number(raw);
606
+ if (!Number.isInteger(parsed) || parsed < 1) return 500;
607
+ return Math.min(parsed, 5e3);
608
+ }
609
+ async function streamLogChanges(cwd, maxCommits) {
603
610
  const changes = [];
604
611
  await gitOk(cwd, ["rev-parse", "--is-inside-work-tree"]);
605
612
  const child = spawn(
@@ -616,6 +623,8 @@ async function streamLogChanges(cwd) {
616
623
  "--format=FSCOMMIT:%H",
617
624
  "--no-textconv",
618
625
  "--no-ext-diff",
626
+ "-n",
627
+ String(maxCommits),
619
628
  "HEAD"
620
629
  ],
621
630
  { stdio: ["ignore", "pipe", "pipe"] }
@@ -678,14 +687,20 @@ async function headSecretKeys(cwd) {
678
687
  hits = scanTextForSecretHits(path, text);
679
688
  seen.set(hash, hits);
680
689
  }
681
- for (const hit of hits) {
682
- 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 ?? ""}`);
683
692
  }
684
693
  }
685
694
  return keys;
686
695
  }
687
- async function scanGitHistory(cwd) {
688
- const changes = await streamLogChanges(cwd);
696
+ async function commitCount(cwd) {
697
+ const raw = await gitOk(cwd, ["rev-list", "--count", "HEAD"]);
698
+ const total = Number(raw.trim());
699
+ return Number.isInteger(total) && total >= 0 ? total : 0;
700
+ }
701
+ async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
702
+ const total = await commitCount(cwd);
703
+ const changes = await streamLogChanges(cwd, maxCommits);
689
704
  const blobHits = /* @__PURE__ */ new Map();
690
705
  const first = /* @__PURE__ */ new Map();
691
706
  for (const change of changes) {
@@ -706,8 +721,8 @@ async function scanGitHistory(cwd) {
706
721
  hits2 = rawHits.filter((h) => allowed.has(h.finding.id));
707
722
  blobHits.set(change.hash, hits2);
708
723
  }
709
- for (const hit of hits2) {
710
- const mapped = remapHit(hit, change.path);
724
+ for (const hit2 of hits2) {
725
+ const mapped = remapHit(hit2, change.path);
711
726
  if (!first.has(mapped.finding.id)) {
712
727
  first.set(mapped.finding.id, { hit: mapped, commit: change.commit });
713
728
  }
@@ -715,14 +730,20 @@ async function scanGitHistory(cwd) {
715
730
  }
716
731
  const atHead = await headSecretKeys(cwd);
717
732
  const hits = [];
718
- for (const { hit, commit } of first.values()) {
719
- 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 ?? ""}`);
720
735
  hits.push({
721
- finding: { ...hit.finding, introducedIn: commit, stillAtHead },
722
- secret: hit.secret
736
+ finding: { ...hit2.finding, introducedIn: commit, stillAtHead },
737
+ secret: hit2.secret
723
738
  });
724
739
  }
725
- return { findings: hits.map((h) => h.finding), hits };
740
+ const scanned = Math.min(maxCommits, total);
741
+ const history = total > maxCommits ? `scanned ${scanned} of ${total} commits` : void 0;
742
+ return {
743
+ findings: hits.map((h) => h.finding),
744
+ hits,
745
+ ...history !== void 0 ? { history } : {}
746
+ };
726
747
  }
727
748
  var execFileAsync2, ZERO_HASH, MAX_BLOB, RAW_LINE;
728
749
  var init_history = __esm({
@@ -955,15 +976,15 @@ function providerFor(ruleId, secret) {
955
976
  async function verifySecretHits(hits, options = {}) {
956
977
  const fetchImpl = options.fetchImpl ?? fetch;
957
978
  const out = [];
958
- for (const hit of hits) {
959
- const secret = hit.secret;
979
+ for (const hit2 of hits) {
980
+ const secret = hit2.secret;
960
981
  if (secret === void 0 || secret.length === 0) {
961
- out.push({ finding: { ...hit.finding, verification: "unknown" }, secret: hit.secret });
982
+ out.push({ finding: { ...hit2.finding, verification: "unknown" }, secret: hit2.secret });
962
983
  continue;
963
984
  }
964
- const run = providerFor(hit.finding.ruleId, secret);
985
+ const run = providerFor(hit2.finding.ruleId, secret);
965
986
  const verification = run === void 0 ? "unknown" : await run(secret, fetchImpl);
966
- out.push({ finding: { ...hit.finding, verification }, secret: hit.secret });
987
+ out.push({ finding: { ...hit2.finding, verification }, secret: hit2.secret });
967
988
  }
968
989
  return out;
969
990
  }
@@ -1070,7 +1091,8 @@ function walk(node, visit) {
1070
1091
  }
1071
1092
  }
1072
1093
  function attachParents(node, parent) {
1073
- node.parent = parent;
1094
+ const current = node;
1095
+ if (parent !== void 0 || current.parent === void 0) current.parent = parent;
1074
1096
  for (const key of Object.keys(node)) {
1075
1097
  if (SKIP_KEYS.has(String(key))) continue;
1076
1098
  const value = node[key];
@@ -1143,6 +1165,10 @@ function mcpCfgEnabled() {
1143
1165
  function taintV2Enabled() {
1144
1166
  return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
1145
1167
  }
1168
+ var LICENSE_FLAG = "FULLSHIELD_EXPERIMENTAL_LICENSE";
1169
+ function licenseEnabled() {
1170
+ return process.env[LICENSE_FLAG] === "1";
1171
+ }
1146
1172
 
1147
1173
  // src/modules/config/finding.ts
1148
1174
  init_finding();
@@ -1518,6 +1544,169 @@ var configModule = {
1518
1544
  // src/modules/deps/index.ts
1519
1545
  init_finding();
1520
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
+
1521
1710
  // src/modules/deps/lockfile.ts
1522
1711
  function basename2(filename) {
1523
1712
  const parts = filename.replace(/\\/g, "/").split("/");
@@ -1547,7 +1736,7 @@ function objectKeys(value) {
1547
1736
  }
1548
1737
  return Object.keys(value);
1549
1738
  }
1550
- function addEdge(edges, from, to) {
1739
+ function addEdge2(edges, from, to) {
1551
1740
  const set = edges.get(from) ?? /* @__PURE__ */ new Set();
1552
1741
  set.add(to);
1553
1742
  edges.set(from, set);
@@ -1627,7 +1816,7 @@ function parseYarnGraph(contents) {
1627
1816
  const depBerry = /^\s{4}(?:"([^"]+)"|(@?[\w./-]+)):/.exec(raw);
1628
1817
  const depName = depClassic?.[1] ?? depClassic?.[2] ?? depBerry?.[1] ?? depBerry?.[2];
1629
1818
  if (depName !== void 0) {
1630
- addEdge(edges, currentName, depName);
1819
+ addEdge2(edges, currentName, depName);
1631
1820
  continue;
1632
1821
  }
1633
1822
  if (/^\s{2}\S/.test(raw)) inDeps = false;
@@ -1722,7 +1911,7 @@ function parsePnpmGraph(contents) {
1722
1911
  const dep = /^ (?:'([^']+)'|"([^"]+)"|([^:]+)):/.exec(raw);
1723
1912
  const depName = dep?.[1] ?? dep?.[2] ?? dep?.[3];
1724
1913
  if (depName !== void 0) {
1725
- addEdge(edges, currentPkg, depName);
1914
+ addEdge2(edges, currentPkg, depName);
1726
1915
  continue;
1727
1916
  }
1728
1917
  if (/^ \S/.test(raw)) inPkgDeps = false;
@@ -1754,7 +1943,7 @@ function graphFromPackagesTable(packages) {
1754
1943
  const name = key === "" ? "" : nameFromPackagesKey(key);
1755
1944
  if (name === void 0 || name.length === 0) continue;
1756
1945
  for (const dep of [...objectKeys(value.dependencies), ...objectKeys(value.optionalDependencies)]) {
1757
- addEdge(edges, name, dep);
1946
+ addEdge2(edges, name, dep);
1758
1947
  }
1759
1948
  }
1760
1949
  return { edges, directProd, directDev };
@@ -1764,7 +1953,7 @@ function graphFromDependenciesTree(deps, edges) {
1764
1953
  const child = value.dependencies;
1765
1954
  if (child === void 0 || typeof child !== "object" || child === null) continue;
1766
1955
  const nested = child;
1767
- for (const childName of Object.keys(nested)) addEdge(edges, name, childName);
1956
+ for (const childName of Object.keys(nested)) addEdge2(edges, name, childName);
1768
1957
  graphFromDependenciesTree(nested, edges);
1769
1958
  }
1770
1959
  }
@@ -1777,8 +1966,16 @@ function parseLockfileGraph(filename, contents) {
1777
1966
  directDev: /* @__PURE__ */ new Set(),
1778
1967
  edgesKnown: false
1779
1968
  };
1780
- if (name === "yarn.lock") return parseYarnGraph(contents);
1781
- 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);
1782
1979
  if (name !== "package-lock.json") return empty;
1783
1980
  try {
1784
1981
  const json = JSON.parse(contents);
@@ -1947,6 +2144,86 @@ function exactPinsFromManifest(pkg) {
1947
2144
  return out;
1948
2145
  }
1949
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(finding3, sites, productionFiles, edgesKnown) {
2189
+ if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") return void 0;
2190
+ const snippet = finding3.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 = finding3.dependencyPath;
2201
+ let reason;
2202
+ switch (finding3.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
+
1950
2227
  // src/modules/deps/reachability.ts
1951
2228
  init_finding();
1952
2229
 
@@ -2054,7 +2331,7 @@ function packageNameFromSnippet(snippet) {
2054
2331
  const at = snippet.indexOf("@");
2055
2332
  return at <= 0 ? snippet : snippet.slice(0, at);
2056
2333
  }
2057
- function packageNameFromSpec(spec) {
2334
+ function packageNameFromSpec2(spec) {
2058
2335
  if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
2059
2336
  if (spec.startsWith("@")) {
2060
2337
  const parts = spec.split("/");
@@ -2090,7 +2367,7 @@ function indexPackageUsage(sources) {
2090
2367
  const local = m[1];
2091
2368
  const spec = m[2];
2092
2369
  if (local === void 0 || spec === void 0) continue;
2093
- const pkg = packageNameFromSpec(spec);
2370
+ const pkg = packageNameFromSpec2(spec);
2094
2371
  if (pkg === void 0) continue;
2095
2372
  bump(pkg, localIsCalled(source, local));
2096
2373
  }
@@ -2100,7 +2377,7 @@ function indexPackageUsage(sources) {
2100
2377
  while ((named = namedRe.exec(source)) !== null) {
2101
2378
  const spec = named[2];
2102
2379
  if (spec === void 0) continue;
2103
- const pkg = packageNameFromSpec(spec);
2380
+ const pkg = packageNameFromSpec2(spec);
2104
2381
  if (pkg === void 0) continue;
2105
2382
  const locals = (named[1] ?? "").split(",").map((s) => s.trim().split(/\s+as\s+/).pop() ?? "");
2106
2383
  bump(
@@ -2132,37 +2409,82 @@ function shortestDependencyPath(target, starts, edges) {
2132
2409
  }
2133
2410
  return void 0;
2134
2411
  }
2412
+ function collectDependencyPaths(target, starts, edges, limit = 8) {
2413
+ const out = [];
2414
+ const queue = [];
2415
+ let steps = 0;
2416
+ for (const start of starts) queue.push([start]);
2417
+ while (queue.length > 0 && out.length < limit && steps < 4e3) {
2418
+ steps += 1;
2419
+ const path = queue.shift();
2420
+ if (path === void 0) continue;
2421
+ const last = path[path.length - 1];
2422
+ if (last === void 0) continue;
2423
+ if (last === target) {
2424
+ if (path.length >= 2) out.push(path);
2425
+ continue;
2426
+ }
2427
+ if (path.length >= 12) continue;
2428
+ for (const next of edges.get(last) ?? []) {
2429
+ if (path.includes(next)) continue;
2430
+ queue.push([...path, next]);
2431
+ }
2432
+ }
2433
+ return out;
2434
+ }
2135
2435
  function reachableFrom(target, roots, edges) {
2136
2436
  if (roots.has(target)) return true;
2137
2437
  return shortestDependencyPath(target, roots, edges) !== void 0;
2138
2438
  }
2139
- function importedDirectNames(usage, graph) {
2140
- const directs = /* @__PURE__ */ new Set([...graph.directProd, ...graph.directDev]);
2439
+ function importedDirectNames(usage, graph2) {
2440
+ const directs = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
2141
2441
  const out = /* @__PURE__ */ new Set();
2142
- for (const [name, hit] of usage) {
2143
- if (!hit.imported) continue;
2442
+ for (const [name, hit2] of usage) {
2443
+ if (!hit2.imported) continue;
2144
2444
  if (directs.size === 0 || directs.has(name)) out.add(name);
2145
2445
  }
2146
2446
  return out;
2147
2447
  }
2148
- function classifyReachability(pkgName, productionUsage, graph) {
2149
- const hit = productionUsage.get(pkgName);
2150
- if (hit?.imported === true) {
2151
- 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" };
2152
2452
  }
2153
- if (graph !== void 0 && graph.edgesKnown) {
2154
- const roots = importedDirectNames(productionUsage, graph);
2155
- const path = shortestDependencyPath(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);
2456
+ const path = paths[0];
2156
2457
  if (path !== void 0 && path.length >= 2) {
2157
- return { reachability: "reachable-via-dependency", path };
2458
+ return { reachability: "reachable-via-dependency", path, paths };
2158
2459
  }
2159
2460
  return { reachability: "not-imported" };
2160
2461
  }
2161
- if (graph !== void 0 && !graph.edgesKnown) {
2462
+ if (graph2 !== void 0 && !graph2.edgesKnown) {
2162
2463
  return { reachability: "reachability-unknown" };
2163
2464
  }
2164
2465
  return { reachability: "not-imported" };
2165
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 finding3 of findings) {
2481
+ if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") continue;
2482
+ if (finding3.snippet === void 0) continue;
2483
+ const pkg = packageNameFromSnippet(finding3.snippet);
2484
+ if (pkg !== void 0 && names.has(pkg)) advisories += 1;
2485
+ }
2486
+ return { packages, advisories };
2487
+ }
2166
2488
  function reachabilitySteps(reachability) {
2167
2489
  if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
2168
2490
  return 0;
@@ -2184,8 +2506,108 @@ function isDevOnly(pkgName, ctx) {
2184
2506
  if (inProd) return false;
2185
2507
  return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
2186
2508
  }
2509
+ var REACH_RANK = {
2510
+ reachable: 0,
2511
+ "symbol-reachable": 1,
2512
+ "reachable-via-dependency": 2,
2513
+ "imported-not-called": 3,
2514
+ "symbol-not-called": 4,
2515
+ "reachability-unknown": 5,
2516
+ "not-imported": 6
2517
+ };
2518
+ function versionFromSnippet(snippet) {
2519
+ const name = packageNameFromSnippet(snippet);
2520
+ if (name === void 0) return snippet;
2521
+ return snippet.slice(name.length + 1);
2522
+ }
2523
+ function pathFields(classified) {
2524
+ const paths = classified.paths ?? (classified.path !== void 0 ? [classified.path] : []);
2525
+ if (paths.length === 0) return {};
2526
+ return {
2527
+ dependencyPath: paths[0],
2528
+ ...paths.length > 1 ? { dependencyPaths: paths } : {}
2529
+ };
2530
+ }
2531
+ function appendOtherPaths(message, paths) {
2532
+ if (paths === void 0 || paths.length < 2) return message;
2533
+ const rest = paths.slice(1).map((p) => p.join(" > ")).join("; ");
2534
+ return `${message} Other paths: ${rest}.`;
2535
+ }
2536
+ function groupPackageAdvisories(findings) {
2537
+ const passthrough = [];
2538
+ const groups = /* @__PURE__ */ new Map();
2539
+ for (const finding3 of findings) {
2540
+ if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") {
2541
+ passthrough.push(finding3);
2542
+ continue;
2543
+ }
2544
+ const name = packageNameFromSnippet(finding3.snippet ?? "") ?? finding3.snippet ?? finding3.id;
2545
+ const advisory = finding3.references?.[0] ?? finding3.id;
2546
+ const key = `${finding3.ruleId}\0${name}\0${advisory}`;
2547
+ const list = groups.get(key);
2548
+ if (list === void 0) groups.set(key, [finding3]);
2549
+ else list.push(finding3);
2550
+ }
2551
+ const merged = [];
2552
+ for (const list of groups.values()) {
2553
+ const only = list[0];
2554
+ if (list.length === 1 && only !== void 0) {
2555
+ merged.push(only);
2556
+ continue;
2557
+ }
2558
+ merged.push(mergeAdvisory(list));
2559
+ }
2560
+ return [...passthrough, ...merged];
2561
+ }
2562
+ function mergeAdvisory(list) {
2563
+ const ranked = [...list].sort(
2564
+ (a, b) => severityRank(b.severity) - severityRank(a.severity) || (REACH_RANK[a.reachability ?? "not-imported"] ?? 9) - (REACH_RANK[b.reachability ?? "not-imported"] ?? 9)
2565
+ );
2566
+ const primary = ranked[0];
2567
+ if (primary === void 0) {
2568
+ throw new Error("empty advisory group");
2569
+ }
2570
+ const name = packageNameFromSnippet(primary.snippet ?? "") ?? "package";
2571
+ const versions = [...new Set(list.map((f) => versionFromSnippet(f.snippet ?? "")))].filter((v) => v.length > 0).sort();
2572
+ const paths = [];
2573
+ const seenPath = /* @__PURE__ */ new Set();
2574
+ for (const finding3 of list) {
2575
+ const candidates = finding3.dependencyPaths ?? (finding3.dependencyPath !== void 0 ? [finding3.dependencyPath] : []);
2576
+ for (const path of candidates) {
2577
+ const key = path.join("\0");
2578
+ if (seenPath.has(key)) continue;
2579
+ seenPath.add(key);
2580
+ paths.push(path);
2581
+ }
2582
+ }
2583
+ paths.sort((a, b) => a.length - b.length || a.join(">").localeCompare(b.join(">")));
2584
+ const advisory = primary.references?.[0] ?? "";
2585
+ const onManifest = list.find((f) => f.file === "package.json");
2586
+ const file = onManifest?.file ?? primary.file;
2587
+ const line = onManifest?.line ?? primary.line;
2588
+ const snippet = versions.length > 0 ? `${name}@${versions.join(",")}` : primary.snippet ?? name;
2589
+ let message = primary.message;
2590
+ if (versions.length > 1) {
2591
+ message = `${message} Versions: ${versions.join(", ")}.`;
2592
+ }
2593
+ if (paths.length > 1) {
2594
+ message = `${message} Dependency paths: ${paths.map((p) => p.join(" > ")).join("; ")}.`;
2595
+ }
2596
+ return {
2597
+ ...primary,
2598
+ id: makeFindingId(primary.ruleId, file, `${name}|${advisory}|${versions.join(",")}`),
2599
+ file,
2600
+ line,
2601
+ snippet,
2602
+ message,
2603
+ severity: primary.severity,
2604
+ reachability: primary.reachability,
2605
+ ...paths[0] !== void 0 ? { dependencyPath: paths[0] } : {},
2606
+ ...paths.length > 1 ? { dependencyPaths: paths } : {}
2607
+ };
2608
+ }
2187
2609
  function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2188
- return findings.map((finding3) => {
2610
+ const mapped = findings.map((finding3) => {
2189
2611
  if (finding3.ruleId === "DEPS-MALICIOUS-001") {
2190
2612
  if (finding3.snippet === void 0) return finding3;
2191
2613
  const pkg2 = packageNameFromSnippet(finding3.snippet);
@@ -2195,7 +2617,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2195
2617
  ...finding3,
2196
2618
  severity: "critical",
2197
2619
  reachability: classified2.reachability,
2198
- ...classified2.path !== void 0 ? { dependencyPath: classified2.path } : {}
2620
+ ...pathFields(classified2)
2199
2621
  };
2200
2622
  }
2201
2623
  if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
@@ -2205,7 +2627,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2205
2627
  const named = symbols.get(finding3.id) ?? [];
2206
2628
  if (named.length > 0 && ctx.sources !== void 0) {
2207
2629
  const symbolClass = classifySymbolUse(named, ctx.sources);
2208
- classified = { reachability: symbolClass, path: classified.path };
2630
+ classified = { reachability: symbolClass, path: classified.path, paths: classified.paths };
2209
2631
  }
2210
2632
  const steps = reachabilitySteps(classified.reachability);
2211
2633
  let severity = steps === 0 ? finding3.severity : downgradeSeverity(finding3.severity, steps);
@@ -2213,6 +2635,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2213
2635
  if (classified.path !== void 0) {
2214
2636
  const shown = classified.path.join(" > ");
2215
2637
  message = `${message} Reachable via ${shown}.`;
2638
+ message = appendOtherPaths(message, classified.paths);
2216
2639
  }
2217
2640
  const devOnly = isDevOnly(pkg, ctx);
2218
2641
  if (devOnly) {
@@ -2222,11 +2645,12 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2222
2645
  return {
2223
2646
  ...finding3,
2224
2647
  reachability: classified.reachability,
2225
- ...classified.path !== void 0 ? { dependencyPath: classified.path } : {},
2648
+ ...pathFields(classified),
2226
2649
  severity,
2227
2650
  message
2228
2651
  };
2229
2652
  });
2653
+ return groupPackageAdvisories(mapped);
2230
2654
  }
2231
2655
 
2232
2656
  // src/modules/deps/semver.ts
@@ -2283,6 +2707,176 @@ function osvSeverity(vuln) {
2283
2707
  return "high";
2284
2708
  }
2285
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 walk2 = (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
+ walk2(index + 1, picked, covered);
2786
+ const grown = new Set(covered);
2787
+ for (const id of next.ids) grown.add(id);
2788
+ walk2(index + 1, [...picked, next], grown);
2789
+ };
2790
+ walk2(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
+
2286
2880
  // src/modules/deps/popular.ts
2287
2881
  var POPULAR_PACKAGES = [
2288
2882
  "express",
@@ -4128,6 +4722,59 @@ function experimentalVex() {
4128
4722
  return process.env.FULLSHIELD_EXPERIMENTAL_VEX === "1";
4129
4723
  }
4130
4724
 
4725
+ // src/modules/deps/license.ts
4726
+ init_finding();
4727
+ var COPYLEFT = /(?:^|[^\w])(?:AGPL|GPL)(?:-|[^\w]|$)/i;
4728
+ function isCopyleftLicense(license) {
4729
+ return COPYLEFT.test(license);
4730
+ }
4731
+ function licenseText(value) {
4732
+ if (typeof value === "string") return value;
4733
+ if (typeof value === "object" && value !== null && "type" in value) {
4734
+ const type = value.type;
4735
+ if (typeof type === "string") return type;
4736
+ }
4737
+ return void 0;
4738
+ }
4739
+ async function readJson(path) {
4740
+ try {
4741
+ return JSON.parse(await readFile(path, "utf8"));
4742
+ } catch {
4743
+ return void 0;
4744
+ }
4745
+ }
4746
+ async function scanCopyleftLicenses(cwd) {
4747
+ const manifest = await readJson(join(cwd, "package.json"));
4748
+ if (typeof manifest !== "object" || manifest === null) return [];
4749
+ const names = /* @__PURE__ */ new Set();
4750
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
4751
+ const block = manifest[field];
4752
+ if (typeof block === "object" && block !== null && !Array.isArray(block)) {
4753
+ for (const name of Object.keys(block)) names.add(name);
4754
+ }
4755
+ }
4756
+ const findings = [];
4757
+ for (const name of names) {
4758
+ const pkg = await readJson(join(cwd, "node_modules", name, "package.json"));
4759
+ if (typeof pkg !== "object" || pkg === null) continue;
4760
+ const license = licenseText(pkg.license);
4761
+ if (license === void 0 || !isCopyleftLicense(license)) continue;
4762
+ findings.push({
4763
+ id: makeFindingId("DEPS-LICENSE-001", "package.json", `${name}|${license}`),
4764
+ ruleId: "DEPS-LICENSE-001",
4765
+ module: "deps",
4766
+ title: `Copyleft dependency ${name}`,
4767
+ severity: "medium",
4768
+ confidence: "high",
4769
+ file: "package.json",
4770
+ snippet: `${name} ${license}`,
4771
+ message: `${name} is licensed ${license}.`,
4772
+ remediation: "Replace the dependency or get a legal review before shipping it in a commercial product."
4773
+ });
4774
+ }
4775
+ return findings;
4776
+ }
4777
+
4131
4778
  // src/fixer/eol.ts
4132
4779
  function detectEol(text) {
4133
4780
  return text.includes("\r\n") ? "\r\n" : "\n";
@@ -4226,7 +4873,7 @@ function walkJs(dir, out = []) {
4226
4873
  }
4227
4874
  return out;
4228
4875
  }
4229
- function readJson(path) {
4876
+ function readJson2(path) {
4230
4877
  return JSON.parse(readFileSync(path, "utf8"));
4231
4878
  }
4232
4879
  var TOKEN_RE = /(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/;
@@ -4240,7 +4887,7 @@ function packageSources(dir) {
4240
4887
  let scripts = "";
4241
4888
  if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
4242
4889
  try {
4243
- const pkg = readJson(manifestPath);
4890
+ const pkg = readJson2(manifestPath);
4244
4891
  const block = pkg.scripts ?? {};
4245
4892
  scripts = Object.values(block).join("\n");
4246
4893
  } catch {
@@ -4261,7 +4908,7 @@ ${scripts}`;
4261
4908
  if (!TOKEN_RE.test(text)) return [];
4262
4909
  const replicates = PUBLISH_RE.test(text) || REGISTRY_PUT_RE.test(text) || GITHUB_CREATE_RE.test(text) || WORKFLOW_PATH_RE.test(text);
4263
4910
  if (!replicates) return [];
4264
- const hit = sources.find(
4911
+ const hit2 = sources.find(
4265
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))
4266
4913
  ) ?? sources.find((s) => TOKEN_RE.test(s.text));
4267
4914
  return [
@@ -4269,7 +4916,7 @@ ${scripts}`;
4269
4916
  "SUPPLY-WORM-001",
4270
4917
  "Self-replicating worm indicators in dependency code",
4271
4918
  "static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",
4272
- hit?.file,
4919
+ hit2?.file,
4273
4920
  "critical"
4274
4921
  )
4275
4922
  ];
@@ -4279,13 +4926,13 @@ function analyzeWorkflowTampering(dir) {
4279
4926
  const text = `${sources.map((s) => s.text).join("\n")}
4280
4927
  ${scripts}`;
4281
4928
  if (!CI_WRITE_RE.test(text)) return [];
4282
- const hit = sources.find((s) => CI_WRITE_RE.test(s.text));
4929
+ const hit2 = sources.find((s) => CI_WRITE_RE.test(s.text));
4283
4930
  return [
4284
4931
  finding(
4285
4932
  "SUPPLY-WORKFLOW-001",
4286
4933
  "Dependency writes GitHub workflow, git hooks, or CI config",
4287
4934
  "static: install script or package code writes .github/, .git/hooks/, or CI configuration",
4288
- hit?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
4935
+ hit2?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
4289
4936
  "critical"
4290
4937
  )
4291
4938
  ];
@@ -4307,8 +4954,8 @@ function parentsOf(name, edges) {
4307
4954
  }
4308
4955
  return out;
4309
4956
  }
4310
- function isDirect(name, graph) {
4311
- return graph.directProd.has(name) || graph.directDev.has(name);
4957
+ function isDirect(name, graph2) {
4958
+ return graph2.directProd.has(name) || graph2.directDev.has(name);
4312
4959
  }
4313
4960
  function packagesWithAFixedRelease(findings) {
4314
4961
  const names = /* @__PURE__ */ new Set();
@@ -4326,17 +4973,17 @@ function targetVersion(finding3) {
4326
4973
  function kindFromLockfile(lockFile) {
4327
4974
  return lockfileKind(lockFile) ?? "npm";
4328
4975
  }
4329
- function attachOverrideProposals(findings, graph, lockFile, manifestRaw) {
4330
- if (!graph.edgesKnown || manifestRaw === void 0) return findings;
4976
+ function attachOverrideProposals(findings, graph2, lockFile, manifestRaw) {
4977
+ if (!graph2.edgesKnown || manifestRaw === void 0) return findings;
4331
4978
  const withFix = packagesWithAFixedRelease(findings);
4332
4979
  const kind = kindFromLockfile(lockFile);
4333
4980
  return findings.map((finding3) => {
4334
4981
  if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
4335
4982
  const name = packageNameFromSnippet2(finding3.snippet);
4336
- if (name.length === 0 || isDirect(name, graph)) return finding3;
4983
+ if (name.length === 0 || isDirect(name, graph2)) return finding3;
4337
4984
  const version = targetVersion(finding3);
4338
4985
  if (version === void 0) return finding3;
4339
- const parentFixed = parentsOf(name, graph.edges).some((parent) => withFix.has(parent));
4986
+ const parentFixed = parentsOf(name, graph2.edges).some((parent) => withFix.has(parent));
4340
4987
  if (parentFixed) return finding3;
4341
4988
  const after = withManifestOverride(manifestRaw, kind, name, version);
4342
4989
  const diff = unifiedDiff("package.json", manifestRaw, after);
@@ -4357,10 +5004,10 @@ function scanInstalledPackages(cwd, packages) {
4357
5004
  const dir = packageDir(cwd, pkg);
4358
5005
  if (dir === void 0) continue;
4359
5006
  const prefix = pkg.path.startsWith("node_modules/") ? pkg.path : `node_modules/${pkg.name}`;
4360
- for (const hit of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
5007
+ for (const hit2 of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
4361
5008
  out.push({
4362
- ...hit,
4363
- 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
4364
5011
  });
4365
5012
  }
4366
5013
  }
@@ -4439,19 +5086,19 @@ function maliciousFinding(lockFile, pkg, vuln) {
4439
5086
  references: [vuln.id, ...vuln.aliases ?? []]
4440
5087
  };
4441
5088
  }
4442
- function typosquatFinding(lockFile, hit) {
4443
- const snippet = `${hit.name}@${hit.version}`;
5089
+ function typosquatFinding(lockFile, hit2) {
5090
+ const snippet = `${hit2.name}@${hit2.version}`;
4444
5091
  return {
4445
5092
  id: makeFindingId("DEPS-TYPOSQUAT-001", lockFile, snippet),
4446
5093
  ruleId: "DEPS-TYPOSQUAT-001",
4447
5094
  module: "deps",
4448
- title: `Possible typosquat: ${hit.name}`,
5095
+ title: `Possible typosquat: ${hit2.name}`,
4449
5096
  severity: "medium",
4450
5097
  confidence: "low",
4451
5098
  file: lockFile,
4452
5099
  snippet,
4453
- message: `Package "${hit.name}" is 1 character from popular package "${hit.lookalike}" (possible typosquat).`,
4454
- 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.`
4455
5102
  };
4456
5103
  }
4457
5104
  function noLockFinding() {
@@ -4485,6 +5132,8 @@ function deprecatedFinding(lockFile, pkg) {
4485
5132
  var depsModule = {
4486
5133
  name: "deps",
4487
5134
  async run(ctx) {
5135
+ depsModule.unusedDirect = void 0;
5136
+ depsModule.upgradePlan = void 0;
4488
5137
  const lockFile = LOCKFILE_NAMES.find((name) => ctx.files.includes(name));
4489
5138
  const cacheDir = ctx.config.deps.cacheDir !== void 0 ? resolve(ctx.cwd, ctx.config.deps.cacheDir) : defaultCacheDir();
4490
5139
  const osvOpts = { cacheDir, offline: ctx.config.deps.offline };
@@ -4510,21 +5159,21 @@ var depsModule = {
4510
5159
  }
4511
5160
  const pins = exactPinsFromManifest(manifest);
4512
5161
  const directNames2 = await readDirectDependencyNames(ctx.cwd);
4513
- for (const hit of findTyposquats(pins, { directNames: directNames2 })) {
4514
- findings2.push(typosquatFinding("package.json", hit));
5162
+ for (const hit2 of findTyposquats(pins, { directNames: directNames2 })) {
5163
+ findings2.push(typosquatFinding("package.json", hit2));
4515
5164
  }
4516
5165
  const osvHits2 = await queryOsv(pins, osvOpts);
4517
5166
  const symbols2 = /* @__PURE__ */ new Map();
4518
- for (const hit of osvHits2) {
4519
- for (const vuln of hit.vulns) {
4520
- const finding3 = 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 finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit2.pkg, vuln) : vulnFinding("package.json", hit2.pkg, vuln, depLoc(hit2.pkg.name));
4521
5170
  const names = affectedFunctionNames(vuln);
4522
5171
  if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols2.set(finding3.id, names);
4523
5172
  findings2.push(finding3);
4524
5173
  }
4525
5174
  }
4526
5175
  const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
4527
- return applyReachability(findings2, noLockCtx, symbols2);
5176
+ return withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, applyReachability(findings2, noLockCtx, symbols2));
4528
5177
  }
4529
5178
  let contents;
4530
5179
  try {
@@ -4532,12 +5181,12 @@ var depsModule = {
4532
5181
  } catch {
4533
5182
  return [];
4534
5183
  }
4535
- const graph = parseLockfileGraph(lockFile, contents);
4536
- const packages = graph.packages;
5184
+ const graph2 = parseLockfileGraph(lockFile, contents);
5185
+ const packages = graph2.packages;
4537
5186
  const findings = [];
4538
5187
  const directNames = await readDirectDependencyNames(ctx.cwd);
4539
- for (const hit of findTyposquats(packages, { directNames })) {
4540
- findings.push(typosquatFinding(lockFile, hit));
5188
+ for (const hit2 of findTyposquats(packages, { directNames })) {
5189
+ findings.push(typosquatFinding(lockFile, hit2));
4541
5190
  }
4542
5191
  for (const pkg of packages) {
4543
5192
  if (pkg.deprecated !== void 0) {
@@ -4546,22 +5195,129 @@ var depsModule = {
4546
5195
  }
4547
5196
  const osvHits = await queryOsv(packages, osvOpts);
4548
5197
  const symbols = /* @__PURE__ */ new Map();
4549
- for (const hit of osvHits) {
4550
- for (const vuln of hit.vulns) {
4551
- const finding3 = 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 finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit2.pkg, vuln) : vulnFinding(lockFile, hit2.pkg, vuln, depLoc(hit2.pkg.name));
4552
5201
  const names = affectedFunctionNames(vuln);
4553
5202
  if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols.set(finding3.id, names);
4554
5203
  findings.push(finding3);
4555
5204
  }
4556
5205
  }
4557
- 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);
4558
5208
  if (experimentalVex()) {
4559
- reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
5209
+ reached = attachOverrideProposals(reached, graph2, lockFile, manifestRaw);
4560
5210
  reached.push(...scanInstalledPackages(ctx.cwd, packages));
4561
5211
  }
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((finding3) => {
5233
+ const evidence = evidenceForFinding(finding3, sites, files, graph2.edgesKnown);
5234
+ return evidence === void 0 ? finding3 : { ...finding3, 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
+ }
4562
5248
  return reached;
4563
5249
  }
4564
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
+ }
4565
5321
  function graphFromManifest(manifest) {
4566
5322
  const keys = (field) => {
4567
5323
  const block = manifest[field];
@@ -4578,7 +5334,7 @@ function graphFromManifest(manifest) {
4578
5334
  edgesKnown: false
4579
5335
  };
4580
5336
  }
4581
- async function reachabilityCtx(cwd, files, graph) {
5337
+ async function reachabilityCtx(cwd, files, graph2) {
4582
5338
  const prodSources = /* @__PURE__ */ new Map();
4583
5339
  for (const rel of files) {
4584
5340
  if (!/\.[cm]?[jt]sx?$/.test(rel)) continue;
@@ -4588,7 +5344,7 @@ async function reachabilityCtx(cwd, files, graph) {
4588
5344
  } catch {
4589
5345
  }
4590
5346
  }
4591
- return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
5347
+ return { productionUsage: indexPackageUsage(prodSources), graph: graph2, sources: prodSources };
4592
5348
  }
4593
5349
 
4594
5350
  // src/modules/mcp/index.ts
@@ -4940,10 +5696,143 @@ init_finding();
4940
5696
  // src/modules/sast/interproc.ts
4941
5697
  init_finding();
4942
5698
 
4943
- // src/modules/sast/frameworks.ts
4944
- var NEXT_HANDLERS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
4945
- var NEST_HTTP = /^(Get|Post|Put|Patch|Delete|All|Head|Options)$/;
4946
- var NEST_INPUT = /^(Body|Query|Param|Headers)$/;
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");
5704
+ }
5705
+ max;
5706
+ map = /* @__PURE__ */ new Map();
5707
+ get size() {
5708
+ return this.map.size;
5709
+ }
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)$/;
4947
5836
  var REQ_READERS = /* @__PURE__ */ new Set(["json", "formData", "text", "arrayBuffer"]);
4948
5837
  var HONO_READERS = /* @__PURE__ */ new Set(["query", "param", "header", "json", "parseBody", "text"]);
4949
5838
  function decoratorExpr(dec) {
@@ -5075,6 +5964,10 @@ function isMcpArgsTaintSource(node) {
5075
5964
  }
5076
5965
 
5077
5966
  // src/modules/sast/taint.ts
5967
+ var customTaint;
5968
+ function setCustomTaint(config) {
5969
+ customTaint = config;
5970
+ }
5078
5971
  var SCHEMA_ROOT = /^(z|zod|joi|yup|schema)$/i;
5079
5972
  function objectChainMentions(node, pattern) {
5080
5973
  let current = node;
@@ -5220,6 +6113,61 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
5220
6113
  return false;
5221
6114
  }
5222
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
+ }
5223
6171
  function exprIsSanitized(node, tainted, names, isCall, returners = NO_RETURNERS) {
5224
6172
  if (node === void 0) return true;
5225
6173
  if (isCall(node)) return true;
@@ -5283,7 +6231,7 @@ function seedMcpToolParams(scope, tainted) {
5283
6231
  for (const name of boundNames(param)) tainted.add(name);
5284
6232
  }
5285
6233
  }
5286
- function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
6234
+ function analyzeScope(scope, seed = [], returners = NO_RETURNERS, filterReturners = NO_RETURNERS, initialBuilt = NO_RETURNERS) {
5287
6235
  const tainted = new Set(seed);
5288
6236
  seedDestructuredRequestParams(scope, tainted);
5289
6237
  seedFrameworkParams(scope, tainted);
@@ -5291,6 +6239,8 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5291
6239
  const urlSanitized = /* @__PURE__ */ new Set();
5292
6240
  const xssSanitized = /* @__PURE__ */ new Set();
5293
6241
  const nosqlSanitized = /* @__PURE__ */ new Set();
6242
+ const sqlCleared = /* @__PURE__ */ new Set();
6243
+ const customPath = /* @__PURE__ */ new Set();
5294
6244
  let changed = true;
5295
6245
  while (changed) {
5296
6246
  changed = false;
@@ -5301,6 +6251,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5301
6251
  for (const name of boundNames(node.id)) nosqlSanitized.add(name);
5302
6252
  }
5303
6253
  if (isSchemaValidatorCall(init)) return;
6254
+ applyCustomCall(init, boundNames(node.id), tainted, sqlCleared, customPath);
5304
6255
  if (isUrlSanitizerCall(init)) {
5305
6256
  for (const name of boundNames(node.id)) urlSanitized.add(name);
5306
6257
  }
@@ -5321,6 +6272,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5321
6272
  for (const name of boundNames(node.left)) nosqlSanitized.add(name);
5322
6273
  }
5323
6274
  if (isSchemaValidatorCall(node.right)) return;
6275
+ applyCustomCall(node.right, boundNames(node.left), tainted, sqlCleared, customPath);
5324
6276
  if (isUrlSanitizerCall(node.right)) {
5325
6277
  for (const name of boundNames(node.left)) urlSanitized.add(name);
5326
6278
  }
@@ -5347,7 +6299,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5347
6299
  for (const name of boundNames(node.left)) resolved.add(name);
5348
6300
  }
5349
6301
  });
5350
- const pathSanitized = /* @__PURE__ */ new Set();
6302
+ const pathSanitized = new Set(customPath);
5351
6303
  walkScope(scope, (node) => {
5352
6304
  if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
5353
6305
  if (propName(node.callee) !== "startsWith") return;
@@ -5389,17 +6341,92 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5389
6341
  for (const name of boundNames(node.left)) requestObjects.add(name);
5390
6342
  }
5391
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
+ }
5392
6370
  return {
5393
6371
  tainted,
5394
6372
  pathSanitized,
6373
+ sqlCleared,
5395
6374
  urlSanitized,
5396
6375
  xssSanitized,
5397
6376
  nosqlSanitized,
5398
6377
  hasPasswordName,
5399
6378
  requestObjects,
6379
+ builtFilters,
6380
+ filterReturners: new Set(filterReturners),
5400
6381
  returners: new Set(returners)
5401
6382
  };
5402
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
+ }
5403
6430
  function functionName(fn) {
5404
6431
  if (fn.type === "FunctionDeclaration" && fn.id !== null) return fn.id.name;
5405
6432
  if (isFunctionNode(fn) && fn.id !== null && fn.id.type === "Identifier") return fn.id.name;
@@ -5474,7 +6501,7 @@ function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
5474
6501
  }
5475
6502
  return changed;
5476
6503
  }
5477
- function analyzeFile(ast, extraReturners = NO_RETURNERS) {
6504
+ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
5478
6505
  attachParents(ast);
5479
6506
  const facts = /* @__PURE__ */ new WeakMap();
5480
6507
  const functions = [];
@@ -5488,12 +6515,13 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
5488
6515
  }
5489
6516
  const seeds = /* @__PURE__ */ new Map();
5490
6517
  for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
5491
- const returners = new Set(extraReturners);
6518
+ const returners = new Set(extraReturners2);
6519
+ const filterReturners = /* @__PURE__ */ new Set();
5492
6520
  for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
5493
6521
  let changed = false;
5494
- facts.set(ast, analyzeScope(ast, [], returners));
6522
+ facts.set(ast, analyzeScope(ast, [], returners, filterReturners));
5495
6523
  for (const fn of functions) {
5496
- facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners));
6524
+ facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners, filterReturners, inheritedBuiltFilters(fn, facts)));
5497
6525
  }
5498
6526
  for (const fn of functions) {
5499
6527
  const name = functionName(fn);
@@ -5503,6 +6531,10 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
5503
6531
  returners.add(name);
5504
6532
  changed = true;
5505
6533
  }
6534
+ if (scopeReturnsBuiltFilter(fn, f) && !filterReturners.has(name)) {
6535
+ filterReturners.add(name);
6536
+ changed = true;
6537
+ }
5506
6538
  }
5507
6539
  for (const fn of functions) {
5508
6540
  const f = facts.get(fn);
@@ -5535,6 +6567,54 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
5535
6567
  }
5536
6568
  return facts;
5537
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
+ }
5538
6618
  function functionsThatReturnTaint(ast) {
5539
6619
  attachParents(ast);
5540
6620
  const names = /* @__PURE__ */ new Set();
@@ -5556,18 +6636,50 @@ function factsFor(node, map) {
5556
6636
  return map.get(enclosingScope(node)) ?? {
5557
6637
  tainted: /* @__PURE__ */ new Set(),
5558
6638
  pathSanitized: /* @__PURE__ */ new Set(),
6639
+ sqlCleared: /* @__PURE__ */ new Set(),
5559
6640
  urlSanitized: /* @__PURE__ */ new Set(),
5560
6641
  xssSanitized: /* @__PURE__ */ new Set(),
5561
6642
  nosqlSanitized: /* @__PURE__ */ new Set(),
5562
6643
  hasPasswordName: false,
5563
6644
  requestObjects: /* @__PURE__ */ new Set(),
6645
+ builtFilters: /* @__PURE__ */ new Set(),
6646
+ filterReturners: /* @__PURE__ */ new Set(),
5564
6647
  returners: /* @__PURE__ */ new Set()
5565
6648
  };
5566
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
+ }
5567
6659
  function nodeIsTainted(node, map) {
5568
6660
  const facts = factsFor(node, map);
5569
6661
  return exprIsTainted(node, facts.tainted, facts.returners);
5570
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
+ }
5571
6683
  function nodeIsPathSanitized(node, map) {
5572
6684
  if (node.type !== "Identifier") return false;
5573
6685
  return factsFor(node, map).pathSanitized.has(node.name);
@@ -5590,6 +6702,147 @@ function nodeIsNosqlSanitized(node, map) {
5590
6702
  return false;
5591
6703
  }
5592
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
+ };
6823
+ }
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;
6844
+ }
6845
+
5593
6846
  // src/modules/sast/interproc.ts
5594
6847
  var MAX_HOPS = 5;
5595
6848
  function collectFunctions(file, ast) {
@@ -5700,22 +6953,22 @@ function collectInstances(ast, ns) {
5700
6953
  if (node.type !== "VariableDeclarator" || node.init === null) return;
5701
6954
  if (node.id.type !== "Identifier" || node.init.type !== "NewExpression") return;
5702
6955
  if (node.init.callee.type !== "Identifier") return;
5703
- const hit = ns.get(node.init.callee.name);
5704
- if (hit === void 0) return;
5705
- 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);
5706
6959
  });
5707
6960
  return instances;
5708
6961
  }
5709
6962
  function resolveCallee(node, ns, instances, currentFile, functions) {
5710
6963
  if (node.type !== "CallExpression") return void 0;
5711
6964
  if (node.callee.type === "Identifier") {
5712
- const hit = ns.get(node.callee.name);
5713
- if (hit !== void 0) {
5714
- if (hit.includes("#")) {
5715
- 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("#");
5716
6969
  if (file !== void 0 && name !== void 0) return { file, name };
5717
6970
  }
5718
- return { file: hit, name: node.callee.name };
6971
+ return { file: hit2, name: node.callee.name };
5719
6972
  }
5720
6973
  if (functions.has(`${currentFile}#${node.callee.name}`)) {
5721
6974
  return { file: currentFile, name: node.callee.name };
@@ -5822,9 +7075,9 @@ function runInterproc(parsed) {
5822
7075
  if (fn === void 0) continue;
5823
7076
  const param = fn.params[seed.paramIndex];
5824
7077
  if (param === void 0) continue;
5825
- const facts = analyzeFunctionWithSeed(fn.node, [param]);
5826
7078
  const p = byFile.get(fn.file);
5827
7079
  if (p === void 0) continue;
7080
+ const facts = taintSummariesEnabled() ? cachedFunctionFacts(p.source, fn.node, [param]) : analyzeFunctionWithSeed(fn.node, [param]);
5828
7081
  const ns = requires.get(fn.file) ?? /* @__PURE__ */ new Map();
5829
7082
  const inst = instances.get(fn.file) ?? /* @__PURE__ */ new Map();
5830
7083
  walk(fn.node, (node) => {
@@ -6247,6 +7500,178 @@ var jwtRule = {
6247
7500
  }
6248
7501
  };
6249
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
+
6250
7675
  // src/modules/sast/rules/nosqli.ts
6251
7676
  var MONGO_SINKS = /* @__PURE__ */ new Set([
6252
7677
  "find",
@@ -6331,6 +7756,7 @@ function isNosqlSafeValue(node, source, isNosqlSanitized) {
6331
7756
  if (isNosqlSanitized(node) || isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) {
6332
7757
  return true;
6333
7758
  }
7759
+ if (isValidatedRouteField(node)) return true;
6334
7760
  return isSanitizeWrap(node, source);
6335
7761
  }
6336
7762
  function objectHasUnsafeTaintedValue(node, isTainted, isSafe) {
@@ -6394,6 +7820,14 @@ var nosqliRule = {
6394
7820
  return;
6395
7821
  }
6396
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
+ }
6397
7831
  if (objectHasUnsafeTaintedValue(filter, (n) => ctx.isTainted(n), isSafe)) {
6398
7832
  ctx.report({
6399
7833
  node,
@@ -6570,7 +8004,7 @@ var sqliRule = {
6570
8004
  CallExpression(node) {
6571
8005
  if (node.type !== "CallExpression" || !isSqlSink2(node)) return;
6572
8006
  const sql = node.arguments[0];
6573
- if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql)) {
8007
+ if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql) && !ctx.isSqlCleared(sql)) {
6574
8008
  ctx.report({
6575
8009
  node,
6576
8010
  message: "Tainted input is concatenated into a SQL string passed to query/execute."
@@ -6720,27 +8154,27 @@ function numberLiteral(node) {
6720
8154
  return node.value;
6721
8155
  }
6722
8156
  function containsIdentMember(node, objectName) {
6723
- let hit = false;
8157
+ let hit2 = false;
6724
8158
  walk(node, (child) => {
6725
8159
  if (child.type === "MemberExpression" && child.object.type === "Identifier" && child.object.name === objectName) {
6726
- hit = true;
8160
+ hit2 = true;
6727
8161
  }
6728
8162
  });
6729
- return hit;
8163
+ return hit2;
6730
8164
  }
6731
8165
  function containsCallNamed(node, name) {
6732
- let hit = false;
8166
+ let hit2 = false;
6733
8167
  walk(node, (child) => {
6734
- if (child.type === "CallExpression" && calleeName(child) === name) hit = true;
8168
+ if (child.type === "CallExpression" && calleeName(child) === name) hit2 = true;
6735
8169
  });
6736
- return hit;
8170
+ return hit2;
6737
8171
  }
6738
8172
  function containsOriginalName(node) {
6739
- let hit = false;
8173
+ let hit2 = false;
6740
8174
  walk(node, (child) => {
6741
- if (child.type === "MemberExpression" && propName(child) === "originalname") hit = true;
8175
+ if (child.type === "MemberExpression" && propName(child) === "originalname") hit2 = true;
6742
8176
  });
6743
- return hit;
8177
+ return hit2;
6744
8178
  }
6745
8179
  function schemaLooksSafe(schema) {
6746
8180
  if (schema === void 0) return false;
@@ -7360,72 +8794,6 @@ function scanPromptInjection(file, source) {
7360
8794
  });
7361
8795
  return findings;
7362
8796
  }
7363
- function packageVersion() {
7364
- let dir = dirname(fileURLToPath(import.meta.url));
7365
- for (let depth = 0; depth < 6; depth += 1) {
7366
- try {
7367
- const raw = readFileSync(join(dir, "package.json"), "utf8");
7368
- const pkg = JSON.parse(raw);
7369
- if (pkg.name === "fullshield" && typeof pkg.version === "string") {
7370
- return pkg.version;
7371
- }
7372
- } catch {
7373
- }
7374
- const parent = dirname(dir);
7375
- if (parent === dir) break;
7376
- dir = parent;
7377
- }
7378
- throw new Error("fullshield package.json not found");
7379
- }
7380
-
7381
- // src/core/parse-pool.ts
7382
- var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
7383
- var cache = /* @__PURE__ */ new Map();
7384
- function parsePoolEnabled() {
7385
- return process.env.FULLSHIELD_PARSE_POOL === "1";
7386
- }
7387
- function parsePoolSize() {
7388
- const raw = process.env.FULLSHIELD_PARSE_WORKERS;
7389
- if (raw !== void 0 && raw !== "") {
7390
- const parsed = Number(raw);
7391
- if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
7392
- }
7393
- return Math.max(1, availableParallelism() - 1);
7394
- }
7395
- function parseCacheKey(code) {
7396
- return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
7397
- }
7398
- function parseOne(code) {
7399
- const key = parseCacheKey(code);
7400
- const hit = cache.get(key);
7401
- if (hit !== void 0) return Promise.resolve(hit);
7402
- return new Promise((resolve5, reject) => {
7403
- const worker = new Worker(fileURLToPath(workerUrl), { workerData: { code } });
7404
- worker.once("message", (ast) => {
7405
- cache.set(key, ast);
7406
- resolve5(ast);
7407
- });
7408
- worker.once("error", reject);
7409
- });
7410
- }
7411
- async function parseWithPool(codes) {
7412
- const size = parsePoolSize();
7413
- const out = new Array(codes.length);
7414
- let next = 0;
7415
- async function run() {
7416
- for (; ; ) {
7417
- const index = next;
7418
- next += 1;
7419
- if (index >= codes.length) return;
7420
- const code = codes[index];
7421
- if (code === void 0) return;
7422
- out[index] = await parseOne(code);
7423
- }
7424
- }
7425
- const workers = Math.min(size, Math.max(1, codes.length));
7426
- await Promise.all(Array.from({ length: workers }, () => run()));
7427
- return out;
7428
- }
7429
8797
 
7430
8798
  // src/modules/sast/engine.ts
7431
8799
  var sastRules = [
@@ -7459,8 +8827,8 @@ function snippetOf(source, node) {
7459
8827
  if (node.range === void 0) return "";
7460
8828
  return source.slice(node.range[0], node.range[1]).replace(/\s+/g, " ").trim().slice(0, 120);
7461
8829
  }
7462
- function findingsFromAst(filename, source, ast, extraReturners = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
7463
- const facts = analyzeFile(ast, extraReturners);
8830
+ function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
8831
+ const facts = analyzeFile(ast, extraReturners2);
7464
8832
  const findings = [];
7465
8833
  for (const rule of sastRules) {
7466
8834
  const ctx = {
@@ -7486,10 +8854,12 @@ function findingsFromAst(filename, source, ast, extraReturners = /* @__PURE__ */
7486
8854
  });
7487
8855
  },
7488
8856
  isTainted: (node) => nodeIsTainted(node, facts),
8857
+ isSqlCleared: (node) => nodeIsSqlCleared(node, facts),
7489
8858
  isPathSanitized: (node) => nodeIsPathSanitized(node, facts),
7490
8859
  isSanitizedFor: (node, kind) => nodeIsSanitizedFor(node, facts, kind),
7491
8860
  isNosqlSanitized: (node) => nodeIsNosqlSanitized(node, facts),
7492
8861
  isRequestObject: (node) => nodeIsRequestObject(node, facts),
8862
+ holdsBuiltFilter: (node) => nodeHoldsBuiltFilter(node, facts),
7493
8863
  functionHasPasswordName: (node) => factsFor(node, facts).hasPasswordName,
7494
8864
  assignedName: (node) => assignedNameOf(node),
7495
8865
  projectCsrfProtected
@@ -7527,10 +8897,12 @@ function findingsFromTemplate(filename, source) {
7527
8897
  });
7528
8898
  },
7529
8899
  isTainted: () => false,
8900
+ isSqlCleared: () => false,
7530
8901
  isPathSanitized: () => false,
7531
8902
  isSanitizedFor: () => false,
7532
8903
  isNosqlSanitized: () => false,
7533
8904
  isRequestObject: () => false,
8905
+ holdsBuiltFilter: () => false,
7534
8906
  functionHasPasswordName: () => false,
7535
8907
  assignedName: () => void 0,
7536
8908
  projectCsrfProtected: false
@@ -7538,52 +8910,224 @@ function findingsFromTemplate(filename, source) {
7538
8910
  xssRule.create(ctx);
7539
8911
  return findings;
7540
8912
  }
8913
+ function dedupeSastSinks(findings) {
8914
+ const seen = /* @__PURE__ */ new Set();
8915
+ const out = [];
8916
+ for (const finding3 of findings) {
8917
+ if (finding3.module !== "sast" || finding3.line === void 0) {
8918
+ out.push(finding3);
8919
+ continue;
8920
+ }
8921
+ const key = `${finding3.ruleId}\0${finding3.file ?? ""}\0${finding3.line}`;
8922
+ if (seen.has(key)) continue;
8923
+ seen.add(key);
8924
+ out.push(finding3);
8925
+ }
8926
+ return out;
8927
+ }
8928
+ function sastStreamThreshold() {
8929
+ const raw = process.env.FULLSHIELD_SAST_STREAM_AT;
8930
+ if (raw !== void 0 && raw !== "") {
8931
+ const parsed = Number(raw);
8932
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
8933
+ }
8934
+ return 4e3;
8935
+ }
8936
+ function sastBatchSize() {
8937
+ const raw = process.env.FULLSHIELD_SAST_BATCH;
8938
+ if (raw !== void 0 && raw !== "") {
8939
+ const parsed = Number(raw);
8940
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
8941
+ }
8942
+ return 150;
8943
+ }
8944
+ function skipFinding(rel, message) {
8945
+ return {
8946
+ id: makeFindingId("SAST-SKIP-001", rel, message),
8947
+ ruleId: "SAST-SKIP-001",
8948
+ module: "sast",
8949
+ title: "File skipped by SAST",
8950
+ severity: "info",
8951
+ confidence: "high",
8952
+ file: rel,
8953
+ message,
8954
+ remediation: "Raise sast.maxFileBytes in fullshield.config.json to parse this file, or leave it skipped."
8955
+ };
8956
+ }
8957
+ function skipReason(source, maxBytes) {
8958
+ if (source.includes("\0")) return "it contains NUL bytes";
8959
+ const bytes = Buffer.byteLength(source);
8960
+ if (bytes > maxBytes) return `${bytes} bytes exceeds the SAST parse limit of ${maxBytes} bytes`;
8961
+ return void 0;
8962
+ }
8963
+ async function readScanSource(cwd, rel) {
8964
+ try {
8965
+ return await readFile(join(cwd, rel), "utf8");
8966
+ } catch {
8967
+ return void 0;
8968
+ }
8969
+ }
8970
+ function cloneAst(value) {
8971
+ if (value === null || typeof value !== "object") return void 0;
8972
+ try {
8973
+ return structuredClone(value);
8974
+ } catch {
8975
+ return void 0;
8976
+ }
8977
+ }
8978
+ async function parseInProcess(source) {
8979
+ try {
8980
+ return await parseSource(source);
8981
+ } catch {
8982
+ return void 0;
8983
+ }
8984
+ }
8985
+ async function parseCode(source) {
8986
+ if (!parsePoolEnabled()) return parseInProcess(source);
8987
+ try {
8988
+ const cloned = cloneAst((await parseWithPool([source]))[0]);
8989
+ if (cloned !== void 0) return cloned;
8990
+ } catch {
8991
+ }
8992
+ return parseInProcess(source);
8993
+ }
8994
+ function releaseParsed(parsed) {
8995
+ for (const entry of parsed) {
8996
+ entry.source = "";
8997
+ entry.ast = void 0;
8998
+ }
8999
+ parsed.length = 0;
9000
+ }
9001
+ function extraReturners(rel, ast, files, returning) {
9002
+ const ns = collectRequires(rel, ast, files);
9003
+ const extra = new Set(returning.get(rel));
9004
+ for (const [local, spec] of ns) {
9005
+ const hash = spec.indexOf("#");
9006
+ const file = hash >= 0 ? spec.slice(0, hash) : spec;
9007
+ const exp = hash >= 0 ? spec.slice(hash + 1) : local;
9008
+ if (returning.get(file)?.has(exp)) extra.add(local);
9009
+ }
9010
+ return extra;
9011
+ }
9012
+ async function runSastStreaming(ctx) {
9013
+ const maxBytes = ctx.config.sast.maxFileBytes;
9014
+ const findings = [];
9015
+ const returning = /* @__PURE__ */ new Map();
9016
+ const parseable = [];
9017
+ const fileSet = /* @__PURE__ */ new Set();
9018
+ let projectCsrf = false;
9019
+ for (const rel of ctx.files) {
9020
+ const isSource = SOURCE_EXT.test(rel);
9021
+ const isTemplate = TEMPLATE_EXT.test(rel);
9022
+ if (!isSource && !isTemplate) continue;
9023
+ const source = await readScanSource(ctx.cwd, rel);
9024
+ if (source === void 0) continue;
9025
+ const why = skipReason(source, maxBytes);
9026
+ if (why !== void 0) {
9027
+ findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
9028
+ continue;
9029
+ }
9030
+ if (sourceHasCsrfGuard(source)) projectCsrf = true;
9031
+ if (isTemplate) {
9032
+ findings.push(...findingsFromTemplate(rel, source));
9033
+ continue;
9034
+ }
9035
+ const ast = await parseCode(source);
9036
+ if (ast === void 0) continue;
9037
+ returning.set(rel, taintSummariesEnabled() ? cachedReturners(source, ast) : functionsThatReturnTaint(ast));
9038
+ parseable.push(rel);
9039
+ fileSet.add(rel);
9040
+ }
9041
+ for (const rel of parseable) {
9042
+ const source = await readScanSource(ctx.cwd, rel);
9043
+ if (source === void 0) continue;
9044
+ const ast = await parseCode(source);
9045
+ if (ast === void 0) continue;
9046
+ findings.push(
9047
+ ...findingsFromAst(rel, source, ast, extraReturners(rel, ast, fileSet, returning), projectCsrf)
9048
+ );
9049
+ }
9050
+ const batch = sastBatchSize();
9051
+ for (let i = 0; i < parseable.length; i += batch) {
9052
+ const parsed = [];
9053
+ for (const rel of parseable.slice(i, i + batch)) {
9054
+ const source = await readScanSource(ctx.cwd, rel);
9055
+ if (source === void 0) continue;
9056
+ const ast = await parseCode(source);
9057
+ if (ast === void 0) continue;
9058
+ parsed.push({ file: rel, source, ast });
9059
+ }
9060
+ findings.push(...runInterproc(parsed));
9061
+ releaseParsed(parsed);
9062
+ }
9063
+ return dedupeSastSinks(findings);
9064
+ }
7541
9065
  var sastModule = {
7542
9066
  name: "sast",
7543
9067
  async run(ctx) {
7544
- const findings = [];
7545
- const parsed = [];
7546
- for (const rel of ctx.files) {
7547
- const isSource = SOURCE_EXT.test(rel);
7548
- const isTemplate = TEMPLATE_EXT.test(rel);
7549
- if (!isSource && !isTemplate) continue;
7550
- let source;
7551
- try {
7552
- source = await readFile(join(ctx.cwd, rel), "utf8");
7553
- } catch {
7554
- continue;
7555
- }
7556
- if (source.length > 1024 * 1024 || source.includes("\0")) continue;
7557
- if (isTemplate) {
7558
- findings.push(...findingsFromTemplate(rel, source));
7559
- 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);
7560
9075
  }
7561
- try {
7562
- const ast = parsePoolEnabled() ? (await parseWithPool([source]))[0] : await parseSource(source);
9076
+ setCustomTaint({ sources, sanitizers });
9077
+ } else {
9078
+ setCustomTaint(void 0);
9079
+ }
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);
7563
9100
  if (ast === void 0) continue;
7564
9101
  parsed.push({ file: rel, source, ast });
7565
- } catch {
7566
9102
  }
7567
- }
7568
- const files = new Set(parsed.map((p) => p.file));
7569
- const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
7570
- const returning = /* @__PURE__ */ new Map();
7571
- for (const p of parsed) {
7572
- returning.set(p.file, functionsThatReturnTaint(p.ast));
7573
- }
7574
- for (const p of parsed) {
7575
- const ns = collectRequires(p.file, p.ast, files);
7576
- const extra = new Set(returning.get(p.file));
7577
- for (const [local, spec] of ns) {
7578
- const hash = spec.indexOf("#");
7579
- const file = hash >= 0 ? spec.slice(0, hash) : spec;
7580
- const exp = hash >= 0 ? spec.slice(hash + 1) : local;
7581
- if (returning.get(file)?.has(exp)) extra.add(local);
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(
9108
+ entry.file,
9109
+ taintSummariesEnabled() ? cachedReturners(entry.source, entry.ast) : functionsThatReturnTaint(entry.ast)
9110
+ );
7582
9111
  }
7583
- findings.push(...findingsFromAst(p.file, p.source, p.ast, extra, projectCsrf));
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);
7584
9130
  }
7585
- findings.push(...runInterproc(parsed));
7586
- return findings;
7587
9131
  }
7588
9132
  };
7589
9133
 
@@ -7860,12 +9404,12 @@ ${text}`);
7860
9404
  const seeds = fixtureLogic(prompt);
7861
9405
  const findings = [];
7862
9406
  for (const seed of seeds) {
7863
- 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(
7864
9408
  (f) => f.text.includes(seed.ruleId === "AI-IDOR-001" ? "findById(req.params.id)" : "Object.assign(user, req.body)")
7865
9409
  );
7866
- if (hit === void 0) continue;
7867
- const line = hit.text.split(/\r?\n/).findIndex((l) => l.includes(seed.ruleId === "AI-IDOR-001" ? "findById" : "Object.assign")) + 1;
7868
- 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));
7869
9413
  }
7870
9414
  return findings;
7871
9415
  }
@@ -8027,16 +9571,37 @@ ${wrapUntrustedCode(req.prompt)}`;
8027
9571
  }
8028
9572
  };
8029
9573
  }
9574
+ function isIsoDate(value) {
9575
+ if (!/^\d{4}-\d{2}-\d{2}(T[\d:.+\-Z]+)?$/.test(value)) return false;
9576
+ return Number.isFinite(Date.parse(value));
9577
+ }
9578
+ function expiresAtMs(expires) {
9579
+ if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) return Date.parse(`${expires}T23:59:59.999Z`);
9580
+ return Date.parse(expires);
9581
+ }
9582
+ function validSuppression(value) {
9583
+ if (typeof value !== "object" || value === null) return void 0;
9584
+ const row = value;
9585
+ if (typeof row.ruleId !== "string" || row.ruleId.length === 0) return void 0;
9586
+ if (typeof row.owner !== "string" || row.owner.length === 0) return void 0;
9587
+ if (typeof row.reason !== "string" || row.reason.length === 0) return void 0;
9588
+ if (typeof row.expires !== "string" || !isIsoDate(row.expires)) return void 0;
9589
+ const file = row.file === void 0 ? void 0 : row.file.replace(/\\/g, "/");
9590
+ if (row.file !== void 0 && (typeof row.file !== "string" || row.file.length === 0)) return void 0;
9591
+ return { ruleId: row.ruleId, file, owner: row.owner, reason: row.reason, expires: row.expires };
9592
+ }
8030
9593
  async function readBaseline(path) {
8031
9594
  try {
8032
9595
  const raw = JSON.parse(await readFile(path, "utf8"));
8033
9596
  if (raw.version !== 1 || !Array.isArray(raw.findings)) return void 0;
9597
+ if (raw.suppressions !== void 0 && !Array.isArray(raw.suppressions)) return void 0;
8034
9598
  return raw;
8035
9599
  } catch {
8036
9600
  return void 0;
8037
9601
  }
8038
9602
  }
8039
9603
  async function writeBaseline(path, findings) {
9604
+ const existing = await readBaseline(path);
8040
9605
  const doc = {
8041
9606
  version: 1,
8042
9607
  findings: findings.map((f) => ({
@@ -8044,7 +9609,8 @@ async function writeBaseline(path, findings) {
8044
9609
  ruleId: f.ruleId,
8045
9610
  file: f.file,
8046
9611
  line: f.line
8047
- }))
9612
+ })),
9613
+ ...existing?.suppressions !== void 0 ? { suppressions: existing.suppressions } : {}
8048
9614
  };
8049
9615
  await writeFile(path, `${JSON.stringify(doc, null, 2)}
8050
9616
  `, "utf8");
@@ -8054,6 +9620,30 @@ function applyBaseline(findings, baseline) {
8054
9620
  const known = new Set(baseline.findings.map((f) => f.id));
8055
9621
  return findings.filter((f) => !known.has(f.id));
8056
9622
  }
9623
+ function applySuppressions(findings, rows, now = /* @__PURE__ */ new Date()) {
9624
+ if (rows === void 0 || rows.length === 0) return { findings: [...findings], errors: [] };
9625
+ const errors = [];
9626
+ const active = [];
9627
+ rows.forEach((row, index) => {
9628
+ const parsed = validSuppression(row);
9629
+ if (parsed === void 0) {
9630
+ errors.push(`suppression ${index + 1} needs ruleId, owner, reason, and an ISO expires date`);
9631
+ return;
9632
+ }
9633
+ if (expiresAtMs(parsed.expires) < now.getTime()) {
9634
+ errors.push(
9635
+ `suppression for ${parsed.ruleId} expired on ${parsed.expires} (owner: ${parsed.owner}); renew or remove it`
9636
+ );
9637
+ return;
9638
+ }
9639
+ active.push(parsed);
9640
+ });
9641
+ const kept = findings.filter((finding3) => {
9642
+ const file = (finding3.file ?? "").replace(/\\/g, "/");
9643
+ return !active.some((row) => row.ruleId === finding3.ruleId && (row.file === void 0 || row.file === file));
9644
+ });
9645
+ return { findings: kept, errors };
9646
+ }
8057
9647
 
8058
9648
  // src/core/config.ts
8059
9649
  init_finding();
@@ -8062,14 +9652,77 @@ var DEFAULT_CONFIG = {
8062
9652
  modules: [...MODULE_NAMES],
8063
9653
  failOn: "high",
8064
9654
  ignore: ["dist/**", "**/*.test.ts"],
9655
+ defaultExcludes: true,
8065
9656
  baseline: ".fullshield-baseline.json",
8066
9657
  notify: { slackWebhookEnv: "FULLSHIELD_SLACK_WEBHOOK" },
8067
9658
  deps: { offline: false },
8068
- ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 }
9659
+ ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 },
9660
+ sast: { maxFileBytes: 1024 * 1024 }
8069
9661
  };
8070
9662
  var ConfigError = class extends Error {
8071
9663
  };
8072
- var KNOWN_KEYS = /* @__PURE__ */ new Set(["modules", "failOn", "ignore", "baseline", "notify", "deps", "ai"]);
9664
+ var KNOWN_KEYS = /* @__PURE__ */ new Set([
9665
+ "modules",
9666
+ "failOn",
9667
+ "ignore",
9668
+ "defaultExcludes",
9669
+ "excludes",
9670
+ "baseline",
9671
+ "notify",
9672
+ "deps",
9673
+ "ai",
9674
+ "sast"
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
+ }
8073
9726
  function validate(raw, source) {
8074
9727
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
8075
9728
  throw new ConfigError(`${source}: config must be a JSON object`);
@@ -8171,6 +9824,42 @@ function validate(raw, source) {
8171
9824
  }
8172
9825
  out.ai = ai;
8173
9826
  }
9827
+ if (record.defaultExcludes !== void 0) {
9828
+ if (typeof record.defaultExcludes !== "boolean") {
9829
+ throw new ConfigError(`${source}: "defaultExcludes" must be a boolean`);
9830
+ }
9831
+ out.defaultExcludes = record.defaultExcludes;
9832
+ }
9833
+ if (record.excludes !== void 0) {
9834
+ if (!Array.isArray(record.excludes) || record.excludes.some((p) => typeof p !== "string")) {
9835
+ throw new ConfigError(`${source}: "excludes" must be an array of glob strings`);
9836
+ }
9837
+ out.excludes = record.excludes;
9838
+ }
9839
+ if (record.sast !== void 0) {
9840
+ if (typeof record.sast !== "object" || record.sast === null || Array.isArray(record.sast)) {
9841
+ throw new ConfigError(`${source}: "sast" must be an object`);
9842
+ }
9843
+ const rawSast = record.sast;
9844
+ const sast = { maxFileBytes: 1024 * 1024 };
9845
+ if (rawSast.maxFileBytes !== void 0) {
9846
+ if (typeof rawSast.maxFileBytes !== "number" || rawSast.maxFileBytes < 1) {
9847
+ throw new ConfigError(`${source}: "sast.maxFileBytes" must be a positive number`);
9848
+ }
9849
+ sast.maxFileBytes = rawSast.maxFileBytes;
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
+ }
9861
+ out.sast = sast;
9862
+ }
8174
9863
  return out;
8175
9864
  }
8176
9865
  async function loadConfig(cwd, configPath) {
@@ -8197,10 +9886,26 @@ async function loadConfig(cwd, configPath) {
8197
9886
  return { ...structuredClone(DEFAULT_CONFIG), ...validate(raw, file) };
8198
9887
  }
8199
9888
  var ignoreFactory = ignoreModule.default;
8200
- var ALWAYS_IGNORED = [".git/", "node_modules/", ".fullshield/"];
9889
+ var HARD_IGNORED = [".git/", ".fullshield/"];
9890
+ var DEFAULT_EXCLUDES = [
9891
+ "node_modules/",
9892
+ "dist/",
9893
+ "build/",
9894
+ ".next/",
9895
+ "coverage/",
9896
+ "vendor/",
9897
+ "vendored/",
9898
+ "third_party/",
9899
+ "third-party/",
9900
+ "compiled/",
9901
+ "*.min.js"
9902
+ ];
8201
9903
  async function walkFiles(options) {
8202
9904
  const ig = ignoreFactory();
8203
- ig.add(ALWAYS_IGNORED);
9905
+ ig.add(HARD_IGNORED);
9906
+ if (options.defaultExcludes !== false) {
9907
+ ig.add(options.excludes ?? DEFAULT_EXCLUDES);
9908
+ }
8204
9909
  try {
8205
9910
  ig.add(await readFile(join(options.cwd, ".gitignore"), "utf8"));
8206
9911
  } catch {
@@ -8382,7 +10087,7 @@ function resolvePolicyBranch(cwd, env = process.env) {
8382
10087
  return "*";
8383
10088
  }
8384
10089
  }
8385
- function expiresAtMs(expires) {
10090
+ function expiresAtMs2(expires) {
8386
10091
  if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
8387
10092
  return Date.parse(`${expires}T23:59:59.999Z`);
8388
10093
  }
@@ -8404,7 +10109,7 @@ function applyPolicy(findings, policy, ctx = {}) {
8404
10109
  });
8405
10110
  const suppressed = /* @__PURE__ */ new Set();
8406
10111
  for (const exception of policy.exceptions ?? []) {
8407
- if (expiresAtMs(exception.expires) < now.getTime()) {
10112
+ if (expiresAtMs2(exception.expires) < now.getTime()) {
8408
10113
  errors.push(
8409
10114
  `policy exception for ${exception.ruleId} expired on ${exception.expires} (owner: ${exception.owner}); renew or remove it`
8410
10115
  );
@@ -8452,6 +10157,8 @@ function dedupe2(findings) {
8452
10157
  async function runScan(options = {}, modules = builtinModules) {
8453
10158
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8454
10159
  const t0 = performance.now();
10160
+ depsModule.unusedDirect = void 0;
10161
+ depsModule.upgradePlan = void 0;
8455
10162
  const cwd = resolve(options.cwd ?? process.cwd());
8456
10163
  const stats = await stat(cwd).catch(() => null);
8457
10164
  if (stats === null || !stats.isDirectory()) {
@@ -8473,7 +10180,12 @@ async function runScan(options = {}, modules = builtinModules) {
8473
10180
  options.modules ?? config.modules,
8474
10181
  loadedPolicy?.requiredModules
8475
10182
  );
8476
- let files = await walkFiles({ cwd, ignore: config.ignore });
10183
+ let files = await walkFiles({
10184
+ cwd,
10185
+ ignore: config.ignore,
10186
+ defaultExcludes: config.defaultExcludes,
10187
+ excludes: config.excludes
10188
+ });
8477
10189
  if (options.staged === true) {
8478
10190
  const staged = new Set(await listStagedFiles(cwd));
8479
10191
  files = files.filter((file) => staged.has(file));
@@ -8507,6 +10219,7 @@ async function runScan(options = {}, modules = builtinModules) {
8507
10219
  );
8508
10220
  }
8509
10221
  const extraHits = [];
10222
+ let historyNotice;
8510
10223
  const secretsWanted = modulesRequested.includes("secrets");
8511
10224
  if (secretsAdvEnabled() && secretsWanted) {
8512
10225
  if (options.history === true) {
@@ -8514,6 +10227,7 @@ async function runScan(options = {}, modules = builtinModules) {
8514
10227
  const hist = await scanGitHistory2(cwd);
8515
10228
  extraHits.push(...hist.hits);
8516
10229
  collected.push(...hist.findings);
10230
+ historyNotice = hist.history;
8517
10231
  }
8518
10232
  if (options.bundle !== void 0 && options.bundle.length > 0) {
8519
10233
  const { scanBundle: scanBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
@@ -8534,13 +10248,17 @@ async function runScan(options = {}, modules = builtinModules) {
8534
10248
  if (options.updateBaseline === true) {
8535
10249
  await writeBaseline(baselinePath, unsuppressed);
8536
10250
  }
8537
- let findings = sortFindings(applyBaseline(unsuppressed, await readBaseline(baselinePath)));
10251
+ const baselineDoc = await readBaseline(baselinePath);
10252
+ let findings = sortFindings(applyBaseline(unsuppressed, baselineDoc));
10253
+ const suppressed = applySuppressions(findings, baselineDoc?.suppressions);
10254
+ findings = sortFindings(suppressed.findings);
10255
+ const suppressionErrors = suppressed.errors;
8538
10256
  if (secretsAdvEnabled() && secretsWanted && options.verifySecrets === true && options.iOwnTheseCredentials === true) {
8539
10257
  const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
8540
10258
  const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
8541
10259
  const secretById = /* @__PURE__ */ new Map();
8542
- for (const hit of extraHits) {
8543
- 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);
8544
10262
  }
8545
10263
  for (const finding3 of findings) {
8546
10264
  if (finding3.module !== "secrets" || finding3.file === void 0 || secretById.has(finding3.id)) {
@@ -8554,8 +10272,8 @@ async function runScan(options = {}, modules = builtinModules) {
8554
10272
  continue;
8555
10273
  }
8556
10274
  }
8557
- for (const hit of scanTextForSecretHits2(finding3.file, text)) {
8558
- if (hit.secret !== void 0) secretById.set(hit.finding.id, hit.secret);
10275
+ for (const hit2 of scanTextForSecretHits2(finding3.file, text)) {
10276
+ if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
8559
10277
  }
8560
10278
  }
8561
10279
  findings = sortFindings(
@@ -8606,10 +10324,14 @@ async function runScan(options = {}, modules = builtinModules) {
8606
10324
  cwd,
8607
10325
  filesScanned: files.length,
8608
10326
  modulesRequested: [...modulesRequested],
8609
- modulesRun
10327
+ modulesRun,
10328
+ ...historyNotice !== void 0 ? { history: historyNotice } : {},
10329
+ ...depsModule.unusedDirect !== void 0 ? { unusedDirect: depsModule.unusedDirect } : {},
10330
+ ...depsModule.upgradePlan !== void 0 ? { upgradePlan: depsModule.upgradePlan } : {}
8610
10331
  },
8611
10332
  config,
8612
- policy: policyResult
10333
+ policy: policyResult,
10334
+ ...suppressionErrors.length > 0 ? { suppressionErrors } : {}
8613
10335
  };
8614
10336
  }
8615
10337
  function scan(options = {}) {