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/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { createHash } from 'crypto';
2
- import { readFile, mkdir, writeFile, stat, unlink, readdir, mkdtemp } from 'fs/promises';
2
+ import { readFile, mkdir, writeFile, readdir, stat, unlink, mkdtemp } 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
7
  import { performance } from 'perf_hooks';
8
+ import { existsSync, mkdirSync, writeFileSync, statSync, readFileSync, rmSync, readdirSync, cpSync } from 'fs';
8
9
  import { homedir, availableParallelism, tmpdir } from 'os';
9
- import { existsSync, statSync, readFileSync, readdirSync, cpSync, mkdirSync, rmSync } from 'fs';
10
10
  import { Worker } from 'worker_threads';
11
11
  import { fileURLToPath } from 'url';
12
12
  import ignoreModule from 'ignore';
@@ -461,7 +461,7 @@ function scanTextForSecretHits(path, text) {
461
461
  return hits;
462
462
  }
463
463
  function scanTextForSecrets(path, text) {
464
- return scanTextForSecretHits(path, text).map((hit) => hit.finding);
464
+ return scanTextForSecretHits(path, text).map((hit2) => hit2.finding);
465
465
  }
466
466
  var secretsRules, REMEDIATION, MAX_FILE_SIZE, secretsModule;
467
467
  var init_secrets = __esm({
@@ -565,6 +565,7 @@ var init_incremental = __esm({
565
565
  // src/modules/secrets/history.ts
566
566
  var history_exports = {};
567
567
  __export(history_exports, {
568
+ historyCommitLimit: () => historyCommitLimit,
568
569
  scanGitHistory: () => scanGitHistory
569
570
  });
570
571
  function gitError(error) {
@@ -598,16 +599,22 @@ async function readBlob(cwd, hash) {
598
599
  return void 0;
599
600
  }
600
601
  }
601
- function remapHit(hit, file) {
602
- const snippet = hit.finding.snippet;
603
- const line = hit.finding.line ?? 0;
604
- const id = makeFindingId(hit.finding.ruleId, file, `${line}|${snippet ?? "file"}`);
602
+ function remapHit(hit2, file) {
603
+ const snippet = hit2.finding.snippet;
604
+ const line = hit2.finding.line ?? 0;
605
+ const id = makeFindingId(hit2.finding.ruleId, file, `${line}|${snippet ?? "file"}`);
605
606
  return {
606
- finding: { ...hit.finding, id, file },
607
- secret: hit.secret
607
+ finding: { ...hit2.finding, id, file },
608
+ secret: hit2.secret
608
609
  };
609
610
  }
610
- async function streamLogChanges(cwd) {
611
+ function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
612
+ if (raw === void 0 || raw.trim() === "") return 500;
613
+ const parsed = Number(raw);
614
+ if (!Number.isInteger(parsed) || parsed < 1) return 500;
615
+ return Math.min(parsed, 5e3);
616
+ }
617
+ async function streamLogChanges(cwd, maxCommits) {
611
618
  const changes = [];
612
619
  await gitOk(cwd, ["rev-parse", "--is-inside-work-tree"]);
613
620
  const child = spawn(
@@ -624,6 +631,8 @@ async function streamLogChanges(cwd) {
624
631
  "--format=FSCOMMIT:%H",
625
632
  "--no-textconv",
626
633
  "--no-ext-diff",
634
+ "-n",
635
+ String(maxCommits),
627
636
  "HEAD"
628
637
  ],
629
638
  { stdio: ["ignore", "pipe", "pipe"] }
@@ -686,14 +695,20 @@ async function headSecretKeys(cwd) {
686
695
  hits = scanTextForSecretHits(path, text);
687
696
  seen.set(hash, hits);
688
697
  }
689
- for (const hit of hits) {
690
- keys.add(`${hit.finding.ruleId}\0${hit.finding.snippet ?? ""}`);
698
+ for (const hit2 of hits) {
699
+ keys.add(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
691
700
  }
692
701
  }
693
702
  return keys;
694
703
  }
695
- async function scanGitHistory(cwd) {
696
- const changes = await streamLogChanges(cwd);
704
+ async function commitCount(cwd) {
705
+ const raw = await gitOk(cwd, ["rev-list", "--count", "HEAD"]);
706
+ const total = Number(raw.trim());
707
+ return Number.isInteger(total) && total >= 0 ? total : 0;
708
+ }
709
+ async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
710
+ const total = await commitCount(cwd);
711
+ const changes = await streamLogChanges(cwd, maxCommits);
697
712
  const blobHits = /* @__PURE__ */ new Map();
698
713
  const first = /* @__PURE__ */ new Map();
699
714
  for (const change of changes) {
@@ -714,8 +729,8 @@ async function scanGitHistory(cwd) {
714
729
  hits2 = rawHits.filter((h) => allowed.has(h.finding.id));
715
730
  blobHits.set(change.hash, hits2);
716
731
  }
717
- for (const hit of hits2) {
718
- const mapped = remapHit(hit, change.path);
732
+ for (const hit2 of hits2) {
733
+ const mapped = remapHit(hit2, change.path);
719
734
  if (!first.has(mapped.finding.id)) {
720
735
  first.set(mapped.finding.id, { hit: mapped, commit: change.commit });
721
736
  }
@@ -723,14 +738,20 @@ async function scanGitHistory(cwd) {
723
738
  }
724
739
  const atHead = await headSecretKeys(cwd);
725
740
  const hits = [];
726
- for (const { hit, commit } of first.values()) {
727
- const stillAtHead = atHead.has(`${hit.finding.ruleId}\0${hit.finding.snippet ?? ""}`);
741
+ for (const { hit: hit2, commit } of first.values()) {
742
+ const stillAtHead = atHead.has(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
728
743
  hits.push({
729
- finding: { ...hit.finding, introducedIn: commit, stillAtHead },
730
- secret: hit.secret
744
+ finding: { ...hit2.finding, introducedIn: commit, stillAtHead },
745
+ secret: hit2.secret
731
746
  });
732
747
  }
733
- return { findings: hits.map((h) => h.finding), hits };
748
+ const scanned = Math.min(maxCommits, total);
749
+ const history = total > maxCommits ? `scanned ${scanned} of ${total} commits` : void 0;
750
+ return {
751
+ findings: hits.map((h) => h.finding),
752
+ hits,
753
+ ...history !== void 0 ? { history } : {}
754
+ };
734
755
  }
735
756
  var execFileAsync2, ZERO_HASH, MAX_BLOB, RAW_LINE;
736
757
  var init_history = __esm({
@@ -963,15 +984,15 @@ function providerFor(ruleId, secret) {
963
984
  async function verifySecretHits(hits, options = {}) {
964
985
  const fetchImpl = options.fetchImpl ?? fetch;
965
986
  const out = [];
966
- for (const hit of hits) {
967
- const secret = hit.secret;
987
+ for (const hit2 of hits) {
988
+ const secret = hit2.secret;
968
989
  if (secret === void 0 || secret.length === 0) {
969
- out.push({ finding: { ...hit.finding, verification: "unknown" }, secret: hit.secret });
990
+ out.push({ finding: { ...hit2.finding, verification: "unknown" }, secret: hit2.secret });
970
991
  continue;
971
992
  }
972
- const run = providerFor(hit.finding.ruleId, secret);
993
+ const run = providerFor(hit2.finding.ruleId, secret);
973
994
  const verification = run === void 0 ? "unknown" : await run(secret, fetchImpl);
974
- out.push({ finding: { ...hit.finding, verification }, secret: hit.secret });
995
+ out.push({ finding: { ...hit2.finding, verification }, secret: hit2.secret });
975
996
  }
976
997
  return out;
977
998
  }
@@ -1078,7 +1099,8 @@ function walk(node, visit) {
1078
1099
  }
1079
1100
  }
1080
1101
  function attachParents(node, parent) {
1081
- node.parent = parent;
1102
+ const current = node;
1103
+ if (parent !== void 0 || current.parent === void 0) current.parent = parent;
1082
1104
  for (const key of Object.keys(node)) {
1083
1105
  if (SKIP_KEYS.has(String(key))) continue;
1084
1106
  const value = node[key];
@@ -1151,6 +1173,11 @@ function mcpCfgEnabled() {
1151
1173
  function taintV2Enabled() {
1152
1174
  return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
1153
1175
  }
1176
+ var DIFF_FLAG = "FULLSHIELD_EXPERIMENTAL_DIFF";
1177
+ var LICENSE_FLAG = "FULLSHIELD_EXPERIMENTAL_LICENSE";
1178
+ function licenseEnabled() {
1179
+ return process.env[LICENSE_FLAG] === "1";
1180
+ }
1154
1181
 
1155
1182
  // src/modules/config/finding.ts
1156
1183
  init_finding();
@@ -1526,6 +1553,169 @@ var configModule = {
1526
1553
  // src/modules/deps/index.ts
1527
1554
  init_finding();
1528
1555
 
1556
+ // src/modules/deps/lockfile-modern.ts
1557
+ function addEdge(edges, from, to) {
1558
+ if (from.length === 0 || to.length === 0 || from === to) return;
1559
+ const list = edges.get(from) ?? /* @__PURE__ */ new Set();
1560
+ list.add(to);
1561
+ edges.set(from, list);
1562
+ }
1563
+ function graph(packages, edges, directProd, directDev, edgesKnown) {
1564
+ return { packages, edges, directProd, directDev, edgesKnown };
1565
+ }
1566
+ function parseYarnBerry(contents) {
1567
+ const packages = [];
1568
+ const edges = /* @__PURE__ */ new Map();
1569
+ const directProd = /* @__PURE__ */ new Set();
1570
+ const directDev = /* @__PURE__ */ new Set();
1571
+ let current;
1572
+ let inDeps = false;
1573
+ let devBlock = false;
1574
+ for (const raw of contents.split(/\r?\n/)) {
1575
+ if (!raw.startsWith(" ") && raw.trimEnd().endsWith(":")) {
1576
+ const key = raw.slice(0, -1).trim().replace(/^"|"$/g, "");
1577
+ current = key.includes("@workspace:.") ? "" : nameFromBerryKey(key);
1578
+ inDeps = false;
1579
+ devBlock = false;
1580
+ continue;
1581
+ }
1582
+ const version = /^\s+version:\s+(.+)$/.exec(raw)?.[1]?.replace(/["']/g, "").trim();
1583
+ if (version !== void 0 && current !== void 0 && current.length > 0 && !inDeps) {
1584
+ packages.push({ name: current, version: versionFromProtocol(version), path: `yarn:${current}` });
1585
+ continue;
1586
+ }
1587
+ if (/^\s+dependencies:\s*$/.test(raw)) {
1588
+ inDeps = true;
1589
+ devBlock = false;
1590
+ continue;
1591
+ }
1592
+ if (/^\s+devDependencies:\s*$/.test(raw)) {
1593
+ inDeps = true;
1594
+ devBlock = true;
1595
+ continue;
1596
+ }
1597
+ if (!inDeps || current === void 0) continue;
1598
+ const dep = /^\s{4}"?([^":]+)"?:\s+(.+)$/.exec(raw);
1599
+ if (dep?.[1] === void 0) continue;
1600
+ const depName = dep[1];
1601
+ if (current.length === 0) {
1602
+ if (devBlock) directDev.add(depName);
1603
+ else directProd.add(depName);
1604
+ } else {
1605
+ addEdge(edges, current, depName);
1606
+ }
1607
+ }
1608
+ return graph(packages, edges, directProd, directDev, true);
1609
+ }
1610
+ function nameFromBerryKey(key) {
1611
+ const first = key.split(",")[0]?.trim() ?? key;
1612
+ if (first.startsWith("@")) {
1613
+ const match = /^(@[^@/]+\/[^@/]+)@/.exec(first);
1614
+ return match?.[1];
1615
+ }
1616
+ const at = first.indexOf("@");
1617
+ return at <= 0 ? first : first.slice(0, at);
1618
+ }
1619
+ function versionFromProtocol(raw) {
1620
+ const patch = /^patch:(.+)#/.exec(raw);
1621
+ const body = patch?.[1] ?? raw;
1622
+ const npm = /^(?:npm:)?(@?[^@]+)@(.+)$/.exec(body);
1623
+ if (npm?.[2] !== void 0) return npm[2];
1624
+ return body;
1625
+ }
1626
+ function parsePnpmV9(contents) {
1627
+ const packages = [];
1628
+ const edges = /* @__PURE__ */ new Map();
1629
+ const directProd = /* @__PURE__ */ new Set();
1630
+ const directDev = /* @__PURE__ */ new Set();
1631
+ let block = "none";
1632
+ let snapshot;
1633
+ let inSnapshotDeps = false;
1634
+ for (const raw of contents.split(/\r?\n/)) {
1635
+ if (raw === "packages:") {
1636
+ block = "packages";
1637
+ snapshot = void 0;
1638
+ continue;
1639
+ }
1640
+ if (raw === "snapshots:") {
1641
+ block = "snapshots";
1642
+ snapshot = void 0;
1643
+ continue;
1644
+ }
1645
+ if (raw === "importers:" || raw.startsWith("lockfileVersion:")) {
1646
+ block = "none";
1647
+ continue;
1648
+ }
1649
+ if (block !== "snapshots" && /^\s{4}dependencies:\s*$/.test(raw)) {
1650
+ block = "importer-deps";
1651
+ continue;
1652
+ }
1653
+ if (/^\s{4}devDependencies:\s*$/.test(raw)) {
1654
+ block = "importer-dev";
1655
+ continue;
1656
+ }
1657
+ if (block === "packages") {
1658
+ const header = /^\s{2}['"]?(@?[^@'\s]+)@([^:'"]+)['"]?:/.exec(raw);
1659
+ if (header?.[1] !== void 0 && header[2] !== void 0) {
1660
+ packages.push({ name: header[1], version: header[2], path: `pnpm:${header[1]}` });
1661
+ }
1662
+ continue;
1663
+ }
1664
+ if (block === "snapshots") {
1665
+ const header = /^\s{2}['"]?(@?[^@'\s]+)@([^:'"]+)['"]?:/.exec(raw);
1666
+ if (header?.[1] !== void 0) {
1667
+ snapshot = header[1];
1668
+ inSnapshotDeps = false;
1669
+ continue;
1670
+ }
1671
+ if (/^\s+dependencies:\s*$/.test(raw)) {
1672
+ inSnapshotDeps = true;
1673
+ continue;
1674
+ }
1675
+ if (inSnapshotDeps && snapshot !== void 0) {
1676
+ const dep = /^\s{6}['"]?([^'":]+)['"]?:/.exec(raw);
1677
+ if (dep?.[1] !== void 0) addEdge(edges, snapshot, dep[1]);
1678
+ }
1679
+ continue;
1680
+ }
1681
+ if (block === "importer-deps" || block === "importer-dev") {
1682
+ const dep = /^\s{6}['"]?([^'":]+)['"]?:/.exec(raw);
1683
+ if (dep?.[1] !== void 0 && !raw.trim().startsWith("specifier") && !raw.trim().startsWith("version")) {
1684
+ if (block === "importer-dev") directDev.add(dep[1]);
1685
+ else directProd.add(dep[1]);
1686
+ }
1687
+ }
1688
+ }
1689
+ return graph(packages, edges, directProd, directDev, true);
1690
+ }
1691
+ function parseBunLock(contents) {
1692
+ const stripped = contents.replace(/^\s*\/\/.*$/gm, "");
1693
+ const packages = [];
1694
+ const edges = /* @__PURE__ */ new Map();
1695
+ const directProd = /* @__PURE__ */ new Set();
1696
+ const directDev = /* @__PURE__ */ new Set();
1697
+ try {
1698
+ const json = JSON.parse(stripped);
1699
+ for (const [name, value] of Object.entries(json.packages ?? {})) {
1700
+ if (!Array.isArray(value)) continue;
1701
+ const entry = Array.from(value);
1702
+ const spec = typeof entry[0] === "string" ? entry[0] : "";
1703
+ const version = spec.includes("@") ? spec.slice(spec.lastIndexOf("@") + 1) : spec;
1704
+ packages.push({ name, version, path: `bun:${name}` });
1705
+ const meta = entry[1];
1706
+ if (meta !== null && typeof meta === "object" && "dependencies" in meta) {
1707
+ const deps = meta.dependencies;
1708
+ if (deps !== null && typeof deps === "object") {
1709
+ for (const dep of Object.keys(deps)) addEdge(edges, name, dep);
1710
+ }
1711
+ }
1712
+ }
1713
+ } catch {
1714
+ return graph([], /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), false);
1715
+ }
1716
+ return graph(packages, edges, directProd, directDev, packages.length > 0);
1717
+ }
1718
+
1529
1719
  // src/modules/deps/lockfile.ts
1530
1720
  function basename2(filename) {
1531
1721
  const parts = filename.replace(/\\/g, "/").split("/");
@@ -1555,7 +1745,7 @@ function objectKeys(value) {
1555
1745
  }
1556
1746
  return Object.keys(value);
1557
1747
  }
1558
- function addEdge(edges, from, to) {
1748
+ function addEdge2(edges, from, to) {
1559
1749
  const set = edges.get(from) ?? /* @__PURE__ */ new Set();
1560
1750
  set.add(to);
1561
1751
  edges.set(from, set);
@@ -1635,7 +1825,7 @@ function parseYarnGraph(contents) {
1635
1825
  const depBerry = /^\s{4}(?:"([^"]+)"|(@?[\w./-]+)):/.exec(raw);
1636
1826
  const depName = depClassic?.[1] ?? depClassic?.[2] ?? depBerry?.[1] ?? depBerry?.[2];
1637
1827
  if (depName !== void 0) {
1638
- addEdge(edges, currentName, depName);
1828
+ addEdge2(edges, currentName, depName);
1639
1829
  continue;
1640
1830
  }
1641
1831
  if (/^\s{2}\S/.test(raw)) inDeps = false;
@@ -1730,7 +1920,7 @@ function parsePnpmGraph(contents) {
1730
1920
  const dep = /^ (?:'([^']+)'|"([^"]+)"|([^:]+)):/.exec(raw);
1731
1921
  const depName = dep?.[1] ?? dep?.[2] ?? dep?.[3];
1732
1922
  if (depName !== void 0) {
1733
- addEdge(edges, currentPkg, depName);
1923
+ addEdge2(edges, currentPkg, depName);
1734
1924
  continue;
1735
1925
  }
1736
1926
  if (/^ \S/.test(raw)) inPkgDeps = false;
@@ -1765,7 +1955,7 @@ function graphFromPackagesTable(packages) {
1765
1955
  const name = key === "" ? "" : nameFromPackagesKey(key);
1766
1956
  if (name === void 0 || name.length === 0) continue;
1767
1957
  for (const dep of [...objectKeys(value.dependencies), ...objectKeys(value.optionalDependencies)]) {
1768
- addEdge(edges, name, dep);
1958
+ addEdge2(edges, name, dep);
1769
1959
  }
1770
1960
  }
1771
1961
  return { edges, directProd, directDev };
@@ -1775,7 +1965,7 @@ function graphFromDependenciesTree(deps, edges) {
1775
1965
  const child = value.dependencies;
1776
1966
  if (child === void 0 || typeof child !== "object" || child === null) continue;
1777
1967
  const nested = child;
1778
- for (const childName of Object.keys(nested)) addEdge(edges, name, childName);
1968
+ for (const childName of Object.keys(nested)) addEdge2(edges, name, childName);
1779
1969
  graphFromDependenciesTree(nested, edges);
1780
1970
  }
1781
1971
  }
@@ -1788,8 +1978,16 @@ function parseLockfileGraph(filename, contents) {
1788
1978
  directDev: /* @__PURE__ */ new Set(),
1789
1979
  edgesKnown: false
1790
1980
  };
1791
- if (name === "yarn.lock") return parseYarnGraph(contents);
1792
- if (name === "pnpm-lock.yaml") return parsePnpmGraph(contents);
1981
+ const modern = process.env.FULLSHIELD_EXPERIMENTAL_LOCKFILES === "1";
1982
+ if (name === "yarn.lock") {
1983
+ if (modern && contents.includes("__metadata")) return parseYarnBerry(contents);
1984
+ return parseYarnGraph(contents);
1985
+ }
1986
+ if (name === "pnpm-lock.yaml") {
1987
+ if (modern && /lockfileVersion:\s*['"]?9/.test(contents)) return parsePnpmV9(contents);
1988
+ return parsePnpmGraph(contents);
1989
+ }
1990
+ if (modern && name === "bun.lock") return parseBunLock(contents);
1793
1991
  if (name !== "package-lock.json") return empty;
1794
1992
  try {
1795
1993
  const json = JSON.parse(contents);
@@ -1958,6 +2156,86 @@ function exactPinsFromManifest(pkg) {
1958
2156
  return out;
1959
2157
  }
1960
2158
 
2159
+ // src/modules/deps/evidence.ts
2160
+ var IMPORT_RE = /(?:require\(\s*['"]([^'"]+)['"]\s*\)|from\s+['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\))/g;
2161
+ function packageNameFromSpec(spec) {
2162
+ if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
2163
+ if (spec.startsWith("@")) {
2164
+ const slash2 = spec.indexOf("/");
2165
+ if (slash2 < 0) return void 0;
2166
+ const rest = spec.slice(slash2 + 1);
2167
+ const cut = rest.indexOf("/");
2168
+ return cut === -1 ? spec : spec.slice(0, slash2 + 1 + cut);
2169
+ }
2170
+ const slash = spec.indexOf("/");
2171
+ return slash === -1 ? spec : spec.slice(0, slash);
2172
+ }
2173
+ function collectImportSites(sources) {
2174
+ const byPkg = /* @__PURE__ */ new Map();
2175
+ const files = [...sources.keys()].sort();
2176
+ for (const file of files) {
2177
+ const text = sources.get(file) ?? "";
2178
+ const lines = text.split(/\r?\n/);
2179
+ for (let i = 0; i < lines.length; i += 1) {
2180
+ const line = lines[i] ?? "";
2181
+ IMPORT_RE.lastIndex = 0;
2182
+ let match;
2183
+ while ((match = IMPORT_RE.exec(line)) !== null) {
2184
+ const spec = match[1] ?? match[2] ?? match[3];
2185
+ if (spec === void 0) continue;
2186
+ const pkg = packageNameFromSpec(spec);
2187
+ if (pkg === void 0) continue;
2188
+ const list = byPkg.get(pkg) ?? [];
2189
+ list.push({ file, line: i + 1, specifier: spec, pkg });
2190
+ byPkg.set(pkg, list);
2191
+ }
2192
+ }
2193
+ }
2194
+ for (const [pkg, list] of byPkg) {
2195
+ list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
2196
+ byPkg.set(pkg, list.slice(0, 5));
2197
+ }
2198
+ return byPkg;
2199
+ }
2200
+ function evidenceForFinding(finding3, sites, productionFiles, edgesKnown) {
2201
+ if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") return void 0;
2202
+ const snippet = finding3.snippet ?? "";
2203
+ const at = snippet.startsWith("@") ? snippet.indexOf("@", 1) : snippet.indexOf("@");
2204
+ const pkg = at <= 0 ? snippet : snippet.slice(0, at);
2205
+ if (pkg.length === 0) return void 0;
2206
+ const imports = (sites.get(pkg) ?? []).map((site) => ({
2207
+ file: site.file,
2208
+ line: site.line,
2209
+ specifier: site.specifier
2210
+ }));
2211
+ const first = imports[0];
2212
+ const path = finding3.dependencyPath;
2213
+ let reason;
2214
+ switch (finding3.reachability) {
2215
+ case "not-imported":
2216
+ reason = `No production file imports ${pkg}. Checked ${productionFiles.length} production file(s)${productionFiles.length > 0 ? `: ${productionFiles.slice(0, 8).join(", ")}` : ""}.`;
2217
+ return { imports: [], reason };
2218
+ case "reachability-unknown":
2219
+ reason = edgesKnown ? `Reachability for ${pkg} is unknown.` : `Lockfile edges are missing, so ${pkg} is reachability-unknown.`;
2220
+ return { imports, ...first !== void 0 ? {} : {}, reason };
2221
+ case "reachable-via-dependency":
2222
+ reason = path !== void 0 && path.length > 0 ? `Required through ${path.join(" > ")}.` : `Required by an imported package.`;
2223
+ return { imports, ...path !== void 0 ? { dependencyPath: path } : {}, reason };
2224
+ case "imported-not-called":
2225
+ reason = first !== void 0 ? `Imported from ${first.file}:${first.line} and not called.` : `Imported and not called.`;
2226
+ return { imports, reason };
2227
+ case "symbol-reachable":
2228
+ reason = first !== void 0 ? `A named vulnerable export is called from ${first.file}:${first.line}.` : `A named vulnerable export is called.`;
2229
+ return { imports, reason };
2230
+ case "symbol-not-called":
2231
+ reason = `A named vulnerable export is not called.`;
2232
+ return { imports, reason };
2233
+ default:
2234
+ reason = first !== void 0 ? `Imported from ${first.file}:${first.line}.` : `Imported by production code.`;
2235
+ return { imports, ...path !== void 0 ? { dependencyPath: path } : {}, reason };
2236
+ }
2237
+ }
2238
+
1961
2239
  // src/modules/deps/reachability.ts
1962
2240
  init_finding();
1963
2241
 
@@ -2065,7 +2343,7 @@ function packageNameFromSnippet(snippet) {
2065
2343
  const at = snippet.indexOf("@");
2066
2344
  return at <= 0 ? snippet : snippet.slice(0, at);
2067
2345
  }
2068
- function packageNameFromSpec(spec) {
2346
+ function packageNameFromSpec2(spec) {
2069
2347
  if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
2070
2348
  if (spec.startsWith("@")) {
2071
2349
  const parts = spec.split("/");
@@ -2101,7 +2379,7 @@ function indexPackageUsage(sources) {
2101
2379
  const local = m[1];
2102
2380
  const spec = m[2];
2103
2381
  if (local === void 0 || spec === void 0) continue;
2104
- const pkg = packageNameFromSpec(spec);
2382
+ const pkg = packageNameFromSpec2(spec);
2105
2383
  if (pkg === void 0) continue;
2106
2384
  bump(pkg, localIsCalled(source, local));
2107
2385
  }
@@ -2111,7 +2389,7 @@ function indexPackageUsage(sources) {
2111
2389
  while ((named = namedRe.exec(source)) !== null) {
2112
2390
  const spec = named[2];
2113
2391
  if (spec === void 0) continue;
2114
- const pkg = packageNameFromSpec(spec);
2392
+ const pkg = packageNameFromSpec2(spec);
2115
2393
  if (pkg === void 0) continue;
2116
2394
  const locals = (named[1] ?? "").split(",").map((s) => s.trim().split(/\s+as\s+/).pop() ?? "");
2117
2395
  bump(
@@ -2143,37 +2421,82 @@ function shortestDependencyPath(target, starts, edges) {
2143
2421
  }
2144
2422
  return void 0;
2145
2423
  }
2424
+ function collectDependencyPaths(target, starts, edges, limit = 8) {
2425
+ const out = [];
2426
+ const queue = [];
2427
+ let steps = 0;
2428
+ for (const start of starts) queue.push([start]);
2429
+ while (queue.length > 0 && out.length < limit && steps < 4e3) {
2430
+ steps += 1;
2431
+ const path = queue.shift();
2432
+ if (path === void 0) continue;
2433
+ const last = path[path.length - 1];
2434
+ if (last === void 0) continue;
2435
+ if (last === target) {
2436
+ if (path.length >= 2) out.push(path);
2437
+ continue;
2438
+ }
2439
+ if (path.length >= 12) continue;
2440
+ for (const next of edges.get(last) ?? []) {
2441
+ if (path.includes(next)) continue;
2442
+ queue.push([...path, next]);
2443
+ }
2444
+ }
2445
+ return out;
2446
+ }
2146
2447
  function reachableFrom(target, roots, edges) {
2147
2448
  if (roots.has(target)) return true;
2148
2449
  return shortestDependencyPath(target, roots, edges) !== void 0;
2149
2450
  }
2150
- function importedDirectNames(usage, graph) {
2151
- const directs = /* @__PURE__ */ new Set([...graph.directProd, ...graph.directDev]);
2451
+ function importedDirectNames(usage, graph2) {
2452
+ const directs = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
2152
2453
  const out = /* @__PURE__ */ new Set();
2153
- for (const [name, hit] of usage) {
2154
- if (!hit.imported) continue;
2454
+ for (const [name, hit2] of usage) {
2455
+ if (!hit2.imported) continue;
2155
2456
  if (directs.size === 0 || directs.has(name)) out.add(name);
2156
2457
  }
2157
2458
  return out;
2158
2459
  }
2159
- function classifyReachability(pkgName, productionUsage, graph) {
2160
- const hit = productionUsage.get(pkgName);
2161
- if (hit?.imported === true) {
2162
- return { reachability: hit.called ? "reachable" : "imported-not-called" };
2460
+ function classifyReachability(pkgName, productionUsage, graph2) {
2461
+ const hit2 = productionUsage.get(pkgName);
2462
+ if (hit2?.imported === true) {
2463
+ return { reachability: hit2.called ? "reachable" : "imported-not-called" };
2163
2464
  }
2164
- if (graph !== void 0 && graph.edgesKnown) {
2165
- const roots = importedDirectNames(productionUsage, graph);
2166
- const path = shortestDependencyPath(pkgName, roots, graph.edges);
2465
+ if (graph2 !== void 0 && graph2.edgesKnown) {
2466
+ const roots = importedDirectNames(productionUsage, graph2);
2467
+ const paths = collectDependencyPaths(pkgName, roots, graph2.edges);
2468
+ const path = paths[0];
2167
2469
  if (path !== void 0 && path.length >= 2) {
2168
- return { reachability: "reachable-via-dependency", path };
2470
+ return { reachability: "reachable-via-dependency", path, paths };
2169
2471
  }
2170
2472
  return { reachability: "not-imported" };
2171
2473
  }
2172
- if (graph !== void 0 && !graph.edgesKnown) {
2474
+ if (graph2 !== void 0 && !graph2.edgesKnown) {
2173
2475
  return { reachability: "reachability-unknown" };
2174
2476
  }
2175
2477
  return { reachability: "not-imported" };
2176
2478
  }
2479
+ function unusedDirectPackages(ctx) {
2480
+ if (!ctx.graph.edgesKnown) return [];
2481
+ const imported = importedDirectNames(ctx.productionUsage, ctx.graph);
2482
+ return [...ctx.graph.directProd].filter((name) => {
2483
+ if (ctx.productionUsage.get(name)?.imported === true) return false;
2484
+ return !reachableFrom(name, imported, ctx.graph.edges);
2485
+ }).sort();
2486
+ }
2487
+ function unusedDirectPlan(findings, ctx) {
2488
+ const packages = unusedDirectPackages(ctx);
2489
+ if (packages.length === 0) return void 0;
2490
+ const names = new Set(packages);
2491
+ let advisories = 0;
2492
+ for (const finding3 of findings) {
2493
+ if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") continue;
2494
+ if (finding3.snippet === void 0) continue;
2495
+ const pkg = packageNameFromSnippet(finding3.snippet);
2496
+ if (pkg !== void 0 && names.has(pkg)) advisories += 1;
2497
+ }
2498
+ return { packages, advisories };
2499
+ }
2177
2500
  function reachabilitySteps(reachability) {
2178
2501
  if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
2179
2502
  return 0;
@@ -2195,8 +2518,108 @@ function isDevOnly(pkgName, ctx) {
2195
2518
  if (inProd) return false;
2196
2519
  return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
2197
2520
  }
2521
+ var REACH_RANK = {
2522
+ reachable: 0,
2523
+ "symbol-reachable": 1,
2524
+ "reachable-via-dependency": 2,
2525
+ "imported-not-called": 3,
2526
+ "symbol-not-called": 4,
2527
+ "reachability-unknown": 5,
2528
+ "not-imported": 6
2529
+ };
2530
+ function versionFromSnippet(snippet) {
2531
+ const name = packageNameFromSnippet(snippet);
2532
+ if (name === void 0) return snippet;
2533
+ return snippet.slice(name.length + 1);
2534
+ }
2535
+ function pathFields(classified) {
2536
+ const paths = classified.paths ?? (classified.path !== void 0 ? [classified.path] : []);
2537
+ if (paths.length === 0) return {};
2538
+ return {
2539
+ dependencyPath: paths[0],
2540
+ ...paths.length > 1 ? { dependencyPaths: paths } : {}
2541
+ };
2542
+ }
2543
+ function appendOtherPaths(message, paths) {
2544
+ if (paths === void 0 || paths.length < 2) return message;
2545
+ const rest = paths.slice(1).map((p) => p.join(" > ")).join("; ");
2546
+ return `${message} Other paths: ${rest}.`;
2547
+ }
2548
+ function groupPackageAdvisories(findings) {
2549
+ const passthrough = [];
2550
+ const groups = /* @__PURE__ */ new Map();
2551
+ for (const finding3 of findings) {
2552
+ if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") {
2553
+ passthrough.push(finding3);
2554
+ continue;
2555
+ }
2556
+ const name = packageNameFromSnippet(finding3.snippet ?? "") ?? finding3.snippet ?? finding3.id;
2557
+ const advisory = finding3.references?.[0] ?? finding3.id;
2558
+ const key = `${finding3.ruleId}\0${name}\0${advisory}`;
2559
+ const list = groups.get(key);
2560
+ if (list === void 0) groups.set(key, [finding3]);
2561
+ else list.push(finding3);
2562
+ }
2563
+ const merged = [];
2564
+ for (const list of groups.values()) {
2565
+ const only = list[0];
2566
+ if (list.length === 1 && only !== void 0) {
2567
+ merged.push(only);
2568
+ continue;
2569
+ }
2570
+ merged.push(mergeAdvisory(list));
2571
+ }
2572
+ return [...passthrough, ...merged];
2573
+ }
2574
+ function mergeAdvisory(list) {
2575
+ const ranked = [...list].sort(
2576
+ (a, b) => severityRank(b.severity) - severityRank(a.severity) || (REACH_RANK[a.reachability ?? "not-imported"] ?? 9) - (REACH_RANK[b.reachability ?? "not-imported"] ?? 9)
2577
+ );
2578
+ const primary = ranked[0];
2579
+ if (primary === void 0) {
2580
+ throw new Error("empty advisory group");
2581
+ }
2582
+ const name = packageNameFromSnippet(primary.snippet ?? "") ?? "package";
2583
+ const versions = [...new Set(list.map((f) => versionFromSnippet(f.snippet ?? "")))].filter((v) => v.length > 0).sort();
2584
+ const paths = [];
2585
+ const seenPath = /* @__PURE__ */ new Set();
2586
+ for (const finding3 of list) {
2587
+ const candidates = finding3.dependencyPaths ?? (finding3.dependencyPath !== void 0 ? [finding3.dependencyPath] : []);
2588
+ for (const path of candidates) {
2589
+ const key = path.join("\0");
2590
+ if (seenPath.has(key)) continue;
2591
+ seenPath.add(key);
2592
+ paths.push(path);
2593
+ }
2594
+ }
2595
+ paths.sort((a, b) => a.length - b.length || a.join(">").localeCompare(b.join(">")));
2596
+ const advisory = primary.references?.[0] ?? "";
2597
+ const onManifest = list.find((f) => f.file === "package.json");
2598
+ const file = onManifest?.file ?? primary.file;
2599
+ const line = onManifest?.line ?? primary.line;
2600
+ const snippet = versions.length > 0 ? `${name}@${versions.join(",")}` : primary.snippet ?? name;
2601
+ let message = primary.message;
2602
+ if (versions.length > 1) {
2603
+ message = `${message} Versions: ${versions.join(", ")}.`;
2604
+ }
2605
+ if (paths.length > 1) {
2606
+ message = `${message} Dependency paths: ${paths.map((p) => p.join(" > ")).join("; ")}.`;
2607
+ }
2608
+ return {
2609
+ ...primary,
2610
+ id: makeFindingId(primary.ruleId, file, `${name}|${advisory}|${versions.join(",")}`),
2611
+ file,
2612
+ line,
2613
+ snippet,
2614
+ message,
2615
+ severity: primary.severity,
2616
+ reachability: primary.reachability,
2617
+ ...paths[0] !== void 0 ? { dependencyPath: paths[0] } : {},
2618
+ ...paths.length > 1 ? { dependencyPaths: paths } : {}
2619
+ };
2620
+ }
2198
2621
  function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2199
- return findings.map((finding3) => {
2622
+ const mapped = findings.map((finding3) => {
2200
2623
  if (finding3.ruleId === "DEPS-MALICIOUS-001") {
2201
2624
  if (finding3.snippet === void 0) return finding3;
2202
2625
  const pkg2 = packageNameFromSnippet(finding3.snippet);
@@ -2206,7 +2629,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2206
2629
  ...finding3,
2207
2630
  severity: "critical",
2208
2631
  reachability: classified2.reachability,
2209
- ...classified2.path !== void 0 ? { dependencyPath: classified2.path } : {}
2632
+ ...pathFields(classified2)
2210
2633
  };
2211
2634
  }
2212
2635
  if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
@@ -2216,7 +2639,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2216
2639
  const named = symbols.get(finding3.id) ?? [];
2217
2640
  if (named.length > 0 && ctx.sources !== void 0) {
2218
2641
  const symbolClass = classifySymbolUse(named, ctx.sources);
2219
- classified = { reachability: symbolClass, path: classified.path };
2642
+ classified = { reachability: symbolClass, path: classified.path, paths: classified.paths };
2220
2643
  }
2221
2644
  const steps = reachabilitySteps(classified.reachability);
2222
2645
  let severity = steps === 0 ? finding3.severity : downgradeSeverity(finding3.severity, steps);
@@ -2224,6 +2647,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2224
2647
  if (classified.path !== void 0) {
2225
2648
  const shown = classified.path.join(" > ");
2226
2649
  message = `${message} Reachable via ${shown}.`;
2650
+ message = appendOtherPaths(message, classified.paths);
2227
2651
  }
2228
2652
  const devOnly = isDevOnly(pkg, ctx);
2229
2653
  if (devOnly) {
@@ -2233,11 +2657,12 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
2233
2657
  return {
2234
2658
  ...finding3,
2235
2659
  reachability: classified.reachability,
2236
- ...classified.path !== void 0 ? { dependencyPath: classified.path } : {},
2660
+ ...pathFields(classified),
2237
2661
  severity,
2238
2662
  message
2239
2663
  };
2240
2664
  });
2665
+ return groupPackageAdvisories(mapped);
2241
2666
  }
2242
2667
 
2243
2668
  // src/modules/deps/semver.ts
@@ -2294,6 +2719,176 @@ function osvSeverity(vuln) {
2294
2719
  return "high";
2295
2720
  }
2296
2721
 
2722
+ // src/modules/deps/upgrade-plan.ts
2723
+ function sentenceFor(bumps, unresolved) {
2724
+ const parts = [];
2725
+ if (bumps.length === 1) {
2726
+ const bump = bumps[0];
2727
+ if (bump !== void 0) {
2728
+ const noun = bump.findings === 1 ? "finding" : "findings";
2729
+ parts.push(`1 bump (${bump.name} ${bump.from} \u2192 ${bump.to}) resolves ${bump.findings} ${noun}.`);
2730
+ }
2731
+ } else if (bumps.length > 1) {
2732
+ const listed = bumps.map((bump) => `${bump.name} ${bump.from} \u2192 ${bump.to}`).join(", ");
2733
+ const total = bumps.reduce((sum, bump) => sum + bump.findings, 0);
2734
+ const noun = total === 1 ? "finding" : "findings";
2735
+ parts.push(`${bumps.length} bumps (${listed}) resolve ${total} ${noun}.`);
2736
+ }
2737
+ if (unresolved.length > 0) {
2738
+ parts.push(`No fixed version in the OSV cache for ${unresolved.join(", ")}.`);
2739
+ }
2740
+ return parts.join(" ");
2741
+ }
2742
+ function chooseUpgradePlan(advisories) {
2743
+ const byName = /* @__PURE__ */ new Map();
2744
+ for (const advisory of advisories) {
2745
+ const list = byName.get(advisory.name);
2746
+ if (list === void 0) byName.set(advisory.name, [advisory]);
2747
+ else list.push(advisory);
2748
+ }
2749
+ const unresolved = [];
2750
+ const candidates = [];
2751
+ for (const [name, list] of byName) {
2752
+ const from = list[0]?.from ?? "0.0.0";
2753
+ if (list.some((advisory) => advisory.fixed === void 0)) {
2754
+ unresolved.push(name);
2755
+ continue;
2756
+ }
2757
+ let to = list[0]?.fixed;
2758
+ for (const advisory of list) {
2759
+ if (advisory.fixed !== void 0 && (to === void 0 || compareSemver(advisory.fixed, to) > 0)) {
2760
+ to = advisory.fixed;
2761
+ }
2762
+ }
2763
+ if (to === void 0 || compareSemver(to, from) <= 0) continue;
2764
+ candidates.push({
2765
+ name,
2766
+ from,
2767
+ to,
2768
+ ids: list.map((advisory) => advisory.id)
2769
+ });
2770
+ }
2771
+ unresolved.sort();
2772
+ const chosen = smallestMaxCover(candidates);
2773
+ if (chosen.length === 0 && unresolved.length === 0) return void 0;
2774
+ const bumps = chosen.map((candidate) => ({
2775
+ name: candidate.name,
2776
+ from: candidate.from,
2777
+ to: candidate.to,
2778
+ safe: sameMajor(candidate.from, candidate.to),
2779
+ findings: candidate.ids.length
2780
+ }));
2781
+ return { bumps, unresolved, sentence: sentenceFor(bumps, unresolved) };
2782
+ }
2783
+ function smallestMaxCover(candidates) {
2784
+ const limit = Math.min(candidates.length, 12);
2785
+ let best = [];
2786
+ let bestCount = -1;
2787
+ const walk2 = (index, picked, covered) => {
2788
+ if (index === limit) {
2789
+ if (covered.size > bestCount || covered.size === bestCount && betterTie(picked, best)) {
2790
+ best = picked;
2791
+ bestCount = covered.size;
2792
+ }
2793
+ return;
2794
+ }
2795
+ const next = candidates[index];
2796
+ if (next === void 0) return;
2797
+ walk2(index + 1, picked, covered);
2798
+ const grown = new Set(covered);
2799
+ for (const id of next.ids) grown.add(id);
2800
+ walk2(index + 1, [...picked, next], grown);
2801
+ };
2802
+ walk2(0, [], /* @__PURE__ */ new Set());
2803
+ return [...best].sort((a, b) => a.name.localeCompare(b.name));
2804
+ }
2805
+ function betterTie(picked, best) {
2806
+ if (picked.length !== best.length) return picked.length < best.length;
2807
+ const left = picked.map((item) => item.name).sort().join("\0");
2808
+ const right = best.map((item) => item.name).sort().join("\0");
2809
+ return left < right;
2810
+ }
2811
+
2812
+ // src/supply/campaign.ts
2813
+ init_finding();
2814
+ var RPC_RE = /getSignaturesForAddress|eth_getLogs/;
2815
+ var NET_RE = /https\.request|http\.request|fetch\s*\(|axios\./;
2816
+ var EVAL_RE = /\beval\s*\(/;
2817
+ var STEAL_RE = /Login Data|leveldb|discord[\\/]Local Storage|wallet-extension|Ethereum\/keystore/i;
2818
+ var PLATFORM_RE = /process\.platform\s*===\s*['"]win32['"]|os\.platform\s*\(\s*\)/;
2819
+ var DOWNLOAD_RE = /https?:\/\/example\.invalid|createWriteStream|https\.get/;
2820
+ var SPAWN_RE = /child_process|spawn\s*\(|execFile\s*\(|\.exe['"]/;
2821
+ function hit(ruleId, title, message, file, severity) {
2822
+ return {
2823
+ id: makeFindingId(ruleId, file, message),
2824
+ ruleId,
2825
+ module: "deps",
2826
+ title,
2827
+ severity,
2828
+ confidence: "high",
2829
+ file,
2830
+ message,
2831
+ remediation: "Do not install this package. Remove it, delete node_modules, and reinstall from a clean lockfile."
2832
+ };
2833
+ }
2834
+ function campaignFindings(files) {
2835
+ const findings = [];
2836
+ for (const source of files) {
2837
+ const hasChain = RPC_RE.test(source.text);
2838
+ const hasNet = NET_RE.test(source.text) || EVAL_RE.test(source.text);
2839
+ if (hasChain && hasNet) {
2840
+ findings.push(
2841
+ hit(
2842
+ "SUPPLY-CHAINC2-001",
2843
+ "Package reads a blockchain RPC and then fetches or evaluates the result",
2844
+ "static: RPC method combined with fetch or eval",
2845
+ source.file,
2846
+ "high"
2847
+ )
2848
+ );
2849
+ }
2850
+ const steals = STEAL_RE.test(source.text);
2851
+ const callsNet = NET_RE.test(source.text);
2852
+ if (steals && callsNet) {
2853
+ findings.push(
2854
+ hit(
2855
+ "SUPPLY-STEALER-001",
2856
+ "Package reads a local credential store and makes a network call",
2857
+ "static: browser, chat, or wallet path combined with a network call",
2858
+ source.file,
2859
+ "critical"
2860
+ )
2861
+ );
2862
+ }
2863
+ const gated2 = PLATFORM_RE.test(source.text);
2864
+ const downloads = DOWNLOAD_RE.test(source.text);
2865
+ const spawns = SPAWN_RE.test(source.text);
2866
+ if (gated2 && downloads && spawns) {
2867
+ findings.push(
2868
+ hit(
2869
+ "SUPPLY-PLATFORMGATE-001",
2870
+ "Package downloads and launches a file only on one platform",
2871
+ "static: platform check combined with a download and a process spawn",
2872
+ source.file,
2873
+ "high"
2874
+ )
2875
+ );
2876
+ }
2877
+ }
2878
+ return findings;
2879
+ }
2880
+ function securityPlaceholderFinding(name, locked, latest) {
2881
+ if (latest !== "0.0.1-security") return void 0;
2882
+ if (locked === "0.0.1-security") return void 0;
2883
+ return hit(
2884
+ "SUPPLY-SECHOLDER-001",
2885
+ `Locked ${name}@${locked} was replaced on the registry by the security placeholder`,
2886
+ `${name} latest is 0.0.1-security while the lockfile pins ${locked}`,
2887
+ "package-lock.json",
2888
+ "critical"
2889
+ );
2890
+ }
2891
+
2297
2892
  // src/modules/deps/popular.ts
2298
2893
  var POPULAR_PACKAGES = [
2299
2894
  "express",
@@ -4139,6 +4734,59 @@ function experimentalVex() {
4139
4734
  return process.env.FULLSHIELD_EXPERIMENTAL_VEX === "1";
4140
4735
  }
4141
4736
 
4737
+ // src/modules/deps/license.ts
4738
+ init_finding();
4739
+ var COPYLEFT = /(?:^|[^\w])(?:AGPL|GPL)(?:-|[^\w]|$)/i;
4740
+ function isCopyleftLicense(license) {
4741
+ return COPYLEFT.test(license);
4742
+ }
4743
+ function licenseText(value) {
4744
+ if (typeof value === "string") return value;
4745
+ if (typeof value === "object" && value !== null && "type" in value) {
4746
+ const type = value.type;
4747
+ if (typeof type === "string") return type;
4748
+ }
4749
+ return void 0;
4750
+ }
4751
+ async function readJson(path) {
4752
+ try {
4753
+ return JSON.parse(await readFile(path, "utf8"));
4754
+ } catch {
4755
+ return void 0;
4756
+ }
4757
+ }
4758
+ async function scanCopyleftLicenses(cwd) {
4759
+ const manifest = await readJson(join(cwd, "package.json"));
4760
+ if (typeof manifest !== "object" || manifest === null) return [];
4761
+ const names = /* @__PURE__ */ new Set();
4762
+ for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
4763
+ const block = manifest[field];
4764
+ if (typeof block === "object" && block !== null && !Array.isArray(block)) {
4765
+ for (const name of Object.keys(block)) names.add(name);
4766
+ }
4767
+ }
4768
+ const findings = [];
4769
+ for (const name of names) {
4770
+ const pkg = await readJson(join(cwd, "node_modules", name, "package.json"));
4771
+ if (typeof pkg !== "object" || pkg === null) continue;
4772
+ const license = licenseText(pkg.license);
4773
+ if (license === void 0 || !isCopyleftLicense(license)) continue;
4774
+ findings.push({
4775
+ id: makeFindingId("DEPS-LICENSE-001", "package.json", `${name}|${license}`),
4776
+ ruleId: "DEPS-LICENSE-001",
4777
+ module: "deps",
4778
+ title: `Copyleft dependency ${name}`,
4779
+ severity: "medium",
4780
+ confidence: "high",
4781
+ file: "package.json",
4782
+ snippet: `${name} ${license}`,
4783
+ message: `${name} is licensed ${license}.`,
4784
+ remediation: "Replace the dependency or get a legal review before shipping it in a commercial product."
4785
+ });
4786
+ }
4787
+ return findings;
4788
+ }
4789
+
4142
4790
  // src/fixer/eol.ts
4143
4791
  function detectEol(text) {
4144
4792
  return text.includes("\r\n") ? "\r\n" : "\n";
@@ -4253,7 +4901,7 @@ function walkJs(dir, out = []) {
4253
4901
  }
4254
4902
  return out;
4255
4903
  }
4256
- function readJson(path) {
4904
+ function readJson2(path) {
4257
4905
  return JSON.parse(readFileSync(path, "utf8"));
4258
4906
  }
4259
4907
  var TOKEN_RE = /(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/;
@@ -4267,7 +4915,7 @@ function packageSources(dir) {
4267
4915
  let scripts = "";
4268
4916
  if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
4269
4917
  try {
4270
- const pkg = readJson(manifestPath);
4918
+ const pkg = readJson2(manifestPath);
4271
4919
  const block = pkg.scripts ?? {};
4272
4920
  scripts = Object.values(block).join("\n");
4273
4921
  } catch {
@@ -4288,7 +4936,7 @@ ${scripts}`;
4288
4936
  if (!TOKEN_RE.test(text)) return [];
4289
4937
  const replicates = PUBLISH_RE.test(text) || REGISTRY_PUT_RE.test(text) || GITHUB_CREATE_RE.test(text) || WORKFLOW_PATH_RE.test(text);
4290
4938
  if (!replicates) return [];
4291
- const hit = sources.find(
4939
+ const hit2 = sources.find(
4292
4940
  (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))
4293
4941
  ) ?? sources.find((s) => TOKEN_RE.test(s.text));
4294
4942
  return [
@@ -4296,7 +4944,7 @@ ${scripts}`;
4296
4944
  "SUPPLY-WORM-001",
4297
4945
  "Self-replicating worm indicators in dependency code",
4298
4946
  "static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",
4299
- hit?.file,
4947
+ hit2?.file,
4300
4948
  "critical"
4301
4949
  )
4302
4950
  ];
@@ -4306,13 +4954,13 @@ function analyzeWorkflowTampering(dir) {
4306
4954
  const text = `${sources.map((s) => s.text).join("\n")}
4307
4955
  ${scripts}`;
4308
4956
  if (!CI_WRITE_RE.test(text)) return [];
4309
- const hit = sources.find((s) => CI_WRITE_RE.test(s.text));
4957
+ const hit2 = sources.find((s) => CI_WRITE_RE.test(s.text));
4310
4958
  return [
4311
4959
  finding(
4312
4960
  "SUPPLY-WORKFLOW-001",
4313
4961
  "Dependency writes GitHub workflow, git hooks, or CI config",
4314
4962
  "static: install script or package code writes .github/, .git/hooks/, or CI configuration",
4315
- hit?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
4963
+ hit2?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
4316
4964
  "critical"
4317
4965
  )
4318
4966
  ];
@@ -4321,9 +4969,9 @@ function provenanceCoverage(packages, known) {
4321
4969
  let withProvenance = 0;
4322
4970
  let unknown = 0;
4323
4971
  for (const pkg of packages) {
4324
- const hit = known.get(`${pkg.name}@${pkg.version}`);
4325
- if (hit === true) withProvenance += 1;
4326
- else if (hit === void 0) unknown += 1;
4972
+ const hit2 = known.get(`${pkg.name}@${pkg.version}`);
4973
+ if (hit2 === true) withProvenance += 1;
4974
+ else if (hit2 === void 0) unknown += 1;
4327
4975
  }
4328
4976
  const total = packages.length;
4329
4977
  return {
@@ -4350,8 +4998,8 @@ function parentsOf(name, edges) {
4350
4998
  }
4351
4999
  return out;
4352
5000
  }
4353
- function isDirect(name, graph) {
4354
- return graph.directProd.has(name) || graph.directDev.has(name);
5001
+ function isDirect(name, graph2) {
5002
+ return graph2.directProd.has(name) || graph2.directDev.has(name);
4355
5003
  }
4356
5004
  function packagesWithAFixedRelease(findings) {
4357
5005
  const names = /* @__PURE__ */ new Set();
@@ -4369,17 +5017,17 @@ function targetVersion(finding3) {
4369
5017
  function kindFromLockfile(lockFile) {
4370
5018
  return lockfileKind(lockFile) ?? "npm";
4371
5019
  }
4372
- function attachOverrideProposals(findings, graph, lockFile, manifestRaw) {
4373
- if (!graph.edgesKnown || manifestRaw === void 0) return findings;
5020
+ function attachOverrideProposals(findings, graph2, lockFile, manifestRaw) {
5021
+ if (!graph2.edgesKnown || manifestRaw === void 0) return findings;
4374
5022
  const withFix = packagesWithAFixedRelease(findings);
4375
5023
  const kind = kindFromLockfile(lockFile);
4376
5024
  return findings.map((finding3) => {
4377
5025
  if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
4378
5026
  const name = packageNameFromSnippet2(finding3.snippet);
4379
- if (name.length === 0 || isDirect(name, graph)) return finding3;
5027
+ if (name.length === 0 || isDirect(name, graph2)) return finding3;
4380
5028
  const version = targetVersion(finding3);
4381
5029
  if (version === void 0) return finding3;
4382
- const parentFixed = parentsOf(name, graph.edges).some((parent) => withFix.has(parent));
5030
+ const parentFixed = parentsOf(name, graph2.edges).some((parent) => withFix.has(parent));
4383
5031
  if (parentFixed) return finding3;
4384
5032
  const after = withManifestOverride(manifestRaw, kind, name, version);
4385
5033
  const diff = unifiedDiff("package.json", manifestRaw, after);
@@ -4400,10 +5048,10 @@ function scanInstalledPackages(cwd, packages) {
4400
5048
  const dir = packageDir(cwd, pkg);
4401
5049
  if (dir === void 0) continue;
4402
5050
  const prefix = pkg.path.startsWith("node_modules/") ? pkg.path : `node_modules/${pkg.name}`;
4403
- for (const hit of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
5051
+ for (const hit2 of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
4404
5052
  out.push({
4405
- ...hit,
4406
- file: hit.file !== void 0 && hit.file.length > 0 ? `${prefix}/${hit.file}` : prefix
5053
+ ...hit2,
5054
+ file: hit2.file !== void 0 && hit2.file.length > 0 ? `${prefix}/${hit2.file}` : prefix
4407
5055
  });
4408
5056
  }
4409
5057
  }
@@ -4482,19 +5130,19 @@ function maliciousFinding(lockFile, pkg, vuln) {
4482
5130
  references: [vuln.id, ...vuln.aliases ?? []]
4483
5131
  };
4484
5132
  }
4485
- function typosquatFinding(lockFile, hit) {
4486
- const snippet = `${hit.name}@${hit.version}`;
5133
+ function typosquatFinding(lockFile, hit2) {
5134
+ const snippet = `${hit2.name}@${hit2.version}`;
4487
5135
  return {
4488
5136
  id: makeFindingId("DEPS-TYPOSQUAT-001", lockFile, snippet),
4489
5137
  ruleId: "DEPS-TYPOSQUAT-001",
4490
5138
  module: "deps",
4491
- title: `Possible typosquat: ${hit.name}`,
5139
+ title: `Possible typosquat: ${hit2.name}`,
4492
5140
  severity: "medium",
4493
5141
  confidence: "low",
4494
5142
  file: lockFile,
4495
5143
  snippet,
4496
- message: `Package "${hit.name}" is 1 character from popular package "${hit.lookalike}" (possible typosquat).`,
4497
- remediation: `Confirm you intended to install "${hit.name}". If not, remove it and depend on "${hit.lookalike}" instead.`
5144
+ message: `Package "${hit2.name}" is 1 character from popular package "${hit2.lookalike}" (possible typosquat).`,
5145
+ remediation: `Confirm you intended to install "${hit2.name}". If not, remove it and depend on "${hit2.lookalike}" instead.`
4498
5146
  };
4499
5147
  }
4500
5148
  function noLockFinding() {
@@ -4528,6 +5176,8 @@ function deprecatedFinding(lockFile, pkg) {
4528
5176
  var depsModule = {
4529
5177
  name: "deps",
4530
5178
  async run(ctx) {
5179
+ depsModule.unusedDirect = void 0;
5180
+ depsModule.upgradePlan = void 0;
4531
5181
  const lockFile = LOCKFILE_NAMES.find((name) => ctx.files.includes(name));
4532
5182
  const cacheDir = ctx.config.deps.cacheDir !== void 0 ? resolve(ctx.cwd, ctx.config.deps.cacheDir) : defaultCacheDir();
4533
5183
  const osvOpts = { cacheDir, offline: ctx.config.deps.offline };
@@ -4553,21 +5203,21 @@ var depsModule = {
4553
5203
  }
4554
5204
  const pins = exactPinsFromManifest(manifest);
4555
5205
  const directNames2 = await readDirectDependencyNames(ctx.cwd);
4556
- for (const hit of findTyposquats(pins, { directNames: directNames2 })) {
4557
- findings2.push(typosquatFinding("package.json", hit));
5206
+ for (const hit2 of findTyposquats(pins, { directNames: directNames2 })) {
5207
+ findings2.push(typosquatFinding("package.json", hit2));
4558
5208
  }
4559
5209
  const osvHits2 = await queryOsv(pins, osvOpts);
4560
5210
  const symbols2 = /* @__PURE__ */ new Map();
4561
- for (const hit of osvHits2) {
4562
- for (const vuln of hit.vulns) {
4563
- const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit.pkg, vuln) : vulnFinding("package.json", hit.pkg, vuln, depLoc(hit.pkg.name));
5211
+ for (const hit2 of osvHits2) {
5212
+ for (const vuln of hit2.vulns) {
5213
+ const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit2.pkg, vuln) : vulnFinding("package.json", hit2.pkg, vuln, depLoc(hit2.pkg.name));
4564
5214
  const names = affectedFunctionNames(vuln);
4565
5215
  if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols2.set(finding3.id, names);
4566
5216
  findings2.push(finding3);
4567
5217
  }
4568
5218
  }
4569
5219
  const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
4570
- return applyReachability(findings2, noLockCtx, symbols2);
5220
+ return withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, applyReachability(findings2, noLockCtx, symbols2));
4571
5221
  }
4572
5222
  let contents;
4573
5223
  try {
@@ -4575,12 +5225,12 @@ var depsModule = {
4575
5225
  } catch {
4576
5226
  return [];
4577
5227
  }
4578
- const graph = parseLockfileGraph(lockFile, contents);
4579
- const packages = graph.packages;
5228
+ const graph2 = parseLockfileGraph(lockFile, contents);
5229
+ const packages = graph2.packages;
4580
5230
  const findings = [];
4581
5231
  const directNames = await readDirectDependencyNames(ctx.cwd);
4582
- for (const hit of findTyposquats(packages, { directNames })) {
4583
- findings.push(typosquatFinding(lockFile, hit));
5232
+ for (const hit2 of findTyposquats(packages, { directNames })) {
5233
+ findings.push(typosquatFinding(lockFile, hit2));
4584
5234
  }
4585
5235
  for (const pkg of packages) {
4586
5236
  if (pkg.deprecated !== void 0) {
@@ -4589,22 +5239,129 @@ var depsModule = {
4589
5239
  }
4590
5240
  const osvHits = await queryOsv(packages, osvOpts);
4591
5241
  const symbols = /* @__PURE__ */ new Map();
4592
- for (const hit of osvHits) {
4593
- for (const vuln of hit.vulns) {
4594
- const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit.pkg, vuln) : vulnFinding(lockFile, hit.pkg, vuln, depLoc(hit.pkg.name));
5242
+ for (const hit2 of osvHits) {
5243
+ for (const vuln of hit2.vulns) {
5244
+ const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit2.pkg, vuln) : vulnFinding(lockFile, hit2.pkg, vuln, depLoc(hit2.pkg.name));
4595
5245
  const names = affectedFunctionNames(vuln);
4596
5246
  if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols.set(finding3.id, names);
4597
5247
  findings.push(finding3);
4598
5248
  }
4599
5249
  }
4600
- let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph), symbols);
5250
+ const reachCtx = await reachabilityCtx(ctx.cwd, ctx.files, graph2);
5251
+ let reached = applyReachability(findings, reachCtx, symbols);
4601
5252
  if (experimentalVex()) {
4602
- reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
5253
+ reached = attachOverrideProposals(reached, graph2, lockFile, manifestRaw);
4603
5254
  reached.push(...scanInstalledPackages(ctx.cwd, packages));
4604
5255
  }
5256
+ if (licenseEnabled()) reached.push(...await scanCopyleftLicenses(ctx.cwd));
5257
+ depsModule.unusedDirect = unusedDirectPlan(reached, reachCtx);
5258
+ const direct = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
5259
+ const advisories = [];
5260
+ for (const hit2 of osvHits) {
5261
+ if (!direct.has(hit2.pkg.name)) continue;
5262
+ for (const vuln of hit2.vulns) {
5263
+ if (isMaliciousAdvisory(vuln)) continue;
5264
+ advisories.push({
5265
+ name: hit2.pkg.name,
5266
+ from: hit2.pkg.version,
5267
+ id: vuln.id,
5268
+ fixed: minFixedVersion(vuln.affected ?? [])
5269
+ });
5270
+ }
5271
+ }
5272
+ depsModule.upgradePlan = chooseUpgradePlan(advisories);
5273
+ if (process.env.FULLSHIELD_EXPERIMENTAL_REACH_EVIDENCE === "1") {
5274
+ const sites = collectImportSites(reachCtx.sources);
5275
+ const files = [...reachCtx.sources.keys()].sort();
5276
+ reached = reached.map((finding3) => {
5277
+ const evidence = evidenceForFinding(finding3, sites, files, graph2.edgesKnown);
5278
+ return evidence === void 0 ? finding3 : { ...finding3, reachabilityEvidence: evidence };
5279
+ });
5280
+ }
5281
+ reached = await withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, reached);
5282
+ if (process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY === "1") {
5283
+ reached.push(...campaignFindings([...reachCtx.sources.entries()].map(([file, text]) => ({ file, text }))));
5284
+ for (const name of [...direct].sort()) {
5285
+ const locked = packages.find((pkg) => pkg.name === name)?.version;
5286
+ if (locked === void 0) continue;
5287
+ const latest = await readPlaceholderLatest(join(cacheDir, "registry", `${name.replaceAll("/", "__")}.json`));
5288
+ const placeholder = securityPlaceholderFinding(name, locked, latest);
5289
+ if (placeholder !== void 0) reached.push(placeholder);
5290
+ }
5291
+ }
4605
5292
  return reached;
4606
5293
  }
4607
5294
  };
5295
+ function workspacesEnabled() {
5296
+ return process.env.FULLSHIELD_EXPERIMENTAL_WORKSPACES === "1";
5297
+ }
5298
+ function workspacePatterns(manifestRaw) {
5299
+ try {
5300
+ const manifest = JSON.parse(manifestRaw);
5301
+ const field = manifest.workspaces;
5302
+ if (Array.isArray(field)) return field.filter((item) => typeof item === "string");
5303
+ if (field !== null && typeof field === "object" && Array.isArray(field.packages)) {
5304
+ return field.packages.filter((item) => typeof item === "string");
5305
+ }
5306
+ } catch {
5307
+ return [];
5308
+ }
5309
+ return [];
5310
+ }
5311
+ async function expandWorkspace(cwd, pattern) {
5312
+ const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
5313
+ if (!normalized.includes("*")) return [normalized];
5314
+ const slash = normalized.indexOf("/*");
5315
+ if (slash < 0) return [];
5316
+ const parent = normalized.slice(0, slash);
5317
+ try {
5318
+ const entries = await readdir(join(cwd, parent), { withFileTypes: true });
5319
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => `${parent}/${entry.name}`).sort();
5320
+ } catch {
5321
+ return [];
5322
+ }
5323
+ }
5324
+ async function workspaceAdvisoryFindings(cwd, manifestRaw, osvOpts) {
5325
+ const dirs = /* @__PURE__ */ new Set();
5326
+ for (const pattern of workspacePatterns(manifestRaw)) {
5327
+ for (const dir of await expandWorkspace(cwd, pattern)) dirs.add(dir);
5328
+ }
5329
+ const findings = [];
5330
+ for (const dir of [...dirs].sort()) {
5331
+ if (dir === "" || dir === ".") continue;
5332
+ const lockName = LOCKFILE_NAMES.find((name) => existsSync(join(cwd, dir, name)));
5333
+ if (lockName === void 0) continue;
5334
+ let text;
5335
+ try {
5336
+ text = await readFile(join(cwd, dir, lockName), "utf8");
5337
+ } catch {
5338
+ continue;
5339
+ }
5340
+ const graph2 = parseLockfileGraph(lockName, text);
5341
+ const hits = await queryOsv(graph2.packages, osvOpts);
5342
+ const file = `${dir}/${lockName}`;
5343
+ for (const hit2 of hits) {
5344
+ for (const vuln of hit2.vulns) {
5345
+ findings.push(
5346
+ isMaliciousAdvisory(vuln) ? maliciousFinding(file, hit2.pkg, vuln) : vulnFinding(file, hit2.pkg, vuln)
5347
+ );
5348
+ }
5349
+ }
5350
+ }
5351
+ return findings;
5352
+ }
5353
+ async function withWorkspaceAdvisories(cwd, manifestRaw, osvOpts, findings) {
5354
+ if (!workspacesEnabled() || manifestRaw === void 0) return findings;
5355
+ return [...findings, ...await workspaceAdvisoryFindings(cwd, manifestRaw, osvOpts)];
5356
+ }
5357
+ async function readPlaceholderLatest(file) {
5358
+ try {
5359
+ const raw = JSON.parse(await readFile(file, "utf8"));
5360
+ return typeof raw.latest === "string" ? raw.latest : void 0;
5361
+ } catch {
5362
+ return void 0;
5363
+ }
5364
+ }
4608
5365
  function graphFromManifest(manifest) {
4609
5366
  const keys = (field) => {
4610
5367
  const block = manifest[field];
@@ -4621,7 +5378,7 @@ function graphFromManifest(manifest) {
4621
5378
  edgesKnown: false
4622
5379
  };
4623
5380
  }
4624
- async function reachabilityCtx(cwd, files, graph) {
5381
+ async function reachabilityCtx(cwd, files, graph2) {
4625
5382
  const prodSources = /* @__PURE__ */ new Map();
4626
5383
  for (const rel of files) {
4627
5384
  if (!/\.[cm]?[jt]sx?$/.test(rel)) continue;
@@ -4631,7 +5388,7 @@ async function reachabilityCtx(cwd, files, graph) {
4631
5388
  } catch {
4632
5389
  }
4633
5390
  }
4634
- return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
5391
+ return { productionUsage: indexPackageUsage(prodSources), graph: graph2, sources: prodSources };
4635
5392
  }
4636
5393
 
4637
5394
  // src/modules/mcp/index.ts
@@ -5006,9 +5763,142 @@ init_finding();
5006
5763
  // src/modules/sast/interproc.ts
5007
5764
  init_finding();
5008
5765
 
5009
- // src/modules/sast/frameworks.ts
5010
- var NEXT_HANDLERS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5011
- var NEST_HTTP = /^(Get|Post|Put|Patch|Delete|All|Head|Options)$/;
5766
+ // src/runtime/lru.ts
5767
+ var LruMap = class {
5768
+ constructor(max) {
5769
+ this.max = max;
5770
+ if (max < 1) throw new Error("LruMap max must be >= 1");
5771
+ }
5772
+ max;
5773
+ map = /* @__PURE__ */ new Map();
5774
+ get size() {
5775
+ return this.map.size;
5776
+ }
5777
+ has(key) {
5778
+ return this.map.has(key);
5779
+ }
5780
+ get(key) {
5781
+ if (!this.map.has(key)) return void 0;
5782
+ const value = this.map.get(key);
5783
+ this.map.delete(key);
5784
+ this.map.set(key, value);
5785
+ return value;
5786
+ }
5787
+ set(key, value) {
5788
+ if (this.map.has(key)) this.map.delete(key);
5789
+ this.map.set(key, value);
5790
+ while (this.map.size > this.max) {
5791
+ const oldest = this.map.keys().next().value;
5792
+ if (oldest === void 0) break;
5793
+ this.map.delete(oldest);
5794
+ }
5795
+ return this;
5796
+ }
5797
+ };
5798
+ function packageVersion() {
5799
+ let dir = dirname(fileURLToPath(import.meta.url));
5800
+ for (let depth = 0; depth < 6; depth += 1) {
5801
+ try {
5802
+ const raw = readFileSync(join(dir, "package.json"), "utf8");
5803
+ const pkg = JSON.parse(raw);
5804
+ if (pkg.name === "fullshield" && typeof pkg.version === "string") {
5805
+ return pkg.version;
5806
+ }
5807
+ } catch {
5808
+ }
5809
+ const parent = dirname(dir);
5810
+ if (parent === dir) break;
5811
+ dir = parent;
5812
+ }
5813
+ throw new Error("fullshield package.json not found");
5814
+ }
5815
+
5816
+ // src/core/parse-pool.ts
5817
+ var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
5818
+ var cache = new LruMap(64);
5819
+ function parsePoolEnabled() {
5820
+ return process.env.FULLSHIELD_PARSE_POOL === "1";
5821
+ }
5822
+ function parsePoolSize() {
5823
+ const raw = process.env.FULLSHIELD_PARSE_WORKERS;
5824
+ if (raw !== void 0 && raw !== "") {
5825
+ const parsed = Number(raw);
5826
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
5827
+ }
5828
+ return Math.min(2, Math.max(1, availableParallelism() - 1));
5829
+ }
5830
+ function workerMemoryMb() {
5831
+ const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
5832
+ const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
5833
+ if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
5834
+ return 512;
5835
+ }
5836
+ function parseCacheDir() {
5837
+ return join(homedir(), ".cache", "fullshield", "parse");
5838
+ }
5839
+ function parseCacheKey(code) {
5840
+ return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
5841
+ }
5842
+ function parseOne(code) {
5843
+ const key = parseCacheKey(code);
5844
+ const hit2 = cache.get(key);
5845
+ if (hit2 !== void 0) return Promise.resolve(hit2);
5846
+ return new Promise((resolve5, reject) => {
5847
+ const worker = new Worker(fileURLToPath(workerUrl), {
5848
+ workerData: { code },
5849
+ resourceLimits: { maxOldGenerationSizeMb: workerMemoryMb() }
5850
+ });
5851
+ let settled = false;
5852
+ const finish = () => {
5853
+ void worker.terminate();
5854
+ };
5855
+ const succeed = (ast) => {
5856
+ if (settled) return;
5857
+ settled = true;
5858
+ cache.set(key, ast);
5859
+ finish();
5860
+ resolve5(ast);
5861
+ };
5862
+ const fail = (error) => {
5863
+ if (settled) return;
5864
+ settled = true;
5865
+ finish();
5866
+ reject(error);
5867
+ };
5868
+ worker.once("message", (ast) => {
5869
+ succeed(ast);
5870
+ });
5871
+ worker.once("error", (error) => {
5872
+ fail(error);
5873
+ });
5874
+ worker.once("exit", (code2) => {
5875
+ if (settled) return;
5876
+ fail(new Error(`parse worker exited ${code2 ?? "null"}`));
5877
+ });
5878
+ });
5879
+ }
5880
+ async function parseWithPool(codes) {
5881
+ const size = parsePoolSize();
5882
+ const out = new Array(codes.length);
5883
+ let next = 0;
5884
+ async function run() {
5885
+ for (; ; ) {
5886
+ const index = next;
5887
+ next += 1;
5888
+ if (index >= codes.length) return;
5889
+ const code = codes[index];
5890
+ if (code === void 0) return;
5891
+ out[index] = await parseOne(code);
5892
+ }
5893
+ }
5894
+ const workers = Math.min(size, Math.max(1, codes.length));
5895
+ await Promise.all(Array.from({ length: workers }, () => run()));
5896
+ return out;
5897
+ }
5898
+
5899
+ // src/modules/sast/frameworks.ts
5900
+ var NEXT_HANDLERS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
5901
+ var NEST_HTTP = /^(Get|Post|Put|Patch|Delete|All|Head|Options)$/;
5012
5902
  var NEST_INPUT = /^(Body|Query|Param|Headers)$/;
5013
5903
  var REQ_READERS = /* @__PURE__ */ new Set(["json", "formData", "text", "arrayBuffer"]);
5014
5904
  var HONO_READERS = /* @__PURE__ */ new Set(["query", "param", "header", "json", "parseBody", "text"]);
@@ -5141,6 +6031,10 @@ function isMcpArgsTaintSource(node) {
5141
6031
  }
5142
6032
 
5143
6033
  // src/modules/sast/taint.ts
6034
+ var customTaint;
6035
+ function setCustomTaint(config) {
6036
+ customTaint = config;
6037
+ }
5144
6038
  var SCHEMA_ROOT = /^(z|zod|joi|yup|schema)$/i;
5145
6039
  function objectChainMentions(node, pattern) {
5146
6040
  let current = node;
@@ -5286,6 +6180,61 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
5286
6180
  return false;
5287
6181
  }
5288
6182
  }
6183
+ function valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners) {
6184
+ if (node === void 0) return false;
6185
+ if (isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) return false;
6186
+ if (node.type === "Identifier") {
6187
+ if (nosqlSanitized.has(node.name)) return false;
6188
+ return built.has(node.name) || tainted.has(node.name);
6189
+ }
6190
+ if (isRequestObjectNode(node)) return true;
6191
+ if (node.type === "AwaitExpression") {
6192
+ return valueFeedsFilter(node.argument, tainted, returners, nosqlSanitized, built, filterReturners);
6193
+ }
6194
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && filterReturners.has(node.callee.name)) {
6195
+ return true;
6196
+ }
6197
+ if (node.type === "ObjectExpression") {
6198
+ return node.properties.some((prop) => {
6199
+ if (prop.type === "SpreadElement") {
6200
+ return valueFeedsFilter(prop.argument, tainted, returners, nosqlSanitized, built, filterReturners);
6201
+ }
6202
+ if (prop.type !== "Property") return false;
6203
+ return valueFeedsFilter(prop.value, tainted, returners, nosqlSanitized, built, filterReturners);
6204
+ });
6205
+ }
6206
+ if (node.type === "ArrayExpression") {
6207
+ return node.elements.some((el) => {
6208
+ if (el === null) return false;
6209
+ if (el.type === "SpreadElement") {
6210
+ return valueFeedsFilter(el.argument, tainted, returners, nosqlSanitized, built, filterReturners);
6211
+ }
6212
+ return valueFeedsFilter(el, tainted, returners, nosqlSanitized, built, filterReturners);
6213
+ });
6214
+ }
6215
+ if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
6216
+ return node.arguments.some(
6217
+ (arg) => arg.type !== "SpreadElement" && valueFeedsFilter(arg, tainted, returners, nosqlSanitized, built, filterReturners)
6218
+ );
6219
+ }
6220
+ if (node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "MemberExpression") {
6221
+ return exprIsTainted(node, tainted, returners);
6222
+ }
6223
+ return false;
6224
+ }
6225
+ function initIsBuiltFilter(node, tainted, returners, nosqlSanitized, built, filterReturners) {
6226
+ if (node.type === "AwaitExpression") {
6227
+ return initIsBuiltFilter(node.argument, tainted, returners, nosqlSanitized, built, filterReturners);
6228
+ }
6229
+ if (node.type === "Identifier") return built.has(node.name) && !nosqlSanitized.has(node.name);
6230
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && filterReturners.has(node.callee.name)) {
6231
+ return true;
6232
+ }
6233
+ if (node.type === "ObjectExpression" || node.type === "ArrayExpression") {
6234
+ return valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners);
6235
+ }
6236
+ 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);
6237
+ }
5289
6238
  function exprIsSanitized(node, tainted, names, isCall, returners = NO_RETURNERS) {
5290
6239
  if (node === void 0) return true;
5291
6240
  if (isCall(node)) return true;
@@ -5349,7 +6298,7 @@ function seedMcpToolParams(scope, tainted) {
5349
6298
  for (const name of boundNames(param)) tainted.add(name);
5350
6299
  }
5351
6300
  }
5352
- function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
6301
+ function analyzeScope(scope, seed = [], returners = NO_RETURNERS, filterReturners = NO_RETURNERS, initialBuilt = NO_RETURNERS) {
5353
6302
  const tainted = new Set(seed);
5354
6303
  seedDestructuredRequestParams(scope, tainted);
5355
6304
  seedFrameworkParams(scope, tainted);
@@ -5357,6 +6306,8 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5357
6306
  const urlSanitized = /* @__PURE__ */ new Set();
5358
6307
  const xssSanitized = /* @__PURE__ */ new Set();
5359
6308
  const nosqlSanitized = /* @__PURE__ */ new Set();
6309
+ const sqlCleared = /* @__PURE__ */ new Set();
6310
+ const customPath = /* @__PURE__ */ new Set();
5360
6311
  let changed = true;
5361
6312
  while (changed) {
5362
6313
  changed = false;
@@ -5367,6 +6318,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5367
6318
  for (const name of boundNames(node.id)) nosqlSanitized.add(name);
5368
6319
  }
5369
6320
  if (isSchemaValidatorCall(init)) return;
6321
+ applyCustomCall(init, boundNames(node.id), tainted, sqlCleared, customPath);
5370
6322
  if (isUrlSanitizerCall(init)) {
5371
6323
  for (const name of boundNames(node.id)) urlSanitized.add(name);
5372
6324
  }
@@ -5387,6 +6339,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5387
6339
  for (const name of boundNames(node.left)) nosqlSanitized.add(name);
5388
6340
  }
5389
6341
  if (isSchemaValidatorCall(node.right)) return;
6342
+ applyCustomCall(node.right, boundNames(node.left), tainted, sqlCleared, customPath);
5390
6343
  if (isUrlSanitizerCall(node.right)) {
5391
6344
  for (const name of boundNames(node.left)) urlSanitized.add(name);
5392
6345
  }
@@ -5413,7 +6366,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5413
6366
  for (const name of boundNames(node.left)) resolved.add(name);
5414
6367
  }
5415
6368
  });
5416
- const pathSanitized = /* @__PURE__ */ new Set();
6369
+ const pathSanitized = new Set(customPath);
5417
6370
  walkScope(scope, (node) => {
5418
6371
  if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
5419
6372
  if (propName(node.callee) !== "startsWith") return;
@@ -5455,17 +6408,92 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
5455
6408
  for (const name of boundNames(node.left)) requestObjects.add(name);
5456
6409
  }
5457
6410
  });
6411
+ const builtFilters = new Set(initialBuilt);
6412
+ let grew = true;
6413
+ while (grew) {
6414
+ grew = false;
6415
+ const add = (names) => {
6416
+ for (const name of names) {
6417
+ if (nosqlSanitized.has(name) || builtFilters.has(name)) continue;
6418
+ builtFilters.add(name);
6419
+ grew = true;
6420
+ }
6421
+ };
6422
+ walkScope(scope, (node) => {
6423
+ if (node.type === "VariableDeclarator" && node.init !== null) {
6424
+ if (initIsBuiltFilter(node.init, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
6425
+ add(boundNames(node.id));
6426
+ }
6427
+ }
6428
+ if (node.type !== "AssignmentExpression") return;
6429
+ if (node.left.type === "Identifier" && initIsBuiltFilter(node.right, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
6430
+ add([node.left.name]);
6431
+ }
6432
+ if (node.left.type === "MemberExpression" && node.left.object.type === "Identifier" && valueFeedsFilter(node.right, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
6433
+ add([node.left.object.name]);
6434
+ }
6435
+ });
6436
+ }
5458
6437
  return {
5459
6438
  tainted,
5460
6439
  pathSanitized,
6440
+ sqlCleared,
5461
6441
  urlSanitized,
5462
6442
  xssSanitized,
5463
6443
  nosqlSanitized,
5464
6444
  hasPasswordName,
5465
6445
  requestObjects,
6446
+ builtFilters,
6447
+ filterReturners: new Set(filterReturners),
5466
6448
  returners: new Set(returners)
5467
6449
  };
5468
6450
  }
6451
+ function locallyDeclared(scope) {
6452
+ const names = /* @__PURE__ */ new Set();
6453
+ if (isFunctionNode(scope)) {
6454
+ for (const param of scope.params) {
6455
+ for (const name of boundNames(param)) names.add(name);
6456
+ }
6457
+ }
6458
+ walkScope(scope, (node) => {
6459
+ if (node.type === "VariableDeclarator") {
6460
+ for (const name of boundNames(node.id)) names.add(name);
6461
+ }
6462
+ if (node.type === "FunctionDeclaration" && node.id !== null) names.add(node.id.name);
6463
+ });
6464
+ return names;
6465
+ }
6466
+ function inheritedBuiltFilters(fn, facts) {
6467
+ const local = locallyDeclared(fn);
6468
+ const inherited = /* @__PURE__ */ new Set();
6469
+ let parent = fn.parent;
6470
+ while (parent !== void 0 && !isFunctionNode(parent)) {
6471
+ parent = parent.parent;
6472
+ }
6473
+ const outer = parent === void 0 ? void 0 : facts.get(parent);
6474
+ if (outer === void 0) return inherited;
6475
+ for (const name of outer.builtFilters) {
6476
+ if (!local.has(name)) inherited.add(name);
6477
+ }
6478
+ return inherited;
6479
+ }
6480
+ function scopeReturnsBuiltFilter(scope, facts) {
6481
+ let yes = false;
6482
+ walkScope(scope, (node) => {
6483
+ if (node.type !== "ReturnStatement" || node.argument === null) return;
6484
+ if (initIsBuiltFilter(
6485
+ node.argument,
6486
+ facts.tainted,
6487
+ facts.returners,
6488
+ facts.nosqlSanitized,
6489
+ facts.builtFilters,
6490
+ facts.filterReturners
6491
+ )) {
6492
+ yes = true;
6493
+ }
6494
+ });
6495
+ return yes;
6496
+ }
5469
6497
  function functionName(fn) {
5470
6498
  if (fn.type === "FunctionDeclaration" && fn.id !== null) return fn.id.name;
5471
6499
  if (isFunctionNode(fn) && fn.id !== null && fn.id.type === "Identifier") return fn.id.name;
@@ -5540,7 +6568,7 @@ function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
5540
6568
  }
5541
6569
  return changed;
5542
6570
  }
5543
- function analyzeFile(ast, extraReturners = NO_RETURNERS) {
6571
+ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
5544
6572
  attachParents(ast);
5545
6573
  const facts = /* @__PURE__ */ new WeakMap();
5546
6574
  const functions = [];
@@ -5554,12 +6582,13 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
5554
6582
  }
5555
6583
  const seeds = /* @__PURE__ */ new Map();
5556
6584
  for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
5557
- const returners = new Set(extraReturners);
6585
+ const returners = new Set(extraReturners2);
6586
+ const filterReturners = /* @__PURE__ */ new Set();
5558
6587
  for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
5559
6588
  let changed = false;
5560
- facts.set(ast, analyzeScope(ast, [], returners));
6589
+ facts.set(ast, analyzeScope(ast, [], returners, filterReturners));
5561
6590
  for (const fn of functions) {
5562
- facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners));
6591
+ facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners, filterReturners, inheritedBuiltFilters(fn, facts)));
5563
6592
  }
5564
6593
  for (const fn of functions) {
5565
6594
  const name = functionName(fn);
@@ -5569,6 +6598,10 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
5569
6598
  returners.add(name);
5570
6599
  changed = true;
5571
6600
  }
6601
+ if (scopeReturnsBuiltFilter(fn, f) && !filterReturners.has(name)) {
6602
+ filterReturners.add(name);
6603
+ changed = true;
6604
+ }
5572
6605
  }
5573
6606
  for (const fn of functions) {
5574
6607
  const f = facts.get(fn);
@@ -5601,6 +6634,54 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
5601
6634
  }
5602
6635
  return facts;
5603
6636
  }
6637
+ var SQL_SINK_NAMES = /* @__PURE__ */ new Set(["query", "execute"]);
6638
+ var CMD_SINK_NAMES = /* @__PURE__ */ new Set(["exec", "execSync", "spawn", "spawnSync"]);
6639
+ var PATH_SINK_NAMES = /* @__PURE__ */ new Set(["readFile", "readFileSync", "writeFile", "writeFileSync"]);
6640
+ function sinkClasses(scope, tainted, returners) {
6641
+ const found = /* @__PURE__ */ new Set();
6642
+ walkScope(scope, (node) => {
6643
+ if (node.type !== "CallExpression") return;
6644
+ const name = calleeName(node);
6645
+ const first = node.arguments[0];
6646
+ if (name === void 0 || first === void 0 || first.type === "SpreadElement") return;
6647
+ if (!exprIsTainted(first, tainted, returners)) return;
6648
+ if (SQL_SINK_NAMES.has(name)) found.add("sqli");
6649
+ if (CMD_SINK_NAMES.has(name)) found.add("cmdi");
6650
+ if (PATH_SINK_NAMES.has(name)) found.add("path");
6651
+ });
6652
+ return [...found].sort();
6653
+ }
6654
+ function fileTaintSummary(ast) {
6655
+ attachParents(ast);
6656
+ const facts = analyzeFile(ast);
6657
+ const returnsTaint = [];
6658
+ const functions = [];
6659
+ walk(ast, (node) => {
6660
+ if (!isFunctionNode(node)) return;
6661
+ const name = functionName(node);
6662
+ const own = facts.get(node);
6663
+ if (name === void 0 || own === void 0) return;
6664
+ if (scopeReturnsTaint(node, own.tainted, own.returners)) returnsTaint.push(name);
6665
+ const paramsToReturn = [];
6666
+ const sinks = [];
6667
+ node.params.forEach((param, index) => {
6668
+ const pname = boundNames(param)[0];
6669
+ if (pname === void 0) return;
6670
+ const seeded = analyzeScope(node, [pname]);
6671
+ if (scopeReturnsTaint(node, seeded.tainted, seeded.returners)) paramsToReturn.push(index);
6672
+ for (const cls of sinkClasses(node, seeded.tainted, seeded.returners)) sinks.push({ param: index, class: cls });
6673
+ });
6674
+ functions.push({
6675
+ name,
6676
+ line: node.loc?.start.line ?? 0,
6677
+ paramsToReturn,
6678
+ sinks
6679
+ });
6680
+ });
6681
+ returnsTaint.sort();
6682
+ functions.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
6683
+ return { returnsTaint, functions };
6684
+ }
5604
6685
  function functionsThatReturnTaint(ast) {
5605
6686
  attachParents(ast);
5606
6687
  const names = /* @__PURE__ */ new Set();
@@ -5622,18 +6703,50 @@ function factsFor(node, map) {
5622
6703
  return map.get(enclosingScope(node)) ?? {
5623
6704
  tainted: /* @__PURE__ */ new Set(),
5624
6705
  pathSanitized: /* @__PURE__ */ new Set(),
6706
+ sqlCleared: /* @__PURE__ */ new Set(),
5625
6707
  urlSanitized: /* @__PURE__ */ new Set(),
5626
6708
  xssSanitized: /* @__PURE__ */ new Set(),
5627
6709
  nosqlSanitized: /* @__PURE__ */ new Set(),
5628
6710
  hasPasswordName: false,
5629
6711
  requestObjects: /* @__PURE__ */ new Set(),
6712
+ builtFilters: /* @__PURE__ */ new Set(),
6713
+ filterReturners: /* @__PURE__ */ new Set(),
5630
6714
  returners: /* @__PURE__ */ new Set()
5631
6715
  };
5632
6716
  }
6717
+ function nodeHoldsBuiltFilter(node, map) {
6718
+ const facts = factsFor(node, map);
6719
+ if (node.type === "Identifier") return facts.builtFilters.has(node.name);
6720
+ if (node.type === "AwaitExpression") return nodeHoldsBuiltFilter(node.argument, map);
6721
+ if (node.type === "CallExpression" && node.callee.type === "Identifier") {
6722
+ return facts.filterReturners.has(node.callee.name);
6723
+ }
6724
+ return false;
6725
+ }
5633
6726
  function nodeIsTainted(node, map) {
5634
6727
  const facts = factsFor(node, map);
5635
6728
  return exprIsTainted(node, facts.tainted, facts.returners);
5636
6729
  }
6730
+ function nodeIsSqlCleared(node, map) {
6731
+ if (node.type === "Identifier") return factsFor(node, map).sqlCleared.has(node.name);
6732
+ return false;
6733
+ }
6734
+ function applyCustomCall(init, names, tainted, sqlCleared, customPath) {
6735
+ if (customTaint === void 0 || init === void 0 || init.type !== "CallExpression") return;
6736
+ const called2 = calleeName(init);
6737
+ if (called2 === void 0) return;
6738
+ if (customTaint.sources.has(called2)) {
6739
+ for (const name of names) tainted.add(name);
6740
+ }
6741
+ const clears = customTaint.sanitizers.get(called2);
6742
+ if (clears === void 0) return;
6743
+ if (clears.has("sqli")) {
6744
+ for (const name of names) sqlCleared.add(name);
6745
+ }
6746
+ if (clears.has("path")) {
6747
+ for (const name of names) customPath.add(name);
6748
+ }
6749
+ }
5637
6750
  function nodeIsPathSanitized(node, map) {
5638
6751
  if (node.type !== "Identifier") return false;
5639
6752
  return factsFor(node, map).pathSanitized.has(node.name);
@@ -5656,6 +6769,147 @@ function nodeIsNosqlSanitized(node, map) {
5656
6769
  return false;
5657
6770
  }
5658
6771
 
6772
+ // src/modules/sast/summaries.ts
6773
+ function taintSummariesEnabled() {
6774
+ return process.env.FULLSHIELD_TAINT_SUMMARIES === "1";
6775
+ }
6776
+ var configHash = "";
6777
+ function setTaintSummaryConfig(config) {
6778
+ fileMemo.clear();
6779
+ if (config === void 0) {
6780
+ configHash = "";
6781
+ return;
6782
+ }
6783
+ configHash = createHash("sha256").update(
6784
+ JSON.stringify({
6785
+ maxFileBytes: config.sast.maxFileBytes,
6786
+ sources: config.sast.sources ?? [],
6787
+ sanitizers: config.sast.sanitizers ?? [],
6788
+ taintV2: taintV2Enabled(),
6789
+ frameworks: frameworksEnabled(),
6790
+ gaps: sastGapsEnabled(),
6791
+ classes: classesEnabled(),
6792
+ custom: process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1"
6793
+ })
6794
+ ).digest("hex");
6795
+ }
6796
+ function summaryDir() {
6797
+ return process.env.FULLSHIELD_TAINT_SUMMARY_DIR ?? join(parseCacheDir(), "taint");
6798
+ }
6799
+ function taintSummaryKey(source, extra = "") {
6800
+ return createHash("sha256").update(packageVersion()).update("\0").update(configHash).update("\0").update(extra).update("\0").update(source).digest("hex");
6801
+ }
6802
+ function summaryPath(key) {
6803
+ return join(summaryDir(), `${key}.json`);
6804
+ }
6805
+ function isStringArray(value) {
6806
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
6807
+ }
6808
+ function readJson3(key) {
6809
+ let text;
6810
+ try {
6811
+ text = readFileSync(summaryPath(key), "utf8");
6812
+ } catch {
6813
+ return void 0;
6814
+ }
6815
+ try {
6816
+ return JSON.parse(text);
6817
+ } catch {
6818
+ discard(key);
6819
+ return void 0;
6820
+ }
6821
+ }
6822
+ function discard(key) {
6823
+ rmSync(summaryPath(key), { force: true });
6824
+ }
6825
+ function writeJson(key, value) {
6826
+ const dir = summaryDir();
6827
+ mkdirSync(dir, { recursive: true });
6828
+ writeFileSync(summaryPath(key), JSON.stringify(value));
6829
+ }
6830
+ function readFileSummary(key) {
6831
+ const raw = readJson3(key);
6832
+ if (typeof raw !== "object" || raw === null) {
6833
+ if (raw !== void 0) discard(key);
6834
+ return void 0;
6835
+ }
6836
+ const row = raw;
6837
+ if (row.v !== 1 || !isStringArray(row.returnsTaint) || !Array.isArray(row.functions)) {
6838
+ discard(key);
6839
+ return void 0;
6840
+ }
6841
+ return row;
6842
+ }
6843
+ var fileMemo = /* @__PURE__ */ new Map();
6844
+ function cachedReturners(source, ast) {
6845
+ const key = taintSummaryKey(source, "file");
6846
+ const memo = fileMemo.get(key);
6847
+ if (memo !== void 0) return new Set(memo.returnsTaint);
6848
+ const cached = readFileSummary(key);
6849
+ if (cached !== void 0) {
6850
+ fileMemo.set(key, cached);
6851
+ return new Set(cached.returnsTaint);
6852
+ }
6853
+ const summary = fileTaintSummary(ast);
6854
+ const stored = { v: 1, returnsTaint: summary.returnsTaint, functions: summary.functions };
6855
+ fileMemo.set(key, stored);
6856
+ writeJson(key, stored);
6857
+ return new Set(stored.returnsTaint);
6858
+ }
6859
+ function lists(facts) {
6860
+ const names = (set) => [...set].sort();
6861
+ return {
6862
+ v: 1,
6863
+ tainted: names(facts.tainted),
6864
+ pathSanitized: names(facts.pathSanitized),
6865
+ sqlCleared: names(facts.sqlCleared),
6866
+ urlSanitized: names(facts.urlSanitized),
6867
+ xssSanitized: names(facts.xssSanitized),
6868
+ nosqlSanitized: names(facts.nosqlSanitized),
6869
+ requestObjects: names(facts.requestObjects),
6870
+ builtFilters: names(facts.builtFilters),
6871
+ filterReturners: names(facts.filterReturners),
6872
+ returners: names(facts.returners),
6873
+ hasPasswordName: facts.hasPasswordName
6874
+ };
6875
+ }
6876
+ function factsFrom(row) {
6877
+ return {
6878
+ tainted: new Set(row.tainted),
6879
+ pathSanitized: new Set(row.pathSanitized),
6880
+ sqlCleared: new Set(row.sqlCleared),
6881
+ urlSanitized: new Set(row.urlSanitized),
6882
+ xssSanitized: new Set(row.xssSanitized),
6883
+ nosqlSanitized: new Set(row.nosqlSanitized),
6884
+ requestObjects: new Set(row.requestObjects),
6885
+ builtFilters: new Set(row.builtFilters),
6886
+ filterReturners: new Set(row.filterReturners),
6887
+ returners: new Set(row.returners),
6888
+ hasPasswordName: row.hasPasswordName
6889
+ };
6890
+ }
6891
+ function readFacts(key) {
6892
+ const raw = readJson3(key);
6893
+ if (typeof raw !== "object" || raw === null) {
6894
+ if (raw !== void 0) discard(key);
6895
+ return void 0;
6896
+ }
6897
+ const row = raw;
6898
+ 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") {
6899
+ discard(key);
6900
+ return void 0;
6901
+ }
6902
+ return factsFrom(row);
6903
+ }
6904
+ function cachedFunctionFacts(fileSource, fn, seed) {
6905
+ const key = taintSummaryKey(fileSource, `seed:${fn.loc?.start.line ?? 0}:${seed.join(",")}`);
6906
+ const cached = readFacts(key);
6907
+ if (cached !== void 0) return cached;
6908
+ const facts = analyzeFunctionWithSeed(fn, seed);
6909
+ writeJson(key, lists(facts));
6910
+ return facts;
6911
+ }
6912
+
5659
6913
  // src/modules/sast/interproc.ts
5660
6914
  var MAX_HOPS = 5;
5661
6915
  function collectFunctions(file, ast) {
@@ -5766,22 +7020,22 @@ function collectInstances(ast, ns) {
5766
7020
  if (node.type !== "VariableDeclarator" || node.init === null) return;
5767
7021
  if (node.id.type !== "Identifier" || node.init.type !== "NewExpression") return;
5768
7022
  if (node.init.callee.type !== "Identifier") return;
5769
- const hit = ns.get(node.init.callee.name);
5770
- if (hit === void 0) return;
5771
- instances.set(node.id.name, hit.includes("#") ? hit.split("#")[0] ?? hit : hit);
7023
+ const hit2 = ns.get(node.init.callee.name);
7024
+ if (hit2 === void 0) return;
7025
+ instances.set(node.id.name, hit2.includes("#") ? hit2.split("#")[0] ?? hit2 : hit2);
5772
7026
  });
5773
7027
  return instances;
5774
7028
  }
5775
7029
  function resolveCallee(node, ns, instances, currentFile, functions) {
5776
7030
  if (node.type !== "CallExpression") return void 0;
5777
7031
  if (node.callee.type === "Identifier") {
5778
- const hit = ns.get(node.callee.name);
5779
- if (hit !== void 0) {
5780
- if (hit.includes("#")) {
5781
- const [file, name] = hit.split("#");
7032
+ const hit2 = ns.get(node.callee.name);
7033
+ if (hit2 !== void 0) {
7034
+ if (hit2.includes("#")) {
7035
+ const [file, name] = hit2.split("#");
5782
7036
  if (file !== void 0 && name !== void 0) return { file, name };
5783
7037
  }
5784
- return { file: hit, name: node.callee.name };
7038
+ return { file: hit2, name: node.callee.name };
5785
7039
  }
5786
7040
  if (functions.has(`${currentFile}#${node.callee.name}`)) {
5787
7041
  return { file: currentFile, name: node.callee.name };
@@ -5888,9 +7142,9 @@ function runInterproc(parsed) {
5888
7142
  if (fn === void 0) continue;
5889
7143
  const param = fn.params[seed.paramIndex];
5890
7144
  if (param === void 0) continue;
5891
- const facts = analyzeFunctionWithSeed(fn.node, [param]);
5892
7145
  const p = byFile.get(fn.file);
5893
7146
  if (p === void 0) continue;
7147
+ const facts = taintSummariesEnabled() ? cachedFunctionFacts(p.source, fn.node, [param]) : analyzeFunctionWithSeed(fn.node, [param]);
5894
7148
  const ns = requires.get(fn.file) ?? /* @__PURE__ */ new Map();
5895
7149
  const inst = instances.get(fn.file) ?? /* @__PURE__ */ new Map();
5896
7150
  walk(fn.node, (node) => {
@@ -6313,6 +7567,178 @@ var jwtRule = {
6313
7567
  }
6314
7568
  };
6315
7569
 
7570
+ // src/modules/sast/validation.ts
7571
+ var PRIMITIVE = /* @__PURE__ */ new Set(["string", "number", "boolean", "IsString", "IsNumber", "IsInt", "IsBoolean"]);
7572
+ var OPEN = /* @__PURE__ */ new Set(["any", "unknown", "object", "array", "record", "lazy", "union", "tuple"]);
7573
+ var SCHEMA_ROOT2 = /* @__PURE__ */ new Set(["z", "zod", "Joi", "joi"]);
7574
+ var ROUTE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
7575
+ function validationEnabled() {
7576
+ return process.env.FULLSHIELD_EXPERIMENTAL_VALIDATION === "1";
7577
+ }
7578
+ function parentOf(node) {
7579
+ return node.parent;
7580
+ }
7581
+ function mentions(node, names) {
7582
+ let current = node;
7583
+ for (let i = 0; i < 12 && current !== void 0; i += 1) {
7584
+ if (current.type === "CallExpression") {
7585
+ const name = calleeName(current);
7586
+ if (name !== void 0 && names.has(name)) return true;
7587
+ current = current.callee.type === "MemberExpression" ? current.callee.object : void 0;
7588
+ continue;
7589
+ }
7590
+ if (current.type === "MemberExpression") {
7591
+ const name = propName(current);
7592
+ if (name !== void 0 && names.has(name)) return true;
7593
+ current = current.object;
7594
+ continue;
7595
+ }
7596
+ if (current.type === "Identifier" && names.has(current.name)) return true;
7597
+ break;
7598
+ }
7599
+ return false;
7600
+ }
7601
+ function rootName(node) {
7602
+ let current = node;
7603
+ for (let i = 0; i < 8 && current !== void 0; i += 1) {
7604
+ if (current.type === "Identifier") return current.name;
7605
+ if (current.type === "CallExpression") {
7606
+ current = current.callee;
7607
+ continue;
7608
+ }
7609
+ if (current.type === "MemberExpression") {
7610
+ current = current.object;
7611
+ continue;
7612
+ }
7613
+ break;
7614
+ }
7615
+ return void 0;
7616
+ }
7617
+ function propertyName(prop) {
7618
+ if (prop.type !== "Property") return void 0;
7619
+ const key = prop.key;
7620
+ if (key.type === "Identifier") return key.name;
7621
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
7622
+ return void 0;
7623
+ }
7624
+ function fieldsOfObjectSchema(node) {
7625
+ if (node.type !== "CallExpression" || calleeName(node) !== "object") return /* @__PURE__ */ new Set();
7626
+ const root = rootName(node.callee);
7627
+ if (root === void 0 || !SCHEMA_ROOT2.has(root)) return /* @__PURE__ */ new Set();
7628
+ const arg = node.arguments[0];
7629
+ if (arg === void 0 || arg.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
7630
+ const fields = /* @__PURE__ */ new Set();
7631
+ for (const prop of arg.properties) {
7632
+ if (prop.type !== "Property") continue;
7633
+ const name = propertyName(prop);
7634
+ if (name === void 0) continue;
7635
+ if (mentions(prop.value, OPEN)) continue;
7636
+ if (mentions(prop.value, PRIMITIVE)) fields.add(name);
7637
+ }
7638
+ return fields;
7639
+ }
7640
+ function decoratorName2(decorator) {
7641
+ const expr = decorator.type === "Decorator" ? decorator.expression : decorator;
7642
+ if (expr === void 0) return void 0;
7643
+ if (expr.type === "CallExpression") return calleeName(expr);
7644
+ if (expr.type === "Identifier") return expr.name;
7645
+ return void 0;
7646
+ }
7647
+ function fieldsOfClass(node) {
7648
+ if (node.type !== "ClassDeclaration" && node.type !== "ClassExpression") return /* @__PURE__ */ new Set();
7649
+ if (node.body.type !== "ClassBody") return /* @__PURE__ */ new Set();
7650
+ const fields = /* @__PURE__ */ new Set();
7651
+ for (const element of node.body.body) {
7652
+ if (element.type !== "PropertyDefinition") continue;
7653
+ const key = element.key;
7654
+ const name = key.type === "Identifier" ? key.name : void 0;
7655
+ if (name === void 0) continue;
7656
+ const decorators = element.decorators ?? [];
7657
+ if (decorators.some((decorator) => PRIMITIVE.has(decoratorName2(decorator) ?? ""))) fields.add(name);
7658
+ }
7659
+ return fields;
7660
+ }
7661
+ function programOf(node) {
7662
+ let current = node;
7663
+ while (current !== void 0 && current.type !== "Program") current = parentOf(current);
7664
+ return current;
7665
+ }
7666
+ function bindings(program) {
7667
+ const map = /* @__PURE__ */ new Map();
7668
+ walk(program, (node) => {
7669
+ if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.init !== null && node.init !== void 0) {
7670
+ const fields = fieldsOfObjectSchema(node.init);
7671
+ if (fields.size > 0) map.set(node.id.name, fields);
7672
+ }
7673
+ if (node.type === "ClassDeclaration" && node.id !== null) {
7674
+ const fields = fieldsOfClass(node);
7675
+ if (fields.size > 0) map.set(node.id.name, fields);
7676
+ }
7677
+ });
7678
+ return map;
7679
+ }
7680
+ var bindingCache = /* @__PURE__ */ new WeakMap();
7681
+ function fieldsOfExpr(expr, known) {
7682
+ if (expr.type === "Identifier") return new Set(known.get(expr.name) ?? []);
7683
+ if (expr.type === "ClassExpression" || expr.type === "ClassDeclaration") return fieldsOfClass(expr);
7684
+ const direct = fieldsOfObjectSchema(expr);
7685
+ if (expr.type !== "CallExpression") return direct;
7686
+ for (const arg of expr.arguments) {
7687
+ if (arg.type === "SpreadElement") continue;
7688
+ for (const name of fieldsOfExpr(arg, known)) direct.add(name);
7689
+ }
7690
+ return direct;
7691
+ }
7692
+ function isRouteCall2(node) {
7693
+ if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return false;
7694
+ const method = propName(node.callee);
7695
+ if (method === void 0 || !ROUTE_METHODS.has(method)) return false;
7696
+ return node.callee.object.type === "Identifier" && /^(app|router)$/i.test(node.callee.object.name);
7697
+ }
7698
+ function routeOf(node) {
7699
+ let current = node;
7700
+ while (current !== void 0) {
7701
+ const parent = parentOf(current);
7702
+ if (isFunctionNode(current) && parent !== void 0 && isRouteCall2(parent)) return parent;
7703
+ current = parent;
7704
+ }
7705
+ return void 0;
7706
+ }
7707
+ function requestField(node) {
7708
+ if (node.type !== "MemberExpression" || node.computed === true) return void 0;
7709
+ const field = node.property.type === "Identifier" ? node.property.name : void 0;
7710
+ if (field === void 0 || node.object.type !== "MemberExpression" || node.object.computed === true) return void 0;
7711
+ const bag = node.object.property.type === "Identifier" ? node.object.property.name : void 0;
7712
+ if (bag !== "body" && bag !== "query" && bag !== "params") return void 0;
7713
+ if (node.object.object.type !== "Identifier" || node.object.object.name !== "req") return void 0;
7714
+ return field;
7715
+ }
7716
+ var routeCache = /* @__PURE__ */ new WeakMap();
7717
+ function fieldsForRoute(route) {
7718
+ const cached = routeCache.get(route);
7719
+ if (cached !== void 0) return cached;
7720
+ const program = programOf(route);
7721
+ const known = program === void 0 ? /* @__PURE__ */ new Map() : bindingCache.get(program) ?? bindings(program);
7722
+ if (program !== void 0) bindingCache.set(program, known);
7723
+ const fields = /* @__PURE__ */ new Set();
7724
+ if (route.type === "CallExpression") {
7725
+ for (const arg of route.arguments) {
7726
+ if (arg.type === "SpreadElement" || isFunctionNode(arg)) continue;
7727
+ for (const name of fieldsOfExpr(arg, known)) fields.add(name);
7728
+ }
7729
+ }
7730
+ routeCache.set(route, fields);
7731
+ return fields;
7732
+ }
7733
+ function isValidatedRouteField(node) {
7734
+ if (!validationEnabled()) return false;
7735
+ const field = requestField(node);
7736
+ if (field === void 0) return false;
7737
+ const route = routeOf(node);
7738
+ if (route === void 0) return false;
7739
+ return fieldsForRoute(route).has(field);
7740
+ }
7741
+
6316
7742
  // src/modules/sast/rules/nosqli.ts
6317
7743
  var MONGO_SINKS = /* @__PURE__ */ new Set([
6318
7744
  "find",
@@ -6397,6 +7823,7 @@ function isNosqlSafeValue(node, source, isNosqlSanitized) {
6397
7823
  if (isNosqlSanitized(node) || isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) {
6398
7824
  return true;
6399
7825
  }
7826
+ if (isValidatedRouteField(node)) return true;
6400
7827
  return isSanitizeWrap(node, source);
6401
7828
  }
6402
7829
  function objectHasUnsafeTaintedValue(node, isTainted, isSafe) {
@@ -6460,6 +7887,14 @@ var nosqliRule = {
6460
7887
  return;
6461
7888
  }
6462
7889
  if (middleware) return;
7890
+ if (ctx.holdsBuiltFilter(filter)) {
7891
+ ctx.report({
7892
+ node,
7893
+ message: "A Mongo filter built from request input is passed to the query.",
7894
+ confidence: "medium"
7895
+ });
7896
+ return;
7897
+ }
6463
7898
  if (objectHasUnsafeTaintedValue(filter, (n) => ctx.isTainted(n), isSafe)) {
6464
7899
  ctx.report({
6465
7900
  node,
@@ -6636,7 +8071,7 @@ var sqliRule = {
6636
8071
  CallExpression(node) {
6637
8072
  if (node.type !== "CallExpression" || !isSqlSink2(node)) return;
6638
8073
  const sql = node.arguments[0];
6639
- if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql)) {
8074
+ if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql) && !ctx.isSqlCleared(sql)) {
6640
8075
  ctx.report({
6641
8076
  node,
6642
8077
  message: "Tainted input is concatenated into a SQL string passed to query/execute."
@@ -6786,27 +8221,27 @@ function numberLiteral(node) {
6786
8221
  return node.value;
6787
8222
  }
6788
8223
  function containsIdentMember(node, objectName) {
6789
- let hit = false;
8224
+ let hit2 = false;
6790
8225
  walk(node, (child) => {
6791
8226
  if (child.type === "MemberExpression" && child.object.type === "Identifier" && child.object.name === objectName) {
6792
- hit = true;
8227
+ hit2 = true;
6793
8228
  }
6794
8229
  });
6795
- return hit;
8230
+ return hit2;
6796
8231
  }
6797
8232
  function containsCallNamed(node, name) {
6798
- let hit = false;
8233
+ let hit2 = false;
6799
8234
  walk(node, (child) => {
6800
- if (child.type === "CallExpression" && calleeName(child) === name) hit = true;
8235
+ if (child.type === "CallExpression" && calleeName(child) === name) hit2 = true;
6801
8236
  });
6802
- return hit;
8237
+ return hit2;
6803
8238
  }
6804
8239
  function containsOriginalName(node) {
6805
- let hit = false;
8240
+ let hit2 = false;
6806
8241
  walk(node, (child) => {
6807
- if (child.type === "MemberExpression" && propName(child) === "originalname") hit = true;
8242
+ if (child.type === "MemberExpression" && propName(child) === "originalname") hit2 = true;
6808
8243
  });
6809
- return hit;
8244
+ return hit2;
6810
8245
  }
6811
8246
  function schemaLooksSafe(schema) {
6812
8247
  if (schema === void 0) return false;
@@ -7426,72 +8861,6 @@ function scanPromptInjection(file, source) {
7426
8861
  });
7427
8862
  return findings;
7428
8863
  }
7429
- function packageVersion() {
7430
- let dir = dirname(fileURLToPath(import.meta.url));
7431
- for (let depth = 0; depth < 6; depth += 1) {
7432
- try {
7433
- const raw = readFileSync(join(dir, "package.json"), "utf8");
7434
- const pkg = JSON.parse(raw);
7435
- if (pkg.name === "fullshield" && typeof pkg.version === "string") {
7436
- return pkg.version;
7437
- }
7438
- } catch {
7439
- }
7440
- const parent = dirname(dir);
7441
- if (parent === dir) break;
7442
- dir = parent;
7443
- }
7444
- throw new Error("fullshield package.json not found");
7445
- }
7446
-
7447
- // src/core/parse-pool.ts
7448
- var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
7449
- var cache = /* @__PURE__ */ new Map();
7450
- function parsePoolEnabled() {
7451
- return process.env.FULLSHIELD_PARSE_POOL === "1";
7452
- }
7453
- function parsePoolSize() {
7454
- const raw = process.env.FULLSHIELD_PARSE_WORKERS;
7455
- if (raw !== void 0 && raw !== "") {
7456
- const parsed = Number(raw);
7457
- if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
7458
- }
7459
- return Math.max(1, availableParallelism() - 1);
7460
- }
7461
- function parseCacheKey(code) {
7462
- return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
7463
- }
7464
- function parseOne(code) {
7465
- const key = parseCacheKey(code);
7466
- const hit = cache.get(key);
7467
- if (hit !== void 0) return Promise.resolve(hit);
7468
- return new Promise((resolve5, reject) => {
7469
- const worker = new Worker(fileURLToPath(workerUrl), { workerData: { code } });
7470
- worker.once("message", (ast) => {
7471
- cache.set(key, ast);
7472
- resolve5(ast);
7473
- });
7474
- worker.once("error", reject);
7475
- });
7476
- }
7477
- async function parseWithPool(codes) {
7478
- const size = parsePoolSize();
7479
- const out = new Array(codes.length);
7480
- let next = 0;
7481
- async function run() {
7482
- for (; ; ) {
7483
- const index = next;
7484
- next += 1;
7485
- if (index >= codes.length) return;
7486
- const code = codes[index];
7487
- if (code === void 0) return;
7488
- out[index] = await parseOne(code);
7489
- }
7490
- }
7491
- const workers = Math.min(size, Math.max(1, codes.length));
7492
- await Promise.all(Array.from({ length: workers }, () => run()));
7493
- return out;
7494
- }
7495
8864
 
7496
8865
  // src/modules/sast/engine.ts
7497
8866
  var sastRules = [
@@ -7525,8 +8894,8 @@ function snippetOf(source, node) {
7525
8894
  if (node.range === void 0) return "";
7526
8895
  return source.slice(node.range[0], node.range[1]).replace(/\s+/g, " ").trim().slice(0, 120);
7527
8896
  }
7528
- function findingsFromAst(filename, source, ast, extraReturners = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
7529
- const facts = analyzeFile(ast, extraReturners);
8897
+ function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
8898
+ const facts = analyzeFile(ast, extraReturners2);
7530
8899
  const findings = [];
7531
8900
  for (const rule of sastRules) {
7532
8901
  const ctx = {
@@ -7552,10 +8921,12 @@ function findingsFromAst(filename, source, ast, extraReturners = /* @__PURE__ */
7552
8921
  });
7553
8922
  },
7554
8923
  isTainted: (node) => nodeIsTainted(node, facts),
8924
+ isSqlCleared: (node) => nodeIsSqlCleared(node, facts),
7555
8925
  isPathSanitized: (node) => nodeIsPathSanitized(node, facts),
7556
8926
  isSanitizedFor: (node, kind) => nodeIsSanitizedFor(node, facts, kind),
7557
8927
  isNosqlSanitized: (node) => nodeIsNosqlSanitized(node, facts),
7558
8928
  isRequestObject: (node) => nodeIsRequestObject(node, facts),
8929
+ holdsBuiltFilter: (node) => nodeHoldsBuiltFilter(node, facts),
7559
8930
  functionHasPasswordName: (node) => factsFor(node, facts).hasPasswordName,
7560
8931
  assignedName: (node) => assignedNameOf(node),
7561
8932
  projectCsrfProtected
@@ -7593,10 +8964,12 @@ function findingsFromTemplate(filename, source) {
7593
8964
  });
7594
8965
  },
7595
8966
  isTainted: () => false,
8967
+ isSqlCleared: () => false,
7596
8968
  isPathSanitized: () => false,
7597
8969
  isSanitizedFor: () => false,
7598
8970
  isNosqlSanitized: () => false,
7599
8971
  isRequestObject: () => false,
8972
+ holdsBuiltFilter: () => false,
7600
8973
  functionHasPasswordName: () => false,
7601
8974
  assignedName: () => void 0,
7602
8975
  projectCsrfProtected: false
@@ -7604,52 +8977,224 @@ function findingsFromTemplate(filename, source) {
7604
8977
  xssRule.create(ctx);
7605
8978
  return findings;
7606
8979
  }
8980
+ function dedupeSastSinks(findings) {
8981
+ const seen = /* @__PURE__ */ new Set();
8982
+ const out = [];
8983
+ for (const finding3 of findings) {
8984
+ if (finding3.module !== "sast" || finding3.line === void 0) {
8985
+ out.push(finding3);
8986
+ continue;
8987
+ }
8988
+ const key = `${finding3.ruleId}\0${finding3.file ?? ""}\0${finding3.line}`;
8989
+ if (seen.has(key)) continue;
8990
+ seen.add(key);
8991
+ out.push(finding3);
8992
+ }
8993
+ return out;
8994
+ }
8995
+ function sastStreamThreshold() {
8996
+ const raw = process.env.FULLSHIELD_SAST_STREAM_AT;
8997
+ if (raw !== void 0 && raw !== "") {
8998
+ const parsed = Number(raw);
8999
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
9000
+ }
9001
+ return 4e3;
9002
+ }
9003
+ function sastBatchSize() {
9004
+ const raw = process.env.FULLSHIELD_SAST_BATCH;
9005
+ if (raw !== void 0 && raw !== "") {
9006
+ const parsed = Number(raw);
9007
+ if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
9008
+ }
9009
+ return 150;
9010
+ }
9011
+ function skipFinding(rel, message) {
9012
+ return {
9013
+ id: makeFindingId("SAST-SKIP-001", rel, message),
9014
+ ruleId: "SAST-SKIP-001",
9015
+ module: "sast",
9016
+ title: "File skipped by SAST",
9017
+ severity: "info",
9018
+ confidence: "high",
9019
+ file: rel,
9020
+ message,
9021
+ remediation: "Raise sast.maxFileBytes in fullshield.config.json to parse this file, or leave it skipped."
9022
+ };
9023
+ }
9024
+ function skipReason(source, maxBytes) {
9025
+ if (source.includes("\0")) return "it contains NUL bytes";
9026
+ const bytes = Buffer.byteLength(source);
9027
+ if (bytes > maxBytes) return `${bytes} bytes exceeds the SAST parse limit of ${maxBytes} bytes`;
9028
+ return void 0;
9029
+ }
9030
+ async function readScanSource(cwd, rel) {
9031
+ try {
9032
+ return await readFile(join(cwd, rel), "utf8");
9033
+ } catch {
9034
+ return void 0;
9035
+ }
9036
+ }
9037
+ function cloneAst(value) {
9038
+ if (value === null || typeof value !== "object") return void 0;
9039
+ try {
9040
+ return structuredClone(value);
9041
+ } catch {
9042
+ return void 0;
9043
+ }
9044
+ }
9045
+ async function parseInProcess(source) {
9046
+ try {
9047
+ return await parseSource(source);
9048
+ } catch {
9049
+ return void 0;
9050
+ }
9051
+ }
9052
+ async function parseCode(source) {
9053
+ if (!parsePoolEnabled()) return parseInProcess(source);
9054
+ try {
9055
+ const cloned = cloneAst((await parseWithPool([source]))[0]);
9056
+ if (cloned !== void 0) return cloned;
9057
+ } catch {
9058
+ }
9059
+ return parseInProcess(source);
9060
+ }
9061
+ function releaseParsed(parsed) {
9062
+ for (const entry of parsed) {
9063
+ entry.source = "";
9064
+ entry.ast = void 0;
9065
+ }
9066
+ parsed.length = 0;
9067
+ }
9068
+ function extraReturners(rel, ast, files, returning) {
9069
+ const ns = collectRequires(rel, ast, files);
9070
+ const extra = new Set(returning.get(rel));
9071
+ for (const [local, spec] of ns) {
9072
+ const hash = spec.indexOf("#");
9073
+ const file = hash >= 0 ? spec.slice(0, hash) : spec;
9074
+ const exp = hash >= 0 ? spec.slice(hash + 1) : local;
9075
+ if (returning.get(file)?.has(exp)) extra.add(local);
9076
+ }
9077
+ return extra;
9078
+ }
9079
+ async function runSastStreaming(ctx) {
9080
+ const maxBytes = ctx.config.sast.maxFileBytes;
9081
+ const findings = [];
9082
+ const returning = /* @__PURE__ */ new Map();
9083
+ const parseable = [];
9084
+ const fileSet = /* @__PURE__ */ new Set();
9085
+ let projectCsrf = false;
9086
+ for (const rel of ctx.files) {
9087
+ const isSource = SOURCE_EXT.test(rel);
9088
+ const isTemplate = TEMPLATE_EXT.test(rel);
9089
+ if (!isSource && !isTemplate) continue;
9090
+ const source = await readScanSource(ctx.cwd, rel);
9091
+ if (source === void 0) continue;
9092
+ const why = skipReason(source, maxBytes);
9093
+ if (why !== void 0) {
9094
+ findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
9095
+ continue;
9096
+ }
9097
+ if (sourceHasCsrfGuard(source)) projectCsrf = true;
9098
+ if (isTemplate) {
9099
+ findings.push(...findingsFromTemplate(rel, source));
9100
+ continue;
9101
+ }
9102
+ const ast = await parseCode(source);
9103
+ if (ast === void 0) continue;
9104
+ returning.set(rel, taintSummariesEnabled() ? cachedReturners(source, ast) : functionsThatReturnTaint(ast));
9105
+ parseable.push(rel);
9106
+ fileSet.add(rel);
9107
+ }
9108
+ for (const rel of parseable) {
9109
+ const source = await readScanSource(ctx.cwd, rel);
9110
+ if (source === void 0) continue;
9111
+ const ast = await parseCode(source);
9112
+ if (ast === void 0) continue;
9113
+ findings.push(
9114
+ ...findingsFromAst(rel, source, ast, extraReturners(rel, ast, fileSet, returning), projectCsrf)
9115
+ );
9116
+ }
9117
+ const batch = sastBatchSize();
9118
+ for (let i = 0; i < parseable.length; i += batch) {
9119
+ const parsed = [];
9120
+ for (const rel of parseable.slice(i, i + batch)) {
9121
+ const source = await readScanSource(ctx.cwd, rel);
9122
+ if (source === void 0) continue;
9123
+ const ast = await parseCode(source);
9124
+ if (ast === void 0) continue;
9125
+ parsed.push({ file: rel, source, ast });
9126
+ }
9127
+ findings.push(...runInterproc(parsed));
9128
+ releaseParsed(parsed);
9129
+ }
9130
+ return dedupeSastSinks(findings);
9131
+ }
7607
9132
  var sastModule = {
7608
9133
  name: "sast",
7609
9134
  async run(ctx) {
7610
- const findings = [];
7611
- const parsed = [];
7612
- for (const rel of ctx.files) {
7613
- const isSource = SOURCE_EXT.test(rel);
7614
- const isTemplate = TEMPLATE_EXT.test(rel);
7615
- if (!isSource && !isTemplate) continue;
7616
- let source;
7617
- try {
7618
- source = await readFile(join(ctx.cwd, rel), "utf8");
7619
- } catch {
7620
- continue;
7621
- }
7622
- if (source.length > 1024 * 1024 || source.includes("\0")) continue;
7623
- if (isTemplate) {
7624
- findings.push(...findingsFromTemplate(rel, source));
7625
- continue;
7626
- }
7627
- try {
7628
- const ast = parsePoolEnabled() ? (await parseWithPool([source]))[0] : await parseSource(source);
9135
+ if (process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1") {
9136
+ const sources = new Set((ctx.config.sast.sources ?? []).map((row) => row.callee));
9137
+ const sanitizers = /* @__PURE__ */ new Map();
9138
+ for (const row of ctx.config.sast.sanitizers ?? []) {
9139
+ const classes = sanitizers.get(row.callee) ?? /* @__PURE__ */ new Set();
9140
+ for (const name of row.clears) classes.add(name);
9141
+ sanitizers.set(row.callee, classes);
9142
+ }
9143
+ setCustomTaint({ sources, sanitizers });
9144
+ } else {
9145
+ setCustomTaint(void 0);
9146
+ }
9147
+ setTaintSummaryConfig(taintSummariesEnabled() ? ctx.config : void 0);
9148
+ try {
9149
+ const candidates = ctx.files.filter((rel) => SOURCE_EXT.test(rel) || TEMPLATE_EXT.test(rel));
9150
+ if (candidates.length > sastStreamThreshold()) return await runSastStreaming(ctx);
9151
+ const maxBytes = ctx.config.sast.maxFileBytes;
9152
+ const findings = [];
9153
+ const parsed = [];
9154
+ for (const rel of candidates) {
9155
+ const source = await readScanSource(ctx.cwd, rel);
9156
+ if (source === void 0) continue;
9157
+ const why = skipReason(source, maxBytes);
9158
+ if (why !== void 0) {
9159
+ findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
9160
+ continue;
9161
+ }
9162
+ if (TEMPLATE_EXT.test(rel)) {
9163
+ findings.push(...findingsFromTemplate(rel, source));
9164
+ continue;
9165
+ }
9166
+ const ast = await parseCode(source);
7629
9167
  if (ast === void 0) continue;
7630
9168
  parsed.push({ file: rel, source, ast });
7631
- } catch {
7632
9169
  }
7633
- }
7634
- const files = new Set(parsed.map((p) => p.file));
7635
- const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
7636
- const returning = /* @__PURE__ */ new Map();
7637
- for (const p of parsed) {
7638
- returning.set(p.file, functionsThatReturnTaint(p.ast));
7639
- }
7640
- for (const p of parsed) {
7641
- const ns = collectRequires(p.file, p.ast, files);
7642
- const extra = new Set(returning.get(p.file));
7643
- for (const [local, spec] of ns) {
7644
- const hash = spec.indexOf("#");
7645
- const file = hash >= 0 ? spec.slice(0, hash) : spec;
7646
- const exp = hash >= 0 ? spec.slice(hash + 1) : local;
7647
- if (returning.get(file)?.has(exp)) extra.add(local);
9170
+ const files = new Set(parsed.map((p) => p.file));
9171
+ const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
9172
+ const returning = /* @__PURE__ */ new Map();
9173
+ for (const entry of parsed) {
9174
+ returning.set(
9175
+ entry.file,
9176
+ taintSummariesEnabled() ? cachedReturners(entry.source, entry.ast) : functionsThatReturnTaint(entry.ast)
9177
+ );
9178
+ }
9179
+ for (const entry of parsed) {
9180
+ findings.push(
9181
+ ...findingsFromAst(
9182
+ entry.file,
9183
+ entry.source,
9184
+ entry.ast,
9185
+ extraReturners(entry.file, entry.ast, files, returning),
9186
+ projectCsrf
9187
+ )
9188
+ );
7648
9189
  }
7649
- findings.push(...findingsFromAst(p.file, p.source, p.ast, extra, projectCsrf));
9190
+ findings.push(...runInterproc(parsed));
9191
+ const out = dedupeSastSinks(findings);
9192
+ releaseParsed(parsed);
9193
+ return out;
9194
+ } finally {
9195
+ setCustomTaint(void 0);
9196
+ setTaintSummaryConfig(void 0);
7650
9197
  }
7651
- findings.push(...runInterproc(parsed));
7652
- return findings;
7653
9198
  }
7654
9199
  };
7655
9200
 
@@ -7943,12 +9488,12 @@ ${text}`);
7943
9488
  const seeds = fixtureLogic(prompt);
7944
9489
  const findings = [];
7945
9490
  for (const seed of seeds) {
7946
- const hit = fileTexts.find((f) => f.file.includes(seed.fileHint) && f.text.includes(seed.fileHint === "user" ? "findById" : "Object.assign")) ?? fileTexts.find(
9491
+ const hit2 = fileTexts.find((f) => f.file.includes(seed.fileHint) && f.text.includes(seed.fileHint === "user" ? "findById" : "Object.assign")) ?? fileTexts.find(
7947
9492
  (f) => f.text.includes(seed.ruleId === "AI-IDOR-001" ? "findById(req.params.id)" : "Object.assign(user, req.body)")
7948
9493
  );
7949
- if (hit === void 0) continue;
7950
- const line = hit.text.split(/\r?\n/).findIndex((l) => l.includes(seed.ruleId === "AI-IDOR-001" ? "findById" : "Object.assign")) + 1;
7951
- findings.push(asAiFinding(seed, hit.file, line > 0 ? line : 1));
9494
+ if (hit2 === void 0) continue;
9495
+ const line = hit2.text.split(/\r?\n/).findIndex((l) => l.includes(seed.ruleId === "AI-IDOR-001" ? "findById" : "Object.assign")) + 1;
9496
+ findings.push(asAiFinding(seed, hit2.file, line > 0 ? line : 1));
7952
9497
  }
7953
9498
  return findings;
7954
9499
  }
@@ -8110,16 +9655,37 @@ ${wrapUntrustedCode(req.prompt)}`;
8110
9655
  }
8111
9656
  };
8112
9657
  }
9658
+ function isIsoDate(value) {
9659
+ if (!/^\d{4}-\d{2}-\d{2}(T[\d:.+\-Z]+)?$/.test(value)) return false;
9660
+ return Number.isFinite(Date.parse(value));
9661
+ }
9662
+ function expiresAtMs(expires) {
9663
+ if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) return Date.parse(`${expires}T23:59:59.999Z`);
9664
+ return Date.parse(expires);
9665
+ }
9666
+ function validSuppression(value) {
9667
+ if (typeof value !== "object" || value === null) return void 0;
9668
+ const row = value;
9669
+ if (typeof row.ruleId !== "string" || row.ruleId.length === 0) return void 0;
9670
+ if (typeof row.owner !== "string" || row.owner.length === 0) return void 0;
9671
+ if (typeof row.reason !== "string" || row.reason.length === 0) return void 0;
9672
+ if (typeof row.expires !== "string" || !isIsoDate(row.expires)) return void 0;
9673
+ const file = row.file === void 0 ? void 0 : row.file.replace(/\\/g, "/");
9674
+ if (row.file !== void 0 && (typeof row.file !== "string" || row.file.length === 0)) return void 0;
9675
+ return { ruleId: row.ruleId, file, owner: row.owner, reason: row.reason, expires: row.expires };
9676
+ }
8113
9677
  async function readBaseline(path) {
8114
9678
  try {
8115
9679
  const raw = JSON.parse(await readFile(path, "utf8"));
8116
9680
  if (raw.version !== 1 || !Array.isArray(raw.findings)) return void 0;
9681
+ if (raw.suppressions !== void 0 && !Array.isArray(raw.suppressions)) return void 0;
8117
9682
  return raw;
8118
9683
  } catch {
8119
9684
  return void 0;
8120
9685
  }
8121
9686
  }
8122
9687
  async function writeBaseline(path, findings) {
9688
+ const existing = await readBaseline(path);
8123
9689
  const doc = {
8124
9690
  version: 1,
8125
9691
  findings: findings.map((f) => ({
@@ -8127,7 +9693,8 @@ async function writeBaseline(path, findings) {
8127
9693
  ruleId: f.ruleId,
8128
9694
  file: f.file,
8129
9695
  line: f.line
8130
- }))
9696
+ })),
9697
+ ...existing?.suppressions !== void 0 ? { suppressions: existing.suppressions } : {}
8131
9698
  };
8132
9699
  await writeFile(path, `${JSON.stringify(doc, null, 2)}
8133
9700
  `, "utf8");
@@ -8137,6 +9704,30 @@ function applyBaseline(findings, baseline) {
8137
9704
  const known = new Set(baseline.findings.map((f) => f.id));
8138
9705
  return findings.filter((f) => !known.has(f.id));
8139
9706
  }
9707
+ function applySuppressions(findings, rows, now = /* @__PURE__ */ new Date()) {
9708
+ if (rows === void 0 || rows.length === 0) return { findings: [...findings], errors: [] };
9709
+ const errors = [];
9710
+ const active = [];
9711
+ rows.forEach((row, index) => {
9712
+ const parsed = validSuppression(row);
9713
+ if (parsed === void 0) {
9714
+ errors.push(`suppression ${index + 1} needs ruleId, owner, reason, and an ISO expires date`);
9715
+ return;
9716
+ }
9717
+ if (expiresAtMs(parsed.expires) < now.getTime()) {
9718
+ errors.push(
9719
+ `suppression for ${parsed.ruleId} expired on ${parsed.expires} (owner: ${parsed.owner}); renew or remove it`
9720
+ );
9721
+ return;
9722
+ }
9723
+ active.push(parsed);
9724
+ });
9725
+ const kept = findings.filter((finding3) => {
9726
+ const file = (finding3.file ?? "").replace(/\\/g, "/");
9727
+ return !active.some((row) => row.ruleId === finding3.ruleId && (row.file === void 0 || row.file === file));
9728
+ });
9729
+ return { findings: kept, errors };
9730
+ }
8140
9731
 
8141
9732
  // src/core/config.ts
8142
9733
  init_finding();
@@ -8145,14 +9736,77 @@ var DEFAULT_CONFIG = {
8145
9736
  modules: [...MODULE_NAMES],
8146
9737
  failOn: "high",
8147
9738
  ignore: ["dist/**", "**/*.test.ts"],
9739
+ defaultExcludes: true,
8148
9740
  baseline: ".fullshield-baseline.json",
8149
9741
  notify: { slackWebhookEnv: "FULLSHIELD_SLACK_WEBHOOK" },
8150
9742
  deps: { offline: false },
8151
- ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 }
9743
+ ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 },
9744
+ sast: { maxFileBytes: 1024 * 1024 }
8152
9745
  };
8153
9746
  var ConfigError = class extends Error {
8154
9747
  };
8155
- var KNOWN_KEYS = /* @__PURE__ */ new Set(["modules", "failOn", "ignore", "baseline", "notify", "deps", "ai"]);
9748
+ var KNOWN_KEYS = /* @__PURE__ */ new Set([
9749
+ "modules",
9750
+ "failOn",
9751
+ "ignore",
9752
+ "defaultExcludes",
9753
+ "excludes",
9754
+ "baseline",
9755
+ "notify",
9756
+ "deps",
9757
+ "ai",
9758
+ "sast"
9759
+ ]);
9760
+ var SAST_CLASSES = /* @__PURE__ */ new Set([
9761
+ "sqli",
9762
+ "nosqli",
9763
+ "cmdi",
9764
+ "path",
9765
+ "xss",
9766
+ "ssrf",
9767
+ "redirect",
9768
+ "eval",
9769
+ "jwt",
9770
+ "crypto",
9771
+ "proto",
9772
+ "secrets",
9773
+ "deps",
9774
+ "config"
9775
+ ]);
9776
+ function parseSastSources(value, source) {
9777
+ if (value === void 0) return void 0;
9778
+ if (!Array.isArray(value)) throw new ConfigError(`${source}: "sast.sources" must be an array`);
9779
+ return value.map((item) => {
9780
+ if (typeof item !== "object" || item === null) {
9781
+ throw new ConfigError(`${source}: "sast.sources" entries must be objects`);
9782
+ }
9783
+ const row = item;
9784
+ if (typeof row.callee !== "string" || row.callee.length === 0) {
9785
+ throw new ConfigError(`${source}: "sast.sources[].callee" must be a string`);
9786
+ }
9787
+ if (row.returns !== "tainted") {
9788
+ throw new ConfigError(`${source}: "sast.sources[].returns" must be "tainted"`);
9789
+ }
9790
+ return { callee: row.callee, returns: "tainted" };
9791
+ });
9792
+ }
9793
+ function parseSastSanitizers(value, source) {
9794
+ if (value === void 0) return void 0;
9795
+ if (!Array.isArray(value)) throw new ConfigError(`${source}: "sast.sanitizers" must be an array`);
9796
+ return value.map((item) => {
9797
+ if (typeof item !== "object" || item === null) {
9798
+ throw new ConfigError(`${source}: "sast.sanitizers" entries must be objects`);
9799
+ }
9800
+ const row = item;
9801
+ if (typeof row.callee !== "string" || row.callee.length === 0) {
9802
+ throw new ConfigError(`${source}: "sast.sanitizers[].callee" must be a string`);
9803
+ }
9804
+ if (!Array.isArray(row.clears) || row.clears.some((name) => typeof name !== "string" || !SAST_CLASSES.has(name))) {
9805
+ throw new ConfigError(`${source}: "sast.sanitizers[].clears" must be B4 class names`);
9806
+ }
9807
+ return { callee: row.callee, clears: row.clears.filter((name) => name !== "secrets" && name !== "deps") };
9808
+ });
9809
+ }
8156
9810
  function validate(raw, source) {
8157
9811
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
8158
9812
  throw new ConfigError(`${source}: config must be a JSON object`);
@@ -8254,6 +9908,42 @@ function validate(raw, source) {
8254
9908
  }
8255
9909
  out.ai = ai;
8256
9910
  }
9911
+ if (record.defaultExcludes !== void 0) {
9912
+ if (typeof record.defaultExcludes !== "boolean") {
9913
+ throw new ConfigError(`${source}: "defaultExcludes" must be a boolean`);
9914
+ }
9915
+ out.defaultExcludes = record.defaultExcludes;
9916
+ }
9917
+ if (record.excludes !== void 0) {
9918
+ if (!Array.isArray(record.excludes) || record.excludes.some((p) => typeof p !== "string")) {
9919
+ throw new ConfigError(`${source}: "excludes" must be an array of glob strings`);
9920
+ }
9921
+ out.excludes = record.excludes;
9922
+ }
9923
+ if (record.sast !== void 0) {
9924
+ if (typeof record.sast !== "object" || record.sast === null || Array.isArray(record.sast)) {
9925
+ throw new ConfigError(`${source}: "sast" must be an object`);
9926
+ }
9927
+ const rawSast = record.sast;
9928
+ const sast = { maxFileBytes: 1024 * 1024 };
9929
+ if (rawSast.maxFileBytes !== void 0) {
9930
+ if (typeof rawSast.maxFileBytes !== "number" || rawSast.maxFileBytes < 1) {
9931
+ throw new ConfigError(`${source}: "sast.maxFileBytes" must be a positive number`);
9932
+ }
9933
+ sast.maxFileBytes = rawSast.maxFileBytes;
9934
+ }
9935
+ const custom = process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1";
9936
+ if (!custom && (rawSast.sources !== void 0 || rawSast.sanitizers !== void 0)) {
9937
+ throw new ConfigError(
9938
+ `${source}: unknown key "sast.sources" (FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS)`
9939
+ );
9940
+ }
9941
+ if (custom) {
9942
+ sast.sources = parseSastSources(rawSast.sources, source);
9943
+ sast.sanitizers = parseSastSanitizers(rawSast.sanitizers, source);
9944
+ }
9945
+ out.sast = sast;
9946
+ }
8257
9947
  return out;
8258
9948
  }
8259
9949
  async function loadConfig(cwd, configPath) {
@@ -8280,10 +9970,26 @@ async function loadConfig(cwd, configPath) {
8280
9970
  return { ...structuredClone(DEFAULT_CONFIG), ...validate(raw, file) };
8281
9971
  }
8282
9972
  var ignoreFactory = ignoreModule.default;
8283
- var ALWAYS_IGNORED = [".git/", "node_modules/", ".fullshield/"];
9973
+ var HARD_IGNORED = [".git/", ".fullshield/"];
9974
+ var DEFAULT_EXCLUDES = [
9975
+ "node_modules/",
9976
+ "dist/",
9977
+ "build/",
9978
+ ".next/",
9979
+ "coverage/",
9980
+ "vendor/",
9981
+ "vendored/",
9982
+ "third_party/",
9983
+ "third-party/",
9984
+ "compiled/",
9985
+ "*.min.js"
9986
+ ];
8284
9987
  async function walkFiles(options) {
8285
9988
  const ig = ignoreFactory();
8286
- ig.add(ALWAYS_IGNORED);
9989
+ ig.add(HARD_IGNORED);
9990
+ if (options.defaultExcludes !== false) {
9991
+ ig.add(options.excludes ?? DEFAULT_EXCLUDES);
9992
+ }
8287
9993
  try {
8288
9994
  ig.add(await readFile(join(options.cwd, ".gitignore"), "utf8"));
8289
9995
  } catch {
@@ -8501,7 +10207,7 @@ function resolvePolicyBranch(cwd, env = process.env) {
8501
10207
  return "*";
8502
10208
  }
8503
10209
  }
8504
- function expiresAtMs(expires) {
10210
+ function expiresAtMs2(expires) {
8505
10211
  if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
8506
10212
  return Date.parse(`${expires}T23:59:59.999Z`);
8507
10213
  }
@@ -8523,7 +10229,7 @@ function applyPolicy(findings, policy, ctx = {}) {
8523
10229
  });
8524
10230
  const suppressed = /* @__PURE__ */ new Set();
8525
10231
  for (const exception of policy.exceptions ?? []) {
8526
- if (expiresAtMs(exception.expires) < now.getTime()) {
10232
+ if (expiresAtMs2(exception.expires) < now.getTime()) {
8527
10233
  errors.push(
8528
10234
  `policy exception for ${exception.ruleId} expired on ${exception.expires} (owner: ${exception.owner}); renew or remove it`
8529
10235
  );
@@ -8571,6 +10277,8 @@ function dedupe2(findings) {
8571
10277
  async function runScan(options = {}, modules = builtinModules) {
8572
10278
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
8573
10279
  const t0 = performance.now();
10280
+ depsModule.unusedDirect = void 0;
10281
+ depsModule.upgradePlan = void 0;
8574
10282
  const cwd = resolve(options.cwd ?? process.cwd());
8575
10283
  const stats = await stat(cwd).catch(() => null);
8576
10284
  if (stats === null || !stats.isDirectory()) {
@@ -8592,7 +10300,12 @@ async function runScan(options = {}, modules = builtinModules) {
8592
10300
  options.modules ?? config.modules,
8593
10301
  loadedPolicy?.requiredModules
8594
10302
  );
8595
- let files = await walkFiles({ cwd, ignore: config.ignore });
10303
+ let files = await walkFiles({
10304
+ cwd,
10305
+ ignore: config.ignore,
10306
+ defaultExcludes: config.defaultExcludes,
10307
+ excludes: config.excludes
10308
+ });
8596
10309
  if (options.staged === true) {
8597
10310
  const staged = new Set(await listStagedFiles(cwd));
8598
10311
  files = files.filter((file) => staged.has(file));
@@ -8626,6 +10339,7 @@ async function runScan(options = {}, modules = builtinModules) {
8626
10339
  );
8627
10340
  }
8628
10341
  const extraHits = [];
10342
+ let historyNotice;
8629
10343
  const secretsWanted = modulesRequested.includes("secrets");
8630
10344
  if (secretsAdvEnabled() && secretsWanted) {
8631
10345
  if (options.history === true) {
@@ -8633,6 +10347,7 @@ async function runScan(options = {}, modules = builtinModules) {
8633
10347
  const hist = await scanGitHistory2(cwd);
8634
10348
  extraHits.push(...hist.hits);
8635
10349
  collected.push(...hist.findings);
10350
+ historyNotice = hist.history;
8636
10351
  }
8637
10352
  if (options.bundle !== void 0 && options.bundle.length > 0) {
8638
10353
  const { scanBundle: scanBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
@@ -8653,13 +10368,17 @@ async function runScan(options = {}, modules = builtinModules) {
8653
10368
  if (options.updateBaseline === true) {
8654
10369
  await writeBaseline(baselinePath, unsuppressed);
8655
10370
  }
8656
- let findings = sortFindings(applyBaseline(unsuppressed, await readBaseline(baselinePath)));
10371
+ const baselineDoc = await readBaseline(baselinePath);
10372
+ let findings = sortFindings(applyBaseline(unsuppressed, baselineDoc));
10373
+ const suppressed = applySuppressions(findings, baselineDoc?.suppressions);
10374
+ findings = sortFindings(suppressed.findings);
10375
+ const suppressionErrors = suppressed.errors;
8657
10376
  if (secretsAdvEnabled() && secretsWanted && options.verifySecrets === true && options.iOwnTheseCredentials === true) {
8658
10377
  const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
8659
10378
  const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
8660
10379
  const secretById = /* @__PURE__ */ new Map();
8661
- for (const hit of extraHits) {
8662
- if (hit.secret !== void 0) secretById.set(hit.finding.id, hit.secret);
10380
+ for (const hit2 of extraHits) {
10381
+ if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
8663
10382
  }
8664
10383
  for (const finding3 of findings) {
8665
10384
  if (finding3.module !== "secrets" || finding3.file === void 0 || secretById.has(finding3.id)) {
@@ -8673,8 +10392,8 @@ async function runScan(options = {}, modules = builtinModules) {
8673
10392
  continue;
8674
10393
  }
8675
10394
  }
8676
- for (const hit of scanTextForSecretHits2(finding3.file, text)) {
8677
- if (hit.secret !== void 0) secretById.set(hit.finding.id, hit.secret);
10395
+ for (const hit2 of scanTextForSecretHits2(finding3.file, text)) {
10396
+ if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
8678
10397
  }
8679
10398
  }
8680
10399
  findings = sortFindings(
@@ -8725,10 +10444,14 @@ async function runScan(options = {}, modules = builtinModules) {
8725
10444
  cwd,
8726
10445
  filesScanned: files.length,
8727
10446
  modulesRequested: [...modulesRequested],
8728
- modulesRun
10447
+ modulesRun,
10448
+ ...historyNotice !== void 0 ? { history: historyNotice } : {},
10449
+ ...depsModule.unusedDirect !== void 0 ? { unusedDirect: depsModule.unusedDirect } : {},
10450
+ ...depsModule.upgradePlan !== void 0 ? { upgradePlan: depsModule.upgradePlan } : {}
8729
10451
  },
8730
10452
  config,
8731
- policy: policyResult
10453
+ policy: policyResult,
10454
+ ...suppressionErrors.length > 0 ? { suppressionErrors } : {}
8732
10455
  };
8733
10456
  }
8734
10457
  function scan(options = {}) {
@@ -8938,6 +10661,28 @@ async function runFix(options = {}) {
8938
10661
  const after = applyUnifiedDiff(source, proposed.diff);
8939
10662
  byFile.set(sourceFile, after);
8940
10663
  }
10664
+ const unused = result.metadata.unusedDirect?.packages ?? [];
10665
+ if (unused.length > 0) {
10666
+ let manifest = byFile.get("package.json");
10667
+ if (manifest === void 0) {
10668
+ try {
10669
+ manifest = await readFile(join(cwd, "package.json"), "utf8");
10670
+ } catch {
10671
+ manifest = void 0;
10672
+ }
10673
+ }
10674
+ if (manifest !== void 0) {
10675
+ const next = removeDirectDependencies(manifest, unused);
10676
+ const diff = dependencyRemovalDiff(manifest, unused);
10677
+ if (diff.length > 0) {
10678
+ patches.push({ file: "package.json", ruleId: "DEPS-VULN-001", safe: false, diff });
10679
+ if (options.unsafe === true) {
10680
+ byFile.set("package.json", next);
10681
+ appliedOverride = true;
10682
+ }
10683
+ }
10684
+ }
10685
+ }
8941
10686
  const notice = appliedOverride ? "run npm install" : void 0;
8942
10687
  if (options.apply !== true) {
8943
10688
  return { applied: false, reverted: false, patches, findings: result.findings, notice };
@@ -8985,6 +10730,29 @@ async function restoreSnapshots(cwd, snapshots) {
8985
10730
  }
8986
10731
  }
8987
10732
  }
10733
+ function dependencyRemovalDiff(manifest, names) {
10734
+ const drop = names.map((name) => `"${name}"`);
10735
+ const removed = manifest.split("\n").filter((line) => drop.some((token) => line.includes(token)));
10736
+ if (removed.length === 0) return "";
10737
+ return ["--- a/package.json", "+++ b/package.json", ...removed.map((line) => `-${line}`)].join("\n");
10738
+ }
10739
+ function removeDirectDependencies(manifest, names) {
10740
+ let parsed;
10741
+ try {
10742
+ parsed = JSON.parse(manifest);
10743
+ } catch {
10744
+ return manifest;
10745
+ }
10746
+ if (parsed.dependencies === void 0 || typeof parsed.dependencies !== "object") return manifest;
10747
+ const drop = new Set(names);
10748
+ const next = {};
10749
+ for (const [name, version] of Object.entries(parsed.dependencies)) {
10750
+ if (!drop.has(name)) next[name] = version;
10751
+ }
10752
+ parsed.dependencies = next;
10753
+ const trailing = manifest.endsWith("\n") ? "\n" : "";
10754
+ return `${JSON.stringify(parsed, null, 2)}${trailing}`;
10755
+ }
8988
10756
  function runProjectTests(cwd) {
8989
10757
  let pkg;
8990
10758
  try {
@@ -9064,6 +10832,27 @@ init_suppression();
9064
10832
 
9065
10833
  // src/reporters/console.ts
9066
10834
  init_finding();
10835
+
10836
+ // src/reporters/redact.ts
10837
+ init_mask();
10838
+ var SECRET = /\b(?:AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk_live_[A-Za-z0-9]{10,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g;
10839
+ function redactSecrets2(text) {
10840
+ return text.replace(SECRET, (match) => maskSecret(match));
10841
+ }
10842
+ function redactFinding(finding3) {
10843
+ return {
10844
+ ...finding3,
10845
+ title: redactSecrets2(finding3.title),
10846
+ message: redactSecrets2(finding3.message),
10847
+ remediation: redactSecrets2(finding3.remediation),
10848
+ snippet: finding3.snippet === void 0 ? void 0 : redactSecrets2(finding3.snippet)
10849
+ };
10850
+ }
10851
+ function redactScan(result) {
10852
+ return { ...result, findings: result.findings.map(redactFinding) };
10853
+ }
10854
+
10855
+ // src/reporters/console.ts
9067
10856
  var SEVERITY_COLOR = {
9068
10857
  critical: (t) => pc.bold(pc.red(t)),
9069
10858
  high: (t) => pc.red(t),
@@ -9103,7 +10892,160 @@ function renderFinding(finding3) {
9103
10892
  }
9104
10893
  return lines;
9105
10894
  }
9106
- function renderConsoleReport(result) {
10895
+ var SEVERITY_RANK = {
10896
+ critical: 0,
10897
+ high: 1,
10898
+ medium: 2,
10899
+ low: 3,
10900
+ info: 4
10901
+ };
10902
+ var SHOWN_VULN_REACH = /* @__PURE__ */ new Set([
10903
+ "reachable",
10904
+ "symbol-reachable",
10905
+ "reachable-via-dependency",
10906
+ "reachability-unknown"
10907
+ ]);
10908
+ function packageNameFromSnippet3(snippet) {
10909
+ if (snippet === void 0 || snippet.length === 0) return void 0;
10910
+ if (snippet.startsWith("@")) {
10911
+ const second = snippet.indexOf("@", 1);
10912
+ return second === -1 ? snippet : snippet.slice(0, second);
10913
+ }
10914
+ const at = snippet.indexOf("@");
10915
+ return at <= 0 ? snippet : snippet.slice(0, at);
10916
+ }
10917
+ function versionsFromSnippet(snippet) {
10918
+ const name = packageNameFromSnippet3(snippet);
10919
+ if (name === void 0 || snippet === void 0) return [];
10920
+ const rest = snippet.slice(name.length + 1);
10921
+ return rest.length === 0 ? [] : rest.split(",").filter((version) => version.length > 0);
10922
+ }
10923
+ function isDevOnly2(finding3) {
10924
+ return finding3.message.includes("(dev-only)");
10925
+ }
10926
+ function isActionable(finding3) {
10927
+ if (finding3.ruleId === "DEPS-MALICIOUS-001") return true;
10928
+ if (finding3.ruleId === "DEPS-VULN-001") {
10929
+ if (isDevOnly2(finding3)) return false;
10930
+ return finding3.reachability !== void 0 && SHOWN_VULN_REACH.has(finding3.reachability);
10931
+ }
10932
+ if (finding3.module === "sast" || finding3.module === "secrets" || finding3.module === "config") {
10933
+ return finding3.severity === "high" || finding3.severity === "critical" || finding3.confidence === "high";
10934
+ }
10935
+ return false;
10936
+ }
10937
+ function reachRank(finding3) {
10938
+ const reachability = finding3.reachability;
10939
+ if (reachability === "reachable" || reachability === "symbol-reachable") return 0;
10940
+ if (reachability === "reachable-via-dependency") return 1;
10941
+ if (reachability === "reachability-unknown") return 2;
10942
+ if (reachability === void 0) return 4;
10943
+ return 3;
10944
+ }
10945
+ function compareActionable(a, b) {
10946
+ const maliciousA = a.ruleId === "DEPS-MALICIOUS-001" ? 0 : 1;
10947
+ const maliciousB = b.ruleId === "DEPS-MALICIOUS-001" ? 0 : 1;
10948
+ return maliciousA - maliciousB || SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || reachRank(a) - reachRank(b) || (a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || a.ruleId.localeCompare(b.ruleId);
10949
+ }
10950
+ function highestSeverity(findings) {
10951
+ return findings.reduce(
10952
+ (best, finding3) => SEVERITY_RANK[finding3.severity] < SEVERITY_RANK[best] ? finding3.severity : best,
10953
+ findings[0]?.severity ?? "info"
10954
+ );
10955
+ }
10956
+ function bestReachability(findings) {
10957
+ let best;
10958
+ let bestRank = 5;
10959
+ for (const finding3 of findings) {
10960
+ const rank = reachRank(finding3);
10961
+ if (finding3.reachability !== void 0 && rank < bestRank) {
10962
+ best = finding3.reachability;
10963
+ bestRank = rank;
10964
+ }
10965
+ }
10966
+ return best;
10967
+ }
10968
+ function shownConsoleRows(findings) {
10969
+ return dependencyRows(findings.filter(isActionable));
10970
+ }
10971
+ function dependencyRows(findings) {
10972
+ const groups = /* @__PURE__ */ new Map();
10973
+ const singles = [];
10974
+ for (const finding3 of findings) {
10975
+ const name = packageNameFromSnippet3(finding3.snippet);
10976
+ if ((finding3.ruleId === "DEPS-VULN-001" || finding3.ruleId === "DEPS-MALICIOUS-001") && name !== void 0) {
10977
+ const list = groups.get(name);
10978
+ if (list === void 0) groups.set(name, [finding3]);
10979
+ else list.push(finding3);
10980
+ } else {
10981
+ singles.push(finding3);
10982
+ }
10983
+ }
10984
+ const rows = singles.map((finding3) => ({
10985
+ findings: [finding3],
10986
+ severity: finding3.severity,
10987
+ reachability: finding3.reachability,
10988
+ ruleId: finding3.ruleId,
10989
+ file: finding3.file,
10990
+ line: finding3.line,
10991
+ versions: [],
10992
+ grouped: false
10993
+ }));
10994
+ for (const [packageName, list] of groups) {
10995
+ const ordered = [...list].sort(compareActionable);
10996
+ const first = ordered[0];
10997
+ if (first === void 0) continue;
10998
+ const versions = [...new Set(list.flatMap((finding3) => versionsFromSnippet(finding3.snippet)))].sort();
10999
+ const malicious = list.find((finding3) => finding3.ruleId === "DEPS-MALICIOUS-001");
11000
+ rows.push({
11001
+ findings: ordered,
11002
+ severity: highestSeverity(list),
11003
+ reachability: bestReachability(list),
11004
+ ruleId: malicious?.ruleId ?? "DEPS-VULN-001",
11005
+ file: first.file,
11006
+ line: first.line,
11007
+ packageName,
11008
+ versions,
11009
+ path: first.dependencyPath,
11010
+ grouped: true
11011
+ });
11012
+ }
11013
+ rows.sort((a, b) => {
11014
+ const probeA = a.findings[0];
11015
+ const probeB = b.findings[0];
11016
+ if (probeA === void 0 || probeB === void 0) return 0;
11017
+ const maliciousA = a.ruleId === "DEPS-MALICIOUS-001" ? 0 : 1;
11018
+ const maliciousB = b.ruleId === "DEPS-MALICIOUS-001" ? 0 : 1;
11019
+ return maliciousA - maliciousB || SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || reachRank({ ...probeA, reachability: a.reachability }) - reachRank({ ...probeB, reachability: b.reachability }) || (a.file ?? "").localeCompare(b.file ?? "") || (a.line ?? 0) - (b.line ?? 0) || a.ruleId.localeCompare(b.ruleId);
11020
+ });
11021
+ return rows;
11022
+ }
11023
+ function renderRow(row) {
11024
+ if (!row.grouped) {
11025
+ const only = row.findings[0];
11026
+ if (only === void 0) return [];
11027
+ return [...renderFinding(only), ` docs: rules/${only.ruleId}.md`];
11028
+ }
11029
+ const reach = row.reachability ?? "unspecified";
11030
+ const lines = [
11031
+ ` ${pc.bold(`[${row.ruleId}]`)} ${row.packageName} \u2014 ${row.findings.length} advisories, ${row.severity}, ${reach}`,
11032
+ ` versions: ${row.versions.join(", ") || "unknown"}`
11033
+ ];
11034
+ if (row.path !== void 0 && row.path.length > 0) {
11035
+ lines.push(` path: ${row.path.join(" > ")}`);
11036
+ }
11037
+ if (row.file !== void 0) {
11038
+ lines.push(` ${row.file}${row.line !== void 0 ? `:${row.line}` : ""}`);
11039
+ }
11040
+ const evidenceImport = row.findings[0]?.reachabilityEvidence?.imports[0];
11041
+ if (evidenceImport !== void 0) {
11042
+ lines.push(` import: ${evidenceImport.file}:${evidenceImport.line} ${evidenceImport.specifier}`);
11043
+ }
11044
+ lines.push(` docs: rules/${row.ruleId}.md`);
11045
+ return lines;
11046
+ }
11047
+ function renderConsoleReport(input, options = {}) {
11048
+ const result = redactScan(input);
9107
11049
  const { findings, summary, metadata } = result;
9108
11050
  const lines = [];
9109
11051
  lines.push(
@@ -9111,34 +11053,64 @@ function renderConsoleReport(result) {
9111
11053
  ` \u2014 scanned ${metadata.filesScanned} files in ${metadata.durationMs}ms (modules: ${metadata.modulesRun.length > 0 ? metadata.modulesRun.join(", ") : "none"})`
9112
11054
  )
9113
11055
  );
11056
+ if (metadata.history !== void 0) lines.push(metadata.history);
11057
+ if (metadata.upgradePlan !== void 0 && metadata.upgradePlan.sentence.length > 0) {
11058
+ lines.push(metadata.upgradePlan.sentence);
11059
+ }
11060
+ if (metadata.unusedDirect !== void 0) {
11061
+ const count = metadata.unusedDirect.packages.length;
11062
+ const advisories = metadata.unusedDirect.advisories;
11063
+ const packageWord = count === 1 ? "package is" : "packages are";
11064
+ const advisoryWord = advisories === 1 ? "advisory" : "advisories";
11065
+ lines.push(
11066
+ `${count} ${packageWord} never imported; removing them clears ${advisories} ${advisoryWord}.`
11067
+ );
11068
+ }
9114
11069
  if (metadata.modulesRun.length < metadata.modulesRequested.length) {
9115
11070
  const pending = metadata.modulesRequested.filter((m) => !metadata.modulesRun.includes(m));
9116
11071
  lines.push(
9117
11072
  pc.dim(`note: requested modules not implemented yet: ${pending.join(", ")}`)
9118
11073
  );
9119
11074
  }
9120
- const bySeverityDesc = [...SEVERITIES].reverse();
9121
- const ordered = bySeverityDesc.flatMap((severity) => findings.filter((f) => f.severity === severity));
9122
- const shown = ordered.slice(0, 10);
9123
- const hidden = ordered.length - shown.length;
9124
- for (const severity of bySeverityDesc) {
9125
- const group = shown.filter((f) => f.severity === severity);
9126
- if (group.length === 0) continue;
9127
- lines.push("");
9128
- lines.push(SEVERITY_COLOR[severity](`${severity.toUpperCase()} (${findings.filter((f) => f.severity === severity).length})`));
9129
- for (const finding3 of group) {
9130
- lines.push(...renderFinding(finding3));
9131
- lines.push(` docs: rules/${finding3.ruleId}.md`);
11075
+ const showAll = options.all === true;
11076
+ const folded = findings.filter((finding3) => !isActionable(finding3));
11077
+ const rows = showAll ? [...findings].sort(compareActionable).map((finding3) => ({
11078
+ findings: [finding3],
11079
+ severity: finding3.severity,
11080
+ reachability: finding3.reachability,
11081
+ ruleId: finding3.ruleId,
11082
+ file: finding3.file,
11083
+ line: finding3.line,
11084
+ versions: [],
11085
+ grouped: false
11086
+ })) : shownConsoleRows(findings);
11087
+ const printed = showAll ? rows : rows.slice(0, 20);
11088
+ const hiddenActionable = showAll ? 0 : rows.length - printed.length;
11089
+ let previous;
11090
+ for (const row of printed) {
11091
+ if (row.severity !== previous) {
11092
+ const count = printed.filter((item) => item.severity === row.severity).length;
11093
+ lines.push("");
11094
+ lines.push(SEVERITY_COLOR[row.severity](`${row.severity.toUpperCase()} (${count})`));
11095
+ previous = row.severity;
9132
11096
  }
11097
+ lines.push(...renderRow(row));
11098
+ }
11099
+ if (hiddenActionable > 0) {
11100
+ lines.push("");
11101
+ lines.push(`${hiddenActionable} more actionable \u2014 use --json`);
9133
11102
  }
9134
- if (hidden > 0) {
11103
+ if (!showAll && folded.length > 0) {
9135
11104
  lines.push("");
9136
- lines.push(`${hidden} more \u2014 use --json`);
11105
+ lines.push(
11106
+ `${folded.length} more (not directly reachable, or below high confidence) \u2014 --all or see JSON`
11107
+ );
9137
11108
  }
9138
11109
  lines.push("");
9139
11110
  if (summary.total === 0) {
9140
11111
  lines.push(pc.green("\u2714 No findings."));
9141
11112
  } else {
11113
+ const bySeverityDesc = [...SEVERITIES].reverse();
9142
11114
  const counts = bySeverityDesc.filter((s) => summary.bySeverity[s] > 0).map((s) => `${summary.bySeverity[s]} ${s}`).join(", ");
9143
11115
  lines.push(pc.red(`\u2716 ${summary.total} finding(s): ${counts}`));
9144
11116
  }
@@ -9154,7 +11126,8 @@ function location2(finding3) {
9154
11126
  if (finding3.file === void 0) return "(project)";
9155
11127
  return finding3.line !== void 0 ? `${finding3.file}:${finding3.line}` : finding3.file;
9156
11128
  }
9157
- function renderHtmlReport(result) {
11129
+ function renderHtmlReport(input) {
11130
+ const result = redactScan(input);
9158
11131
  const sections = [];
9159
11132
  const bySeverityDesc = [...SEVERITIES].reverse();
9160
11133
  for (const severity of bySeverityDesc) {
@@ -9201,7 +11174,7 @@ ${items}`);
9201
11174
 
9202
11175
  // src/reporters/json.ts
9203
11176
  function publicFinding(finding3) {
9204
- const copy = { ...finding3 };
11177
+ const copy = redactFinding(finding3);
9205
11178
  delete copy.aiVerdict;
9206
11179
  delete copy.exploitScenario;
9207
11180
  delete copy.aiReasoning;
@@ -9225,7 +11198,8 @@ function location3(finding3) {
9225
11198
  if (finding3.file === void 0) return "(project)";
9226
11199
  return finding3.line !== void 0 ? `${finding3.file}:${finding3.line}` : finding3.file;
9227
11200
  }
9228
- function renderMarkdownReport(result) {
11201
+ function renderMarkdownReport(input) {
11202
+ const result = redactScan(input);
9229
11203
  const lines = [
9230
11204
  "# FullShield report",
9231
11205
  "",
@@ -9267,6 +11241,14 @@ function renderMarkdownReport(result) {
9267
11241
  return lines.join("\n");
9268
11242
  }
9269
11243
 
11244
+ // src/core/fingerprint.ts
11245
+ init_finding();
11246
+ function lineIndependentFingerprint(finding3) {
11247
+ const file = (finding3.file ?? "").replace(/\\/g, "/");
11248
+ const text = (finding3.snippet ?? finding3.message ?? "").replace(/\s+/g, " ").trim();
11249
+ return makeFindingId(finding3.ruleId, file, `${finding3.severity}\0${text}`);
11250
+ }
11251
+
9270
11252
  // src/reporters/sarif.ts
9271
11253
  var SCHEMA_URI = "https://json.schemastore.org/sarif-2.1.0.json";
9272
11254
  function sarifLevel(severity) {
@@ -9277,14 +11259,37 @@ function sarifLevel(severity) {
9277
11259
  function uri(file) {
9278
11260
  return (file ?? "").replace(/\\/g, "/");
9279
11261
  }
9280
- function renderSarifReport(result) {
11262
+ function sarifV2() {
11263
+ return process.env.FULLSHIELD_EXPERIMENTAL_SARIF_V2 === "1";
11264
+ }
11265
+ function ruleMarkdown(ruleId) {
11266
+ const path = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "rules", `${ruleId}.md`);
11267
+ if (!existsSync(path)) return void 0;
11268
+ return readFileSync(path, "utf8").slice(0, 2048);
11269
+ }
11270
+ function relatedLocations(finding3) {
11271
+ if (finding3.dependencyPath === void 0 || finding3.dependencyPath.length === 0) return void 0;
11272
+ return finding3.dependencyPath.map((step) => ({
11273
+ message: { text: step },
11274
+ physicalLocation: {
11275
+ artifactLocation: { uri: uri(finding3.file) },
11276
+ ...finding3.line !== void 0 ? { region: { startLine: finding3.line } } : {}
11277
+ }
11278
+ }));
11279
+ }
11280
+ function renderSarifReport(input) {
11281
+ const result = redactScan(input);
11282
+ const v2 = sarifV2();
9281
11283
  const rules = /* @__PURE__ */ new Map();
9282
11284
  for (const finding3 of result.findings) {
9283
11285
  if (!rules.has(finding3.ruleId)) {
11286
+ const markdown = v2 ? ruleMarkdown(finding3.ruleId) : void 0;
9284
11287
  rules.set(finding3.ruleId, {
9285
11288
  id: finding3.ruleId,
9286
11289
  name: finding3.ruleId,
9287
- shortDescription: { text: finding3.title }
11290
+ shortDescription: { text: finding3.title },
11291
+ ...markdown !== void 0 ? { help: { markdown } } : {},
11292
+ ...v2 ? { properties: { precision: "unreviewed" } } : {}
9288
11293
  });
9289
11294
  }
9290
11295
  }
@@ -9313,6 +11318,7 @@ function renderSarifReport(result) {
9313
11318
  }
9314
11319
  }
9315
11320
  ] : [];
11321
+ const related = v2 ? relatedLocations(finding3) : void 0;
9316
11322
  const codeFlows = finding3.trace !== void 0 && finding3.trace.length > 0 ? [
9317
11323
  {
9318
11324
  threadFlows: [
@@ -9335,7 +11341,12 @@ function renderSarifReport(result) {
9335
11341
  level: sarifLevel(finding3.severity),
9336
11342
  message: { text: finding3.message },
9337
11343
  locations: location4,
9338
- ...codeFlows !== void 0 ? { codeFlows } : {}
11344
+ ...codeFlows !== void 0 ? { codeFlows } : {},
11345
+ ...v2 ? {
11346
+ partialFingerprints: { "fullshield/v1": lineIndependentFingerprint(finding3) },
11347
+ ...related !== void 0 ? { relatedLocations: related } : {},
11348
+ ...finding3.module === "deps" && finding3.reachability !== void 0 ? { properties: { reachability: finding3.reachability } } : {}
11349
+ } : {}
9339
11350
  };
9340
11351
  })
9341
11352
  }
@@ -9384,6 +11395,249 @@ function validateSarif21(doc) {
9384
11395
  return errors;
9385
11396
  }
9386
11397
 
11398
+ // src/reporters/ci.ts
11399
+ init_finding();
11400
+ var MESSAGE_CAP = 4e3;
11401
+ function posix2(file) {
11402
+ return file.replace(/\\/g, "/");
11403
+ }
11404
+ function clip(text) {
11405
+ return text.length <= MESSAGE_CAP ? text : `${text.slice(0, MESSAGE_CAP)}\u2026`;
11406
+ }
11407
+ function githubEscape(value, property) {
11408
+ let out = value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A");
11409
+ if (property) out = out.replace(/,/g, "%2C");
11410
+ return out;
11411
+ }
11412
+ function githubLevel(severity) {
11413
+ if (severity === "critical" || severity === "high") return "error";
11414
+ if (severity === "medium") return "warning";
11415
+ return "notice";
11416
+ }
11417
+ function gitlabSeverity(severity) {
11418
+ if (severity === "info") return "info";
11419
+ if (severity === "low") return "minor";
11420
+ if (severity === "medium") return "major";
11421
+ if (severity === "high") return "critical";
11422
+ return "blocker";
11423
+ }
11424
+ function xmlEscape(value) {
11425
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
11426
+ }
11427
+ function oneLine(value) {
11428
+ return value.replace(/\s+/g, " ").trim();
11429
+ }
11430
+ function renderGithubAnnotations(input) {
11431
+ const result = redactScan(input);
11432
+ if (result.findings.length === 0) {
11433
+ return "::notice title=FullShield::No findings\n";
11434
+ }
11435
+ const lines = result.findings.map((finding3) => {
11436
+ const props = [];
11437
+ if (finding3.file !== void 0) props.push(`file=${githubEscape(posix2(finding3.file), true)}`);
11438
+ if (finding3.line !== void 0) props.push(`line=${finding3.line}`);
11439
+ if (finding3.column !== void 0) props.push(`col=${finding3.column + 1}`);
11440
+ props.push(`title=${githubEscape(finding3.ruleId, true)}`);
11441
+ const message = githubEscape(clip(finding3.message), false);
11442
+ return `::${githubLevel(finding3.severity)} ${props.join(",")}::${message}`;
11443
+ });
11444
+ return `${lines.join("\n")}
11445
+ `;
11446
+ }
11447
+ function junitCase(finding3) {
11448
+ const file = finding3.file !== void 0 ? posix2(finding3.file) : "fullshield";
11449
+ const attrs = [
11450
+ `name="${xmlEscape(finding3.ruleId)}"`,
11451
+ `classname="${xmlEscape(file)}"`,
11452
+ `file="${xmlEscape(file)}"`
11453
+ ];
11454
+ if (finding3.line !== void 0) attrs.push(`line="${finding3.line}"`);
11455
+ const open = `<testcase ${attrs.join(" ")}>`;
11456
+ if (!meetsThreshold(finding3.severity, "medium")) {
11457
+ return `${open}<skipped message="${xmlEscape(oneLine(clip(finding3.message)))}"/></testcase>`;
11458
+ }
11459
+ const detail = clip(`${finding3.message}${finding3.remediation.length > 0 ? `
11460
+ Fix: ${finding3.remediation}` : ""}`);
11461
+ return `${open}<failure type="${finding3.severity}" message="${xmlEscape(oneLine(clip(finding3.message)))}">${xmlEscape(detail)}</failure></testcase>`;
11462
+ }
11463
+ function renderJunitReport(input) {
11464
+ const result = redactScan(input);
11465
+ const header = '<?xml version="1.0" encoding="UTF-8"?>';
11466
+ if (result.findings.length === 0) {
11467
+ return `${header}
11468
+ <testsuites name="fullshield" tests="1" failures="0" skipped="0"><testsuite name="fullshield" tests="1" failures="0" skipped="0"><testcase name="scan" classname="fullshield"/></testsuite></testsuites>
11469
+ `;
11470
+ }
11471
+ const groups = /* @__PURE__ */ new Map();
11472
+ for (const finding3 of result.findings) {
11473
+ const list = groups.get(finding3.module) ?? [];
11474
+ list.push(finding3);
11475
+ groups.set(finding3.module, list);
11476
+ }
11477
+ let failures = 0;
11478
+ let skipped = 0;
11479
+ const suites = [];
11480
+ for (const [name, findings] of groups) {
11481
+ let suiteFailures = 0;
11482
+ let suiteSkipped = 0;
11483
+ const cases = findings.map((finding3) => {
11484
+ if (meetsThreshold(finding3.severity, "medium")) suiteFailures += 1;
11485
+ else suiteSkipped += 1;
11486
+ return junitCase(finding3);
11487
+ });
11488
+ failures += suiteFailures;
11489
+ skipped += suiteSkipped;
11490
+ suites.push(
11491
+ `<testsuite name="${xmlEscape(name)}" tests="${findings.length}" failures="${suiteFailures}" skipped="${suiteSkipped}">${cases.join("")}</testsuite>`
11492
+ );
11493
+ }
11494
+ return `${header}
11495
+ <testsuites name="fullshield" tests="${result.findings.length}" failures="${failures}" skipped="${skipped}">${suites.join("")}</testsuites>
11496
+ `;
11497
+ }
11498
+ function renderGitlabCodeQuality(input) {
11499
+ const result = redactScan(input);
11500
+ const report = result.findings.map((finding3) => {
11501
+ const path = finding3.file !== void 0 ? posix2(finding3.file) : ".";
11502
+ const location4 = { path };
11503
+ if (finding3.line !== void 0) location4.lines = { begin: finding3.line };
11504
+ return {
11505
+ description: clip(finding3.message),
11506
+ check_name: finding3.ruleId,
11507
+ fingerprint: lineIndependentFingerprint(finding3),
11508
+ severity: gitlabSeverity(finding3.severity),
11509
+ location: location4
11510
+ };
11511
+ });
11512
+ return `${JSON.stringify(report, null, 2)}
11513
+ `;
11514
+ }
11515
+
11516
+ // src/diff/index.ts
11517
+ init_finding();
11518
+ function posix3(file) {
11519
+ return (file ?? "").replace(/\\/g, "/");
11520
+ }
11521
+ function diffKey(finding3) {
11522
+ return lineIndependentFingerprint(finding3);
11523
+ }
11524
+ function groupById(findings) {
11525
+ const groups = /* @__PURE__ */ new Map();
11526
+ for (const finding3 of findings) {
11527
+ if (finding3.id === void 0 || finding3.id.length === 0) continue;
11528
+ const list = groups.get(finding3.id) ?? [];
11529
+ list.push(finding3);
11530
+ groups.set(finding3.id, list);
11531
+ }
11532
+ return groups;
11533
+ }
11534
+ function pairMoves(removed, added) {
11535
+ const removedIds = groupById(removed);
11536
+ const addedIds = groupById(added);
11537
+ const moved = [];
11538
+ for (const [id, group] of removedIds) {
11539
+ const match = addedIds.get(id);
11540
+ if (group.length === 1 && match?.length === 1 && group[0] !== void 0 && match[0] !== void 0) {
11541
+ moved.push({ before: group[0], after: match[0] });
11542
+ }
11543
+ }
11544
+ return moved;
11545
+ }
11546
+ function byLocation(a, b) {
11547
+ return (a.line ?? 0) - (b.line ?? 0) || a.id.localeCompare(b.id) || a.ruleId.localeCompare(b.ruleId);
11548
+ }
11549
+ function groupFindings(findings) {
11550
+ const groups = /* @__PURE__ */ new Map();
11551
+ for (const finding3 of findings) {
11552
+ const key = diffKey(finding3);
11553
+ const list = groups.get(key) ?? [];
11554
+ list.push(finding3);
11555
+ groups.set(key, list);
11556
+ }
11557
+ return groups;
11558
+ }
11559
+ function diffScans(before, after) {
11560
+ const beforeGroups = groupFindings(before);
11561
+ const afterGroups = groupFindings(after);
11562
+ const unchanged = [];
11563
+ const added = [];
11564
+ const removed = [];
11565
+ const moved = [];
11566
+ const keys = [];
11567
+ for (const key of beforeGroups.keys()) keys.push(key);
11568
+ for (const key of afterGroups.keys()) {
11569
+ if (!beforeGroups.has(key)) keys.push(key);
11570
+ }
11571
+ for (const key of keys) {
11572
+ const left = [...beforeGroups.get(key) ?? []].sort(byLocation);
11573
+ const right = [...afterGroups.get(key) ?? []].sort(byLocation);
11574
+ const paired = Math.min(left.length, right.length);
11575
+ for (let i = 0; i < paired; i += 1) {
11576
+ const earlier = left[i];
11577
+ const later = right[i];
11578
+ if (earlier === void 0 || later === void 0) continue;
11579
+ if ((earlier.line ?? 0) === (later.line ?? 0)) unchanged.push(later);
11580
+ else moved.push({ before: earlier, after: later });
11581
+ }
11582
+ for (const finding3 of left.slice(paired)) removed.push(finding3);
11583
+ for (const finding3 of right.slice(paired)) added.push(finding3);
11584
+ }
11585
+ const idMoves = pairMoves(removed, added);
11586
+ const movedBefore = new Set(idMoves.map((item) => item.before));
11587
+ const movedAfter = new Set(idMoves.map((item) => item.after));
11588
+ return {
11589
+ unchanged,
11590
+ added: added.filter((finding3) => !movedAfter.has(finding3)),
11591
+ removed: removed.filter((finding3) => !movedBefore.has(finding3)),
11592
+ moved: [...moved, ...idMoves]
11593
+ };
11594
+ }
11595
+ function parseScanDocument(text) {
11596
+ let parsed;
11597
+ try {
11598
+ parsed = JSON.parse(text);
11599
+ } catch {
11600
+ throw new Error("scan JSON must be an object with a findings array or a findings array");
11601
+ }
11602
+ const list = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.findings) ? parsed.findings : void 0;
11603
+ if (list === void 0) {
11604
+ throw new Error("scan JSON must be an object with a findings array or a findings array");
11605
+ }
11606
+ return list.map((item) => {
11607
+ if (typeof item !== "object" || item === null) {
11608
+ throw new Error("scan JSON finding is missing ruleId or severity");
11609
+ }
11610
+ const record = item;
11611
+ const ruleId = record.ruleId;
11612
+ const severity = record.severity;
11613
+ if (typeof ruleId !== "string" || ruleId.length === 0 || typeof severity !== "string") {
11614
+ throw new Error("scan JSON finding is missing ruleId or severity");
11615
+ }
11616
+ if (!isSeverity(severity)) {
11617
+ throw new Error(`scan JSON finding has an unknown severity "${severity}"`);
11618
+ }
11619
+ if (record.line !== void 0 && (typeof record.line !== "number" || !Number.isInteger(record.line) || record.line < 0)) {
11620
+ throw new Error("scan JSON finding line must be an integer >= 0");
11621
+ }
11622
+ return item;
11623
+ });
11624
+ }
11625
+ var ADDED_CAP = 20;
11626
+ function addedLine(finding3) {
11627
+ const where = finding3.file !== void 0 ? ` ${posix3(finding3.file)}:${finding3.line ?? 0}` : "";
11628
+ return `${finding3.severity} ${finding3.ruleId}${where}`;
11629
+ }
11630
+ function renderDiffText(diff) {
11631
+ const lines = [
11632
+ `fullshield diff: unchanged ${diff.unchanged.length}, added ${diff.added.length}, removed ${diff.removed.length}, moved ${diff.moved.length}`
11633
+ ];
11634
+ if (diff.added.length === 0) return lines.join("\n");
11635
+ lines.push("added:");
11636
+ for (const finding3 of diff.added.slice(0, ADDED_CAP)) lines.push(addedLine(finding3));
11637
+ if (diff.added.length > ADDED_CAP) lines.push(`${diff.added.length - ADDED_CAP} more \u2014 use --json`);
11638
+ return lines.join("\n");
11639
+ }
11640
+
9387
11641
  // src/sbom/cyclonedx.ts
9388
11642
  function validateCycloneDx15(doc) {
9389
11643
  const errors = [];
@@ -9396,6 +11650,16 @@ function validateCycloneDx15(doc) {
9396
11650
  if (!Array.isArray(root.components)) errors.push("components must be an array");
9397
11651
  return errors;
9398
11652
  }
11653
+ function reachProperties(finding3) {
11654
+ const properties = [
11655
+ { name: "fullshield:reachability", value: finding3.reachability ?? "unspecified" },
11656
+ { name: "fullshield:devOnly", value: finding3.message.includes("(dev-only)") ? "true" : "false" }
11657
+ ];
11658
+ if (finding3.dependencyPath !== void 0 && finding3.dependencyPath.length > 0) {
11659
+ properties.splice(1, 0, { name: "fullshield:dependencyPath", value: finding3.dependencyPath.join(" > ") });
11660
+ }
11661
+ return properties;
11662
+ }
9399
11663
  function renderCycloneDx(input) {
9400
11664
  const vulns = (input.findings ?? []).filter((f) => f.ruleId === "DEPS-VULN-001");
9401
11665
  return {
@@ -9406,13 +11670,18 @@ function renderCycloneDx(input) {
9406
11670
  metadata: {
9407
11671
  component: { type: "application", name: input.name, version: input.version }
9408
11672
  },
9409
- components: input.components.map((c) => ({
9410
- type: "library",
9411
- name: c.name,
9412
- version: c.version,
9413
- purl: c.purl ?? `pkg:npm/${encodeURIComponent(c.name)}@${c.version}`,
9414
- ...c.license !== void 0 ? { licenses: [{ license: { id: c.license } }] } : {}
9415
- })),
11673
+ components: input.components.map((c) => {
11674
+ const match = (input.findings ?? []).find((finding3) => finding3.snippet?.startsWith(`${c.name}@`));
11675
+ const properties = input.includeReach === true && match !== void 0 ? reachProperties(match) : void 0;
11676
+ return {
11677
+ type: "library",
11678
+ name: c.name,
11679
+ version: c.version,
11680
+ purl: c.purl ?? `pkg:npm/${encodeURIComponent(c.name)}@${c.version}`,
11681
+ ...c.license !== void 0 ? { licenses: [{ license: { id: c.license } }] } : {},
11682
+ ...properties !== void 0 ? { properties } : {}
11683
+ };
11684
+ }),
9416
11685
  vulnerabilities: vulns.map((f) => ({
9417
11686
  id: f.references?.find((r) => r.startsWith("GHSA-") || r.startsWith("CVE-")) ?? f.id,
9418
11687
  description: f.message,
@@ -9423,7 +11692,7 @@ function renderCycloneDx(input) {
9423
11692
 
9424
11693
  // src/sbom/index.ts
9425
11694
  var LOCKFILE_NAMES2 = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
9426
- async function buildSbom(cwd) {
11695
+ async function buildSbom(cwd, options = {}) {
9427
11696
  const result = await scan({ cwd, modules: ["deps"] });
9428
11697
  let name = "app";
9429
11698
  let version = "0.0.0";
@@ -9449,7 +11718,13 @@ async function buildSbom(cwd) {
9449
11718
  if (components.length === 0) {
9450
11719
  components = exactPinsFromManifest(manifest).map((p) => ({ name: p.name, version: p.version }));
9451
11720
  }
9452
- return renderCycloneDx({ name, version, components, findings: result.findings });
11721
+ return renderCycloneDx({
11722
+ name,
11723
+ version,
11724
+ components,
11725
+ findings: result.findings,
11726
+ includeReach: options.includeReach === true
11727
+ });
9453
11728
  }
9454
11729
  var OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
9455
11730
  var OPENVEX_NOT_AFFECTED_JUSTIFICATION = "vulnerable_code_not_in_execute_path";
@@ -9484,14 +11759,16 @@ function openVexStatement(finding3) {
9484
11759
  vulnerability: { name: vulnName(finding3) },
9485
11760
  products: [{ "@id": npmPurl(finding3.snippet) }]
9486
11761
  };
11762
+ const detail = process.env.FULLSHIELD_EXPERIMENTAL_SBOM_REACH === "1" && finding3.reachabilityEvidence?.reason !== void 0 ? { detail: finding3.reachabilityEvidence.reason } : {};
9487
11763
  if (notImported(finding3)) {
9488
11764
  return {
9489
11765
  ...base,
9490
11766
  status: "not_affected",
9491
- justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION
11767
+ justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION,
11768
+ ...detail
9492
11769
  };
9493
11770
  }
9494
- return { ...base, status: "under_investigation" };
11771
+ return { ...base, status: "under_investigation", ...detail };
9495
11772
  }
9496
11773
  function buildOpenVex(findings) {
9497
11774
  const statements = depFindings(findings).map(openVexStatement);
@@ -9610,4 +11887,4 @@ function buildVex(findings, format, meta = { name: "app", version: "0.0.0" }) {
9610
11887
  return buildOpenVex(findings);
9611
11888
  }
9612
11889
 
9613
- export { CONFIG_FILE_NAME, ConfigError, DEFAULT_CONFIG, IMPLEMENTED_MODULES, MODULE_NAMES, POLICY_FILE_NAME, POLICY_FLAG, POLICY_PEER_ERROR, PolicyError, SEVERITIES, analyzeWorkflowTampering, analyzeWormIndicators, applyBaseline, applyInlineSuppressions, applyPolicy, buildCycloneDxVex, buildOpenVex, buildSbom, buildVex, builtinModules, createAiClient, experimentalVex, failsCi, guardTools, isInternalAi, isLiveAi, isModuleName, isPolicyEnabled, isSeverity, loadConfig, loadPolicy, makeFindingId, mcpCfgEnabled2 as mcpCfgEnabled, mcpHomeConfigPaths, meetsThreshold, packageVersion, parseIgnore, parsePolicy, provenanceCoverage, readBaseline, renderConsoleReport, renderCycloneDx, renderHtmlReport, renderJsonReport, renderMarkdownReport, renderPrSummary, renderSarifReport, ruleDocsUrl, runAiFix, runFix, runScan, scan, scanMcpConfig, scanTextForSecrets, severityRank, sortFindings, summarize, validateCycloneDx15, validateCycloneDxVex, validateOpenVex, validateSarif21, walkFiles, writeBaseline };
11890
+ export { CONFIG_FILE_NAME, ConfigError, DEFAULT_CONFIG, DIFF_FLAG, IMPLEMENTED_MODULES, MODULE_NAMES, POLICY_FILE_NAME, POLICY_FLAG, POLICY_PEER_ERROR, PolicyError, SEVERITIES, analyzeWorkflowTampering, analyzeWormIndicators, applyBaseline, applyInlineSuppressions, applyPolicy, buildCycloneDxVex, buildOpenVex, buildSbom, buildVex, builtinModules, createAiClient, diffScans, experimentalVex, failsCi, guardTools, isInternalAi, isLiveAi, isModuleName, isPolicyEnabled, isSeverity, loadConfig, loadPolicy, makeFindingId, mcpCfgEnabled2 as mcpCfgEnabled, mcpHomeConfigPaths, meetsThreshold, packageVersion, parseIgnore, parsePolicy, parseScanDocument, provenanceCoverage, readBaseline, renderConsoleReport, renderCycloneDx, renderDiffText, renderGithubAnnotations, renderGitlabCodeQuality, renderHtmlReport, renderJsonReport, renderJunitReport, renderMarkdownReport, renderPrSummary, renderSarifReport, ruleDocsUrl, runAiFix, runFix, runScan, scan, scanMcpConfig, scanTextForSecrets, severityRank, sortFindings, summarize, validateCycloneDx15, validateCycloneDxVex, validateOpenVex, validateSarif21, walkFiles, writeBaseline };