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