fullshield 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +4 -4
- package/dist/agent.js +1535 -348
- package/dist/cli/index.js +2175 -387
- package/dist/compliance.js +4 -0
- package/dist/index.d.ts +54 -3
- package/dist/index.js +1890 -426
- package/dist/mcp.js +1546 -359
- package/dist/vex.js +4 -2
- package/package.json +2 -2
- package/rules/README.md +2 -0
- package/rules/SUPPLY-CHAINC2-001.md +3 -0
- package/rules/SUPPLY-PLATFORMGATE-001.md +3 -0
- package/rules/SUPPLY-SECHOLDER-001.md +3 -0
- package/rules/SUPPLY-STEALER-001.md +3 -0
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import { readFile, mkdir, writeFile, stat, unlink,
|
|
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((
|
|
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({
|
|
@@ -599,13 +599,13 @@ async function readBlob(cwd, hash) {
|
|
|
599
599
|
return void 0;
|
|
600
600
|
}
|
|
601
601
|
}
|
|
602
|
-
function remapHit(
|
|
603
|
-
const snippet =
|
|
604
|
-
const line =
|
|
605
|
-
const id = makeFindingId(
|
|
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"}`);
|
|
606
606
|
return {
|
|
607
|
-
finding: { ...
|
|
608
|
-
secret:
|
|
607
|
+
finding: { ...hit2.finding, id, file },
|
|
608
|
+
secret: hit2.secret
|
|
609
609
|
};
|
|
610
610
|
}
|
|
611
611
|
function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
|
|
@@ -695,8 +695,8 @@ async function headSecretKeys(cwd) {
|
|
|
695
695
|
hits = scanTextForSecretHits(path, text);
|
|
696
696
|
seen.set(hash, hits);
|
|
697
697
|
}
|
|
698
|
-
for (const
|
|
699
|
-
keys.add(`${
|
|
698
|
+
for (const hit2 of hits) {
|
|
699
|
+
keys.add(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
|
|
700
700
|
}
|
|
701
701
|
}
|
|
702
702
|
return keys;
|
|
@@ -729,8 +729,8 @@ async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
|
729
729
|
hits2 = rawHits.filter((h) => allowed.has(h.finding.id));
|
|
730
730
|
blobHits.set(change.hash, hits2);
|
|
731
731
|
}
|
|
732
|
-
for (const
|
|
733
|
-
const mapped = remapHit(
|
|
732
|
+
for (const hit2 of hits2) {
|
|
733
|
+
const mapped = remapHit(hit2, change.path);
|
|
734
734
|
if (!first.has(mapped.finding.id)) {
|
|
735
735
|
first.set(mapped.finding.id, { hit: mapped, commit: change.commit });
|
|
736
736
|
}
|
|
@@ -738,11 +738,11 @@ async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
|
738
738
|
}
|
|
739
739
|
const atHead = await headSecretKeys(cwd);
|
|
740
740
|
const hits = [];
|
|
741
|
-
for (const { hit, commit } of first.values()) {
|
|
742
|
-
const stillAtHead = atHead.has(`${
|
|
741
|
+
for (const { hit: hit2, commit } of first.values()) {
|
|
742
|
+
const stillAtHead = atHead.has(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
|
|
743
743
|
hits.push({
|
|
744
|
-
finding: { ...
|
|
745
|
-
secret:
|
|
744
|
+
finding: { ...hit2.finding, introducedIn: commit, stillAtHead },
|
|
745
|
+
secret: hit2.secret
|
|
746
746
|
});
|
|
747
747
|
}
|
|
748
748
|
const scanned = Math.min(maxCommits, total);
|
|
@@ -984,15 +984,15 @@ function providerFor(ruleId, secret) {
|
|
|
984
984
|
async function verifySecretHits(hits, options = {}) {
|
|
985
985
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
986
986
|
const out = [];
|
|
987
|
-
for (const
|
|
988
|
-
const secret =
|
|
987
|
+
for (const hit2 of hits) {
|
|
988
|
+
const secret = hit2.secret;
|
|
989
989
|
if (secret === void 0 || secret.length === 0) {
|
|
990
|
-
out.push({ finding: { ...
|
|
990
|
+
out.push({ finding: { ...hit2.finding, verification: "unknown" }, secret: hit2.secret });
|
|
991
991
|
continue;
|
|
992
992
|
}
|
|
993
|
-
const run = providerFor(
|
|
993
|
+
const run = providerFor(hit2.finding.ruleId, secret);
|
|
994
994
|
const verification = run === void 0 ? "unknown" : await run(secret, fetchImpl);
|
|
995
|
-
out.push({ finding: { ...
|
|
995
|
+
out.push({ finding: { ...hit2.finding, verification }, secret: hit2.secret });
|
|
996
996
|
}
|
|
997
997
|
return out;
|
|
998
998
|
}
|
|
@@ -1553,6 +1553,169 @@ var configModule = {
|
|
|
1553
1553
|
// src/modules/deps/index.ts
|
|
1554
1554
|
init_finding();
|
|
1555
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
|
+
|
|
1556
1719
|
// src/modules/deps/lockfile.ts
|
|
1557
1720
|
function basename2(filename) {
|
|
1558
1721
|
const parts = filename.replace(/\\/g, "/").split("/");
|
|
@@ -1582,7 +1745,7 @@ function objectKeys(value) {
|
|
|
1582
1745
|
}
|
|
1583
1746
|
return Object.keys(value);
|
|
1584
1747
|
}
|
|
1585
|
-
function
|
|
1748
|
+
function addEdge2(edges, from, to) {
|
|
1586
1749
|
const set = edges.get(from) ?? /* @__PURE__ */ new Set();
|
|
1587
1750
|
set.add(to);
|
|
1588
1751
|
edges.set(from, set);
|
|
@@ -1662,7 +1825,7 @@ function parseYarnGraph(contents) {
|
|
|
1662
1825
|
const depBerry = /^\s{4}(?:"([^"]+)"|(@?[\w./-]+)):/.exec(raw);
|
|
1663
1826
|
const depName = depClassic?.[1] ?? depClassic?.[2] ?? depBerry?.[1] ?? depBerry?.[2];
|
|
1664
1827
|
if (depName !== void 0) {
|
|
1665
|
-
|
|
1828
|
+
addEdge2(edges, currentName, depName);
|
|
1666
1829
|
continue;
|
|
1667
1830
|
}
|
|
1668
1831
|
if (/^\s{2}\S/.test(raw)) inDeps = false;
|
|
@@ -1757,7 +1920,7 @@ function parsePnpmGraph(contents) {
|
|
|
1757
1920
|
const dep = /^ (?:'([^']+)'|"([^"]+)"|([^:]+)):/.exec(raw);
|
|
1758
1921
|
const depName = dep?.[1] ?? dep?.[2] ?? dep?.[3];
|
|
1759
1922
|
if (depName !== void 0) {
|
|
1760
|
-
|
|
1923
|
+
addEdge2(edges, currentPkg, depName);
|
|
1761
1924
|
continue;
|
|
1762
1925
|
}
|
|
1763
1926
|
if (/^ \S/.test(raw)) inPkgDeps = false;
|
|
@@ -1792,7 +1955,7 @@ function graphFromPackagesTable(packages) {
|
|
|
1792
1955
|
const name = key === "" ? "" : nameFromPackagesKey(key);
|
|
1793
1956
|
if (name === void 0 || name.length === 0) continue;
|
|
1794
1957
|
for (const dep of [...objectKeys(value.dependencies), ...objectKeys(value.optionalDependencies)]) {
|
|
1795
|
-
|
|
1958
|
+
addEdge2(edges, name, dep);
|
|
1796
1959
|
}
|
|
1797
1960
|
}
|
|
1798
1961
|
return { edges, directProd, directDev };
|
|
@@ -1802,7 +1965,7 @@ function graphFromDependenciesTree(deps, edges) {
|
|
|
1802
1965
|
const child = value.dependencies;
|
|
1803
1966
|
if (child === void 0 || typeof child !== "object" || child === null) continue;
|
|
1804
1967
|
const nested = child;
|
|
1805
|
-
for (const childName of Object.keys(nested))
|
|
1968
|
+
for (const childName of Object.keys(nested)) addEdge2(edges, name, childName);
|
|
1806
1969
|
graphFromDependenciesTree(nested, edges);
|
|
1807
1970
|
}
|
|
1808
1971
|
}
|
|
@@ -1815,8 +1978,16 @@ function parseLockfileGraph(filename, contents) {
|
|
|
1815
1978
|
directDev: /* @__PURE__ */ new Set(),
|
|
1816
1979
|
edgesKnown: false
|
|
1817
1980
|
};
|
|
1818
|
-
|
|
1819
|
-
if (name === "
|
|
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);
|
|
1820
1991
|
if (name !== "package-lock.json") return empty;
|
|
1821
1992
|
try {
|
|
1822
1993
|
const json = JSON.parse(contents);
|
|
@@ -1985,6 +2156,86 @@ function exactPinsFromManifest(pkg) {
|
|
|
1985
2156
|
return out;
|
|
1986
2157
|
}
|
|
1987
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
|
+
|
|
1988
2239
|
// src/modules/deps/reachability.ts
|
|
1989
2240
|
init_finding();
|
|
1990
2241
|
|
|
@@ -2092,7 +2343,7 @@ function packageNameFromSnippet(snippet) {
|
|
|
2092
2343
|
const at = snippet.indexOf("@");
|
|
2093
2344
|
return at <= 0 ? snippet : snippet.slice(0, at);
|
|
2094
2345
|
}
|
|
2095
|
-
function
|
|
2346
|
+
function packageNameFromSpec2(spec) {
|
|
2096
2347
|
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
|
|
2097
2348
|
if (spec.startsWith("@")) {
|
|
2098
2349
|
const parts = spec.split("/");
|
|
@@ -2128,7 +2379,7 @@ function indexPackageUsage(sources) {
|
|
|
2128
2379
|
const local = m[1];
|
|
2129
2380
|
const spec = m[2];
|
|
2130
2381
|
if (local === void 0 || spec === void 0) continue;
|
|
2131
|
-
const pkg =
|
|
2382
|
+
const pkg = packageNameFromSpec2(spec);
|
|
2132
2383
|
if (pkg === void 0) continue;
|
|
2133
2384
|
bump(pkg, localIsCalled(source, local));
|
|
2134
2385
|
}
|
|
@@ -2138,7 +2389,7 @@ function indexPackageUsage(sources) {
|
|
|
2138
2389
|
while ((named = namedRe.exec(source)) !== null) {
|
|
2139
2390
|
const spec = named[2];
|
|
2140
2391
|
if (spec === void 0) continue;
|
|
2141
|
-
const pkg =
|
|
2392
|
+
const pkg = packageNameFromSpec2(spec);
|
|
2142
2393
|
if (pkg === void 0) continue;
|
|
2143
2394
|
const locals = (named[1] ?? "").split(",").map((s) => s.trim().split(/\s+as\s+/).pop() ?? "");
|
|
2144
2395
|
bump(
|
|
@@ -2197,34 +2448,55 @@ function reachableFrom(target, roots, edges) {
|
|
|
2197
2448
|
if (roots.has(target)) return true;
|
|
2198
2449
|
return shortestDependencyPath(target, roots, edges) !== void 0;
|
|
2199
2450
|
}
|
|
2200
|
-
function importedDirectNames(usage,
|
|
2201
|
-
const directs = /* @__PURE__ */ new Set([...
|
|
2451
|
+
function importedDirectNames(usage, graph2) {
|
|
2452
|
+
const directs = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
|
|
2202
2453
|
const out = /* @__PURE__ */ new Set();
|
|
2203
|
-
for (const [name,
|
|
2204
|
-
if (!
|
|
2454
|
+
for (const [name, hit2] of usage) {
|
|
2455
|
+
if (!hit2.imported) continue;
|
|
2205
2456
|
if (directs.size === 0 || directs.has(name)) out.add(name);
|
|
2206
2457
|
}
|
|
2207
2458
|
return out;
|
|
2208
2459
|
}
|
|
2209
|
-
function classifyReachability(pkgName, productionUsage,
|
|
2210
|
-
const
|
|
2211
|
-
if (
|
|
2212
|
-
return { reachability:
|
|
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" };
|
|
2213
2464
|
}
|
|
2214
|
-
if (
|
|
2215
|
-
const roots = importedDirectNames(productionUsage,
|
|
2216
|
-
const paths = collectDependencyPaths(pkgName, roots,
|
|
2465
|
+
if (graph2 !== void 0 && graph2.edgesKnown) {
|
|
2466
|
+
const roots = importedDirectNames(productionUsage, graph2);
|
|
2467
|
+
const paths = collectDependencyPaths(pkgName, roots, graph2.edges);
|
|
2217
2468
|
const path = paths[0];
|
|
2218
2469
|
if (path !== void 0 && path.length >= 2) {
|
|
2219
2470
|
return { reachability: "reachable-via-dependency", path, paths };
|
|
2220
2471
|
}
|
|
2221
2472
|
return { reachability: "not-imported" };
|
|
2222
2473
|
}
|
|
2223
|
-
if (
|
|
2474
|
+
if (graph2 !== void 0 && !graph2.edgesKnown) {
|
|
2224
2475
|
return { reachability: "reachability-unknown" };
|
|
2225
2476
|
}
|
|
2226
2477
|
return { reachability: "not-imported" };
|
|
2227
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
|
+
}
|
|
2228
2500
|
function reachabilitySteps(reachability) {
|
|
2229
2501
|
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
|
|
2230
2502
|
return 0;
|
|
@@ -2447,6 +2719,176 @@ function osvSeverity(vuln) {
|
|
|
2447
2719
|
return "high";
|
|
2448
2720
|
}
|
|
2449
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
|
+
|
|
2450
2892
|
// src/modules/deps/popular.ts
|
|
2451
2893
|
var POPULAR_PACKAGES = [
|
|
2452
2894
|
"express",
|
|
@@ -4494,7 +4936,7 @@ ${scripts}`;
|
|
|
4494
4936
|
if (!TOKEN_RE.test(text)) return [];
|
|
4495
4937
|
const replicates = PUBLISH_RE.test(text) || REGISTRY_PUT_RE.test(text) || GITHUB_CREATE_RE.test(text) || WORKFLOW_PATH_RE.test(text);
|
|
4496
4938
|
if (!replicates) return [];
|
|
4497
|
-
const
|
|
4939
|
+
const hit2 = sources.find(
|
|
4498
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))
|
|
4499
4941
|
) ?? sources.find((s) => TOKEN_RE.test(s.text));
|
|
4500
4942
|
return [
|
|
@@ -4502,7 +4944,7 @@ ${scripts}`;
|
|
|
4502
4944
|
"SUPPLY-WORM-001",
|
|
4503
4945
|
"Self-replicating worm indicators in dependency code",
|
|
4504
4946
|
"static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",
|
|
4505
|
-
|
|
4947
|
+
hit2?.file,
|
|
4506
4948
|
"critical"
|
|
4507
4949
|
)
|
|
4508
4950
|
];
|
|
@@ -4512,13 +4954,13 @@ function analyzeWorkflowTampering(dir) {
|
|
|
4512
4954
|
const text = `${sources.map((s) => s.text).join("\n")}
|
|
4513
4955
|
${scripts}`;
|
|
4514
4956
|
if (!CI_WRITE_RE.test(text)) return [];
|
|
4515
|
-
const
|
|
4957
|
+
const hit2 = sources.find((s) => CI_WRITE_RE.test(s.text));
|
|
4516
4958
|
return [
|
|
4517
4959
|
finding(
|
|
4518
4960
|
"SUPPLY-WORKFLOW-001",
|
|
4519
4961
|
"Dependency writes GitHub workflow, git hooks, or CI config",
|
|
4520
4962
|
"static: install script or package code writes .github/, .git/hooks/, or CI configuration",
|
|
4521
|
-
|
|
4963
|
+
hit2?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
|
|
4522
4964
|
"critical"
|
|
4523
4965
|
)
|
|
4524
4966
|
];
|
|
@@ -4527,9 +4969,9 @@ function provenanceCoverage(packages, known) {
|
|
|
4527
4969
|
let withProvenance = 0;
|
|
4528
4970
|
let unknown = 0;
|
|
4529
4971
|
for (const pkg of packages) {
|
|
4530
|
-
const
|
|
4531
|
-
if (
|
|
4532
|
-
else if (
|
|
4972
|
+
const hit2 = known.get(`${pkg.name}@${pkg.version}`);
|
|
4973
|
+
if (hit2 === true) withProvenance += 1;
|
|
4974
|
+
else if (hit2 === void 0) unknown += 1;
|
|
4533
4975
|
}
|
|
4534
4976
|
const total = packages.length;
|
|
4535
4977
|
return {
|
|
@@ -4556,8 +4998,8 @@ function parentsOf(name, edges) {
|
|
|
4556
4998
|
}
|
|
4557
4999
|
return out;
|
|
4558
5000
|
}
|
|
4559
|
-
function isDirect(name,
|
|
4560
|
-
return
|
|
5001
|
+
function isDirect(name, graph2) {
|
|
5002
|
+
return graph2.directProd.has(name) || graph2.directDev.has(name);
|
|
4561
5003
|
}
|
|
4562
5004
|
function packagesWithAFixedRelease(findings) {
|
|
4563
5005
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -4575,17 +5017,17 @@ function targetVersion(finding3) {
|
|
|
4575
5017
|
function kindFromLockfile(lockFile) {
|
|
4576
5018
|
return lockfileKind(lockFile) ?? "npm";
|
|
4577
5019
|
}
|
|
4578
|
-
function attachOverrideProposals(findings,
|
|
4579
|
-
if (!
|
|
5020
|
+
function attachOverrideProposals(findings, graph2, lockFile, manifestRaw) {
|
|
5021
|
+
if (!graph2.edgesKnown || manifestRaw === void 0) return findings;
|
|
4580
5022
|
const withFix = packagesWithAFixedRelease(findings);
|
|
4581
5023
|
const kind = kindFromLockfile(lockFile);
|
|
4582
5024
|
return findings.map((finding3) => {
|
|
4583
5025
|
if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
|
|
4584
5026
|
const name = packageNameFromSnippet2(finding3.snippet);
|
|
4585
|
-
if (name.length === 0 || isDirect(name,
|
|
5027
|
+
if (name.length === 0 || isDirect(name, graph2)) return finding3;
|
|
4586
5028
|
const version = targetVersion(finding3);
|
|
4587
5029
|
if (version === void 0) return finding3;
|
|
4588
|
-
const parentFixed = parentsOf(name,
|
|
5030
|
+
const parentFixed = parentsOf(name, graph2.edges).some((parent) => withFix.has(parent));
|
|
4589
5031
|
if (parentFixed) return finding3;
|
|
4590
5032
|
const after = withManifestOverride(manifestRaw, kind, name, version);
|
|
4591
5033
|
const diff = unifiedDiff("package.json", manifestRaw, after);
|
|
@@ -4606,10 +5048,10 @@ function scanInstalledPackages(cwd, packages) {
|
|
|
4606
5048
|
const dir = packageDir(cwd, pkg);
|
|
4607
5049
|
if (dir === void 0) continue;
|
|
4608
5050
|
const prefix = pkg.path.startsWith("node_modules/") ? pkg.path : `node_modules/${pkg.name}`;
|
|
4609
|
-
for (const
|
|
5051
|
+
for (const hit2 of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
|
|
4610
5052
|
out.push({
|
|
4611
|
-
...
|
|
4612
|
-
file:
|
|
5053
|
+
...hit2,
|
|
5054
|
+
file: hit2.file !== void 0 && hit2.file.length > 0 ? `${prefix}/${hit2.file}` : prefix
|
|
4613
5055
|
});
|
|
4614
5056
|
}
|
|
4615
5057
|
}
|
|
@@ -4688,19 +5130,19 @@ function maliciousFinding(lockFile, pkg, vuln) {
|
|
|
4688
5130
|
references: [vuln.id, ...vuln.aliases ?? []]
|
|
4689
5131
|
};
|
|
4690
5132
|
}
|
|
4691
|
-
function typosquatFinding(lockFile,
|
|
4692
|
-
const snippet = `${
|
|
5133
|
+
function typosquatFinding(lockFile, hit2) {
|
|
5134
|
+
const snippet = `${hit2.name}@${hit2.version}`;
|
|
4693
5135
|
return {
|
|
4694
5136
|
id: makeFindingId("DEPS-TYPOSQUAT-001", lockFile, snippet),
|
|
4695
5137
|
ruleId: "DEPS-TYPOSQUAT-001",
|
|
4696
5138
|
module: "deps",
|
|
4697
|
-
title: `Possible typosquat: ${
|
|
5139
|
+
title: `Possible typosquat: ${hit2.name}`,
|
|
4698
5140
|
severity: "medium",
|
|
4699
5141
|
confidence: "low",
|
|
4700
5142
|
file: lockFile,
|
|
4701
5143
|
snippet,
|
|
4702
|
-
message: `Package "${
|
|
4703
|
-
remediation: `Confirm you intended to install "${
|
|
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.`
|
|
4704
5146
|
};
|
|
4705
5147
|
}
|
|
4706
5148
|
function noLockFinding() {
|
|
@@ -4734,6 +5176,8 @@ function deprecatedFinding(lockFile, pkg) {
|
|
|
4734
5176
|
var depsModule = {
|
|
4735
5177
|
name: "deps",
|
|
4736
5178
|
async run(ctx) {
|
|
5179
|
+
depsModule.unusedDirect = void 0;
|
|
5180
|
+
depsModule.upgradePlan = void 0;
|
|
4737
5181
|
const lockFile = LOCKFILE_NAMES.find((name) => ctx.files.includes(name));
|
|
4738
5182
|
const cacheDir = ctx.config.deps.cacheDir !== void 0 ? resolve(ctx.cwd, ctx.config.deps.cacheDir) : defaultCacheDir();
|
|
4739
5183
|
const osvOpts = { cacheDir, offline: ctx.config.deps.offline };
|
|
@@ -4759,21 +5203,21 @@ var depsModule = {
|
|
|
4759
5203
|
}
|
|
4760
5204
|
const pins = exactPinsFromManifest(manifest);
|
|
4761
5205
|
const directNames2 = await readDirectDependencyNames(ctx.cwd);
|
|
4762
|
-
for (const
|
|
4763
|
-
findings2.push(typosquatFinding("package.json",
|
|
5206
|
+
for (const hit2 of findTyposquats(pins, { directNames: directNames2 })) {
|
|
5207
|
+
findings2.push(typosquatFinding("package.json", hit2));
|
|
4764
5208
|
}
|
|
4765
5209
|
const osvHits2 = await queryOsv(pins, osvOpts);
|
|
4766
5210
|
const symbols2 = /* @__PURE__ */ new Map();
|
|
4767
|
-
for (const
|
|
4768
|
-
for (const vuln of
|
|
4769
|
-
const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json",
|
|
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));
|
|
4770
5214
|
const names = affectedFunctionNames(vuln);
|
|
4771
5215
|
if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols2.set(finding3.id, names);
|
|
4772
5216
|
findings2.push(finding3);
|
|
4773
5217
|
}
|
|
4774
5218
|
}
|
|
4775
5219
|
const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
|
|
4776
|
-
return applyReachability(findings2, noLockCtx, symbols2);
|
|
5220
|
+
return withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, applyReachability(findings2, noLockCtx, symbols2));
|
|
4777
5221
|
}
|
|
4778
5222
|
let contents;
|
|
4779
5223
|
try {
|
|
@@ -4781,12 +5225,12 @@ var depsModule = {
|
|
|
4781
5225
|
} catch {
|
|
4782
5226
|
return [];
|
|
4783
5227
|
}
|
|
4784
|
-
const
|
|
4785
|
-
const packages =
|
|
5228
|
+
const graph2 = parseLockfileGraph(lockFile, contents);
|
|
5229
|
+
const packages = graph2.packages;
|
|
4786
5230
|
const findings = [];
|
|
4787
5231
|
const directNames = await readDirectDependencyNames(ctx.cwd);
|
|
4788
|
-
for (const
|
|
4789
|
-
findings.push(typosquatFinding(lockFile,
|
|
5232
|
+
for (const hit2 of findTyposquats(packages, { directNames })) {
|
|
5233
|
+
findings.push(typosquatFinding(lockFile, hit2));
|
|
4790
5234
|
}
|
|
4791
5235
|
for (const pkg of packages) {
|
|
4792
5236
|
if (pkg.deprecated !== void 0) {
|
|
@@ -4795,23 +5239,129 @@ var depsModule = {
|
|
|
4795
5239
|
}
|
|
4796
5240
|
const osvHits = await queryOsv(packages, osvOpts);
|
|
4797
5241
|
const symbols = /* @__PURE__ */ new Map();
|
|
4798
|
-
for (const
|
|
4799
|
-
for (const vuln of
|
|
4800
|
-
const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile,
|
|
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));
|
|
4801
5245
|
const names = affectedFunctionNames(vuln);
|
|
4802
5246
|
if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols.set(finding3.id, names);
|
|
4803
5247
|
findings.push(finding3);
|
|
4804
5248
|
}
|
|
4805
5249
|
}
|
|
4806
|
-
|
|
5250
|
+
const reachCtx = await reachabilityCtx(ctx.cwd, ctx.files, graph2);
|
|
5251
|
+
let reached = applyReachability(findings, reachCtx, symbols);
|
|
4807
5252
|
if (experimentalVex()) {
|
|
4808
|
-
reached = attachOverrideProposals(reached,
|
|
5253
|
+
reached = attachOverrideProposals(reached, graph2, lockFile, manifestRaw);
|
|
4809
5254
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
4810
5255
|
}
|
|
4811
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
|
+
}
|
|
4812
5292
|
return reached;
|
|
4813
5293
|
}
|
|
4814
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
|
+
}
|
|
4815
5365
|
function graphFromManifest(manifest) {
|
|
4816
5366
|
const keys = (field) => {
|
|
4817
5367
|
const block = manifest[field];
|
|
@@ -4828,7 +5378,7 @@ function graphFromManifest(manifest) {
|
|
|
4828
5378
|
edgesKnown: false
|
|
4829
5379
|
};
|
|
4830
5380
|
}
|
|
4831
|
-
async function reachabilityCtx(cwd, files,
|
|
5381
|
+
async function reachabilityCtx(cwd, files, graph2) {
|
|
4832
5382
|
const prodSources = /* @__PURE__ */ new Map();
|
|
4833
5383
|
for (const rel of files) {
|
|
4834
5384
|
if (!/\.[cm]?[jt]sx?$/.test(rel)) continue;
|
|
@@ -4838,7 +5388,7 @@ async function reachabilityCtx(cwd, files, graph) {
|
|
|
4838
5388
|
} catch {
|
|
4839
5389
|
}
|
|
4840
5390
|
}
|
|
4841
|
-
return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
|
|
5391
|
+
return { productionUsage: indexPackageUsage(prodSources), graph: graph2, sources: prodSources };
|
|
4842
5392
|
}
|
|
4843
5393
|
|
|
4844
5394
|
// src/modules/mcp/index.ts
|
|
@@ -5213,67 +5763,200 @@ init_finding();
|
|
|
5213
5763
|
// src/modules/sast/interproc.ts
|
|
5214
5764
|
init_finding();
|
|
5215
5765
|
|
|
5216
|
-
// src/
|
|
5217
|
-
var
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
var HONO_READERS = /* @__PURE__ */ new Set(["query", "param", "header", "json", "parseBody", "text"]);
|
|
5222
|
-
function decoratorExpr(dec) {
|
|
5223
|
-
if (dec.type !== "Decorator") return void 0;
|
|
5224
|
-
return dec.expression;
|
|
5225
|
-
}
|
|
5226
|
-
function decoratorName(dec) {
|
|
5227
|
-
const expr = decoratorExpr(dec);
|
|
5228
|
-
if (expr === void 0) return void 0;
|
|
5229
|
-
if (expr.type === "Identifier") return expr.name;
|
|
5230
|
-
if (expr.type === "CallExpression") return calleeName(expr);
|
|
5231
|
-
return void 0;
|
|
5232
|
-
}
|
|
5233
|
-
function nodeDecorators(node) {
|
|
5234
|
-
const decs = node.decorators;
|
|
5235
|
-
return Array.isArray(decs) ? decs : [];
|
|
5236
|
-
}
|
|
5237
|
-
function nestInputDecorator(param) {
|
|
5238
|
-
if (!frameworksEnabled()) return void 0;
|
|
5239
|
-
for (const dec of nodeDecorators(param)) {
|
|
5240
|
-
const name = decoratorName(dec);
|
|
5241
|
-
if (name !== void 0 && NEST_INPUT.test(name)) return name;
|
|
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");
|
|
5242
5771
|
}
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
if (decoratorName(dec) !== name) continue;
|
|
5248
|
-
const expr = decoratorExpr(dec);
|
|
5249
|
-
if (expr?.type === "CallExpression") return expr.arguments.length;
|
|
5250
|
-
return 0;
|
|
5772
|
+
max;
|
|
5773
|
+
map = /* @__PURE__ */ new Map();
|
|
5774
|
+
get size() {
|
|
5775
|
+
return this.map.size;
|
|
5251
5776
|
}
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
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)$/;
|
|
5902
|
+
var NEST_INPUT = /^(Body|Query|Param|Headers)$/;
|
|
5903
|
+
var REQ_READERS = /* @__PURE__ */ new Set(["json", "formData", "text", "arrayBuffer"]);
|
|
5904
|
+
var HONO_READERS = /* @__PURE__ */ new Set(["query", "param", "header", "json", "parseBody", "text"]);
|
|
5905
|
+
function decoratorExpr(dec) {
|
|
5906
|
+
if (dec.type !== "Decorator") return void 0;
|
|
5907
|
+
return dec.expression;
|
|
5908
|
+
}
|
|
5909
|
+
function decoratorName(dec) {
|
|
5910
|
+
const expr = decoratorExpr(dec);
|
|
5911
|
+
if (expr === void 0) return void 0;
|
|
5912
|
+
if (expr.type === "Identifier") return expr.name;
|
|
5913
|
+
if (expr.type === "CallExpression") return calleeName(expr);
|
|
5914
|
+
return void 0;
|
|
5915
|
+
}
|
|
5916
|
+
function nodeDecorators(node) {
|
|
5917
|
+
const decs = node.decorators;
|
|
5918
|
+
return Array.isArray(decs) ? decs : [];
|
|
5919
|
+
}
|
|
5920
|
+
function nestInputDecorator(param) {
|
|
5921
|
+
if (!frameworksEnabled()) return void 0;
|
|
5922
|
+
for (const dec of nodeDecorators(param)) {
|
|
5923
|
+
const name = decoratorName(dec);
|
|
5924
|
+
if (name !== void 0 && NEST_INPUT.test(name)) return name;
|
|
5925
|
+
}
|
|
5926
|
+
return void 0;
|
|
5927
|
+
}
|
|
5928
|
+
function nestDecoratorArgCount(param, name) {
|
|
5929
|
+
for (const dec of nodeDecorators(param)) {
|
|
5930
|
+
if (decoratorName(dec) !== name) continue;
|
|
5931
|
+
const expr = decoratorExpr(dec);
|
|
5932
|
+
if (expr?.type === "CallExpression") return expr.arguments.length;
|
|
5933
|
+
return 0;
|
|
5934
|
+
}
|
|
5935
|
+
return 0;
|
|
5936
|
+
}
|
|
5937
|
+
function isCtxIdent(node) {
|
|
5938
|
+
return node.type === "Identifier" && node.name === "ctx";
|
|
5939
|
+
}
|
|
5940
|
+
function isCReq(node) {
|
|
5941
|
+
return node.type === "MemberExpression" && node.object.type === "Identifier" && node.object.name === "c" && propName(node) === "req";
|
|
5942
|
+
}
|
|
5943
|
+
function isCtxRequest(node) {
|
|
5944
|
+
return node.type === "MemberExpression" && isCtxIdent(node.object) && propName(node) === "request";
|
|
5945
|
+
}
|
|
5946
|
+
function isFrameworkMemberTaint(node) {
|
|
5947
|
+
if (!frameworksEnabled() || node.type !== "MemberExpression") return false;
|
|
5948
|
+
const name = propName(node);
|
|
5949
|
+
if (name === void 0) return false;
|
|
5950
|
+
if (isCtxIdent(node.object) && (REQUEST_SOURCE_PROPS.has(name) || name === "query")) return true;
|
|
5951
|
+
if (isCtxRequest(node.object) && REQUEST_SOURCE_PROPS.has(name)) return true;
|
|
5952
|
+
return false;
|
|
5953
|
+
}
|
|
5954
|
+
function isFrameworkTaintCall(node) {
|
|
5955
|
+
if (!frameworksEnabled() || node.type !== "CallExpression") return false;
|
|
5956
|
+
const name = calleeName(node);
|
|
5957
|
+
if (name === void 0 || node.callee.type !== "MemberExpression") return false;
|
|
5958
|
+
const obj = node.callee.object;
|
|
5959
|
+
if (obj.type === "Identifier" && obj.name === "request" && REQ_READERS.has(name)) return true;
|
|
5277
5960
|
if (obj.type === "Identifier" && (obj.name === "formData" || obj.name === "searchParams") && name === "get") {
|
|
5278
5961
|
return true;
|
|
5279
5962
|
}
|
|
@@ -5348,6 +6031,10 @@ function isMcpArgsTaintSource(node) {
|
|
|
5348
6031
|
}
|
|
5349
6032
|
|
|
5350
6033
|
// src/modules/sast/taint.ts
|
|
6034
|
+
var customTaint;
|
|
6035
|
+
function setCustomTaint(config) {
|
|
6036
|
+
customTaint = config;
|
|
6037
|
+
}
|
|
5351
6038
|
var SCHEMA_ROOT = /^(z|zod|joi|yup|schema)$/i;
|
|
5352
6039
|
function objectChainMentions(node, pattern) {
|
|
5353
6040
|
let current = node;
|
|
@@ -5493,6 +6180,61 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
|
5493
6180
|
return false;
|
|
5494
6181
|
}
|
|
5495
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
|
+
}
|
|
5496
6238
|
function exprIsSanitized(node, tainted, names, isCall, returners = NO_RETURNERS) {
|
|
5497
6239
|
if (node === void 0) return true;
|
|
5498
6240
|
if (isCall(node)) return true;
|
|
@@ -5556,7 +6298,7 @@ function seedMcpToolParams(scope, tainted) {
|
|
|
5556
6298
|
for (const name of boundNames(param)) tainted.add(name);
|
|
5557
6299
|
}
|
|
5558
6300
|
}
|
|
5559
|
-
function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
6301
|
+
function analyzeScope(scope, seed = [], returners = NO_RETURNERS, filterReturners = NO_RETURNERS, initialBuilt = NO_RETURNERS) {
|
|
5560
6302
|
const tainted = new Set(seed);
|
|
5561
6303
|
seedDestructuredRequestParams(scope, tainted);
|
|
5562
6304
|
seedFrameworkParams(scope, tainted);
|
|
@@ -5564,6 +6306,8 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5564
6306
|
const urlSanitized = /* @__PURE__ */ new Set();
|
|
5565
6307
|
const xssSanitized = /* @__PURE__ */ new Set();
|
|
5566
6308
|
const nosqlSanitized = /* @__PURE__ */ new Set();
|
|
6309
|
+
const sqlCleared = /* @__PURE__ */ new Set();
|
|
6310
|
+
const customPath = /* @__PURE__ */ new Set();
|
|
5567
6311
|
let changed = true;
|
|
5568
6312
|
while (changed) {
|
|
5569
6313
|
changed = false;
|
|
@@ -5574,6 +6318,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5574
6318
|
for (const name of boundNames(node.id)) nosqlSanitized.add(name);
|
|
5575
6319
|
}
|
|
5576
6320
|
if (isSchemaValidatorCall(init)) return;
|
|
6321
|
+
applyCustomCall(init, boundNames(node.id), tainted, sqlCleared, customPath);
|
|
5577
6322
|
if (isUrlSanitizerCall(init)) {
|
|
5578
6323
|
for (const name of boundNames(node.id)) urlSanitized.add(name);
|
|
5579
6324
|
}
|
|
@@ -5594,6 +6339,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5594
6339
|
for (const name of boundNames(node.left)) nosqlSanitized.add(name);
|
|
5595
6340
|
}
|
|
5596
6341
|
if (isSchemaValidatorCall(node.right)) return;
|
|
6342
|
+
applyCustomCall(node.right, boundNames(node.left), tainted, sqlCleared, customPath);
|
|
5597
6343
|
if (isUrlSanitizerCall(node.right)) {
|
|
5598
6344
|
for (const name of boundNames(node.left)) urlSanitized.add(name);
|
|
5599
6345
|
}
|
|
@@ -5620,7 +6366,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5620
6366
|
for (const name of boundNames(node.left)) resolved.add(name);
|
|
5621
6367
|
}
|
|
5622
6368
|
});
|
|
5623
|
-
const pathSanitized =
|
|
6369
|
+
const pathSanitized = new Set(customPath);
|
|
5624
6370
|
walkScope(scope, (node) => {
|
|
5625
6371
|
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
5626
6372
|
if (propName(node.callee) !== "startsWith") return;
|
|
@@ -5662,17 +6408,92 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5662
6408
|
for (const name of boundNames(node.left)) requestObjects.add(name);
|
|
5663
6409
|
}
|
|
5664
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
|
+
}
|
|
5665
6437
|
return {
|
|
5666
6438
|
tainted,
|
|
5667
6439
|
pathSanitized,
|
|
6440
|
+
sqlCleared,
|
|
5668
6441
|
urlSanitized,
|
|
5669
6442
|
xssSanitized,
|
|
5670
6443
|
nosqlSanitized,
|
|
5671
6444
|
hasPasswordName,
|
|
5672
6445
|
requestObjects,
|
|
6446
|
+
builtFilters,
|
|
6447
|
+
filterReturners: new Set(filterReturners),
|
|
5673
6448
|
returners: new Set(returners)
|
|
5674
6449
|
};
|
|
5675
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
|
+
}
|
|
5676
6497
|
function functionName(fn) {
|
|
5677
6498
|
if (fn.type === "FunctionDeclaration" && fn.id !== null) return fn.id.name;
|
|
5678
6499
|
if (isFunctionNode(fn) && fn.id !== null && fn.id.type === "Identifier") return fn.id.name;
|
|
@@ -5762,11 +6583,12 @@ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
|
5762
6583
|
const seeds = /* @__PURE__ */ new Map();
|
|
5763
6584
|
for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
|
|
5764
6585
|
const returners = new Set(extraReturners2);
|
|
6586
|
+
const filterReturners = /* @__PURE__ */ new Set();
|
|
5765
6587
|
for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
|
|
5766
6588
|
let changed = false;
|
|
5767
|
-
facts.set(ast, analyzeScope(ast, [], returners));
|
|
6589
|
+
facts.set(ast, analyzeScope(ast, [], returners, filterReturners));
|
|
5768
6590
|
for (const fn of functions) {
|
|
5769
|
-
facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners));
|
|
6591
|
+
facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners, filterReturners, inheritedBuiltFilters(fn, facts)));
|
|
5770
6592
|
}
|
|
5771
6593
|
for (const fn of functions) {
|
|
5772
6594
|
const name = functionName(fn);
|
|
@@ -5776,6 +6598,10 @@ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
|
5776
6598
|
returners.add(name);
|
|
5777
6599
|
changed = true;
|
|
5778
6600
|
}
|
|
6601
|
+
if (scopeReturnsBuiltFilter(fn, f) && !filterReturners.has(name)) {
|
|
6602
|
+
filterReturners.add(name);
|
|
6603
|
+
changed = true;
|
|
6604
|
+
}
|
|
5779
6605
|
}
|
|
5780
6606
|
for (const fn of functions) {
|
|
5781
6607
|
const f = facts.get(fn);
|
|
@@ -5808,6 +6634,54 @@ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
|
5808
6634
|
}
|
|
5809
6635
|
return facts;
|
|
5810
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
|
+
}
|
|
5811
6685
|
function functionsThatReturnTaint(ast) {
|
|
5812
6686
|
attachParents(ast);
|
|
5813
6687
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -5829,18 +6703,50 @@ function factsFor(node, map) {
|
|
|
5829
6703
|
return map.get(enclosingScope(node)) ?? {
|
|
5830
6704
|
tainted: /* @__PURE__ */ new Set(),
|
|
5831
6705
|
pathSanitized: /* @__PURE__ */ new Set(),
|
|
6706
|
+
sqlCleared: /* @__PURE__ */ new Set(),
|
|
5832
6707
|
urlSanitized: /* @__PURE__ */ new Set(),
|
|
5833
6708
|
xssSanitized: /* @__PURE__ */ new Set(),
|
|
5834
6709
|
nosqlSanitized: /* @__PURE__ */ new Set(),
|
|
5835
6710
|
hasPasswordName: false,
|
|
5836
6711
|
requestObjects: /* @__PURE__ */ new Set(),
|
|
6712
|
+
builtFilters: /* @__PURE__ */ new Set(),
|
|
6713
|
+
filterReturners: /* @__PURE__ */ new Set(),
|
|
5837
6714
|
returners: /* @__PURE__ */ new Set()
|
|
5838
6715
|
};
|
|
5839
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
|
+
}
|
|
5840
6726
|
function nodeIsTainted(node, map) {
|
|
5841
6727
|
const facts = factsFor(node, map);
|
|
5842
6728
|
return exprIsTainted(node, facts.tainted, facts.returners);
|
|
5843
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
|
+
}
|
|
5844
6750
|
function nodeIsPathSanitized(node, map) {
|
|
5845
6751
|
if (node.type !== "Identifier") return false;
|
|
5846
6752
|
return factsFor(node, map).pathSanitized.has(node.name);
|
|
@@ -5863,6 +6769,147 @@ function nodeIsNosqlSanitized(node, map) {
|
|
|
5863
6769
|
return false;
|
|
5864
6770
|
}
|
|
5865
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
|
+
|
|
5866
6913
|
// src/modules/sast/interproc.ts
|
|
5867
6914
|
var MAX_HOPS = 5;
|
|
5868
6915
|
function collectFunctions(file, ast) {
|
|
@@ -5973,22 +7020,22 @@ function collectInstances(ast, ns) {
|
|
|
5973
7020
|
if (node.type !== "VariableDeclarator" || node.init === null) return;
|
|
5974
7021
|
if (node.id.type !== "Identifier" || node.init.type !== "NewExpression") return;
|
|
5975
7022
|
if (node.init.callee.type !== "Identifier") return;
|
|
5976
|
-
const
|
|
5977
|
-
if (
|
|
5978
|
-
instances.set(node.id.name,
|
|
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);
|
|
5979
7026
|
});
|
|
5980
7027
|
return instances;
|
|
5981
7028
|
}
|
|
5982
7029
|
function resolveCallee(node, ns, instances, currentFile, functions) {
|
|
5983
7030
|
if (node.type !== "CallExpression") return void 0;
|
|
5984
7031
|
if (node.callee.type === "Identifier") {
|
|
5985
|
-
const
|
|
5986
|
-
if (
|
|
5987
|
-
if (
|
|
5988
|
-
const [file, name] =
|
|
7032
|
+
const hit2 = ns.get(node.callee.name);
|
|
7033
|
+
if (hit2 !== void 0) {
|
|
7034
|
+
if (hit2.includes("#")) {
|
|
7035
|
+
const [file, name] = hit2.split("#");
|
|
5989
7036
|
if (file !== void 0 && name !== void 0) return { file, name };
|
|
5990
7037
|
}
|
|
5991
|
-
return { file:
|
|
7038
|
+
return { file: hit2, name: node.callee.name };
|
|
5992
7039
|
}
|
|
5993
7040
|
if (functions.has(`${currentFile}#${node.callee.name}`)) {
|
|
5994
7041
|
return { file: currentFile, name: node.callee.name };
|
|
@@ -6095,9 +7142,9 @@ function runInterproc(parsed) {
|
|
|
6095
7142
|
if (fn === void 0) continue;
|
|
6096
7143
|
const param = fn.params[seed.paramIndex];
|
|
6097
7144
|
if (param === void 0) continue;
|
|
6098
|
-
const facts = analyzeFunctionWithSeed(fn.node, [param]);
|
|
6099
7145
|
const p = byFile.get(fn.file);
|
|
6100
7146
|
if (p === void 0) continue;
|
|
7147
|
+
const facts = taintSummariesEnabled() ? cachedFunctionFacts(p.source, fn.node, [param]) : analyzeFunctionWithSeed(fn.node, [param]);
|
|
6101
7148
|
const ns = requires.get(fn.file) ?? /* @__PURE__ */ new Map();
|
|
6102
7149
|
const inst = instances.get(fn.file) ?? /* @__PURE__ */ new Map();
|
|
6103
7150
|
walk(fn.node, (node) => {
|
|
@@ -6520,6 +7567,178 @@ var jwtRule = {
|
|
|
6520
7567
|
}
|
|
6521
7568
|
};
|
|
6522
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
|
+
|
|
6523
7742
|
// src/modules/sast/rules/nosqli.ts
|
|
6524
7743
|
var MONGO_SINKS = /* @__PURE__ */ new Set([
|
|
6525
7744
|
"find",
|
|
@@ -6604,6 +7823,7 @@ function isNosqlSafeValue(node, source, isNosqlSanitized) {
|
|
|
6604
7823
|
if (isNosqlSanitized(node) || isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) {
|
|
6605
7824
|
return true;
|
|
6606
7825
|
}
|
|
7826
|
+
if (isValidatedRouteField(node)) return true;
|
|
6607
7827
|
return isSanitizeWrap(node, source);
|
|
6608
7828
|
}
|
|
6609
7829
|
function objectHasUnsafeTaintedValue(node, isTainted, isSafe) {
|
|
@@ -6667,6 +7887,14 @@ var nosqliRule = {
|
|
|
6667
7887
|
return;
|
|
6668
7888
|
}
|
|
6669
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
|
+
}
|
|
6670
7898
|
if (objectHasUnsafeTaintedValue(filter, (n) => ctx.isTainted(n), isSafe)) {
|
|
6671
7899
|
ctx.report({
|
|
6672
7900
|
node,
|
|
@@ -6843,7 +8071,7 @@ var sqliRule = {
|
|
|
6843
8071
|
CallExpression(node) {
|
|
6844
8072
|
if (node.type !== "CallExpression" || !isSqlSink2(node)) return;
|
|
6845
8073
|
const sql = node.arguments[0];
|
|
6846
|
-
if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql)) {
|
|
8074
|
+
if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql) && !ctx.isSqlCleared(sql)) {
|
|
6847
8075
|
ctx.report({
|
|
6848
8076
|
node,
|
|
6849
8077
|
message: "Tainted input is concatenated into a SQL string passed to query/execute."
|
|
@@ -6993,27 +8221,27 @@ function numberLiteral(node) {
|
|
|
6993
8221
|
return node.value;
|
|
6994
8222
|
}
|
|
6995
8223
|
function containsIdentMember(node, objectName) {
|
|
6996
|
-
let
|
|
8224
|
+
let hit2 = false;
|
|
6997
8225
|
walk(node, (child) => {
|
|
6998
8226
|
if (child.type === "MemberExpression" && child.object.type === "Identifier" && child.object.name === objectName) {
|
|
6999
|
-
|
|
8227
|
+
hit2 = true;
|
|
7000
8228
|
}
|
|
7001
8229
|
});
|
|
7002
|
-
return
|
|
8230
|
+
return hit2;
|
|
7003
8231
|
}
|
|
7004
8232
|
function containsCallNamed(node, name) {
|
|
7005
|
-
let
|
|
8233
|
+
let hit2 = false;
|
|
7006
8234
|
walk(node, (child) => {
|
|
7007
|
-
if (child.type === "CallExpression" && calleeName(child) === name)
|
|
8235
|
+
if (child.type === "CallExpression" && calleeName(child) === name) hit2 = true;
|
|
7008
8236
|
});
|
|
7009
|
-
return
|
|
8237
|
+
return hit2;
|
|
7010
8238
|
}
|
|
7011
8239
|
function containsOriginalName(node) {
|
|
7012
|
-
let
|
|
8240
|
+
let hit2 = false;
|
|
7013
8241
|
walk(node, (child) => {
|
|
7014
|
-
if (child.type === "MemberExpression" && propName(child) === "originalname")
|
|
8242
|
+
if (child.type === "MemberExpression" && propName(child) === "originalname") hit2 = true;
|
|
7015
8243
|
});
|
|
7016
|
-
return
|
|
8244
|
+
return hit2;
|
|
7017
8245
|
}
|
|
7018
8246
|
function schemaLooksSafe(schema) {
|
|
7019
8247
|
if (schema === void 0) return false;
|
|
@@ -7580,188 +8808,58 @@ function reportGqlServer(ctx, node, optsArg, mentionsProduction) {
|
|
|
7580
8808
|
const opts = optsArg !== void 0 && optsArg.type !== "SpreadElement" ? optsArg : void 0;
|
|
7581
8809
|
const introspection = objectProp2(opts, "introspection");
|
|
7582
8810
|
const rules = objectProp2(opts, "validationRules");
|
|
7583
|
-
const hasDepth = rules !== void 0 && containsCallNamed(rules, "depthLimit") || objectProp2(opts, "depthLimit") !== void 0 || /depthLimit|createComplexity|queryComplexity|costAnalysis/.test(ctx.source);
|
|
7584
|
-
const introOn = boolLiteral(introspection) === true;
|
|
7585
|
-
if (introOn && mentionsProduction) {
|
|
7586
|
-
ctx.report({ node, message: "GraphQL introspection is enabled in production." });
|
|
7587
|
-
}
|
|
7588
|
-
if (!hasDepth) {
|
|
7589
|
-
ctx.report({ node, message: "GraphQL server has no depth or complexity limit." });
|
|
7590
|
-
}
|
|
7591
|
-
}
|
|
7592
|
-
var phase29Rules = [
|
|
7593
|
-
gated(redosRule),
|
|
7594
|
-
gated(deserRule),
|
|
7595
|
-
gated(xxeRule),
|
|
7596
|
-
gated(uploadRule),
|
|
7597
|
-
gated(cookieRule),
|
|
7598
|
-
gated(csrfRule),
|
|
7599
|
-
gated(massAssignRule),
|
|
7600
|
-
gated(timingRule),
|
|
7601
|
-
gated(hashRule),
|
|
7602
|
-
gated(gqlRule)
|
|
7603
|
-
];
|
|
7604
|
-
|
|
7605
|
-
// src/ai/injection.ts
|
|
7606
|
-
init_finding();
|
|
7607
|
-
var PHRASES = [/ignore previous/i, /mark as safe/i, /ai reviewer/i];
|
|
7608
|
-
function isCommentLine(line) {
|
|
7609
|
-
const trimmed = line.trim();
|
|
7610
|
-
return trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*");
|
|
7611
|
-
}
|
|
7612
|
-
function scanPromptInjection(file, source) {
|
|
7613
|
-
const findings = [];
|
|
7614
|
-
const lines = source.split(/\r?\n/);
|
|
7615
|
-
lines.forEach((line, index) => {
|
|
7616
|
-
if (!isCommentLine(line)) return;
|
|
7617
|
-
if (!PHRASES.some((p) => p.test(line))) return;
|
|
7618
|
-
findings.push({
|
|
7619
|
-
id: makeFindingId("AI-INJECT-001", file, `${index + 1}|${line.trim()}`),
|
|
7620
|
-
ruleId: "AI-INJECT-001",
|
|
7621
|
-
module: "sast",
|
|
7622
|
-
title: "Prompt-injection comment aimed at AI reviewers",
|
|
7623
|
-
severity: "medium",
|
|
7624
|
-
confidence: "high",
|
|
7625
|
-
file,
|
|
7626
|
-
line: index + 1,
|
|
7627
|
-
snippet: line.trim().slice(0, 120),
|
|
7628
|
-
message: "Code contains text attempting to manipulate AI reviewers.",
|
|
7629
|
-
remediation: "Remove comments that try to instruct an AI reviewer.",
|
|
7630
|
-
references: ["AI-INJECT-001"],
|
|
7631
|
-
source: "deterministic"
|
|
7632
|
-
});
|
|
7633
|
-
});
|
|
7634
|
-
return findings;
|
|
7635
|
-
}
|
|
7636
|
-
|
|
7637
|
-
// src/runtime/lru.ts
|
|
7638
|
-
var LruMap = class {
|
|
7639
|
-
constructor(max) {
|
|
7640
|
-
this.max = max;
|
|
7641
|
-
if (max < 1) throw new Error("LruMap max must be >= 1");
|
|
7642
|
-
}
|
|
7643
|
-
max;
|
|
7644
|
-
map = /* @__PURE__ */ new Map();
|
|
7645
|
-
get size() {
|
|
7646
|
-
return this.map.size;
|
|
7647
|
-
}
|
|
7648
|
-
has(key) {
|
|
7649
|
-
return this.map.has(key);
|
|
7650
|
-
}
|
|
7651
|
-
get(key) {
|
|
7652
|
-
if (!this.map.has(key)) return void 0;
|
|
7653
|
-
const value = this.map.get(key);
|
|
7654
|
-
this.map.delete(key);
|
|
7655
|
-
this.map.set(key, value);
|
|
7656
|
-
return value;
|
|
7657
|
-
}
|
|
7658
|
-
set(key, value) {
|
|
7659
|
-
if (this.map.has(key)) this.map.delete(key);
|
|
7660
|
-
this.map.set(key, value);
|
|
7661
|
-
while (this.map.size > this.max) {
|
|
7662
|
-
const oldest = this.map.keys().next().value;
|
|
7663
|
-
if (oldest === void 0) break;
|
|
7664
|
-
this.map.delete(oldest);
|
|
7665
|
-
}
|
|
7666
|
-
return this;
|
|
7667
|
-
}
|
|
7668
|
-
};
|
|
7669
|
-
function packageVersion() {
|
|
7670
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7671
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
7672
|
-
try {
|
|
7673
|
-
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
7674
|
-
const pkg = JSON.parse(raw);
|
|
7675
|
-
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
7676
|
-
return pkg.version;
|
|
7677
|
-
}
|
|
7678
|
-
} catch {
|
|
7679
|
-
}
|
|
7680
|
-
const parent = dirname(dir);
|
|
7681
|
-
if (parent === dir) break;
|
|
7682
|
-
dir = parent;
|
|
7683
|
-
}
|
|
7684
|
-
throw new Error("fullshield package.json not found");
|
|
7685
|
-
}
|
|
7686
|
-
|
|
7687
|
-
// src/core/parse-pool.ts
|
|
7688
|
-
var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7689
|
-
var cache = new LruMap(64);
|
|
7690
|
-
function parsePoolEnabled() {
|
|
7691
|
-
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
7692
|
-
}
|
|
7693
|
-
function parsePoolSize() {
|
|
7694
|
-
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
7695
|
-
if (raw !== void 0 && raw !== "") {
|
|
7696
|
-
const parsed = Number(raw);
|
|
7697
|
-
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
|
|
8811
|
+
const hasDepth = rules !== void 0 && containsCallNamed(rules, "depthLimit") || objectProp2(opts, "depthLimit") !== void 0 || /depthLimit|createComplexity|queryComplexity|costAnalysis/.test(ctx.source);
|
|
8812
|
+
const introOn = boolLiteral(introspection) === true;
|
|
8813
|
+
if (introOn && mentionsProduction) {
|
|
8814
|
+
ctx.report({ node, message: "GraphQL introspection is enabled in production." });
|
|
8815
|
+
}
|
|
8816
|
+
if (!hasDepth) {
|
|
8817
|
+
ctx.report({ node, message: "GraphQL server has no depth or complexity limit." });
|
|
7698
8818
|
}
|
|
7699
|
-
return Math.min(2, Math.max(1, availableParallelism() - 1));
|
|
7700
|
-
}
|
|
7701
|
-
function workerMemoryMb() {
|
|
7702
|
-
const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
|
|
7703
|
-
const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
|
|
7704
|
-
if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
|
|
7705
|
-
return 512;
|
|
7706
8819
|
}
|
|
7707
|
-
|
|
7708
|
-
|
|
8820
|
+
var phase29Rules = [
|
|
8821
|
+
gated(redosRule),
|
|
8822
|
+
gated(deserRule),
|
|
8823
|
+
gated(xxeRule),
|
|
8824
|
+
gated(uploadRule),
|
|
8825
|
+
gated(cookieRule),
|
|
8826
|
+
gated(csrfRule),
|
|
8827
|
+
gated(massAssignRule),
|
|
8828
|
+
gated(timingRule),
|
|
8829
|
+
gated(hashRule),
|
|
8830
|
+
gated(gqlRule)
|
|
8831
|
+
];
|
|
8832
|
+
|
|
8833
|
+
// src/ai/injection.ts
|
|
8834
|
+
init_finding();
|
|
8835
|
+
var PHRASES = [/ignore previous/i, /mark as safe/i, /ai reviewer/i];
|
|
8836
|
+
function isCommentLine(line) {
|
|
8837
|
+
const trimmed = line.trim();
|
|
8838
|
+
return trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*");
|
|
7709
8839
|
}
|
|
7710
|
-
function
|
|
7711
|
-
const
|
|
7712
|
-
const
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
const fail = (error) => {
|
|
7731
|
-
if (settled) return;
|
|
7732
|
-
settled = true;
|
|
7733
|
-
finish();
|
|
7734
|
-
reject(error);
|
|
7735
|
-
};
|
|
7736
|
-
worker.once("message", (ast) => {
|
|
7737
|
-
succeed(ast);
|
|
7738
|
-
});
|
|
7739
|
-
worker.once("error", (error) => {
|
|
7740
|
-
fail(error);
|
|
7741
|
-
});
|
|
7742
|
-
worker.once("exit", (code2) => {
|
|
7743
|
-
if (settled) return;
|
|
7744
|
-
fail(new Error(`parse worker exited ${code2 ?? "null"}`));
|
|
8840
|
+
function scanPromptInjection(file, source) {
|
|
8841
|
+
const findings = [];
|
|
8842
|
+
const lines = source.split(/\r?\n/);
|
|
8843
|
+
lines.forEach((line, index) => {
|
|
8844
|
+
if (!isCommentLine(line)) return;
|
|
8845
|
+
if (!PHRASES.some((p) => p.test(line))) return;
|
|
8846
|
+
findings.push({
|
|
8847
|
+
id: makeFindingId("AI-INJECT-001", file, `${index + 1}|${line.trim()}`),
|
|
8848
|
+
ruleId: "AI-INJECT-001",
|
|
8849
|
+
module: "sast",
|
|
8850
|
+
title: "Prompt-injection comment aimed at AI reviewers",
|
|
8851
|
+
severity: "medium",
|
|
8852
|
+
confidence: "high",
|
|
8853
|
+
file,
|
|
8854
|
+
line: index + 1,
|
|
8855
|
+
snippet: line.trim().slice(0, 120),
|
|
8856
|
+
message: "Code contains text attempting to manipulate AI reviewers.",
|
|
8857
|
+
remediation: "Remove comments that try to instruct an AI reviewer.",
|
|
8858
|
+
references: ["AI-INJECT-001"],
|
|
8859
|
+
source: "deterministic"
|
|
7745
8860
|
});
|
|
7746
8861
|
});
|
|
7747
|
-
|
|
7748
|
-
async function parseWithPool(codes) {
|
|
7749
|
-
const size = parsePoolSize();
|
|
7750
|
-
const out = new Array(codes.length);
|
|
7751
|
-
let next = 0;
|
|
7752
|
-
async function run() {
|
|
7753
|
-
for (; ; ) {
|
|
7754
|
-
const index = next;
|
|
7755
|
-
next += 1;
|
|
7756
|
-
if (index >= codes.length) return;
|
|
7757
|
-
const code = codes[index];
|
|
7758
|
-
if (code === void 0) return;
|
|
7759
|
-
out[index] = await parseOne(code);
|
|
7760
|
-
}
|
|
7761
|
-
}
|
|
7762
|
-
const workers = Math.min(size, Math.max(1, codes.length));
|
|
7763
|
-
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
7764
|
-
return out;
|
|
8862
|
+
return findings;
|
|
7765
8863
|
}
|
|
7766
8864
|
|
|
7767
8865
|
// src/modules/sast/engine.ts
|
|
@@ -7823,10 +8921,12 @@ function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ *
|
|
|
7823
8921
|
});
|
|
7824
8922
|
},
|
|
7825
8923
|
isTainted: (node) => nodeIsTainted(node, facts),
|
|
8924
|
+
isSqlCleared: (node) => nodeIsSqlCleared(node, facts),
|
|
7826
8925
|
isPathSanitized: (node) => nodeIsPathSanitized(node, facts),
|
|
7827
8926
|
isSanitizedFor: (node, kind) => nodeIsSanitizedFor(node, facts, kind),
|
|
7828
8927
|
isNosqlSanitized: (node) => nodeIsNosqlSanitized(node, facts),
|
|
7829
8928
|
isRequestObject: (node) => nodeIsRequestObject(node, facts),
|
|
8929
|
+
holdsBuiltFilter: (node) => nodeHoldsBuiltFilter(node, facts),
|
|
7830
8930
|
functionHasPasswordName: (node) => factsFor(node, facts).hasPasswordName,
|
|
7831
8931
|
assignedName: (node) => assignedNameOf(node),
|
|
7832
8932
|
projectCsrfProtected
|
|
@@ -7864,10 +8964,12 @@ function findingsFromTemplate(filename, source) {
|
|
|
7864
8964
|
});
|
|
7865
8965
|
},
|
|
7866
8966
|
isTainted: () => false,
|
|
8967
|
+
isSqlCleared: () => false,
|
|
7867
8968
|
isPathSanitized: () => false,
|
|
7868
8969
|
isSanitizedFor: () => false,
|
|
7869
8970
|
isNosqlSanitized: () => false,
|
|
7870
8971
|
isRequestObject: () => false,
|
|
8972
|
+
holdsBuiltFilter: () => false,
|
|
7871
8973
|
functionHasPasswordName: () => false,
|
|
7872
8974
|
assignedName: () => void 0,
|
|
7873
8975
|
projectCsrfProtected: false
|
|
@@ -7999,7 +9101,7 @@ async function runSastStreaming(ctx) {
|
|
|
7999
9101
|
}
|
|
8000
9102
|
const ast = await parseCode(source);
|
|
8001
9103
|
if (ast === void 0) continue;
|
|
8002
|
-
returning.set(rel, functionsThatReturnTaint(ast));
|
|
9104
|
+
returning.set(rel, taintSummariesEnabled() ? cachedReturners(source, ast) : functionsThatReturnTaint(ast));
|
|
8003
9105
|
parseable.push(rel);
|
|
8004
9106
|
fileSet.add(rel);
|
|
8005
9107
|
}
|
|
@@ -8030,48 +9132,69 @@ async function runSastStreaming(ctx) {
|
|
|
8030
9132
|
var sastModule = {
|
|
8031
9133
|
name: "sast",
|
|
8032
9134
|
async run(ctx) {
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
continue;
|
|
8045
|
-
}
|
|
8046
|
-
if (TEMPLATE_EXT.test(rel)) {
|
|
8047
|
-
findings.push(...findingsFromTemplate(rel, source));
|
|
8048
|
-
continue;
|
|
8049
|
-
}
|
|
8050
|
-
const ast = await parseCode(source);
|
|
8051
|
-
if (ast === void 0) continue;
|
|
8052
|
-
parsed.push({ file: rel, source, ast });
|
|
8053
|
-
}
|
|
8054
|
-
const files = new Set(parsed.map((p) => p.file));
|
|
8055
|
-
const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
|
|
8056
|
-
const returning = /* @__PURE__ */ new Map();
|
|
8057
|
-
for (const entry of parsed) {
|
|
8058
|
-
returning.set(entry.file, functionsThatReturnTaint(entry.ast));
|
|
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);
|
|
8059
9146
|
}
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
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);
|
|
9167
|
+
if (ast === void 0) continue;
|
|
9168
|
+
parsed.push({ file: rel, source, ast });
|
|
9169
|
+
}
|
|
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(
|
|
8063
9175
|
entry.file,
|
|
8064
|
-
entry.source,
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
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
|
+
);
|
|
9189
|
+
}
|
|
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);
|
|
8070
9197
|
}
|
|
8071
|
-
findings.push(...runInterproc(parsed));
|
|
8072
|
-
const out = dedupeSastSinks(findings);
|
|
8073
|
-
releaseParsed(parsed);
|
|
8074
|
-
return out;
|
|
8075
9198
|
}
|
|
8076
9199
|
};
|
|
8077
9200
|
|
|
@@ -8365,12 +9488,12 @@ ${text}`);
|
|
|
8365
9488
|
const seeds = fixtureLogic(prompt);
|
|
8366
9489
|
const findings = [];
|
|
8367
9490
|
for (const seed of seeds) {
|
|
8368
|
-
const
|
|
9491
|
+
const hit2 = fileTexts.find((f) => f.file.includes(seed.fileHint) && f.text.includes(seed.fileHint === "user" ? "findById" : "Object.assign")) ?? fileTexts.find(
|
|
8369
9492
|
(f) => f.text.includes(seed.ruleId === "AI-IDOR-001" ? "findById(req.params.id)" : "Object.assign(user, req.body)")
|
|
8370
9493
|
);
|
|
8371
|
-
if (
|
|
8372
|
-
const line =
|
|
8373
|
-
findings.push(asAiFinding(seed,
|
|
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));
|
|
8374
9497
|
}
|
|
8375
9498
|
return findings;
|
|
8376
9499
|
}
|
|
@@ -8634,6 +9757,56 @@ var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
|
8634
9757
|
"ai",
|
|
8635
9758
|
"sast"
|
|
8636
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
|
+
}
|
|
8637
9810
|
function validate(raw, source) {
|
|
8638
9811
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
8639
9812
|
throw new ConfigError(`${source}: config must be a JSON object`);
|
|
@@ -8759,6 +9932,16 @@ function validate(raw, source) {
|
|
|
8759
9932
|
}
|
|
8760
9933
|
sast.maxFileBytes = rawSast.maxFileBytes;
|
|
8761
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
|
+
}
|
|
8762
9945
|
out.sast = sast;
|
|
8763
9946
|
}
|
|
8764
9947
|
return out;
|
|
@@ -9094,6 +10277,8 @@ function dedupe2(findings) {
|
|
|
9094
10277
|
async function runScan(options = {}, modules = builtinModules) {
|
|
9095
10278
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9096
10279
|
const t0 = performance.now();
|
|
10280
|
+
depsModule.unusedDirect = void 0;
|
|
10281
|
+
depsModule.upgradePlan = void 0;
|
|
9097
10282
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
9098
10283
|
const stats = await stat(cwd).catch(() => null);
|
|
9099
10284
|
if (stats === null || !stats.isDirectory()) {
|
|
@@ -9192,8 +10377,8 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9192
10377
|
const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
9193
10378
|
const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
9194
10379
|
const secretById = /* @__PURE__ */ new Map();
|
|
9195
|
-
for (const
|
|
9196
|
-
if (
|
|
10380
|
+
for (const hit2 of extraHits) {
|
|
10381
|
+
if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
|
|
9197
10382
|
}
|
|
9198
10383
|
for (const finding3 of findings) {
|
|
9199
10384
|
if (finding3.module !== "secrets" || finding3.file === void 0 || secretById.has(finding3.id)) {
|
|
@@ -9207,8 +10392,8 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9207
10392
|
continue;
|
|
9208
10393
|
}
|
|
9209
10394
|
}
|
|
9210
|
-
for (const
|
|
9211
|
-
if (
|
|
10395
|
+
for (const hit2 of scanTextForSecretHits2(finding3.file, text)) {
|
|
10396
|
+
if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
|
|
9212
10397
|
}
|
|
9213
10398
|
}
|
|
9214
10399
|
findings = sortFindings(
|
|
@@ -9260,7 +10445,9 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9260
10445
|
filesScanned: files.length,
|
|
9261
10446
|
modulesRequested: [...modulesRequested],
|
|
9262
10447
|
modulesRun,
|
|
9263
|
-
...historyNotice !== void 0 ? { history: historyNotice } : {}
|
|
10448
|
+
...historyNotice !== void 0 ? { history: historyNotice } : {},
|
|
10449
|
+
...depsModule.unusedDirect !== void 0 ? { unusedDirect: depsModule.unusedDirect } : {},
|
|
10450
|
+
...depsModule.upgradePlan !== void 0 ? { upgradePlan: depsModule.upgradePlan } : {}
|
|
9264
10451
|
},
|
|
9265
10452
|
config,
|
|
9266
10453
|
policy: policyResult,
|
|
@@ -9474,6 +10661,28 @@ async function runFix(options = {}) {
|
|
|
9474
10661
|
const after = applyUnifiedDiff(source, proposed.diff);
|
|
9475
10662
|
byFile.set(sourceFile, after);
|
|
9476
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
|
+
}
|
|
9477
10686
|
const notice = appliedOverride ? "run npm install" : void 0;
|
|
9478
10687
|
if (options.apply !== true) {
|
|
9479
10688
|
return { applied: false, reverted: false, patches, findings: result.findings, notice };
|
|
@@ -9521,6 +10730,29 @@ async function restoreSnapshots(cwd, snapshots) {
|
|
|
9521
10730
|
}
|
|
9522
10731
|
}
|
|
9523
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
|
+
}
|
|
9524
10756
|
function runProjectTests(cwd) {
|
|
9525
10757
|
let pkg;
|
|
9526
10758
|
try {
|
|
@@ -9660,7 +10892,159 @@ function renderFinding(finding3) {
|
|
|
9660
10892
|
}
|
|
9661
10893
|
return lines;
|
|
9662
10894
|
}
|
|
9663
|
-
|
|
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 = {}) {
|
|
9664
11048
|
const result = redactScan(input);
|
|
9665
11049
|
const { findings, summary, metadata } = result;
|
|
9666
11050
|
const lines = [];
|
|
@@ -9670,34 +11054,63 @@ function renderConsoleReport(input) {
|
|
|
9670
11054
|
)
|
|
9671
11055
|
);
|
|
9672
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
|
+
}
|
|
9673
11069
|
if (metadata.modulesRun.length < metadata.modulesRequested.length) {
|
|
9674
11070
|
const pending = metadata.modulesRequested.filter((m) => !metadata.modulesRun.includes(m));
|
|
9675
11071
|
lines.push(
|
|
9676
11072
|
pc.dim(`note: requested modules not implemented yet: ${pending.join(", ")}`)
|
|
9677
11073
|
);
|
|
9678
11074
|
}
|
|
9679
|
-
const
|
|
9680
|
-
const
|
|
9681
|
-
const
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
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;
|
|
9691
11096
|
}
|
|
11097
|
+
lines.push(...renderRow(row));
|
|
9692
11098
|
}
|
|
9693
|
-
if (
|
|
11099
|
+
if (hiddenActionable > 0) {
|
|
9694
11100
|
lines.push("");
|
|
9695
|
-
lines.push(`${
|
|
11101
|
+
lines.push(`${hiddenActionable} more actionable \u2014 use --json`);
|
|
11102
|
+
}
|
|
11103
|
+
if (!showAll && folded.length > 0) {
|
|
11104
|
+
lines.push("");
|
|
11105
|
+
lines.push(
|
|
11106
|
+
`${folded.length} more (not directly reachable, or below high confidence) \u2014 --all or see JSON`
|
|
11107
|
+
);
|
|
9696
11108
|
}
|
|
9697
11109
|
lines.push("");
|
|
9698
11110
|
if (summary.total === 0) {
|
|
9699
11111
|
lines.push(pc.green("\u2714 No findings."));
|
|
9700
11112
|
} else {
|
|
11113
|
+
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
9701
11114
|
const counts = bySeverityDesc.filter((s) => summary.bySeverity[s] > 0).map((s) => `${summary.bySeverity[s]} ${s}`).join(", ");
|
|
9702
11115
|
lines.push(pc.red(`\u2716 ${summary.total} finding(s): ${counts}`));
|
|
9703
11116
|
}
|
|
@@ -9828,6 +11241,14 @@ function renderMarkdownReport(input) {
|
|
|
9828
11241
|
return lines.join("\n");
|
|
9829
11242
|
}
|
|
9830
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
|
+
|
|
9831
11252
|
// src/reporters/sarif.ts
|
|
9832
11253
|
var SCHEMA_URI = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
9833
11254
|
function sarifLevel(severity) {
|
|
@@ -9838,15 +11259,37 @@ function sarifLevel(severity) {
|
|
|
9838
11259
|
function uri(file) {
|
|
9839
11260
|
return (file ?? "").replace(/\\/g, "/");
|
|
9840
11261
|
}
|
|
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
|
+
}
|
|
9841
11280
|
function renderSarifReport(input) {
|
|
9842
11281
|
const result = redactScan(input);
|
|
11282
|
+
const v2 = sarifV2();
|
|
9843
11283
|
const rules = /* @__PURE__ */ new Map();
|
|
9844
11284
|
for (const finding3 of result.findings) {
|
|
9845
11285
|
if (!rules.has(finding3.ruleId)) {
|
|
11286
|
+
const markdown = v2 ? ruleMarkdown(finding3.ruleId) : void 0;
|
|
9846
11287
|
rules.set(finding3.ruleId, {
|
|
9847
11288
|
id: finding3.ruleId,
|
|
9848
11289
|
name: finding3.ruleId,
|
|
9849
|
-
shortDescription: { text: finding3.title }
|
|
11290
|
+
shortDescription: { text: finding3.title },
|
|
11291
|
+
...markdown !== void 0 ? { help: { markdown } } : {},
|
|
11292
|
+
...v2 ? { properties: { precision: "unreviewed" } } : {}
|
|
9850
11293
|
});
|
|
9851
11294
|
}
|
|
9852
11295
|
}
|
|
@@ -9875,6 +11318,7 @@ function renderSarifReport(input) {
|
|
|
9875
11318
|
}
|
|
9876
11319
|
}
|
|
9877
11320
|
] : [];
|
|
11321
|
+
const related = v2 ? relatedLocations(finding3) : void 0;
|
|
9878
11322
|
const codeFlows = finding3.trace !== void 0 && finding3.trace.length > 0 ? [
|
|
9879
11323
|
{
|
|
9880
11324
|
threadFlows: [
|
|
@@ -9897,7 +11341,12 @@ function renderSarifReport(input) {
|
|
|
9897
11341
|
level: sarifLevel(finding3.severity),
|
|
9898
11342
|
message: { text: finding3.message },
|
|
9899
11343
|
locations: location4,
|
|
9900
|
-
...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
|
+
} : {}
|
|
9901
11350
|
};
|
|
9902
11351
|
})
|
|
9903
11352
|
}
|
|
@@ -9946,14 +11395,6 @@ function validateSarif21(doc) {
|
|
|
9946
11395
|
return errors;
|
|
9947
11396
|
}
|
|
9948
11397
|
|
|
9949
|
-
// src/core/fingerprint.ts
|
|
9950
|
-
init_finding();
|
|
9951
|
-
function lineIndependentFingerprint(finding3) {
|
|
9952
|
-
const file = (finding3.file ?? "").replace(/\\/g, "/");
|
|
9953
|
-
const text = (finding3.snippet ?? finding3.message ?? "").replace(/\s+/g, " ").trim();
|
|
9954
|
-
return makeFindingId(finding3.ruleId, file, `${finding3.severity}\0${text}`);
|
|
9955
|
-
}
|
|
9956
|
-
|
|
9957
11398
|
// src/reporters/ci.ts
|
|
9958
11399
|
init_finding();
|
|
9959
11400
|
var MESSAGE_CAP = 4e3;
|
|
@@ -10209,6 +11650,16 @@ function validateCycloneDx15(doc) {
|
|
|
10209
11650
|
if (!Array.isArray(root.components)) errors.push("components must be an array");
|
|
10210
11651
|
return errors;
|
|
10211
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
|
+
}
|
|
10212
11663
|
function renderCycloneDx(input) {
|
|
10213
11664
|
const vulns = (input.findings ?? []).filter((f) => f.ruleId === "DEPS-VULN-001");
|
|
10214
11665
|
return {
|
|
@@ -10219,13 +11670,18 @@ function renderCycloneDx(input) {
|
|
|
10219
11670
|
metadata: {
|
|
10220
11671
|
component: { type: "application", name: input.name, version: input.version }
|
|
10221
11672
|
},
|
|
10222
|
-
components: input.components.map((c) =>
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
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
|
+
}),
|
|
10229
11685
|
vulnerabilities: vulns.map((f) => ({
|
|
10230
11686
|
id: f.references?.find((r) => r.startsWith("GHSA-") || r.startsWith("CVE-")) ?? f.id,
|
|
10231
11687
|
description: f.message,
|
|
@@ -10236,7 +11692,7 @@ function renderCycloneDx(input) {
|
|
|
10236
11692
|
|
|
10237
11693
|
// src/sbom/index.ts
|
|
10238
11694
|
var LOCKFILE_NAMES2 = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
10239
|
-
async function buildSbom(cwd) {
|
|
11695
|
+
async function buildSbom(cwd, options = {}) {
|
|
10240
11696
|
const result = await scan({ cwd, modules: ["deps"] });
|
|
10241
11697
|
let name = "app";
|
|
10242
11698
|
let version = "0.0.0";
|
|
@@ -10262,7 +11718,13 @@ async function buildSbom(cwd) {
|
|
|
10262
11718
|
if (components.length === 0) {
|
|
10263
11719
|
components = exactPinsFromManifest(manifest).map((p) => ({ name: p.name, version: p.version }));
|
|
10264
11720
|
}
|
|
10265
|
-
return renderCycloneDx({
|
|
11721
|
+
return renderCycloneDx({
|
|
11722
|
+
name,
|
|
11723
|
+
version,
|
|
11724
|
+
components,
|
|
11725
|
+
findings: result.findings,
|
|
11726
|
+
includeReach: options.includeReach === true
|
|
11727
|
+
});
|
|
10266
11728
|
}
|
|
10267
11729
|
var OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
|
|
10268
11730
|
var OPENVEX_NOT_AFFECTED_JUSTIFICATION = "vulnerable_code_not_in_execute_path";
|
|
@@ -10297,14 +11759,16 @@ function openVexStatement(finding3) {
|
|
|
10297
11759
|
vulnerability: { name: vulnName(finding3) },
|
|
10298
11760
|
products: [{ "@id": npmPurl(finding3.snippet) }]
|
|
10299
11761
|
};
|
|
11762
|
+
const detail = process.env.FULLSHIELD_EXPERIMENTAL_SBOM_REACH === "1" && finding3.reachabilityEvidence?.reason !== void 0 ? { detail: finding3.reachabilityEvidence.reason } : {};
|
|
10300
11763
|
if (notImported(finding3)) {
|
|
10301
11764
|
return {
|
|
10302
11765
|
...base,
|
|
10303
11766
|
status: "not_affected",
|
|
10304
|
-
justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION
|
|
11767
|
+
justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION,
|
|
11768
|
+
...detail
|
|
10305
11769
|
};
|
|
10306
11770
|
}
|
|
10307
|
-
return { ...base, status: "under_investigation" };
|
|
11771
|
+
return { ...base, status: "under_investigation", ...detail };
|
|
10308
11772
|
}
|
|
10309
11773
|
function buildOpenVex(findings) {
|
|
10310
11774
|
const statements = depFindings(findings).map(openVexStatement);
|