fullshield 0.2.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/README.md +10 -5
- package/dist/agent.js +1966 -244
- package/dist/cli/index.js +3159 -381
- package/dist/compliance.js +9 -0
- package/dist/dashboard.js +18 -1
- package/dist/iac.js +48 -0
- package/dist/index.d.ts +121 -7
- package/dist/index.js +2557 -280
- package/dist/mcp.js +1977 -255
- package/dist/supply.js +13 -3
- package/dist/vex.js +4 -2
- package/package.json +1 -1
- package/rules/README.md +2 -0
- package/rules/SAST-SKIP-001.md +9 -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({
|
|
@@ -557,6 +557,7 @@ var init_incremental = __esm({
|
|
|
557
557
|
// src/modules/secrets/history.ts
|
|
558
558
|
var history_exports = {};
|
|
559
559
|
__export(history_exports, {
|
|
560
|
+
historyCommitLimit: () => historyCommitLimit,
|
|
560
561
|
scanGitHistory: () => scanGitHistory
|
|
561
562
|
});
|
|
562
563
|
function gitError(error) {
|
|
@@ -590,16 +591,22 @@ async function readBlob(cwd, hash) {
|
|
|
590
591
|
return void 0;
|
|
591
592
|
}
|
|
592
593
|
}
|
|
593
|
-
function remapHit(
|
|
594
|
-
const snippet =
|
|
595
|
-
const line =
|
|
596
|
-
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"}`);
|
|
597
598
|
return {
|
|
598
|
-
finding: { ...
|
|
599
|
-
secret:
|
|
599
|
+
finding: { ...hit2.finding, id, file },
|
|
600
|
+
secret: hit2.secret
|
|
600
601
|
};
|
|
601
602
|
}
|
|
602
|
-
|
|
603
|
+
function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
|
|
604
|
+
if (raw === void 0 || raw.trim() === "") return 500;
|
|
605
|
+
const parsed = Number(raw);
|
|
606
|
+
if (!Number.isInteger(parsed) || parsed < 1) return 500;
|
|
607
|
+
return Math.min(parsed, 5e3);
|
|
608
|
+
}
|
|
609
|
+
async function streamLogChanges(cwd, maxCommits) {
|
|
603
610
|
const changes = [];
|
|
604
611
|
await gitOk(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
605
612
|
const child = spawn(
|
|
@@ -616,6 +623,8 @@ async function streamLogChanges(cwd) {
|
|
|
616
623
|
"--format=FSCOMMIT:%H",
|
|
617
624
|
"--no-textconv",
|
|
618
625
|
"--no-ext-diff",
|
|
626
|
+
"-n",
|
|
627
|
+
String(maxCommits),
|
|
619
628
|
"HEAD"
|
|
620
629
|
],
|
|
621
630
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -678,14 +687,20 @@ async function headSecretKeys(cwd) {
|
|
|
678
687
|
hits = scanTextForSecretHits(path, text);
|
|
679
688
|
seen.set(hash, hits);
|
|
680
689
|
}
|
|
681
|
-
for (const
|
|
682
|
-
keys.add(`${
|
|
690
|
+
for (const hit2 of hits) {
|
|
691
|
+
keys.add(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
|
|
683
692
|
}
|
|
684
693
|
}
|
|
685
694
|
return keys;
|
|
686
695
|
}
|
|
687
|
-
async function
|
|
688
|
-
const
|
|
696
|
+
async function commitCount(cwd) {
|
|
697
|
+
const raw = await gitOk(cwd, ["rev-list", "--count", "HEAD"]);
|
|
698
|
+
const total = Number(raw.trim());
|
|
699
|
+
return Number.isInteger(total) && total >= 0 ? total : 0;
|
|
700
|
+
}
|
|
701
|
+
async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
702
|
+
const total = await commitCount(cwd);
|
|
703
|
+
const changes = await streamLogChanges(cwd, maxCommits);
|
|
689
704
|
const blobHits = /* @__PURE__ */ new Map();
|
|
690
705
|
const first = /* @__PURE__ */ new Map();
|
|
691
706
|
for (const change of changes) {
|
|
@@ -706,8 +721,8 @@ async function scanGitHistory(cwd) {
|
|
|
706
721
|
hits2 = rawHits.filter((h) => allowed.has(h.finding.id));
|
|
707
722
|
blobHits.set(change.hash, hits2);
|
|
708
723
|
}
|
|
709
|
-
for (const
|
|
710
|
-
const mapped = remapHit(
|
|
724
|
+
for (const hit2 of hits2) {
|
|
725
|
+
const mapped = remapHit(hit2, change.path);
|
|
711
726
|
if (!first.has(mapped.finding.id)) {
|
|
712
727
|
first.set(mapped.finding.id, { hit: mapped, commit: change.commit });
|
|
713
728
|
}
|
|
@@ -715,14 +730,20 @@ async function scanGitHistory(cwd) {
|
|
|
715
730
|
}
|
|
716
731
|
const atHead = await headSecretKeys(cwd);
|
|
717
732
|
const hits = [];
|
|
718
|
-
for (const { hit, commit } of first.values()) {
|
|
719
|
-
const stillAtHead = atHead.has(`${
|
|
733
|
+
for (const { hit: hit2, commit } of first.values()) {
|
|
734
|
+
const stillAtHead = atHead.has(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
|
|
720
735
|
hits.push({
|
|
721
|
-
finding: { ...
|
|
722
|
-
secret:
|
|
736
|
+
finding: { ...hit2.finding, introducedIn: commit, stillAtHead },
|
|
737
|
+
secret: hit2.secret
|
|
723
738
|
});
|
|
724
739
|
}
|
|
725
|
-
|
|
740
|
+
const scanned = Math.min(maxCommits, total);
|
|
741
|
+
const history = total > maxCommits ? `scanned ${scanned} of ${total} commits` : void 0;
|
|
742
|
+
return {
|
|
743
|
+
findings: hits.map((h) => h.finding),
|
|
744
|
+
hits,
|
|
745
|
+
...history !== void 0 ? { history } : {}
|
|
746
|
+
};
|
|
726
747
|
}
|
|
727
748
|
var execFileAsync2, ZERO_HASH, MAX_BLOB, RAW_LINE;
|
|
728
749
|
var init_history = __esm({
|
|
@@ -955,15 +976,15 @@ function providerFor(ruleId, secret) {
|
|
|
955
976
|
async function verifySecretHits(hits, options = {}) {
|
|
956
977
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
957
978
|
const out = [];
|
|
958
|
-
for (const
|
|
959
|
-
const secret =
|
|
979
|
+
for (const hit2 of hits) {
|
|
980
|
+
const secret = hit2.secret;
|
|
960
981
|
if (secret === void 0 || secret.length === 0) {
|
|
961
|
-
out.push({ finding: { ...
|
|
982
|
+
out.push({ finding: { ...hit2.finding, verification: "unknown" }, secret: hit2.secret });
|
|
962
983
|
continue;
|
|
963
984
|
}
|
|
964
|
-
const run = providerFor(
|
|
985
|
+
const run = providerFor(hit2.finding.ruleId, secret);
|
|
965
986
|
const verification = run === void 0 ? "unknown" : await run(secret, fetchImpl);
|
|
966
|
-
out.push({ finding: { ...
|
|
987
|
+
out.push({ finding: { ...hit2.finding, verification }, secret: hit2.secret });
|
|
967
988
|
}
|
|
968
989
|
return out;
|
|
969
990
|
}
|
|
@@ -1070,7 +1091,8 @@ function walk(node, visit) {
|
|
|
1070
1091
|
}
|
|
1071
1092
|
}
|
|
1072
1093
|
function attachParents(node, parent) {
|
|
1073
|
-
|
|
1094
|
+
const current = node;
|
|
1095
|
+
if (parent !== void 0 || current.parent === void 0) current.parent = parent;
|
|
1074
1096
|
for (const key of Object.keys(node)) {
|
|
1075
1097
|
if (SKIP_KEYS.has(String(key))) continue;
|
|
1076
1098
|
const value = node[key];
|
|
@@ -1143,6 +1165,10 @@ function mcpCfgEnabled() {
|
|
|
1143
1165
|
function taintV2Enabled() {
|
|
1144
1166
|
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
1145
1167
|
}
|
|
1168
|
+
var LICENSE_FLAG = "FULLSHIELD_EXPERIMENTAL_LICENSE";
|
|
1169
|
+
function licenseEnabled() {
|
|
1170
|
+
return process.env[LICENSE_FLAG] === "1";
|
|
1171
|
+
}
|
|
1146
1172
|
|
|
1147
1173
|
// src/modules/config/finding.ts
|
|
1148
1174
|
init_finding();
|
|
@@ -1518,6 +1544,169 @@ var configModule = {
|
|
|
1518
1544
|
// src/modules/deps/index.ts
|
|
1519
1545
|
init_finding();
|
|
1520
1546
|
|
|
1547
|
+
// src/modules/deps/lockfile-modern.ts
|
|
1548
|
+
function addEdge(edges, from, to) {
|
|
1549
|
+
if (from.length === 0 || to.length === 0 || from === to) return;
|
|
1550
|
+
const list = edges.get(from) ?? /* @__PURE__ */ new Set();
|
|
1551
|
+
list.add(to);
|
|
1552
|
+
edges.set(from, list);
|
|
1553
|
+
}
|
|
1554
|
+
function graph(packages, edges, directProd, directDev, edgesKnown) {
|
|
1555
|
+
return { packages, edges, directProd, directDev, edgesKnown };
|
|
1556
|
+
}
|
|
1557
|
+
function parseYarnBerry(contents) {
|
|
1558
|
+
const packages = [];
|
|
1559
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1560
|
+
const directProd = /* @__PURE__ */ new Set();
|
|
1561
|
+
const directDev = /* @__PURE__ */ new Set();
|
|
1562
|
+
let current;
|
|
1563
|
+
let inDeps = false;
|
|
1564
|
+
let devBlock = false;
|
|
1565
|
+
for (const raw of contents.split(/\r?\n/)) {
|
|
1566
|
+
if (!raw.startsWith(" ") && raw.trimEnd().endsWith(":")) {
|
|
1567
|
+
const key = raw.slice(0, -1).trim().replace(/^"|"$/g, "");
|
|
1568
|
+
current = key.includes("@workspace:.") ? "" : nameFromBerryKey(key);
|
|
1569
|
+
inDeps = false;
|
|
1570
|
+
devBlock = false;
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1573
|
+
const version = /^\s+version:\s+(.+)$/.exec(raw)?.[1]?.replace(/["']/g, "").trim();
|
|
1574
|
+
if (version !== void 0 && current !== void 0 && current.length > 0 && !inDeps) {
|
|
1575
|
+
packages.push({ name: current, version: versionFromProtocol(version), path: `yarn:${current}` });
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (/^\s+dependencies:\s*$/.test(raw)) {
|
|
1579
|
+
inDeps = true;
|
|
1580
|
+
devBlock = false;
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
if (/^\s+devDependencies:\s*$/.test(raw)) {
|
|
1584
|
+
inDeps = true;
|
|
1585
|
+
devBlock = true;
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
if (!inDeps || current === void 0) continue;
|
|
1589
|
+
const dep = /^\s{4}"?([^":]+)"?:\s+(.+)$/.exec(raw);
|
|
1590
|
+
if (dep?.[1] === void 0) continue;
|
|
1591
|
+
const depName = dep[1];
|
|
1592
|
+
if (current.length === 0) {
|
|
1593
|
+
if (devBlock) directDev.add(depName);
|
|
1594
|
+
else directProd.add(depName);
|
|
1595
|
+
} else {
|
|
1596
|
+
addEdge(edges, current, depName);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return graph(packages, edges, directProd, directDev, true);
|
|
1600
|
+
}
|
|
1601
|
+
function nameFromBerryKey(key) {
|
|
1602
|
+
const first = key.split(",")[0]?.trim() ?? key;
|
|
1603
|
+
if (first.startsWith("@")) {
|
|
1604
|
+
const match = /^(@[^@/]+\/[^@/]+)@/.exec(first);
|
|
1605
|
+
return match?.[1];
|
|
1606
|
+
}
|
|
1607
|
+
const at = first.indexOf("@");
|
|
1608
|
+
return at <= 0 ? first : first.slice(0, at);
|
|
1609
|
+
}
|
|
1610
|
+
function versionFromProtocol(raw) {
|
|
1611
|
+
const patch = /^patch:(.+)#/.exec(raw);
|
|
1612
|
+
const body = patch?.[1] ?? raw;
|
|
1613
|
+
const npm = /^(?:npm:)?(@?[^@]+)@(.+)$/.exec(body);
|
|
1614
|
+
if (npm?.[2] !== void 0) return npm[2];
|
|
1615
|
+
return body;
|
|
1616
|
+
}
|
|
1617
|
+
function parsePnpmV9(contents) {
|
|
1618
|
+
const packages = [];
|
|
1619
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1620
|
+
const directProd = /* @__PURE__ */ new Set();
|
|
1621
|
+
const directDev = /* @__PURE__ */ new Set();
|
|
1622
|
+
let block = "none";
|
|
1623
|
+
let snapshot;
|
|
1624
|
+
let inSnapshotDeps = false;
|
|
1625
|
+
for (const raw of contents.split(/\r?\n/)) {
|
|
1626
|
+
if (raw === "packages:") {
|
|
1627
|
+
block = "packages";
|
|
1628
|
+
snapshot = void 0;
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
if (raw === "snapshots:") {
|
|
1632
|
+
block = "snapshots";
|
|
1633
|
+
snapshot = void 0;
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
if (raw === "importers:" || raw.startsWith("lockfileVersion:")) {
|
|
1637
|
+
block = "none";
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
if (block !== "snapshots" && /^\s{4}dependencies:\s*$/.test(raw)) {
|
|
1641
|
+
block = "importer-deps";
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
if (/^\s{4}devDependencies:\s*$/.test(raw)) {
|
|
1645
|
+
block = "importer-dev";
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
if (block === "packages") {
|
|
1649
|
+
const header = /^\s{2}['"]?(@?[^@'\s]+)@([^:'"]+)['"]?:/.exec(raw);
|
|
1650
|
+
if (header?.[1] !== void 0 && header[2] !== void 0) {
|
|
1651
|
+
packages.push({ name: header[1], version: header[2], path: `pnpm:${header[1]}` });
|
|
1652
|
+
}
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
if (block === "snapshots") {
|
|
1656
|
+
const header = /^\s{2}['"]?(@?[^@'\s]+)@([^:'"]+)['"]?:/.exec(raw);
|
|
1657
|
+
if (header?.[1] !== void 0) {
|
|
1658
|
+
snapshot = header[1];
|
|
1659
|
+
inSnapshotDeps = false;
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
if (/^\s+dependencies:\s*$/.test(raw)) {
|
|
1663
|
+
inSnapshotDeps = true;
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
if (inSnapshotDeps && snapshot !== void 0) {
|
|
1667
|
+
const dep = /^\s{6}['"]?([^'":]+)['"]?:/.exec(raw);
|
|
1668
|
+
if (dep?.[1] !== void 0) addEdge(edges, snapshot, dep[1]);
|
|
1669
|
+
}
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
if (block === "importer-deps" || block === "importer-dev") {
|
|
1673
|
+
const dep = /^\s{6}['"]?([^'":]+)['"]?:/.exec(raw);
|
|
1674
|
+
if (dep?.[1] !== void 0 && !raw.trim().startsWith("specifier") && !raw.trim().startsWith("version")) {
|
|
1675
|
+
if (block === "importer-dev") directDev.add(dep[1]);
|
|
1676
|
+
else directProd.add(dep[1]);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
return graph(packages, edges, directProd, directDev, true);
|
|
1681
|
+
}
|
|
1682
|
+
function parseBunLock(contents) {
|
|
1683
|
+
const stripped = contents.replace(/^\s*\/\/.*$/gm, "");
|
|
1684
|
+
const packages = [];
|
|
1685
|
+
const edges = /* @__PURE__ */ new Map();
|
|
1686
|
+
const directProd = /* @__PURE__ */ new Set();
|
|
1687
|
+
const directDev = /* @__PURE__ */ new Set();
|
|
1688
|
+
try {
|
|
1689
|
+
const json = JSON.parse(stripped);
|
|
1690
|
+
for (const [name, value] of Object.entries(json.packages ?? {})) {
|
|
1691
|
+
if (!Array.isArray(value)) continue;
|
|
1692
|
+
const entry = Array.from(value);
|
|
1693
|
+
const spec = typeof entry[0] === "string" ? entry[0] : "";
|
|
1694
|
+
const version = spec.includes("@") ? spec.slice(spec.lastIndexOf("@") + 1) : spec;
|
|
1695
|
+
packages.push({ name, version, path: `bun:${name}` });
|
|
1696
|
+
const meta = entry[1];
|
|
1697
|
+
if (meta !== null && typeof meta === "object" && "dependencies" in meta) {
|
|
1698
|
+
const deps = meta.dependencies;
|
|
1699
|
+
if (deps !== null && typeof deps === "object") {
|
|
1700
|
+
for (const dep of Object.keys(deps)) addEdge(edges, name, dep);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
} catch {
|
|
1705
|
+
return graph([], /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), false);
|
|
1706
|
+
}
|
|
1707
|
+
return graph(packages, edges, directProd, directDev, packages.length > 0);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1521
1710
|
// src/modules/deps/lockfile.ts
|
|
1522
1711
|
function basename2(filename) {
|
|
1523
1712
|
const parts = filename.replace(/\\/g, "/").split("/");
|
|
@@ -1547,7 +1736,7 @@ function objectKeys(value) {
|
|
|
1547
1736
|
}
|
|
1548
1737
|
return Object.keys(value);
|
|
1549
1738
|
}
|
|
1550
|
-
function
|
|
1739
|
+
function addEdge2(edges, from, to) {
|
|
1551
1740
|
const set = edges.get(from) ?? /* @__PURE__ */ new Set();
|
|
1552
1741
|
set.add(to);
|
|
1553
1742
|
edges.set(from, set);
|
|
@@ -1627,7 +1816,7 @@ function parseYarnGraph(contents) {
|
|
|
1627
1816
|
const depBerry = /^\s{4}(?:"([^"]+)"|(@?[\w./-]+)):/.exec(raw);
|
|
1628
1817
|
const depName = depClassic?.[1] ?? depClassic?.[2] ?? depBerry?.[1] ?? depBerry?.[2];
|
|
1629
1818
|
if (depName !== void 0) {
|
|
1630
|
-
|
|
1819
|
+
addEdge2(edges, currentName, depName);
|
|
1631
1820
|
continue;
|
|
1632
1821
|
}
|
|
1633
1822
|
if (/^\s{2}\S/.test(raw)) inDeps = false;
|
|
@@ -1722,7 +1911,7 @@ function parsePnpmGraph(contents) {
|
|
|
1722
1911
|
const dep = /^ (?:'([^']+)'|"([^"]+)"|([^:]+)):/.exec(raw);
|
|
1723
1912
|
const depName = dep?.[1] ?? dep?.[2] ?? dep?.[3];
|
|
1724
1913
|
if (depName !== void 0) {
|
|
1725
|
-
|
|
1914
|
+
addEdge2(edges, currentPkg, depName);
|
|
1726
1915
|
continue;
|
|
1727
1916
|
}
|
|
1728
1917
|
if (/^ \S/.test(raw)) inPkgDeps = false;
|
|
@@ -1754,7 +1943,7 @@ function graphFromPackagesTable(packages) {
|
|
|
1754
1943
|
const name = key === "" ? "" : nameFromPackagesKey(key);
|
|
1755
1944
|
if (name === void 0 || name.length === 0) continue;
|
|
1756
1945
|
for (const dep of [...objectKeys(value.dependencies), ...objectKeys(value.optionalDependencies)]) {
|
|
1757
|
-
|
|
1946
|
+
addEdge2(edges, name, dep);
|
|
1758
1947
|
}
|
|
1759
1948
|
}
|
|
1760
1949
|
return { edges, directProd, directDev };
|
|
@@ -1764,7 +1953,7 @@ function graphFromDependenciesTree(deps, edges) {
|
|
|
1764
1953
|
const child = value.dependencies;
|
|
1765
1954
|
if (child === void 0 || typeof child !== "object" || child === null) continue;
|
|
1766
1955
|
const nested = child;
|
|
1767
|
-
for (const childName of Object.keys(nested))
|
|
1956
|
+
for (const childName of Object.keys(nested)) addEdge2(edges, name, childName);
|
|
1768
1957
|
graphFromDependenciesTree(nested, edges);
|
|
1769
1958
|
}
|
|
1770
1959
|
}
|
|
@@ -1777,8 +1966,16 @@ function parseLockfileGraph(filename, contents) {
|
|
|
1777
1966
|
directDev: /* @__PURE__ */ new Set(),
|
|
1778
1967
|
edgesKnown: false
|
|
1779
1968
|
};
|
|
1780
|
-
|
|
1781
|
-
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);
|
|
1782
1979
|
if (name !== "package-lock.json") return empty;
|
|
1783
1980
|
try {
|
|
1784
1981
|
const json = JSON.parse(contents);
|
|
@@ -1947,6 +2144,86 @@ function exactPinsFromManifest(pkg) {
|
|
|
1947
2144
|
return out;
|
|
1948
2145
|
}
|
|
1949
2146
|
|
|
2147
|
+
// src/modules/deps/evidence.ts
|
|
2148
|
+
var IMPORT_RE = /(?:require\(\s*['"]([^'"]+)['"]\s*\)|from\s+['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\))/g;
|
|
2149
|
+
function packageNameFromSpec(spec) {
|
|
2150
|
+
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
|
|
2151
|
+
if (spec.startsWith("@")) {
|
|
2152
|
+
const slash2 = spec.indexOf("/");
|
|
2153
|
+
if (slash2 < 0) return void 0;
|
|
2154
|
+
const rest = spec.slice(slash2 + 1);
|
|
2155
|
+
const cut = rest.indexOf("/");
|
|
2156
|
+
return cut === -1 ? spec : spec.slice(0, slash2 + 1 + cut);
|
|
2157
|
+
}
|
|
2158
|
+
const slash = spec.indexOf("/");
|
|
2159
|
+
return slash === -1 ? spec : spec.slice(0, slash);
|
|
2160
|
+
}
|
|
2161
|
+
function collectImportSites(sources) {
|
|
2162
|
+
const byPkg = /* @__PURE__ */ new Map();
|
|
2163
|
+
const files = [...sources.keys()].sort();
|
|
2164
|
+
for (const file of files) {
|
|
2165
|
+
const text = sources.get(file) ?? "";
|
|
2166
|
+
const lines = text.split(/\r?\n/);
|
|
2167
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
2168
|
+
const line = lines[i] ?? "";
|
|
2169
|
+
IMPORT_RE.lastIndex = 0;
|
|
2170
|
+
let match;
|
|
2171
|
+
while ((match = IMPORT_RE.exec(line)) !== null) {
|
|
2172
|
+
const spec = match[1] ?? match[2] ?? match[3];
|
|
2173
|
+
if (spec === void 0) continue;
|
|
2174
|
+
const pkg = packageNameFromSpec(spec);
|
|
2175
|
+
if (pkg === void 0) continue;
|
|
2176
|
+
const list = byPkg.get(pkg) ?? [];
|
|
2177
|
+
list.push({ file, line: i + 1, specifier: spec, pkg });
|
|
2178
|
+
byPkg.set(pkg, list);
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
for (const [pkg, list] of byPkg) {
|
|
2183
|
+
list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
2184
|
+
byPkg.set(pkg, list.slice(0, 5));
|
|
2185
|
+
}
|
|
2186
|
+
return byPkg;
|
|
2187
|
+
}
|
|
2188
|
+
function evidenceForFinding(finding3, sites, productionFiles, edgesKnown) {
|
|
2189
|
+
if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") return void 0;
|
|
2190
|
+
const snippet = finding3.snippet ?? "";
|
|
2191
|
+
const at = snippet.startsWith("@") ? snippet.indexOf("@", 1) : snippet.indexOf("@");
|
|
2192
|
+
const pkg = at <= 0 ? snippet : snippet.slice(0, at);
|
|
2193
|
+
if (pkg.length === 0) return void 0;
|
|
2194
|
+
const imports = (sites.get(pkg) ?? []).map((site) => ({
|
|
2195
|
+
file: site.file,
|
|
2196
|
+
line: site.line,
|
|
2197
|
+
specifier: site.specifier
|
|
2198
|
+
}));
|
|
2199
|
+
const first = imports[0];
|
|
2200
|
+
const path = finding3.dependencyPath;
|
|
2201
|
+
let reason;
|
|
2202
|
+
switch (finding3.reachability) {
|
|
2203
|
+
case "not-imported":
|
|
2204
|
+
reason = `No production file imports ${pkg}. Checked ${productionFiles.length} production file(s)${productionFiles.length > 0 ? `: ${productionFiles.slice(0, 8).join(", ")}` : ""}.`;
|
|
2205
|
+
return { imports: [], reason };
|
|
2206
|
+
case "reachability-unknown":
|
|
2207
|
+
reason = edgesKnown ? `Reachability for ${pkg} is unknown.` : `Lockfile edges are missing, so ${pkg} is reachability-unknown.`;
|
|
2208
|
+
return { imports, ...first !== void 0 ? {} : {}, reason };
|
|
2209
|
+
case "reachable-via-dependency":
|
|
2210
|
+
reason = path !== void 0 && path.length > 0 ? `Required through ${path.join(" > ")}.` : `Required by an imported package.`;
|
|
2211
|
+
return { imports, ...path !== void 0 ? { dependencyPath: path } : {}, reason };
|
|
2212
|
+
case "imported-not-called":
|
|
2213
|
+
reason = first !== void 0 ? `Imported from ${first.file}:${first.line} and not called.` : `Imported and not called.`;
|
|
2214
|
+
return { imports, reason };
|
|
2215
|
+
case "symbol-reachable":
|
|
2216
|
+
reason = first !== void 0 ? `A named vulnerable export is called from ${first.file}:${first.line}.` : `A named vulnerable export is called.`;
|
|
2217
|
+
return { imports, reason };
|
|
2218
|
+
case "symbol-not-called":
|
|
2219
|
+
reason = `A named vulnerable export is not called.`;
|
|
2220
|
+
return { imports, reason };
|
|
2221
|
+
default:
|
|
2222
|
+
reason = first !== void 0 ? `Imported from ${first.file}:${first.line}.` : `Imported by production code.`;
|
|
2223
|
+
return { imports, ...path !== void 0 ? { dependencyPath: path } : {}, reason };
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
1950
2227
|
// src/modules/deps/reachability.ts
|
|
1951
2228
|
init_finding();
|
|
1952
2229
|
|
|
@@ -2054,7 +2331,7 @@ function packageNameFromSnippet(snippet) {
|
|
|
2054
2331
|
const at = snippet.indexOf("@");
|
|
2055
2332
|
return at <= 0 ? snippet : snippet.slice(0, at);
|
|
2056
2333
|
}
|
|
2057
|
-
function
|
|
2334
|
+
function packageNameFromSpec2(spec) {
|
|
2058
2335
|
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
|
|
2059
2336
|
if (spec.startsWith("@")) {
|
|
2060
2337
|
const parts = spec.split("/");
|
|
@@ -2090,7 +2367,7 @@ function indexPackageUsage(sources) {
|
|
|
2090
2367
|
const local = m[1];
|
|
2091
2368
|
const spec = m[2];
|
|
2092
2369
|
if (local === void 0 || spec === void 0) continue;
|
|
2093
|
-
const pkg =
|
|
2370
|
+
const pkg = packageNameFromSpec2(spec);
|
|
2094
2371
|
if (pkg === void 0) continue;
|
|
2095
2372
|
bump(pkg, localIsCalled(source, local));
|
|
2096
2373
|
}
|
|
@@ -2100,7 +2377,7 @@ function indexPackageUsage(sources) {
|
|
|
2100
2377
|
while ((named = namedRe.exec(source)) !== null) {
|
|
2101
2378
|
const spec = named[2];
|
|
2102
2379
|
if (spec === void 0) continue;
|
|
2103
|
-
const pkg =
|
|
2380
|
+
const pkg = packageNameFromSpec2(spec);
|
|
2104
2381
|
if (pkg === void 0) continue;
|
|
2105
2382
|
const locals = (named[1] ?? "").split(",").map((s) => s.trim().split(/\s+as\s+/).pop() ?? "");
|
|
2106
2383
|
bump(
|
|
@@ -2132,37 +2409,82 @@ function shortestDependencyPath(target, starts, edges) {
|
|
|
2132
2409
|
}
|
|
2133
2410
|
return void 0;
|
|
2134
2411
|
}
|
|
2412
|
+
function collectDependencyPaths(target, starts, edges, limit = 8) {
|
|
2413
|
+
const out = [];
|
|
2414
|
+
const queue = [];
|
|
2415
|
+
let steps = 0;
|
|
2416
|
+
for (const start of starts) queue.push([start]);
|
|
2417
|
+
while (queue.length > 0 && out.length < limit && steps < 4e3) {
|
|
2418
|
+
steps += 1;
|
|
2419
|
+
const path = queue.shift();
|
|
2420
|
+
if (path === void 0) continue;
|
|
2421
|
+
const last = path[path.length - 1];
|
|
2422
|
+
if (last === void 0) continue;
|
|
2423
|
+
if (last === target) {
|
|
2424
|
+
if (path.length >= 2) out.push(path);
|
|
2425
|
+
continue;
|
|
2426
|
+
}
|
|
2427
|
+
if (path.length >= 12) continue;
|
|
2428
|
+
for (const next of edges.get(last) ?? []) {
|
|
2429
|
+
if (path.includes(next)) continue;
|
|
2430
|
+
queue.push([...path, next]);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
return out;
|
|
2434
|
+
}
|
|
2135
2435
|
function reachableFrom(target, roots, edges) {
|
|
2136
2436
|
if (roots.has(target)) return true;
|
|
2137
2437
|
return shortestDependencyPath(target, roots, edges) !== void 0;
|
|
2138
2438
|
}
|
|
2139
|
-
function importedDirectNames(usage,
|
|
2140
|
-
const directs = /* @__PURE__ */ new Set([...
|
|
2439
|
+
function importedDirectNames(usage, graph2) {
|
|
2440
|
+
const directs = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
|
|
2141
2441
|
const out = /* @__PURE__ */ new Set();
|
|
2142
|
-
for (const [name,
|
|
2143
|
-
if (!
|
|
2442
|
+
for (const [name, hit2] of usage) {
|
|
2443
|
+
if (!hit2.imported) continue;
|
|
2144
2444
|
if (directs.size === 0 || directs.has(name)) out.add(name);
|
|
2145
2445
|
}
|
|
2146
2446
|
return out;
|
|
2147
2447
|
}
|
|
2148
|
-
function classifyReachability(pkgName, productionUsage,
|
|
2149
|
-
const
|
|
2150
|
-
if (
|
|
2151
|
-
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" };
|
|
2152
2452
|
}
|
|
2153
|
-
if (
|
|
2154
|
-
const roots = importedDirectNames(productionUsage,
|
|
2155
|
-
const
|
|
2453
|
+
if (graph2 !== void 0 && graph2.edgesKnown) {
|
|
2454
|
+
const roots = importedDirectNames(productionUsage, graph2);
|
|
2455
|
+
const paths = collectDependencyPaths(pkgName, roots, graph2.edges);
|
|
2456
|
+
const path = paths[0];
|
|
2156
2457
|
if (path !== void 0 && path.length >= 2) {
|
|
2157
|
-
return { reachability: "reachable-via-dependency", path };
|
|
2458
|
+
return { reachability: "reachable-via-dependency", path, paths };
|
|
2158
2459
|
}
|
|
2159
2460
|
return { reachability: "not-imported" };
|
|
2160
2461
|
}
|
|
2161
|
-
if (
|
|
2462
|
+
if (graph2 !== void 0 && !graph2.edgesKnown) {
|
|
2162
2463
|
return { reachability: "reachability-unknown" };
|
|
2163
2464
|
}
|
|
2164
2465
|
return { reachability: "not-imported" };
|
|
2165
2466
|
}
|
|
2467
|
+
function unusedDirectPackages(ctx) {
|
|
2468
|
+
if (!ctx.graph.edgesKnown) return [];
|
|
2469
|
+
const imported = importedDirectNames(ctx.productionUsage, ctx.graph);
|
|
2470
|
+
return [...ctx.graph.directProd].filter((name) => {
|
|
2471
|
+
if (ctx.productionUsage.get(name)?.imported === true) return false;
|
|
2472
|
+
return !reachableFrom(name, imported, ctx.graph.edges);
|
|
2473
|
+
}).sort();
|
|
2474
|
+
}
|
|
2475
|
+
function unusedDirectPlan(findings, ctx) {
|
|
2476
|
+
const packages = unusedDirectPackages(ctx);
|
|
2477
|
+
if (packages.length === 0) return void 0;
|
|
2478
|
+
const names = new Set(packages);
|
|
2479
|
+
let advisories = 0;
|
|
2480
|
+
for (const finding3 of findings) {
|
|
2481
|
+
if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") continue;
|
|
2482
|
+
if (finding3.snippet === void 0) continue;
|
|
2483
|
+
const pkg = packageNameFromSnippet(finding3.snippet);
|
|
2484
|
+
if (pkg !== void 0 && names.has(pkg)) advisories += 1;
|
|
2485
|
+
}
|
|
2486
|
+
return { packages, advisories };
|
|
2487
|
+
}
|
|
2166
2488
|
function reachabilitySteps(reachability) {
|
|
2167
2489
|
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
|
|
2168
2490
|
return 0;
|
|
@@ -2184,8 +2506,108 @@ function isDevOnly(pkgName, ctx) {
|
|
|
2184
2506
|
if (inProd) return false;
|
|
2185
2507
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
2186
2508
|
}
|
|
2509
|
+
var REACH_RANK = {
|
|
2510
|
+
reachable: 0,
|
|
2511
|
+
"symbol-reachable": 1,
|
|
2512
|
+
"reachable-via-dependency": 2,
|
|
2513
|
+
"imported-not-called": 3,
|
|
2514
|
+
"symbol-not-called": 4,
|
|
2515
|
+
"reachability-unknown": 5,
|
|
2516
|
+
"not-imported": 6
|
|
2517
|
+
};
|
|
2518
|
+
function versionFromSnippet(snippet) {
|
|
2519
|
+
const name = packageNameFromSnippet(snippet);
|
|
2520
|
+
if (name === void 0) return snippet;
|
|
2521
|
+
return snippet.slice(name.length + 1);
|
|
2522
|
+
}
|
|
2523
|
+
function pathFields(classified) {
|
|
2524
|
+
const paths = classified.paths ?? (classified.path !== void 0 ? [classified.path] : []);
|
|
2525
|
+
if (paths.length === 0) return {};
|
|
2526
|
+
return {
|
|
2527
|
+
dependencyPath: paths[0],
|
|
2528
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
function appendOtherPaths(message, paths) {
|
|
2532
|
+
if (paths === void 0 || paths.length < 2) return message;
|
|
2533
|
+
const rest = paths.slice(1).map((p) => p.join(" > ")).join("; ");
|
|
2534
|
+
return `${message} Other paths: ${rest}.`;
|
|
2535
|
+
}
|
|
2536
|
+
function groupPackageAdvisories(findings) {
|
|
2537
|
+
const passthrough = [];
|
|
2538
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2539
|
+
for (const finding3 of findings) {
|
|
2540
|
+
if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") {
|
|
2541
|
+
passthrough.push(finding3);
|
|
2542
|
+
continue;
|
|
2543
|
+
}
|
|
2544
|
+
const name = packageNameFromSnippet(finding3.snippet ?? "") ?? finding3.snippet ?? finding3.id;
|
|
2545
|
+
const advisory = finding3.references?.[0] ?? finding3.id;
|
|
2546
|
+
const key = `${finding3.ruleId}\0${name}\0${advisory}`;
|
|
2547
|
+
const list = groups.get(key);
|
|
2548
|
+
if (list === void 0) groups.set(key, [finding3]);
|
|
2549
|
+
else list.push(finding3);
|
|
2550
|
+
}
|
|
2551
|
+
const merged = [];
|
|
2552
|
+
for (const list of groups.values()) {
|
|
2553
|
+
const only = list[0];
|
|
2554
|
+
if (list.length === 1 && only !== void 0) {
|
|
2555
|
+
merged.push(only);
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
merged.push(mergeAdvisory(list));
|
|
2559
|
+
}
|
|
2560
|
+
return [...passthrough, ...merged];
|
|
2561
|
+
}
|
|
2562
|
+
function mergeAdvisory(list) {
|
|
2563
|
+
const ranked = [...list].sort(
|
|
2564
|
+
(a, b) => severityRank(b.severity) - severityRank(a.severity) || (REACH_RANK[a.reachability ?? "not-imported"] ?? 9) - (REACH_RANK[b.reachability ?? "not-imported"] ?? 9)
|
|
2565
|
+
);
|
|
2566
|
+
const primary = ranked[0];
|
|
2567
|
+
if (primary === void 0) {
|
|
2568
|
+
throw new Error("empty advisory group");
|
|
2569
|
+
}
|
|
2570
|
+
const name = packageNameFromSnippet(primary.snippet ?? "") ?? "package";
|
|
2571
|
+
const versions = [...new Set(list.map((f) => versionFromSnippet(f.snippet ?? "")))].filter((v) => v.length > 0).sort();
|
|
2572
|
+
const paths = [];
|
|
2573
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
2574
|
+
for (const finding3 of list) {
|
|
2575
|
+
const candidates = finding3.dependencyPaths ?? (finding3.dependencyPath !== void 0 ? [finding3.dependencyPath] : []);
|
|
2576
|
+
for (const path of candidates) {
|
|
2577
|
+
const key = path.join("\0");
|
|
2578
|
+
if (seenPath.has(key)) continue;
|
|
2579
|
+
seenPath.add(key);
|
|
2580
|
+
paths.push(path);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
paths.sort((a, b) => a.length - b.length || a.join(">").localeCompare(b.join(">")));
|
|
2584
|
+
const advisory = primary.references?.[0] ?? "";
|
|
2585
|
+
const onManifest = list.find((f) => f.file === "package.json");
|
|
2586
|
+
const file = onManifest?.file ?? primary.file;
|
|
2587
|
+
const line = onManifest?.line ?? primary.line;
|
|
2588
|
+
const snippet = versions.length > 0 ? `${name}@${versions.join(",")}` : primary.snippet ?? name;
|
|
2589
|
+
let message = primary.message;
|
|
2590
|
+
if (versions.length > 1) {
|
|
2591
|
+
message = `${message} Versions: ${versions.join(", ")}.`;
|
|
2592
|
+
}
|
|
2593
|
+
if (paths.length > 1) {
|
|
2594
|
+
message = `${message} Dependency paths: ${paths.map((p) => p.join(" > ")).join("; ")}.`;
|
|
2595
|
+
}
|
|
2596
|
+
return {
|
|
2597
|
+
...primary,
|
|
2598
|
+
id: makeFindingId(primary.ruleId, file, `${name}|${advisory}|${versions.join(",")}`),
|
|
2599
|
+
file,
|
|
2600
|
+
line,
|
|
2601
|
+
snippet,
|
|
2602
|
+
message,
|
|
2603
|
+
severity: primary.severity,
|
|
2604
|
+
reachability: primary.reachability,
|
|
2605
|
+
...paths[0] !== void 0 ? { dependencyPath: paths[0] } : {},
|
|
2606
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2187
2609
|
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
2188
|
-
|
|
2610
|
+
const mapped = findings.map((finding3) => {
|
|
2189
2611
|
if (finding3.ruleId === "DEPS-MALICIOUS-001") {
|
|
2190
2612
|
if (finding3.snippet === void 0) return finding3;
|
|
2191
2613
|
const pkg2 = packageNameFromSnippet(finding3.snippet);
|
|
@@ -2195,7 +2617,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2195
2617
|
...finding3,
|
|
2196
2618
|
severity: "critical",
|
|
2197
2619
|
reachability: classified2.reachability,
|
|
2198
|
-
...classified2
|
|
2620
|
+
...pathFields(classified2)
|
|
2199
2621
|
};
|
|
2200
2622
|
}
|
|
2201
2623
|
if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
|
|
@@ -2205,7 +2627,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2205
2627
|
const named = symbols.get(finding3.id) ?? [];
|
|
2206
2628
|
if (named.length > 0 && ctx.sources !== void 0) {
|
|
2207
2629
|
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
2208
|
-
classified = { reachability: symbolClass, path: classified.path };
|
|
2630
|
+
classified = { reachability: symbolClass, path: classified.path, paths: classified.paths };
|
|
2209
2631
|
}
|
|
2210
2632
|
const steps = reachabilitySteps(classified.reachability);
|
|
2211
2633
|
let severity = steps === 0 ? finding3.severity : downgradeSeverity(finding3.severity, steps);
|
|
@@ -2213,6 +2635,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2213
2635
|
if (classified.path !== void 0) {
|
|
2214
2636
|
const shown = classified.path.join(" > ");
|
|
2215
2637
|
message = `${message} Reachable via ${shown}.`;
|
|
2638
|
+
message = appendOtherPaths(message, classified.paths);
|
|
2216
2639
|
}
|
|
2217
2640
|
const devOnly = isDevOnly(pkg, ctx);
|
|
2218
2641
|
if (devOnly) {
|
|
@@ -2222,11 +2645,12 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2222
2645
|
return {
|
|
2223
2646
|
...finding3,
|
|
2224
2647
|
reachability: classified.reachability,
|
|
2225
|
-
...classified
|
|
2648
|
+
...pathFields(classified),
|
|
2226
2649
|
severity,
|
|
2227
2650
|
message
|
|
2228
2651
|
};
|
|
2229
2652
|
});
|
|
2653
|
+
return groupPackageAdvisories(mapped);
|
|
2230
2654
|
}
|
|
2231
2655
|
|
|
2232
2656
|
// src/modules/deps/semver.ts
|
|
@@ -2283,6 +2707,176 @@ function osvSeverity(vuln) {
|
|
|
2283
2707
|
return "high";
|
|
2284
2708
|
}
|
|
2285
2709
|
|
|
2710
|
+
// src/modules/deps/upgrade-plan.ts
|
|
2711
|
+
function sentenceFor(bumps, unresolved) {
|
|
2712
|
+
const parts = [];
|
|
2713
|
+
if (bumps.length === 1) {
|
|
2714
|
+
const bump = bumps[0];
|
|
2715
|
+
if (bump !== void 0) {
|
|
2716
|
+
const noun = bump.findings === 1 ? "finding" : "findings";
|
|
2717
|
+
parts.push(`1 bump (${bump.name} ${bump.from} \u2192 ${bump.to}) resolves ${bump.findings} ${noun}.`);
|
|
2718
|
+
}
|
|
2719
|
+
} else if (bumps.length > 1) {
|
|
2720
|
+
const listed = bumps.map((bump) => `${bump.name} ${bump.from} \u2192 ${bump.to}`).join(", ");
|
|
2721
|
+
const total = bumps.reduce((sum, bump) => sum + bump.findings, 0);
|
|
2722
|
+
const noun = total === 1 ? "finding" : "findings";
|
|
2723
|
+
parts.push(`${bumps.length} bumps (${listed}) resolve ${total} ${noun}.`);
|
|
2724
|
+
}
|
|
2725
|
+
if (unresolved.length > 0) {
|
|
2726
|
+
parts.push(`No fixed version in the OSV cache for ${unresolved.join(", ")}.`);
|
|
2727
|
+
}
|
|
2728
|
+
return parts.join(" ");
|
|
2729
|
+
}
|
|
2730
|
+
function chooseUpgradePlan(advisories) {
|
|
2731
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2732
|
+
for (const advisory of advisories) {
|
|
2733
|
+
const list = byName.get(advisory.name);
|
|
2734
|
+
if (list === void 0) byName.set(advisory.name, [advisory]);
|
|
2735
|
+
else list.push(advisory);
|
|
2736
|
+
}
|
|
2737
|
+
const unresolved = [];
|
|
2738
|
+
const candidates = [];
|
|
2739
|
+
for (const [name, list] of byName) {
|
|
2740
|
+
const from = list[0]?.from ?? "0.0.0";
|
|
2741
|
+
if (list.some((advisory) => advisory.fixed === void 0)) {
|
|
2742
|
+
unresolved.push(name);
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
let to = list[0]?.fixed;
|
|
2746
|
+
for (const advisory of list) {
|
|
2747
|
+
if (advisory.fixed !== void 0 && (to === void 0 || compareSemver(advisory.fixed, to) > 0)) {
|
|
2748
|
+
to = advisory.fixed;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
if (to === void 0 || compareSemver(to, from) <= 0) continue;
|
|
2752
|
+
candidates.push({
|
|
2753
|
+
name,
|
|
2754
|
+
from,
|
|
2755
|
+
to,
|
|
2756
|
+
ids: list.map((advisory) => advisory.id)
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
unresolved.sort();
|
|
2760
|
+
const chosen = smallestMaxCover(candidates);
|
|
2761
|
+
if (chosen.length === 0 && unresolved.length === 0) return void 0;
|
|
2762
|
+
const bumps = chosen.map((candidate) => ({
|
|
2763
|
+
name: candidate.name,
|
|
2764
|
+
from: candidate.from,
|
|
2765
|
+
to: candidate.to,
|
|
2766
|
+
safe: sameMajor(candidate.from, candidate.to),
|
|
2767
|
+
findings: candidate.ids.length
|
|
2768
|
+
}));
|
|
2769
|
+
return { bumps, unresolved, sentence: sentenceFor(bumps, unresolved) };
|
|
2770
|
+
}
|
|
2771
|
+
function smallestMaxCover(candidates) {
|
|
2772
|
+
const limit = Math.min(candidates.length, 12);
|
|
2773
|
+
let best = [];
|
|
2774
|
+
let bestCount = -1;
|
|
2775
|
+
const walk2 = (index, picked, covered) => {
|
|
2776
|
+
if (index === limit) {
|
|
2777
|
+
if (covered.size > bestCount || covered.size === bestCount && betterTie(picked, best)) {
|
|
2778
|
+
best = picked;
|
|
2779
|
+
bestCount = covered.size;
|
|
2780
|
+
}
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2783
|
+
const next = candidates[index];
|
|
2784
|
+
if (next === void 0) return;
|
|
2785
|
+
walk2(index + 1, picked, covered);
|
|
2786
|
+
const grown = new Set(covered);
|
|
2787
|
+
for (const id of next.ids) grown.add(id);
|
|
2788
|
+
walk2(index + 1, [...picked, next], grown);
|
|
2789
|
+
};
|
|
2790
|
+
walk2(0, [], /* @__PURE__ */ new Set());
|
|
2791
|
+
return [...best].sort((a, b) => a.name.localeCompare(b.name));
|
|
2792
|
+
}
|
|
2793
|
+
function betterTie(picked, best) {
|
|
2794
|
+
if (picked.length !== best.length) return picked.length < best.length;
|
|
2795
|
+
const left = picked.map((item) => item.name).sort().join("\0");
|
|
2796
|
+
const right = best.map((item) => item.name).sort().join("\0");
|
|
2797
|
+
return left < right;
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
// src/supply/campaign.ts
|
|
2801
|
+
init_finding();
|
|
2802
|
+
var RPC_RE = /getSignaturesForAddress|eth_getLogs/;
|
|
2803
|
+
var NET_RE = /https\.request|http\.request|fetch\s*\(|axios\./;
|
|
2804
|
+
var EVAL_RE = /\beval\s*\(/;
|
|
2805
|
+
var STEAL_RE = /Login Data|leveldb|discord[\\/]Local Storage|wallet-extension|Ethereum\/keystore/i;
|
|
2806
|
+
var PLATFORM_RE = /process\.platform\s*===\s*['"]win32['"]|os\.platform\s*\(\s*\)/;
|
|
2807
|
+
var DOWNLOAD_RE = /https?:\/\/example\.invalid|createWriteStream|https\.get/;
|
|
2808
|
+
var SPAWN_RE = /child_process|spawn\s*\(|execFile\s*\(|\.exe['"]/;
|
|
2809
|
+
function hit(ruleId, title, message, file, severity) {
|
|
2810
|
+
return {
|
|
2811
|
+
id: makeFindingId(ruleId, file, message),
|
|
2812
|
+
ruleId,
|
|
2813
|
+
module: "deps",
|
|
2814
|
+
title,
|
|
2815
|
+
severity,
|
|
2816
|
+
confidence: "high",
|
|
2817
|
+
file,
|
|
2818
|
+
message,
|
|
2819
|
+
remediation: "Do not install this package. Remove it, delete node_modules, and reinstall from a clean lockfile."
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
function campaignFindings(files) {
|
|
2823
|
+
const findings = [];
|
|
2824
|
+
for (const source of files) {
|
|
2825
|
+
const hasChain = RPC_RE.test(source.text);
|
|
2826
|
+
const hasNet = NET_RE.test(source.text) || EVAL_RE.test(source.text);
|
|
2827
|
+
if (hasChain && hasNet) {
|
|
2828
|
+
findings.push(
|
|
2829
|
+
hit(
|
|
2830
|
+
"SUPPLY-CHAINC2-001",
|
|
2831
|
+
"Package reads a blockchain RPC and then fetches or evaluates the result",
|
|
2832
|
+
"static: RPC method combined with fetch or eval",
|
|
2833
|
+
source.file,
|
|
2834
|
+
"high"
|
|
2835
|
+
)
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
const steals = STEAL_RE.test(source.text);
|
|
2839
|
+
const callsNet = NET_RE.test(source.text);
|
|
2840
|
+
if (steals && callsNet) {
|
|
2841
|
+
findings.push(
|
|
2842
|
+
hit(
|
|
2843
|
+
"SUPPLY-STEALER-001",
|
|
2844
|
+
"Package reads a local credential store and makes a network call",
|
|
2845
|
+
"static: browser, chat, or wallet path combined with a network call",
|
|
2846
|
+
source.file,
|
|
2847
|
+
"critical"
|
|
2848
|
+
)
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
const gated2 = PLATFORM_RE.test(source.text);
|
|
2852
|
+
const downloads = DOWNLOAD_RE.test(source.text);
|
|
2853
|
+
const spawns = SPAWN_RE.test(source.text);
|
|
2854
|
+
if (gated2 && downloads && spawns) {
|
|
2855
|
+
findings.push(
|
|
2856
|
+
hit(
|
|
2857
|
+
"SUPPLY-PLATFORMGATE-001",
|
|
2858
|
+
"Package downloads and launches a file only on one platform",
|
|
2859
|
+
"static: platform check combined with a download and a process spawn",
|
|
2860
|
+
source.file,
|
|
2861
|
+
"high"
|
|
2862
|
+
)
|
|
2863
|
+
);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
return findings;
|
|
2867
|
+
}
|
|
2868
|
+
function securityPlaceholderFinding(name, locked, latest) {
|
|
2869
|
+
if (latest !== "0.0.1-security") return void 0;
|
|
2870
|
+
if (locked === "0.0.1-security") return void 0;
|
|
2871
|
+
return hit(
|
|
2872
|
+
"SUPPLY-SECHOLDER-001",
|
|
2873
|
+
`Locked ${name}@${locked} was replaced on the registry by the security placeholder`,
|
|
2874
|
+
`${name} latest is 0.0.1-security while the lockfile pins ${locked}`,
|
|
2875
|
+
"package-lock.json",
|
|
2876
|
+
"critical"
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2286
2880
|
// src/modules/deps/popular.ts
|
|
2287
2881
|
var POPULAR_PACKAGES = [
|
|
2288
2882
|
"express",
|
|
@@ -4128,6 +4722,59 @@ function experimentalVex() {
|
|
|
4128
4722
|
return process.env.FULLSHIELD_EXPERIMENTAL_VEX === "1";
|
|
4129
4723
|
}
|
|
4130
4724
|
|
|
4725
|
+
// src/modules/deps/license.ts
|
|
4726
|
+
init_finding();
|
|
4727
|
+
var COPYLEFT = /(?:^|[^\w])(?:AGPL|GPL)(?:-|[^\w]|$)/i;
|
|
4728
|
+
function isCopyleftLicense(license) {
|
|
4729
|
+
return COPYLEFT.test(license);
|
|
4730
|
+
}
|
|
4731
|
+
function licenseText(value) {
|
|
4732
|
+
if (typeof value === "string") return value;
|
|
4733
|
+
if (typeof value === "object" && value !== null && "type" in value) {
|
|
4734
|
+
const type = value.type;
|
|
4735
|
+
if (typeof type === "string") return type;
|
|
4736
|
+
}
|
|
4737
|
+
return void 0;
|
|
4738
|
+
}
|
|
4739
|
+
async function readJson(path) {
|
|
4740
|
+
try {
|
|
4741
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
4742
|
+
} catch {
|
|
4743
|
+
return void 0;
|
|
4744
|
+
}
|
|
4745
|
+
}
|
|
4746
|
+
async function scanCopyleftLicenses(cwd) {
|
|
4747
|
+
const manifest = await readJson(join(cwd, "package.json"));
|
|
4748
|
+
if (typeof manifest !== "object" || manifest === null) return [];
|
|
4749
|
+
const names = /* @__PURE__ */ new Set();
|
|
4750
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
4751
|
+
const block = manifest[field];
|
|
4752
|
+
if (typeof block === "object" && block !== null && !Array.isArray(block)) {
|
|
4753
|
+
for (const name of Object.keys(block)) names.add(name);
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
const findings = [];
|
|
4757
|
+
for (const name of names) {
|
|
4758
|
+
const pkg = await readJson(join(cwd, "node_modules", name, "package.json"));
|
|
4759
|
+
if (typeof pkg !== "object" || pkg === null) continue;
|
|
4760
|
+
const license = licenseText(pkg.license);
|
|
4761
|
+
if (license === void 0 || !isCopyleftLicense(license)) continue;
|
|
4762
|
+
findings.push({
|
|
4763
|
+
id: makeFindingId("DEPS-LICENSE-001", "package.json", `${name}|${license}`),
|
|
4764
|
+
ruleId: "DEPS-LICENSE-001",
|
|
4765
|
+
module: "deps",
|
|
4766
|
+
title: `Copyleft dependency ${name}`,
|
|
4767
|
+
severity: "medium",
|
|
4768
|
+
confidence: "high",
|
|
4769
|
+
file: "package.json",
|
|
4770
|
+
snippet: `${name} ${license}`,
|
|
4771
|
+
message: `${name} is licensed ${license}.`,
|
|
4772
|
+
remediation: "Replace the dependency or get a legal review before shipping it in a commercial product."
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
return findings;
|
|
4776
|
+
}
|
|
4777
|
+
|
|
4131
4778
|
// src/fixer/eol.ts
|
|
4132
4779
|
function detectEol(text) {
|
|
4133
4780
|
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -4226,7 +4873,7 @@ function walkJs(dir, out = []) {
|
|
|
4226
4873
|
}
|
|
4227
4874
|
return out;
|
|
4228
4875
|
}
|
|
4229
|
-
function
|
|
4876
|
+
function readJson2(path) {
|
|
4230
4877
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
4231
4878
|
}
|
|
4232
4879
|
var TOKEN_RE = /(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/;
|
|
@@ -4240,7 +4887,7 @@ function packageSources(dir) {
|
|
|
4240
4887
|
let scripts = "";
|
|
4241
4888
|
if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
|
|
4242
4889
|
try {
|
|
4243
|
-
const pkg =
|
|
4890
|
+
const pkg = readJson2(manifestPath);
|
|
4244
4891
|
const block = pkg.scripts ?? {};
|
|
4245
4892
|
scripts = Object.values(block).join("\n");
|
|
4246
4893
|
} catch {
|
|
@@ -4261,7 +4908,7 @@ ${scripts}`;
|
|
|
4261
4908
|
if (!TOKEN_RE.test(text)) return [];
|
|
4262
4909
|
const replicates = PUBLISH_RE.test(text) || REGISTRY_PUT_RE.test(text) || GITHUB_CREATE_RE.test(text) || WORKFLOW_PATH_RE.test(text);
|
|
4263
4910
|
if (!replicates) return [];
|
|
4264
|
-
const
|
|
4911
|
+
const hit2 = sources.find(
|
|
4265
4912
|
(s) => TOKEN_RE.test(s.text) && (PUBLISH_RE.test(s.text) || REGISTRY_PUT_RE.test(s.text) || GITHUB_CREATE_RE.test(s.text) || WORKFLOW_PATH_RE.test(s.text))
|
|
4266
4913
|
) ?? sources.find((s) => TOKEN_RE.test(s.text));
|
|
4267
4914
|
return [
|
|
@@ -4269,7 +4916,7 @@ ${scripts}`;
|
|
|
4269
4916
|
"SUPPLY-WORM-001",
|
|
4270
4917
|
"Self-replicating worm indicators in dependency code",
|
|
4271
4918
|
"static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",
|
|
4272
|
-
|
|
4919
|
+
hit2?.file,
|
|
4273
4920
|
"critical"
|
|
4274
4921
|
)
|
|
4275
4922
|
];
|
|
@@ -4279,13 +4926,13 @@ function analyzeWorkflowTampering(dir) {
|
|
|
4279
4926
|
const text = `${sources.map((s) => s.text).join("\n")}
|
|
4280
4927
|
${scripts}`;
|
|
4281
4928
|
if (!CI_WRITE_RE.test(text)) return [];
|
|
4282
|
-
const
|
|
4929
|
+
const hit2 = sources.find((s) => CI_WRITE_RE.test(s.text));
|
|
4283
4930
|
return [
|
|
4284
4931
|
finding(
|
|
4285
4932
|
"SUPPLY-WORKFLOW-001",
|
|
4286
4933
|
"Dependency writes GitHub workflow, git hooks, or CI config",
|
|
4287
4934
|
"static: install script or package code writes .github/, .git/hooks/, or CI configuration",
|
|
4288
|
-
|
|
4935
|
+
hit2?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
|
|
4289
4936
|
"critical"
|
|
4290
4937
|
)
|
|
4291
4938
|
];
|
|
@@ -4307,8 +4954,8 @@ function parentsOf(name, edges) {
|
|
|
4307
4954
|
}
|
|
4308
4955
|
return out;
|
|
4309
4956
|
}
|
|
4310
|
-
function isDirect(name,
|
|
4311
|
-
return
|
|
4957
|
+
function isDirect(name, graph2) {
|
|
4958
|
+
return graph2.directProd.has(name) || graph2.directDev.has(name);
|
|
4312
4959
|
}
|
|
4313
4960
|
function packagesWithAFixedRelease(findings) {
|
|
4314
4961
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -4326,17 +4973,17 @@ function targetVersion(finding3) {
|
|
|
4326
4973
|
function kindFromLockfile(lockFile) {
|
|
4327
4974
|
return lockfileKind(lockFile) ?? "npm";
|
|
4328
4975
|
}
|
|
4329
|
-
function attachOverrideProposals(findings,
|
|
4330
|
-
if (!
|
|
4976
|
+
function attachOverrideProposals(findings, graph2, lockFile, manifestRaw) {
|
|
4977
|
+
if (!graph2.edgesKnown || manifestRaw === void 0) return findings;
|
|
4331
4978
|
const withFix = packagesWithAFixedRelease(findings);
|
|
4332
4979
|
const kind = kindFromLockfile(lockFile);
|
|
4333
4980
|
return findings.map((finding3) => {
|
|
4334
4981
|
if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
|
|
4335
4982
|
const name = packageNameFromSnippet2(finding3.snippet);
|
|
4336
|
-
if (name.length === 0 || isDirect(name,
|
|
4983
|
+
if (name.length === 0 || isDirect(name, graph2)) return finding3;
|
|
4337
4984
|
const version = targetVersion(finding3);
|
|
4338
4985
|
if (version === void 0) return finding3;
|
|
4339
|
-
const parentFixed = parentsOf(name,
|
|
4986
|
+
const parentFixed = parentsOf(name, graph2.edges).some((parent) => withFix.has(parent));
|
|
4340
4987
|
if (parentFixed) return finding3;
|
|
4341
4988
|
const after = withManifestOverride(manifestRaw, kind, name, version);
|
|
4342
4989
|
const diff = unifiedDiff("package.json", manifestRaw, after);
|
|
@@ -4357,10 +5004,10 @@ function scanInstalledPackages(cwd, packages) {
|
|
|
4357
5004
|
const dir = packageDir(cwd, pkg);
|
|
4358
5005
|
if (dir === void 0) continue;
|
|
4359
5006
|
const prefix = pkg.path.startsWith("node_modules/") ? pkg.path : `node_modules/${pkg.name}`;
|
|
4360
|
-
for (const
|
|
5007
|
+
for (const hit2 of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
|
|
4361
5008
|
out.push({
|
|
4362
|
-
...
|
|
4363
|
-
file:
|
|
5009
|
+
...hit2,
|
|
5010
|
+
file: hit2.file !== void 0 && hit2.file.length > 0 ? `${prefix}/${hit2.file}` : prefix
|
|
4364
5011
|
});
|
|
4365
5012
|
}
|
|
4366
5013
|
}
|
|
@@ -4439,19 +5086,19 @@ function maliciousFinding(lockFile, pkg, vuln) {
|
|
|
4439
5086
|
references: [vuln.id, ...vuln.aliases ?? []]
|
|
4440
5087
|
};
|
|
4441
5088
|
}
|
|
4442
|
-
function typosquatFinding(lockFile,
|
|
4443
|
-
const snippet = `${
|
|
5089
|
+
function typosquatFinding(lockFile, hit2) {
|
|
5090
|
+
const snippet = `${hit2.name}@${hit2.version}`;
|
|
4444
5091
|
return {
|
|
4445
5092
|
id: makeFindingId("DEPS-TYPOSQUAT-001", lockFile, snippet),
|
|
4446
5093
|
ruleId: "DEPS-TYPOSQUAT-001",
|
|
4447
5094
|
module: "deps",
|
|
4448
|
-
title: `Possible typosquat: ${
|
|
5095
|
+
title: `Possible typosquat: ${hit2.name}`,
|
|
4449
5096
|
severity: "medium",
|
|
4450
5097
|
confidence: "low",
|
|
4451
5098
|
file: lockFile,
|
|
4452
5099
|
snippet,
|
|
4453
|
-
message: `Package "${
|
|
4454
|
-
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.`
|
|
4455
5102
|
};
|
|
4456
5103
|
}
|
|
4457
5104
|
function noLockFinding() {
|
|
@@ -4485,6 +5132,8 @@ function deprecatedFinding(lockFile, pkg) {
|
|
|
4485
5132
|
var depsModule = {
|
|
4486
5133
|
name: "deps",
|
|
4487
5134
|
async run(ctx) {
|
|
5135
|
+
depsModule.unusedDirect = void 0;
|
|
5136
|
+
depsModule.upgradePlan = void 0;
|
|
4488
5137
|
const lockFile = LOCKFILE_NAMES.find((name) => ctx.files.includes(name));
|
|
4489
5138
|
const cacheDir = ctx.config.deps.cacheDir !== void 0 ? resolve(ctx.cwd, ctx.config.deps.cacheDir) : defaultCacheDir();
|
|
4490
5139
|
const osvOpts = { cacheDir, offline: ctx.config.deps.offline };
|
|
@@ -4510,21 +5159,21 @@ var depsModule = {
|
|
|
4510
5159
|
}
|
|
4511
5160
|
const pins = exactPinsFromManifest(manifest);
|
|
4512
5161
|
const directNames2 = await readDirectDependencyNames(ctx.cwd);
|
|
4513
|
-
for (const
|
|
4514
|
-
findings2.push(typosquatFinding("package.json",
|
|
5162
|
+
for (const hit2 of findTyposquats(pins, { directNames: directNames2 })) {
|
|
5163
|
+
findings2.push(typosquatFinding("package.json", hit2));
|
|
4515
5164
|
}
|
|
4516
5165
|
const osvHits2 = await queryOsv(pins, osvOpts);
|
|
4517
5166
|
const symbols2 = /* @__PURE__ */ new Map();
|
|
4518
|
-
for (const
|
|
4519
|
-
for (const vuln of
|
|
4520
|
-
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));
|
|
4521
5170
|
const names = affectedFunctionNames(vuln);
|
|
4522
5171
|
if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols2.set(finding3.id, names);
|
|
4523
5172
|
findings2.push(finding3);
|
|
4524
5173
|
}
|
|
4525
5174
|
}
|
|
4526
5175
|
const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
|
|
4527
|
-
return applyReachability(findings2, noLockCtx, symbols2);
|
|
5176
|
+
return withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, applyReachability(findings2, noLockCtx, symbols2));
|
|
4528
5177
|
}
|
|
4529
5178
|
let contents;
|
|
4530
5179
|
try {
|
|
@@ -4532,12 +5181,12 @@ var depsModule = {
|
|
|
4532
5181
|
} catch {
|
|
4533
5182
|
return [];
|
|
4534
5183
|
}
|
|
4535
|
-
const
|
|
4536
|
-
const packages =
|
|
5184
|
+
const graph2 = parseLockfileGraph(lockFile, contents);
|
|
5185
|
+
const packages = graph2.packages;
|
|
4537
5186
|
const findings = [];
|
|
4538
5187
|
const directNames = await readDirectDependencyNames(ctx.cwd);
|
|
4539
|
-
for (const
|
|
4540
|
-
findings.push(typosquatFinding(lockFile,
|
|
5188
|
+
for (const hit2 of findTyposquats(packages, { directNames })) {
|
|
5189
|
+
findings.push(typosquatFinding(lockFile, hit2));
|
|
4541
5190
|
}
|
|
4542
5191
|
for (const pkg of packages) {
|
|
4543
5192
|
if (pkg.deprecated !== void 0) {
|
|
@@ -4546,22 +5195,129 @@ var depsModule = {
|
|
|
4546
5195
|
}
|
|
4547
5196
|
const osvHits = await queryOsv(packages, osvOpts);
|
|
4548
5197
|
const symbols = /* @__PURE__ */ new Map();
|
|
4549
|
-
for (const
|
|
4550
|
-
for (const vuln of
|
|
4551
|
-
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));
|
|
4552
5201
|
const names = affectedFunctionNames(vuln);
|
|
4553
5202
|
if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols.set(finding3.id, names);
|
|
4554
5203
|
findings.push(finding3);
|
|
4555
5204
|
}
|
|
4556
5205
|
}
|
|
4557
|
-
|
|
5206
|
+
const reachCtx = await reachabilityCtx(ctx.cwd, ctx.files, graph2);
|
|
5207
|
+
let reached = applyReachability(findings, reachCtx, symbols);
|
|
4558
5208
|
if (experimentalVex()) {
|
|
4559
|
-
reached = attachOverrideProposals(reached,
|
|
5209
|
+
reached = attachOverrideProposals(reached, graph2, lockFile, manifestRaw);
|
|
4560
5210
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
4561
5211
|
}
|
|
5212
|
+
if (licenseEnabled()) reached.push(...await scanCopyleftLicenses(ctx.cwd));
|
|
5213
|
+
depsModule.unusedDirect = unusedDirectPlan(reached, reachCtx);
|
|
5214
|
+
const direct = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
|
|
5215
|
+
const advisories = [];
|
|
5216
|
+
for (const hit2 of osvHits) {
|
|
5217
|
+
if (!direct.has(hit2.pkg.name)) continue;
|
|
5218
|
+
for (const vuln of hit2.vulns) {
|
|
5219
|
+
if (isMaliciousAdvisory(vuln)) continue;
|
|
5220
|
+
advisories.push({
|
|
5221
|
+
name: hit2.pkg.name,
|
|
5222
|
+
from: hit2.pkg.version,
|
|
5223
|
+
id: vuln.id,
|
|
5224
|
+
fixed: minFixedVersion(vuln.affected ?? [])
|
|
5225
|
+
});
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
depsModule.upgradePlan = chooseUpgradePlan(advisories);
|
|
5229
|
+
if (process.env.FULLSHIELD_EXPERIMENTAL_REACH_EVIDENCE === "1") {
|
|
5230
|
+
const sites = collectImportSites(reachCtx.sources);
|
|
5231
|
+
const files = [...reachCtx.sources.keys()].sort();
|
|
5232
|
+
reached = reached.map((finding3) => {
|
|
5233
|
+
const evidence = evidenceForFinding(finding3, sites, files, graph2.edgesKnown);
|
|
5234
|
+
return evidence === void 0 ? finding3 : { ...finding3, reachabilityEvidence: evidence };
|
|
5235
|
+
});
|
|
5236
|
+
}
|
|
5237
|
+
reached = await withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, reached);
|
|
5238
|
+
if (process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY === "1") {
|
|
5239
|
+
reached.push(...campaignFindings([...reachCtx.sources.entries()].map(([file, text]) => ({ file, text }))));
|
|
5240
|
+
for (const name of [...direct].sort()) {
|
|
5241
|
+
const locked = packages.find((pkg) => pkg.name === name)?.version;
|
|
5242
|
+
if (locked === void 0) continue;
|
|
5243
|
+
const latest = await readPlaceholderLatest(join(cacheDir, "registry", `${name.replaceAll("/", "__")}.json`));
|
|
5244
|
+
const placeholder = securityPlaceholderFinding(name, locked, latest);
|
|
5245
|
+
if (placeholder !== void 0) reached.push(placeholder);
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
4562
5248
|
return reached;
|
|
4563
5249
|
}
|
|
4564
5250
|
};
|
|
5251
|
+
function workspacesEnabled() {
|
|
5252
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_WORKSPACES === "1";
|
|
5253
|
+
}
|
|
5254
|
+
function workspacePatterns(manifestRaw) {
|
|
5255
|
+
try {
|
|
5256
|
+
const manifest = JSON.parse(manifestRaw);
|
|
5257
|
+
const field = manifest.workspaces;
|
|
5258
|
+
if (Array.isArray(field)) return field.filter((item) => typeof item === "string");
|
|
5259
|
+
if (field !== null && typeof field === "object" && Array.isArray(field.packages)) {
|
|
5260
|
+
return field.packages.filter((item) => typeof item === "string");
|
|
5261
|
+
}
|
|
5262
|
+
} catch {
|
|
5263
|
+
return [];
|
|
5264
|
+
}
|
|
5265
|
+
return [];
|
|
5266
|
+
}
|
|
5267
|
+
async function expandWorkspace(cwd, pattern) {
|
|
5268
|
+
const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
5269
|
+
if (!normalized.includes("*")) return [normalized];
|
|
5270
|
+
const slash = normalized.indexOf("/*");
|
|
5271
|
+
if (slash < 0) return [];
|
|
5272
|
+
const parent = normalized.slice(0, slash);
|
|
5273
|
+
try {
|
|
5274
|
+
const entries = await readdir(join(cwd, parent), { withFileTypes: true });
|
|
5275
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => `${parent}/${entry.name}`).sort();
|
|
5276
|
+
} catch {
|
|
5277
|
+
return [];
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
async function workspaceAdvisoryFindings(cwd, manifestRaw, osvOpts) {
|
|
5281
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
5282
|
+
for (const pattern of workspacePatterns(manifestRaw)) {
|
|
5283
|
+
for (const dir of await expandWorkspace(cwd, pattern)) dirs.add(dir);
|
|
5284
|
+
}
|
|
5285
|
+
const findings = [];
|
|
5286
|
+
for (const dir of [...dirs].sort()) {
|
|
5287
|
+
if (dir === "" || dir === ".") continue;
|
|
5288
|
+
const lockName = LOCKFILE_NAMES.find((name) => existsSync(join(cwd, dir, name)));
|
|
5289
|
+
if (lockName === void 0) continue;
|
|
5290
|
+
let text;
|
|
5291
|
+
try {
|
|
5292
|
+
text = await readFile(join(cwd, dir, lockName), "utf8");
|
|
5293
|
+
} catch {
|
|
5294
|
+
continue;
|
|
5295
|
+
}
|
|
5296
|
+
const graph2 = parseLockfileGraph(lockName, text);
|
|
5297
|
+
const hits = await queryOsv(graph2.packages, osvOpts);
|
|
5298
|
+
const file = `${dir}/${lockName}`;
|
|
5299
|
+
for (const hit2 of hits) {
|
|
5300
|
+
for (const vuln of hit2.vulns) {
|
|
5301
|
+
findings.push(
|
|
5302
|
+
isMaliciousAdvisory(vuln) ? maliciousFinding(file, hit2.pkg, vuln) : vulnFinding(file, hit2.pkg, vuln)
|
|
5303
|
+
);
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
return findings;
|
|
5308
|
+
}
|
|
5309
|
+
async function withWorkspaceAdvisories(cwd, manifestRaw, osvOpts, findings) {
|
|
5310
|
+
if (!workspacesEnabled() || manifestRaw === void 0) return findings;
|
|
5311
|
+
return [...findings, ...await workspaceAdvisoryFindings(cwd, manifestRaw, osvOpts)];
|
|
5312
|
+
}
|
|
5313
|
+
async function readPlaceholderLatest(file) {
|
|
5314
|
+
try {
|
|
5315
|
+
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
5316
|
+
return typeof raw.latest === "string" ? raw.latest : void 0;
|
|
5317
|
+
} catch {
|
|
5318
|
+
return void 0;
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
4565
5321
|
function graphFromManifest(manifest) {
|
|
4566
5322
|
const keys = (field) => {
|
|
4567
5323
|
const block = manifest[field];
|
|
@@ -4578,7 +5334,7 @@ function graphFromManifest(manifest) {
|
|
|
4578
5334
|
edgesKnown: false
|
|
4579
5335
|
};
|
|
4580
5336
|
}
|
|
4581
|
-
async function reachabilityCtx(cwd, files,
|
|
5337
|
+
async function reachabilityCtx(cwd, files, graph2) {
|
|
4582
5338
|
const prodSources = /* @__PURE__ */ new Map();
|
|
4583
5339
|
for (const rel of files) {
|
|
4584
5340
|
if (!/\.[cm]?[jt]sx?$/.test(rel)) continue;
|
|
@@ -4588,7 +5344,7 @@ async function reachabilityCtx(cwd, files, graph) {
|
|
|
4588
5344
|
} catch {
|
|
4589
5345
|
}
|
|
4590
5346
|
}
|
|
4591
|
-
return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
|
|
5347
|
+
return { productionUsage: indexPackageUsage(prodSources), graph: graph2, sources: prodSources };
|
|
4592
5348
|
}
|
|
4593
5349
|
|
|
4594
5350
|
// src/modules/mcp/index.ts
|
|
@@ -4940,10 +5696,143 @@ init_finding();
|
|
|
4940
5696
|
// src/modules/sast/interproc.ts
|
|
4941
5697
|
init_finding();
|
|
4942
5698
|
|
|
4943
|
-
// src/
|
|
4944
|
-
var
|
|
4945
|
-
|
|
4946
|
-
|
|
5699
|
+
// src/runtime/lru.ts
|
|
5700
|
+
var LruMap = class {
|
|
5701
|
+
constructor(max) {
|
|
5702
|
+
this.max = max;
|
|
5703
|
+
if (max < 1) throw new Error("LruMap max must be >= 1");
|
|
5704
|
+
}
|
|
5705
|
+
max;
|
|
5706
|
+
map = /* @__PURE__ */ new Map();
|
|
5707
|
+
get size() {
|
|
5708
|
+
return this.map.size;
|
|
5709
|
+
}
|
|
5710
|
+
has(key) {
|
|
5711
|
+
return this.map.has(key);
|
|
5712
|
+
}
|
|
5713
|
+
get(key) {
|
|
5714
|
+
if (!this.map.has(key)) return void 0;
|
|
5715
|
+
const value = this.map.get(key);
|
|
5716
|
+
this.map.delete(key);
|
|
5717
|
+
this.map.set(key, value);
|
|
5718
|
+
return value;
|
|
5719
|
+
}
|
|
5720
|
+
set(key, value) {
|
|
5721
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
5722
|
+
this.map.set(key, value);
|
|
5723
|
+
while (this.map.size > this.max) {
|
|
5724
|
+
const oldest = this.map.keys().next().value;
|
|
5725
|
+
if (oldest === void 0) break;
|
|
5726
|
+
this.map.delete(oldest);
|
|
5727
|
+
}
|
|
5728
|
+
return this;
|
|
5729
|
+
}
|
|
5730
|
+
};
|
|
5731
|
+
function packageVersion() {
|
|
5732
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
5733
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
5734
|
+
try {
|
|
5735
|
+
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
5736
|
+
const pkg = JSON.parse(raw);
|
|
5737
|
+
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
5738
|
+
return pkg.version;
|
|
5739
|
+
}
|
|
5740
|
+
} catch {
|
|
5741
|
+
}
|
|
5742
|
+
const parent = dirname(dir);
|
|
5743
|
+
if (parent === dir) break;
|
|
5744
|
+
dir = parent;
|
|
5745
|
+
}
|
|
5746
|
+
throw new Error("fullshield package.json not found");
|
|
5747
|
+
}
|
|
5748
|
+
|
|
5749
|
+
// src/core/parse-pool.ts
|
|
5750
|
+
var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
5751
|
+
var cache = new LruMap(64);
|
|
5752
|
+
function parsePoolEnabled() {
|
|
5753
|
+
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
5754
|
+
}
|
|
5755
|
+
function parsePoolSize() {
|
|
5756
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
5757
|
+
if (raw !== void 0 && raw !== "") {
|
|
5758
|
+
const parsed = Number(raw);
|
|
5759
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
|
|
5760
|
+
}
|
|
5761
|
+
return Math.min(2, Math.max(1, availableParallelism() - 1));
|
|
5762
|
+
}
|
|
5763
|
+
function workerMemoryMb() {
|
|
5764
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
|
|
5765
|
+
const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
|
|
5766
|
+
if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
|
|
5767
|
+
return 512;
|
|
5768
|
+
}
|
|
5769
|
+
function parseCacheDir() {
|
|
5770
|
+
return join(homedir(), ".cache", "fullshield", "parse");
|
|
5771
|
+
}
|
|
5772
|
+
function parseCacheKey(code) {
|
|
5773
|
+
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
5774
|
+
}
|
|
5775
|
+
function parseOne(code) {
|
|
5776
|
+
const key = parseCacheKey(code);
|
|
5777
|
+
const hit2 = cache.get(key);
|
|
5778
|
+
if (hit2 !== void 0) return Promise.resolve(hit2);
|
|
5779
|
+
return new Promise((resolve5, reject) => {
|
|
5780
|
+
const worker = new Worker(fileURLToPath(workerUrl), {
|
|
5781
|
+
workerData: { code },
|
|
5782
|
+
resourceLimits: { maxOldGenerationSizeMb: workerMemoryMb() }
|
|
5783
|
+
});
|
|
5784
|
+
let settled = false;
|
|
5785
|
+
const finish = () => {
|
|
5786
|
+
void worker.terminate();
|
|
5787
|
+
};
|
|
5788
|
+
const succeed = (ast) => {
|
|
5789
|
+
if (settled) return;
|
|
5790
|
+
settled = true;
|
|
5791
|
+
cache.set(key, ast);
|
|
5792
|
+
finish();
|
|
5793
|
+
resolve5(ast);
|
|
5794
|
+
};
|
|
5795
|
+
const fail = (error) => {
|
|
5796
|
+
if (settled) return;
|
|
5797
|
+
settled = true;
|
|
5798
|
+
finish();
|
|
5799
|
+
reject(error);
|
|
5800
|
+
};
|
|
5801
|
+
worker.once("message", (ast) => {
|
|
5802
|
+
succeed(ast);
|
|
5803
|
+
});
|
|
5804
|
+
worker.once("error", (error) => {
|
|
5805
|
+
fail(error);
|
|
5806
|
+
});
|
|
5807
|
+
worker.once("exit", (code2) => {
|
|
5808
|
+
if (settled) return;
|
|
5809
|
+
fail(new Error(`parse worker exited ${code2 ?? "null"}`));
|
|
5810
|
+
});
|
|
5811
|
+
});
|
|
5812
|
+
}
|
|
5813
|
+
async function parseWithPool(codes) {
|
|
5814
|
+
const size = parsePoolSize();
|
|
5815
|
+
const out = new Array(codes.length);
|
|
5816
|
+
let next = 0;
|
|
5817
|
+
async function run() {
|
|
5818
|
+
for (; ; ) {
|
|
5819
|
+
const index = next;
|
|
5820
|
+
next += 1;
|
|
5821
|
+
if (index >= codes.length) return;
|
|
5822
|
+
const code = codes[index];
|
|
5823
|
+
if (code === void 0) return;
|
|
5824
|
+
out[index] = await parseOne(code);
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
const workers = Math.min(size, Math.max(1, codes.length));
|
|
5828
|
+
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
5829
|
+
return out;
|
|
5830
|
+
}
|
|
5831
|
+
|
|
5832
|
+
// src/modules/sast/frameworks.ts
|
|
5833
|
+
var NEXT_HANDLERS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
5834
|
+
var NEST_HTTP = /^(Get|Post|Put|Patch|Delete|All|Head|Options)$/;
|
|
5835
|
+
var NEST_INPUT = /^(Body|Query|Param|Headers)$/;
|
|
4947
5836
|
var REQ_READERS = /* @__PURE__ */ new Set(["json", "formData", "text", "arrayBuffer"]);
|
|
4948
5837
|
var HONO_READERS = /* @__PURE__ */ new Set(["query", "param", "header", "json", "parseBody", "text"]);
|
|
4949
5838
|
function decoratorExpr(dec) {
|
|
@@ -5075,6 +5964,10 @@ function isMcpArgsTaintSource(node) {
|
|
|
5075
5964
|
}
|
|
5076
5965
|
|
|
5077
5966
|
// src/modules/sast/taint.ts
|
|
5967
|
+
var customTaint;
|
|
5968
|
+
function setCustomTaint(config) {
|
|
5969
|
+
customTaint = config;
|
|
5970
|
+
}
|
|
5078
5971
|
var SCHEMA_ROOT = /^(z|zod|joi|yup|schema)$/i;
|
|
5079
5972
|
function objectChainMentions(node, pattern) {
|
|
5080
5973
|
let current = node;
|
|
@@ -5220,6 +6113,61 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
|
5220
6113
|
return false;
|
|
5221
6114
|
}
|
|
5222
6115
|
}
|
|
6116
|
+
function valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners) {
|
|
6117
|
+
if (node === void 0) return false;
|
|
6118
|
+
if (isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) return false;
|
|
6119
|
+
if (node.type === "Identifier") {
|
|
6120
|
+
if (nosqlSanitized.has(node.name)) return false;
|
|
6121
|
+
return built.has(node.name) || tainted.has(node.name);
|
|
6122
|
+
}
|
|
6123
|
+
if (isRequestObjectNode(node)) return true;
|
|
6124
|
+
if (node.type === "AwaitExpression") {
|
|
6125
|
+
return valueFeedsFilter(node.argument, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
6126
|
+
}
|
|
6127
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && filterReturners.has(node.callee.name)) {
|
|
6128
|
+
return true;
|
|
6129
|
+
}
|
|
6130
|
+
if (node.type === "ObjectExpression") {
|
|
6131
|
+
return node.properties.some((prop) => {
|
|
6132
|
+
if (prop.type === "SpreadElement") {
|
|
6133
|
+
return valueFeedsFilter(prop.argument, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
6134
|
+
}
|
|
6135
|
+
if (prop.type !== "Property") return false;
|
|
6136
|
+
return valueFeedsFilter(prop.value, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
6137
|
+
});
|
|
6138
|
+
}
|
|
6139
|
+
if (node.type === "ArrayExpression") {
|
|
6140
|
+
return node.elements.some((el) => {
|
|
6141
|
+
if (el === null) return false;
|
|
6142
|
+
if (el.type === "SpreadElement") {
|
|
6143
|
+
return valueFeedsFilter(el.argument, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
6144
|
+
}
|
|
6145
|
+
return valueFeedsFilter(el, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
6146
|
+
});
|
|
6147
|
+
}
|
|
6148
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
|
|
6149
|
+
return node.arguments.some(
|
|
6150
|
+
(arg) => arg.type !== "SpreadElement" && valueFeedsFilter(arg, tainted, returners, nosqlSanitized, built, filterReturners)
|
|
6151
|
+
);
|
|
6152
|
+
}
|
|
6153
|
+
if (node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "MemberExpression") {
|
|
6154
|
+
return exprIsTainted(node, tainted, returners);
|
|
6155
|
+
}
|
|
6156
|
+
return false;
|
|
6157
|
+
}
|
|
6158
|
+
function initIsBuiltFilter(node, tainted, returners, nosqlSanitized, built, filterReturners) {
|
|
6159
|
+
if (node.type === "AwaitExpression") {
|
|
6160
|
+
return initIsBuiltFilter(node.argument, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
6161
|
+
}
|
|
6162
|
+
if (node.type === "Identifier") return built.has(node.name) && !nosqlSanitized.has(node.name);
|
|
6163
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && filterReturners.has(node.callee.name)) {
|
|
6164
|
+
return true;
|
|
6165
|
+
}
|
|
6166
|
+
if (node.type === "ObjectExpression" || node.type === "ArrayExpression") {
|
|
6167
|
+
return valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
6168
|
+
}
|
|
6169
|
+
return node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign" && valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
6170
|
+
}
|
|
5223
6171
|
function exprIsSanitized(node, tainted, names, isCall, returners = NO_RETURNERS) {
|
|
5224
6172
|
if (node === void 0) return true;
|
|
5225
6173
|
if (isCall(node)) return true;
|
|
@@ -5283,7 +6231,7 @@ function seedMcpToolParams(scope, tainted) {
|
|
|
5283
6231
|
for (const name of boundNames(param)) tainted.add(name);
|
|
5284
6232
|
}
|
|
5285
6233
|
}
|
|
5286
|
-
function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
6234
|
+
function analyzeScope(scope, seed = [], returners = NO_RETURNERS, filterReturners = NO_RETURNERS, initialBuilt = NO_RETURNERS) {
|
|
5287
6235
|
const tainted = new Set(seed);
|
|
5288
6236
|
seedDestructuredRequestParams(scope, tainted);
|
|
5289
6237
|
seedFrameworkParams(scope, tainted);
|
|
@@ -5291,6 +6239,8 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5291
6239
|
const urlSanitized = /* @__PURE__ */ new Set();
|
|
5292
6240
|
const xssSanitized = /* @__PURE__ */ new Set();
|
|
5293
6241
|
const nosqlSanitized = /* @__PURE__ */ new Set();
|
|
6242
|
+
const sqlCleared = /* @__PURE__ */ new Set();
|
|
6243
|
+
const customPath = /* @__PURE__ */ new Set();
|
|
5294
6244
|
let changed = true;
|
|
5295
6245
|
while (changed) {
|
|
5296
6246
|
changed = false;
|
|
@@ -5301,6 +6251,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5301
6251
|
for (const name of boundNames(node.id)) nosqlSanitized.add(name);
|
|
5302
6252
|
}
|
|
5303
6253
|
if (isSchemaValidatorCall(init)) return;
|
|
6254
|
+
applyCustomCall(init, boundNames(node.id), tainted, sqlCleared, customPath);
|
|
5304
6255
|
if (isUrlSanitizerCall(init)) {
|
|
5305
6256
|
for (const name of boundNames(node.id)) urlSanitized.add(name);
|
|
5306
6257
|
}
|
|
@@ -5321,6 +6272,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5321
6272
|
for (const name of boundNames(node.left)) nosqlSanitized.add(name);
|
|
5322
6273
|
}
|
|
5323
6274
|
if (isSchemaValidatorCall(node.right)) return;
|
|
6275
|
+
applyCustomCall(node.right, boundNames(node.left), tainted, sqlCleared, customPath);
|
|
5324
6276
|
if (isUrlSanitizerCall(node.right)) {
|
|
5325
6277
|
for (const name of boundNames(node.left)) urlSanitized.add(name);
|
|
5326
6278
|
}
|
|
@@ -5347,7 +6299,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5347
6299
|
for (const name of boundNames(node.left)) resolved.add(name);
|
|
5348
6300
|
}
|
|
5349
6301
|
});
|
|
5350
|
-
const pathSanitized =
|
|
6302
|
+
const pathSanitized = new Set(customPath);
|
|
5351
6303
|
walkScope(scope, (node) => {
|
|
5352
6304
|
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
5353
6305
|
if (propName(node.callee) !== "startsWith") return;
|
|
@@ -5389,17 +6341,92 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5389
6341
|
for (const name of boundNames(node.left)) requestObjects.add(name);
|
|
5390
6342
|
}
|
|
5391
6343
|
});
|
|
6344
|
+
const builtFilters = new Set(initialBuilt);
|
|
6345
|
+
let grew = true;
|
|
6346
|
+
while (grew) {
|
|
6347
|
+
grew = false;
|
|
6348
|
+
const add = (names) => {
|
|
6349
|
+
for (const name of names) {
|
|
6350
|
+
if (nosqlSanitized.has(name) || builtFilters.has(name)) continue;
|
|
6351
|
+
builtFilters.add(name);
|
|
6352
|
+
grew = true;
|
|
6353
|
+
}
|
|
6354
|
+
};
|
|
6355
|
+
walkScope(scope, (node) => {
|
|
6356
|
+
if (node.type === "VariableDeclarator" && node.init !== null) {
|
|
6357
|
+
if (initIsBuiltFilter(node.init, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
|
|
6358
|
+
add(boundNames(node.id));
|
|
6359
|
+
}
|
|
6360
|
+
}
|
|
6361
|
+
if (node.type !== "AssignmentExpression") return;
|
|
6362
|
+
if (node.left.type === "Identifier" && initIsBuiltFilter(node.right, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
|
|
6363
|
+
add([node.left.name]);
|
|
6364
|
+
}
|
|
6365
|
+
if (node.left.type === "MemberExpression" && node.left.object.type === "Identifier" && valueFeedsFilter(node.right, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
|
|
6366
|
+
add([node.left.object.name]);
|
|
6367
|
+
}
|
|
6368
|
+
});
|
|
6369
|
+
}
|
|
5392
6370
|
return {
|
|
5393
6371
|
tainted,
|
|
5394
6372
|
pathSanitized,
|
|
6373
|
+
sqlCleared,
|
|
5395
6374
|
urlSanitized,
|
|
5396
6375
|
xssSanitized,
|
|
5397
6376
|
nosqlSanitized,
|
|
5398
6377
|
hasPasswordName,
|
|
5399
6378
|
requestObjects,
|
|
6379
|
+
builtFilters,
|
|
6380
|
+
filterReturners: new Set(filterReturners),
|
|
5400
6381
|
returners: new Set(returners)
|
|
5401
6382
|
};
|
|
5402
6383
|
}
|
|
6384
|
+
function locallyDeclared(scope) {
|
|
6385
|
+
const names = /* @__PURE__ */ new Set();
|
|
6386
|
+
if (isFunctionNode(scope)) {
|
|
6387
|
+
for (const param of scope.params) {
|
|
6388
|
+
for (const name of boundNames(param)) names.add(name);
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
walkScope(scope, (node) => {
|
|
6392
|
+
if (node.type === "VariableDeclarator") {
|
|
6393
|
+
for (const name of boundNames(node.id)) names.add(name);
|
|
6394
|
+
}
|
|
6395
|
+
if (node.type === "FunctionDeclaration" && node.id !== null) names.add(node.id.name);
|
|
6396
|
+
});
|
|
6397
|
+
return names;
|
|
6398
|
+
}
|
|
6399
|
+
function inheritedBuiltFilters(fn, facts) {
|
|
6400
|
+
const local = locallyDeclared(fn);
|
|
6401
|
+
const inherited = /* @__PURE__ */ new Set();
|
|
6402
|
+
let parent = fn.parent;
|
|
6403
|
+
while (parent !== void 0 && !isFunctionNode(parent)) {
|
|
6404
|
+
parent = parent.parent;
|
|
6405
|
+
}
|
|
6406
|
+
const outer = parent === void 0 ? void 0 : facts.get(parent);
|
|
6407
|
+
if (outer === void 0) return inherited;
|
|
6408
|
+
for (const name of outer.builtFilters) {
|
|
6409
|
+
if (!local.has(name)) inherited.add(name);
|
|
6410
|
+
}
|
|
6411
|
+
return inherited;
|
|
6412
|
+
}
|
|
6413
|
+
function scopeReturnsBuiltFilter(scope, facts) {
|
|
6414
|
+
let yes = false;
|
|
6415
|
+
walkScope(scope, (node) => {
|
|
6416
|
+
if (node.type !== "ReturnStatement" || node.argument === null) return;
|
|
6417
|
+
if (initIsBuiltFilter(
|
|
6418
|
+
node.argument,
|
|
6419
|
+
facts.tainted,
|
|
6420
|
+
facts.returners,
|
|
6421
|
+
facts.nosqlSanitized,
|
|
6422
|
+
facts.builtFilters,
|
|
6423
|
+
facts.filterReturners
|
|
6424
|
+
)) {
|
|
6425
|
+
yes = true;
|
|
6426
|
+
}
|
|
6427
|
+
});
|
|
6428
|
+
return yes;
|
|
6429
|
+
}
|
|
5403
6430
|
function functionName(fn) {
|
|
5404
6431
|
if (fn.type === "FunctionDeclaration" && fn.id !== null) return fn.id.name;
|
|
5405
6432
|
if (isFunctionNode(fn) && fn.id !== null && fn.id.type === "Identifier") return fn.id.name;
|
|
@@ -5474,7 +6501,7 @@ function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
|
5474
6501
|
}
|
|
5475
6502
|
return changed;
|
|
5476
6503
|
}
|
|
5477
|
-
function analyzeFile(ast,
|
|
6504
|
+
function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
5478
6505
|
attachParents(ast);
|
|
5479
6506
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
5480
6507
|
const functions = [];
|
|
@@ -5488,12 +6515,13 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5488
6515
|
}
|
|
5489
6516
|
const seeds = /* @__PURE__ */ new Map();
|
|
5490
6517
|
for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
|
|
5491
|
-
const returners = new Set(
|
|
6518
|
+
const returners = new Set(extraReturners2);
|
|
6519
|
+
const filterReturners = /* @__PURE__ */ new Set();
|
|
5492
6520
|
for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
|
|
5493
6521
|
let changed = false;
|
|
5494
|
-
facts.set(ast, analyzeScope(ast, [], returners));
|
|
6522
|
+
facts.set(ast, analyzeScope(ast, [], returners, filterReturners));
|
|
5495
6523
|
for (const fn of functions) {
|
|
5496
|
-
facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners));
|
|
6524
|
+
facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners, filterReturners, inheritedBuiltFilters(fn, facts)));
|
|
5497
6525
|
}
|
|
5498
6526
|
for (const fn of functions) {
|
|
5499
6527
|
const name = functionName(fn);
|
|
@@ -5503,6 +6531,10 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5503
6531
|
returners.add(name);
|
|
5504
6532
|
changed = true;
|
|
5505
6533
|
}
|
|
6534
|
+
if (scopeReturnsBuiltFilter(fn, f) && !filterReturners.has(name)) {
|
|
6535
|
+
filterReturners.add(name);
|
|
6536
|
+
changed = true;
|
|
6537
|
+
}
|
|
5506
6538
|
}
|
|
5507
6539
|
for (const fn of functions) {
|
|
5508
6540
|
const f = facts.get(fn);
|
|
@@ -5535,6 +6567,54 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5535
6567
|
}
|
|
5536
6568
|
return facts;
|
|
5537
6569
|
}
|
|
6570
|
+
var SQL_SINK_NAMES = /* @__PURE__ */ new Set(["query", "execute"]);
|
|
6571
|
+
var CMD_SINK_NAMES = /* @__PURE__ */ new Set(["exec", "execSync", "spawn", "spawnSync"]);
|
|
6572
|
+
var PATH_SINK_NAMES = /* @__PURE__ */ new Set(["readFile", "readFileSync", "writeFile", "writeFileSync"]);
|
|
6573
|
+
function sinkClasses(scope, tainted, returners) {
|
|
6574
|
+
const found = /* @__PURE__ */ new Set();
|
|
6575
|
+
walkScope(scope, (node) => {
|
|
6576
|
+
if (node.type !== "CallExpression") return;
|
|
6577
|
+
const name = calleeName(node);
|
|
6578
|
+
const first = node.arguments[0];
|
|
6579
|
+
if (name === void 0 || first === void 0 || first.type === "SpreadElement") return;
|
|
6580
|
+
if (!exprIsTainted(first, tainted, returners)) return;
|
|
6581
|
+
if (SQL_SINK_NAMES.has(name)) found.add("sqli");
|
|
6582
|
+
if (CMD_SINK_NAMES.has(name)) found.add("cmdi");
|
|
6583
|
+
if (PATH_SINK_NAMES.has(name)) found.add("path");
|
|
6584
|
+
});
|
|
6585
|
+
return [...found].sort();
|
|
6586
|
+
}
|
|
6587
|
+
function fileTaintSummary(ast) {
|
|
6588
|
+
attachParents(ast);
|
|
6589
|
+
const facts = analyzeFile(ast);
|
|
6590
|
+
const returnsTaint = [];
|
|
6591
|
+
const functions = [];
|
|
6592
|
+
walk(ast, (node) => {
|
|
6593
|
+
if (!isFunctionNode(node)) return;
|
|
6594
|
+
const name = functionName(node);
|
|
6595
|
+
const own = facts.get(node);
|
|
6596
|
+
if (name === void 0 || own === void 0) return;
|
|
6597
|
+
if (scopeReturnsTaint(node, own.tainted, own.returners)) returnsTaint.push(name);
|
|
6598
|
+
const paramsToReturn = [];
|
|
6599
|
+
const sinks = [];
|
|
6600
|
+
node.params.forEach((param, index) => {
|
|
6601
|
+
const pname = boundNames(param)[0];
|
|
6602
|
+
if (pname === void 0) return;
|
|
6603
|
+
const seeded = analyzeScope(node, [pname]);
|
|
6604
|
+
if (scopeReturnsTaint(node, seeded.tainted, seeded.returners)) paramsToReturn.push(index);
|
|
6605
|
+
for (const cls of sinkClasses(node, seeded.tainted, seeded.returners)) sinks.push({ param: index, class: cls });
|
|
6606
|
+
});
|
|
6607
|
+
functions.push({
|
|
6608
|
+
name,
|
|
6609
|
+
line: node.loc?.start.line ?? 0,
|
|
6610
|
+
paramsToReturn,
|
|
6611
|
+
sinks
|
|
6612
|
+
});
|
|
6613
|
+
});
|
|
6614
|
+
returnsTaint.sort();
|
|
6615
|
+
functions.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
|
|
6616
|
+
return { returnsTaint, functions };
|
|
6617
|
+
}
|
|
5538
6618
|
function functionsThatReturnTaint(ast) {
|
|
5539
6619
|
attachParents(ast);
|
|
5540
6620
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -5556,18 +6636,50 @@ function factsFor(node, map) {
|
|
|
5556
6636
|
return map.get(enclosingScope(node)) ?? {
|
|
5557
6637
|
tainted: /* @__PURE__ */ new Set(),
|
|
5558
6638
|
pathSanitized: /* @__PURE__ */ new Set(),
|
|
6639
|
+
sqlCleared: /* @__PURE__ */ new Set(),
|
|
5559
6640
|
urlSanitized: /* @__PURE__ */ new Set(),
|
|
5560
6641
|
xssSanitized: /* @__PURE__ */ new Set(),
|
|
5561
6642
|
nosqlSanitized: /* @__PURE__ */ new Set(),
|
|
5562
6643
|
hasPasswordName: false,
|
|
5563
6644
|
requestObjects: /* @__PURE__ */ new Set(),
|
|
6645
|
+
builtFilters: /* @__PURE__ */ new Set(),
|
|
6646
|
+
filterReturners: /* @__PURE__ */ new Set(),
|
|
5564
6647
|
returners: /* @__PURE__ */ new Set()
|
|
5565
6648
|
};
|
|
5566
6649
|
}
|
|
6650
|
+
function nodeHoldsBuiltFilter(node, map) {
|
|
6651
|
+
const facts = factsFor(node, map);
|
|
6652
|
+
if (node.type === "Identifier") return facts.builtFilters.has(node.name);
|
|
6653
|
+
if (node.type === "AwaitExpression") return nodeHoldsBuiltFilter(node.argument, map);
|
|
6654
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier") {
|
|
6655
|
+
return facts.filterReturners.has(node.callee.name);
|
|
6656
|
+
}
|
|
6657
|
+
return false;
|
|
6658
|
+
}
|
|
5567
6659
|
function nodeIsTainted(node, map) {
|
|
5568
6660
|
const facts = factsFor(node, map);
|
|
5569
6661
|
return exprIsTainted(node, facts.tainted, facts.returners);
|
|
5570
6662
|
}
|
|
6663
|
+
function nodeIsSqlCleared(node, map) {
|
|
6664
|
+
if (node.type === "Identifier") return factsFor(node, map).sqlCleared.has(node.name);
|
|
6665
|
+
return false;
|
|
6666
|
+
}
|
|
6667
|
+
function applyCustomCall(init, names, tainted, sqlCleared, customPath) {
|
|
6668
|
+
if (customTaint === void 0 || init === void 0 || init.type !== "CallExpression") return;
|
|
6669
|
+
const called2 = calleeName(init);
|
|
6670
|
+
if (called2 === void 0) return;
|
|
6671
|
+
if (customTaint.sources.has(called2)) {
|
|
6672
|
+
for (const name of names) tainted.add(name);
|
|
6673
|
+
}
|
|
6674
|
+
const clears = customTaint.sanitizers.get(called2);
|
|
6675
|
+
if (clears === void 0) return;
|
|
6676
|
+
if (clears.has("sqli")) {
|
|
6677
|
+
for (const name of names) sqlCleared.add(name);
|
|
6678
|
+
}
|
|
6679
|
+
if (clears.has("path")) {
|
|
6680
|
+
for (const name of names) customPath.add(name);
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
5571
6683
|
function nodeIsPathSanitized(node, map) {
|
|
5572
6684
|
if (node.type !== "Identifier") return false;
|
|
5573
6685
|
return factsFor(node, map).pathSanitized.has(node.name);
|
|
@@ -5590,6 +6702,147 @@ function nodeIsNosqlSanitized(node, map) {
|
|
|
5590
6702
|
return false;
|
|
5591
6703
|
}
|
|
5592
6704
|
|
|
6705
|
+
// src/modules/sast/summaries.ts
|
|
6706
|
+
function taintSummariesEnabled() {
|
|
6707
|
+
return process.env.FULLSHIELD_TAINT_SUMMARIES === "1";
|
|
6708
|
+
}
|
|
6709
|
+
var configHash = "";
|
|
6710
|
+
function setTaintSummaryConfig(config) {
|
|
6711
|
+
fileMemo.clear();
|
|
6712
|
+
if (config === void 0) {
|
|
6713
|
+
configHash = "";
|
|
6714
|
+
return;
|
|
6715
|
+
}
|
|
6716
|
+
configHash = createHash("sha256").update(
|
|
6717
|
+
JSON.stringify({
|
|
6718
|
+
maxFileBytes: config.sast.maxFileBytes,
|
|
6719
|
+
sources: config.sast.sources ?? [],
|
|
6720
|
+
sanitizers: config.sast.sanitizers ?? [],
|
|
6721
|
+
taintV2: taintV2Enabled(),
|
|
6722
|
+
frameworks: frameworksEnabled(),
|
|
6723
|
+
gaps: sastGapsEnabled(),
|
|
6724
|
+
classes: classesEnabled(),
|
|
6725
|
+
custom: process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1"
|
|
6726
|
+
})
|
|
6727
|
+
).digest("hex");
|
|
6728
|
+
}
|
|
6729
|
+
function summaryDir() {
|
|
6730
|
+
return process.env.FULLSHIELD_TAINT_SUMMARY_DIR ?? join(parseCacheDir(), "taint");
|
|
6731
|
+
}
|
|
6732
|
+
function taintSummaryKey(source, extra = "") {
|
|
6733
|
+
return createHash("sha256").update(packageVersion()).update("\0").update(configHash).update("\0").update(extra).update("\0").update(source).digest("hex");
|
|
6734
|
+
}
|
|
6735
|
+
function summaryPath(key) {
|
|
6736
|
+
return join(summaryDir(), `${key}.json`);
|
|
6737
|
+
}
|
|
6738
|
+
function isStringArray(value) {
|
|
6739
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
6740
|
+
}
|
|
6741
|
+
function readJson3(key) {
|
|
6742
|
+
let text;
|
|
6743
|
+
try {
|
|
6744
|
+
text = readFileSync(summaryPath(key), "utf8");
|
|
6745
|
+
} catch {
|
|
6746
|
+
return void 0;
|
|
6747
|
+
}
|
|
6748
|
+
try {
|
|
6749
|
+
return JSON.parse(text);
|
|
6750
|
+
} catch {
|
|
6751
|
+
discard(key);
|
|
6752
|
+
return void 0;
|
|
6753
|
+
}
|
|
6754
|
+
}
|
|
6755
|
+
function discard(key) {
|
|
6756
|
+
rmSync(summaryPath(key), { force: true });
|
|
6757
|
+
}
|
|
6758
|
+
function writeJson(key, value) {
|
|
6759
|
+
const dir = summaryDir();
|
|
6760
|
+
mkdirSync(dir, { recursive: true });
|
|
6761
|
+
writeFileSync(summaryPath(key), JSON.stringify(value));
|
|
6762
|
+
}
|
|
6763
|
+
function readFileSummary(key) {
|
|
6764
|
+
const raw = readJson3(key);
|
|
6765
|
+
if (typeof raw !== "object" || raw === null) {
|
|
6766
|
+
if (raw !== void 0) discard(key);
|
|
6767
|
+
return void 0;
|
|
6768
|
+
}
|
|
6769
|
+
const row = raw;
|
|
6770
|
+
if (row.v !== 1 || !isStringArray(row.returnsTaint) || !Array.isArray(row.functions)) {
|
|
6771
|
+
discard(key);
|
|
6772
|
+
return void 0;
|
|
6773
|
+
}
|
|
6774
|
+
return row;
|
|
6775
|
+
}
|
|
6776
|
+
var fileMemo = /* @__PURE__ */ new Map();
|
|
6777
|
+
function cachedReturners(source, ast) {
|
|
6778
|
+
const key = taintSummaryKey(source, "file");
|
|
6779
|
+
const memo = fileMemo.get(key);
|
|
6780
|
+
if (memo !== void 0) return new Set(memo.returnsTaint);
|
|
6781
|
+
const cached = readFileSummary(key);
|
|
6782
|
+
if (cached !== void 0) {
|
|
6783
|
+
fileMemo.set(key, cached);
|
|
6784
|
+
return new Set(cached.returnsTaint);
|
|
6785
|
+
}
|
|
6786
|
+
const summary = fileTaintSummary(ast);
|
|
6787
|
+
const stored = { v: 1, returnsTaint: summary.returnsTaint, functions: summary.functions };
|
|
6788
|
+
fileMemo.set(key, stored);
|
|
6789
|
+
writeJson(key, stored);
|
|
6790
|
+
return new Set(stored.returnsTaint);
|
|
6791
|
+
}
|
|
6792
|
+
function lists(facts) {
|
|
6793
|
+
const names = (set) => [...set].sort();
|
|
6794
|
+
return {
|
|
6795
|
+
v: 1,
|
|
6796
|
+
tainted: names(facts.tainted),
|
|
6797
|
+
pathSanitized: names(facts.pathSanitized),
|
|
6798
|
+
sqlCleared: names(facts.sqlCleared),
|
|
6799
|
+
urlSanitized: names(facts.urlSanitized),
|
|
6800
|
+
xssSanitized: names(facts.xssSanitized),
|
|
6801
|
+
nosqlSanitized: names(facts.nosqlSanitized),
|
|
6802
|
+
requestObjects: names(facts.requestObjects),
|
|
6803
|
+
builtFilters: names(facts.builtFilters),
|
|
6804
|
+
filterReturners: names(facts.filterReturners),
|
|
6805
|
+
returners: names(facts.returners),
|
|
6806
|
+
hasPasswordName: facts.hasPasswordName
|
|
6807
|
+
};
|
|
6808
|
+
}
|
|
6809
|
+
function factsFrom(row) {
|
|
6810
|
+
return {
|
|
6811
|
+
tainted: new Set(row.tainted),
|
|
6812
|
+
pathSanitized: new Set(row.pathSanitized),
|
|
6813
|
+
sqlCleared: new Set(row.sqlCleared),
|
|
6814
|
+
urlSanitized: new Set(row.urlSanitized),
|
|
6815
|
+
xssSanitized: new Set(row.xssSanitized),
|
|
6816
|
+
nosqlSanitized: new Set(row.nosqlSanitized),
|
|
6817
|
+
requestObjects: new Set(row.requestObjects),
|
|
6818
|
+
builtFilters: new Set(row.builtFilters),
|
|
6819
|
+
filterReturners: new Set(row.filterReturners),
|
|
6820
|
+
returners: new Set(row.returners),
|
|
6821
|
+
hasPasswordName: row.hasPasswordName
|
|
6822
|
+
};
|
|
6823
|
+
}
|
|
6824
|
+
function readFacts(key) {
|
|
6825
|
+
const raw = readJson3(key);
|
|
6826
|
+
if (typeof raw !== "object" || raw === null) {
|
|
6827
|
+
if (raw !== void 0) discard(key);
|
|
6828
|
+
return void 0;
|
|
6829
|
+
}
|
|
6830
|
+
const row = raw;
|
|
6831
|
+
if (row.v !== 1 || !isStringArray(row.tainted) || !isStringArray(row.pathSanitized) || !isStringArray(row.sqlCleared) || !isStringArray(row.urlSanitized) || !isStringArray(row.xssSanitized) || !isStringArray(row.nosqlSanitized) || !isStringArray(row.requestObjects) || !isStringArray(row.builtFilters) || !isStringArray(row.filterReturners) || !isStringArray(row.returners) || typeof row.hasPasswordName !== "boolean") {
|
|
6832
|
+
discard(key);
|
|
6833
|
+
return void 0;
|
|
6834
|
+
}
|
|
6835
|
+
return factsFrom(row);
|
|
6836
|
+
}
|
|
6837
|
+
function cachedFunctionFacts(fileSource, fn, seed) {
|
|
6838
|
+
const key = taintSummaryKey(fileSource, `seed:${fn.loc?.start.line ?? 0}:${seed.join(",")}`);
|
|
6839
|
+
const cached = readFacts(key);
|
|
6840
|
+
if (cached !== void 0) return cached;
|
|
6841
|
+
const facts = analyzeFunctionWithSeed(fn, seed);
|
|
6842
|
+
writeJson(key, lists(facts));
|
|
6843
|
+
return facts;
|
|
6844
|
+
}
|
|
6845
|
+
|
|
5593
6846
|
// src/modules/sast/interproc.ts
|
|
5594
6847
|
var MAX_HOPS = 5;
|
|
5595
6848
|
function collectFunctions(file, ast) {
|
|
@@ -5700,22 +6953,22 @@ function collectInstances(ast, ns) {
|
|
|
5700
6953
|
if (node.type !== "VariableDeclarator" || node.init === null) return;
|
|
5701
6954
|
if (node.id.type !== "Identifier" || node.init.type !== "NewExpression") return;
|
|
5702
6955
|
if (node.init.callee.type !== "Identifier") return;
|
|
5703
|
-
const
|
|
5704
|
-
if (
|
|
5705
|
-
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);
|
|
5706
6959
|
});
|
|
5707
6960
|
return instances;
|
|
5708
6961
|
}
|
|
5709
6962
|
function resolveCallee(node, ns, instances, currentFile, functions) {
|
|
5710
6963
|
if (node.type !== "CallExpression") return void 0;
|
|
5711
6964
|
if (node.callee.type === "Identifier") {
|
|
5712
|
-
const
|
|
5713
|
-
if (
|
|
5714
|
-
if (
|
|
5715
|
-
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("#");
|
|
5716
6969
|
if (file !== void 0 && name !== void 0) return { file, name };
|
|
5717
6970
|
}
|
|
5718
|
-
return { file:
|
|
6971
|
+
return { file: hit2, name: node.callee.name };
|
|
5719
6972
|
}
|
|
5720
6973
|
if (functions.has(`${currentFile}#${node.callee.name}`)) {
|
|
5721
6974
|
return { file: currentFile, name: node.callee.name };
|
|
@@ -5822,9 +7075,9 @@ function runInterproc(parsed) {
|
|
|
5822
7075
|
if (fn === void 0) continue;
|
|
5823
7076
|
const param = fn.params[seed.paramIndex];
|
|
5824
7077
|
if (param === void 0) continue;
|
|
5825
|
-
const facts = analyzeFunctionWithSeed(fn.node, [param]);
|
|
5826
7078
|
const p = byFile.get(fn.file);
|
|
5827
7079
|
if (p === void 0) continue;
|
|
7080
|
+
const facts = taintSummariesEnabled() ? cachedFunctionFacts(p.source, fn.node, [param]) : analyzeFunctionWithSeed(fn.node, [param]);
|
|
5828
7081
|
const ns = requires.get(fn.file) ?? /* @__PURE__ */ new Map();
|
|
5829
7082
|
const inst = instances.get(fn.file) ?? /* @__PURE__ */ new Map();
|
|
5830
7083
|
walk(fn.node, (node) => {
|
|
@@ -6247,6 +7500,178 @@ var jwtRule = {
|
|
|
6247
7500
|
}
|
|
6248
7501
|
};
|
|
6249
7502
|
|
|
7503
|
+
// src/modules/sast/validation.ts
|
|
7504
|
+
var PRIMITIVE = /* @__PURE__ */ new Set(["string", "number", "boolean", "IsString", "IsNumber", "IsInt", "IsBoolean"]);
|
|
7505
|
+
var OPEN = /* @__PURE__ */ new Set(["any", "unknown", "object", "array", "record", "lazy", "union", "tuple"]);
|
|
7506
|
+
var SCHEMA_ROOT2 = /* @__PURE__ */ new Set(["z", "zod", "Joi", "joi"]);
|
|
7507
|
+
var ROUTE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
|
|
7508
|
+
function validationEnabled() {
|
|
7509
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_VALIDATION === "1";
|
|
7510
|
+
}
|
|
7511
|
+
function parentOf(node) {
|
|
7512
|
+
return node.parent;
|
|
7513
|
+
}
|
|
7514
|
+
function mentions(node, names) {
|
|
7515
|
+
let current = node;
|
|
7516
|
+
for (let i = 0; i < 12 && current !== void 0; i += 1) {
|
|
7517
|
+
if (current.type === "CallExpression") {
|
|
7518
|
+
const name = calleeName(current);
|
|
7519
|
+
if (name !== void 0 && names.has(name)) return true;
|
|
7520
|
+
current = current.callee.type === "MemberExpression" ? current.callee.object : void 0;
|
|
7521
|
+
continue;
|
|
7522
|
+
}
|
|
7523
|
+
if (current.type === "MemberExpression") {
|
|
7524
|
+
const name = propName(current);
|
|
7525
|
+
if (name !== void 0 && names.has(name)) return true;
|
|
7526
|
+
current = current.object;
|
|
7527
|
+
continue;
|
|
7528
|
+
}
|
|
7529
|
+
if (current.type === "Identifier" && names.has(current.name)) return true;
|
|
7530
|
+
break;
|
|
7531
|
+
}
|
|
7532
|
+
return false;
|
|
7533
|
+
}
|
|
7534
|
+
function rootName(node) {
|
|
7535
|
+
let current = node;
|
|
7536
|
+
for (let i = 0; i < 8 && current !== void 0; i += 1) {
|
|
7537
|
+
if (current.type === "Identifier") return current.name;
|
|
7538
|
+
if (current.type === "CallExpression") {
|
|
7539
|
+
current = current.callee;
|
|
7540
|
+
continue;
|
|
7541
|
+
}
|
|
7542
|
+
if (current.type === "MemberExpression") {
|
|
7543
|
+
current = current.object;
|
|
7544
|
+
continue;
|
|
7545
|
+
}
|
|
7546
|
+
break;
|
|
7547
|
+
}
|
|
7548
|
+
return void 0;
|
|
7549
|
+
}
|
|
7550
|
+
function propertyName(prop) {
|
|
7551
|
+
if (prop.type !== "Property") return void 0;
|
|
7552
|
+
const key = prop.key;
|
|
7553
|
+
if (key.type === "Identifier") return key.name;
|
|
7554
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
7555
|
+
return void 0;
|
|
7556
|
+
}
|
|
7557
|
+
function fieldsOfObjectSchema(node) {
|
|
7558
|
+
if (node.type !== "CallExpression" || calleeName(node) !== "object") return /* @__PURE__ */ new Set();
|
|
7559
|
+
const root = rootName(node.callee);
|
|
7560
|
+
if (root === void 0 || !SCHEMA_ROOT2.has(root)) return /* @__PURE__ */ new Set();
|
|
7561
|
+
const arg = node.arguments[0];
|
|
7562
|
+
if (arg === void 0 || arg.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
|
|
7563
|
+
const fields = /* @__PURE__ */ new Set();
|
|
7564
|
+
for (const prop of arg.properties) {
|
|
7565
|
+
if (prop.type !== "Property") continue;
|
|
7566
|
+
const name = propertyName(prop);
|
|
7567
|
+
if (name === void 0) continue;
|
|
7568
|
+
if (mentions(prop.value, OPEN)) continue;
|
|
7569
|
+
if (mentions(prop.value, PRIMITIVE)) fields.add(name);
|
|
7570
|
+
}
|
|
7571
|
+
return fields;
|
|
7572
|
+
}
|
|
7573
|
+
function decoratorName2(decorator) {
|
|
7574
|
+
const expr = decorator.type === "Decorator" ? decorator.expression : decorator;
|
|
7575
|
+
if (expr === void 0) return void 0;
|
|
7576
|
+
if (expr.type === "CallExpression") return calleeName(expr);
|
|
7577
|
+
if (expr.type === "Identifier") return expr.name;
|
|
7578
|
+
return void 0;
|
|
7579
|
+
}
|
|
7580
|
+
function fieldsOfClass(node) {
|
|
7581
|
+
if (node.type !== "ClassDeclaration" && node.type !== "ClassExpression") return /* @__PURE__ */ new Set();
|
|
7582
|
+
if (node.body.type !== "ClassBody") return /* @__PURE__ */ new Set();
|
|
7583
|
+
const fields = /* @__PURE__ */ new Set();
|
|
7584
|
+
for (const element of node.body.body) {
|
|
7585
|
+
if (element.type !== "PropertyDefinition") continue;
|
|
7586
|
+
const key = element.key;
|
|
7587
|
+
const name = key.type === "Identifier" ? key.name : void 0;
|
|
7588
|
+
if (name === void 0) continue;
|
|
7589
|
+
const decorators = element.decorators ?? [];
|
|
7590
|
+
if (decorators.some((decorator) => PRIMITIVE.has(decoratorName2(decorator) ?? ""))) fields.add(name);
|
|
7591
|
+
}
|
|
7592
|
+
return fields;
|
|
7593
|
+
}
|
|
7594
|
+
function programOf(node) {
|
|
7595
|
+
let current = node;
|
|
7596
|
+
while (current !== void 0 && current.type !== "Program") current = parentOf(current);
|
|
7597
|
+
return current;
|
|
7598
|
+
}
|
|
7599
|
+
function bindings(program) {
|
|
7600
|
+
const map = /* @__PURE__ */ new Map();
|
|
7601
|
+
walk(program, (node) => {
|
|
7602
|
+
if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.init !== null && node.init !== void 0) {
|
|
7603
|
+
const fields = fieldsOfObjectSchema(node.init);
|
|
7604
|
+
if (fields.size > 0) map.set(node.id.name, fields);
|
|
7605
|
+
}
|
|
7606
|
+
if (node.type === "ClassDeclaration" && node.id !== null) {
|
|
7607
|
+
const fields = fieldsOfClass(node);
|
|
7608
|
+
if (fields.size > 0) map.set(node.id.name, fields);
|
|
7609
|
+
}
|
|
7610
|
+
});
|
|
7611
|
+
return map;
|
|
7612
|
+
}
|
|
7613
|
+
var bindingCache = /* @__PURE__ */ new WeakMap();
|
|
7614
|
+
function fieldsOfExpr(expr, known) {
|
|
7615
|
+
if (expr.type === "Identifier") return new Set(known.get(expr.name) ?? []);
|
|
7616
|
+
if (expr.type === "ClassExpression" || expr.type === "ClassDeclaration") return fieldsOfClass(expr);
|
|
7617
|
+
const direct = fieldsOfObjectSchema(expr);
|
|
7618
|
+
if (expr.type !== "CallExpression") return direct;
|
|
7619
|
+
for (const arg of expr.arguments) {
|
|
7620
|
+
if (arg.type === "SpreadElement") continue;
|
|
7621
|
+
for (const name of fieldsOfExpr(arg, known)) direct.add(name);
|
|
7622
|
+
}
|
|
7623
|
+
return direct;
|
|
7624
|
+
}
|
|
7625
|
+
function isRouteCall2(node) {
|
|
7626
|
+
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return false;
|
|
7627
|
+
const method = propName(node.callee);
|
|
7628
|
+
if (method === void 0 || !ROUTE_METHODS.has(method)) return false;
|
|
7629
|
+
return node.callee.object.type === "Identifier" && /^(app|router)$/i.test(node.callee.object.name);
|
|
7630
|
+
}
|
|
7631
|
+
function routeOf(node) {
|
|
7632
|
+
let current = node;
|
|
7633
|
+
while (current !== void 0) {
|
|
7634
|
+
const parent = parentOf(current);
|
|
7635
|
+
if (isFunctionNode(current) && parent !== void 0 && isRouteCall2(parent)) return parent;
|
|
7636
|
+
current = parent;
|
|
7637
|
+
}
|
|
7638
|
+
return void 0;
|
|
7639
|
+
}
|
|
7640
|
+
function requestField(node) {
|
|
7641
|
+
if (node.type !== "MemberExpression" || node.computed === true) return void 0;
|
|
7642
|
+
const field = node.property.type === "Identifier" ? node.property.name : void 0;
|
|
7643
|
+
if (field === void 0 || node.object.type !== "MemberExpression" || node.object.computed === true) return void 0;
|
|
7644
|
+
const bag = node.object.property.type === "Identifier" ? node.object.property.name : void 0;
|
|
7645
|
+
if (bag !== "body" && bag !== "query" && bag !== "params") return void 0;
|
|
7646
|
+
if (node.object.object.type !== "Identifier" || node.object.object.name !== "req") return void 0;
|
|
7647
|
+
return field;
|
|
7648
|
+
}
|
|
7649
|
+
var routeCache = /* @__PURE__ */ new WeakMap();
|
|
7650
|
+
function fieldsForRoute(route) {
|
|
7651
|
+
const cached = routeCache.get(route);
|
|
7652
|
+
if (cached !== void 0) return cached;
|
|
7653
|
+
const program = programOf(route);
|
|
7654
|
+
const known = program === void 0 ? /* @__PURE__ */ new Map() : bindingCache.get(program) ?? bindings(program);
|
|
7655
|
+
if (program !== void 0) bindingCache.set(program, known);
|
|
7656
|
+
const fields = /* @__PURE__ */ new Set();
|
|
7657
|
+
if (route.type === "CallExpression") {
|
|
7658
|
+
for (const arg of route.arguments) {
|
|
7659
|
+
if (arg.type === "SpreadElement" || isFunctionNode(arg)) continue;
|
|
7660
|
+
for (const name of fieldsOfExpr(arg, known)) fields.add(name);
|
|
7661
|
+
}
|
|
7662
|
+
}
|
|
7663
|
+
routeCache.set(route, fields);
|
|
7664
|
+
return fields;
|
|
7665
|
+
}
|
|
7666
|
+
function isValidatedRouteField(node) {
|
|
7667
|
+
if (!validationEnabled()) return false;
|
|
7668
|
+
const field = requestField(node);
|
|
7669
|
+
if (field === void 0) return false;
|
|
7670
|
+
const route = routeOf(node);
|
|
7671
|
+
if (route === void 0) return false;
|
|
7672
|
+
return fieldsForRoute(route).has(field);
|
|
7673
|
+
}
|
|
7674
|
+
|
|
6250
7675
|
// src/modules/sast/rules/nosqli.ts
|
|
6251
7676
|
var MONGO_SINKS = /* @__PURE__ */ new Set([
|
|
6252
7677
|
"find",
|
|
@@ -6331,6 +7756,7 @@ function isNosqlSafeValue(node, source, isNosqlSanitized) {
|
|
|
6331
7756
|
if (isNosqlSanitized(node) || isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) {
|
|
6332
7757
|
return true;
|
|
6333
7758
|
}
|
|
7759
|
+
if (isValidatedRouteField(node)) return true;
|
|
6334
7760
|
return isSanitizeWrap(node, source);
|
|
6335
7761
|
}
|
|
6336
7762
|
function objectHasUnsafeTaintedValue(node, isTainted, isSafe) {
|
|
@@ -6394,6 +7820,14 @@ var nosqliRule = {
|
|
|
6394
7820
|
return;
|
|
6395
7821
|
}
|
|
6396
7822
|
if (middleware) return;
|
|
7823
|
+
if (ctx.holdsBuiltFilter(filter)) {
|
|
7824
|
+
ctx.report({
|
|
7825
|
+
node,
|
|
7826
|
+
message: "A Mongo filter built from request input is passed to the query.",
|
|
7827
|
+
confidence: "medium"
|
|
7828
|
+
});
|
|
7829
|
+
return;
|
|
7830
|
+
}
|
|
6397
7831
|
if (objectHasUnsafeTaintedValue(filter, (n) => ctx.isTainted(n), isSafe)) {
|
|
6398
7832
|
ctx.report({
|
|
6399
7833
|
node,
|
|
@@ -6570,7 +8004,7 @@ var sqliRule = {
|
|
|
6570
8004
|
CallExpression(node) {
|
|
6571
8005
|
if (node.type !== "CallExpression" || !isSqlSink2(node)) return;
|
|
6572
8006
|
const sql = node.arguments[0];
|
|
6573
|
-
if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql)) {
|
|
8007
|
+
if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql) && !ctx.isSqlCleared(sql)) {
|
|
6574
8008
|
ctx.report({
|
|
6575
8009
|
node,
|
|
6576
8010
|
message: "Tainted input is concatenated into a SQL string passed to query/execute."
|
|
@@ -6720,27 +8154,27 @@ function numberLiteral(node) {
|
|
|
6720
8154
|
return node.value;
|
|
6721
8155
|
}
|
|
6722
8156
|
function containsIdentMember(node, objectName) {
|
|
6723
|
-
let
|
|
8157
|
+
let hit2 = false;
|
|
6724
8158
|
walk(node, (child) => {
|
|
6725
8159
|
if (child.type === "MemberExpression" && child.object.type === "Identifier" && child.object.name === objectName) {
|
|
6726
|
-
|
|
8160
|
+
hit2 = true;
|
|
6727
8161
|
}
|
|
6728
8162
|
});
|
|
6729
|
-
return
|
|
8163
|
+
return hit2;
|
|
6730
8164
|
}
|
|
6731
8165
|
function containsCallNamed(node, name) {
|
|
6732
|
-
let
|
|
8166
|
+
let hit2 = false;
|
|
6733
8167
|
walk(node, (child) => {
|
|
6734
|
-
if (child.type === "CallExpression" && calleeName(child) === name)
|
|
8168
|
+
if (child.type === "CallExpression" && calleeName(child) === name) hit2 = true;
|
|
6735
8169
|
});
|
|
6736
|
-
return
|
|
8170
|
+
return hit2;
|
|
6737
8171
|
}
|
|
6738
8172
|
function containsOriginalName(node) {
|
|
6739
|
-
let
|
|
8173
|
+
let hit2 = false;
|
|
6740
8174
|
walk(node, (child) => {
|
|
6741
|
-
if (child.type === "MemberExpression" && propName(child) === "originalname")
|
|
8175
|
+
if (child.type === "MemberExpression" && propName(child) === "originalname") hit2 = true;
|
|
6742
8176
|
});
|
|
6743
|
-
return
|
|
8177
|
+
return hit2;
|
|
6744
8178
|
}
|
|
6745
8179
|
function schemaLooksSafe(schema) {
|
|
6746
8180
|
if (schema === void 0) return false;
|
|
@@ -7360,72 +8794,6 @@ function scanPromptInjection(file, source) {
|
|
|
7360
8794
|
});
|
|
7361
8795
|
return findings;
|
|
7362
8796
|
}
|
|
7363
|
-
function packageVersion() {
|
|
7364
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7365
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
7366
|
-
try {
|
|
7367
|
-
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
7368
|
-
const pkg = JSON.parse(raw);
|
|
7369
|
-
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
7370
|
-
return pkg.version;
|
|
7371
|
-
}
|
|
7372
|
-
} catch {
|
|
7373
|
-
}
|
|
7374
|
-
const parent = dirname(dir);
|
|
7375
|
-
if (parent === dir) break;
|
|
7376
|
-
dir = parent;
|
|
7377
|
-
}
|
|
7378
|
-
throw new Error("fullshield package.json not found");
|
|
7379
|
-
}
|
|
7380
|
-
|
|
7381
|
-
// src/core/parse-pool.ts
|
|
7382
|
-
var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7383
|
-
var cache = /* @__PURE__ */ new Map();
|
|
7384
|
-
function parsePoolEnabled() {
|
|
7385
|
-
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
7386
|
-
}
|
|
7387
|
-
function parsePoolSize() {
|
|
7388
|
-
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
7389
|
-
if (raw !== void 0 && raw !== "") {
|
|
7390
|
-
const parsed = Number(raw);
|
|
7391
|
-
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7392
|
-
}
|
|
7393
|
-
return Math.max(1, availableParallelism() - 1);
|
|
7394
|
-
}
|
|
7395
|
-
function parseCacheKey(code) {
|
|
7396
|
-
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
7397
|
-
}
|
|
7398
|
-
function parseOne(code) {
|
|
7399
|
-
const key = parseCacheKey(code);
|
|
7400
|
-
const hit = cache.get(key);
|
|
7401
|
-
if (hit !== void 0) return Promise.resolve(hit);
|
|
7402
|
-
return new Promise((resolve5, reject) => {
|
|
7403
|
-
const worker = new Worker(fileURLToPath(workerUrl), { workerData: { code } });
|
|
7404
|
-
worker.once("message", (ast) => {
|
|
7405
|
-
cache.set(key, ast);
|
|
7406
|
-
resolve5(ast);
|
|
7407
|
-
});
|
|
7408
|
-
worker.once("error", reject);
|
|
7409
|
-
});
|
|
7410
|
-
}
|
|
7411
|
-
async function parseWithPool(codes) {
|
|
7412
|
-
const size = parsePoolSize();
|
|
7413
|
-
const out = new Array(codes.length);
|
|
7414
|
-
let next = 0;
|
|
7415
|
-
async function run() {
|
|
7416
|
-
for (; ; ) {
|
|
7417
|
-
const index = next;
|
|
7418
|
-
next += 1;
|
|
7419
|
-
if (index >= codes.length) return;
|
|
7420
|
-
const code = codes[index];
|
|
7421
|
-
if (code === void 0) return;
|
|
7422
|
-
out[index] = await parseOne(code);
|
|
7423
|
-
}
|
|
7424
|
-
}
|
|
7425
|
-
const workers = Math.min(size, Math.max(1, codes.length));
|
|
7426
|
-
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
7427
|
-
return out;
|
|
7428
|
-
}
|
|
7429
8797
|
|
|
7430
8798
|
// src/modules/sast/engine.ts
|
|
7431
8799
|
var sastRules = [
|
|
@@ -7459,8 +8827,8 @@ function snippetOf(source, node) {
|
|
|
7459
8827
|
if (node.range === void 0) return "";
|
|
7460
8828
|
return source.slice(node.range[0], node.range[1]).replace(/\s+/g, " ").trim().slice(0, 120);
|
|
7461
8829
|
}
|
|
7462
|
-
function findingsFromAst(filename, source, ast,
|
|
7463
|
-
const facts = analyzeFile(ast,
|
|
8830
|
+
function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
|
|
8831
|
+
const facts = analyzeFile(ast, extraReturners2);
|
|
7464
8832
|
const findings = [];
|
|
7465
8833
|
for (const rule of sastRules) {
|
|
7466
8834
|
const ctx = {
|
|
@@ -7486,10 +8854,12 @@ function findingsFromAst(filename, source, ast, extraReturners = /* @__PURE__ */
|
|
|
7486
8854
|
});
|
|
7487
8855
|
},
|
|
7488
8856
|
isTainted: (node) => nodeIsTainted(node, facts),
|
|
8857
|
+
isSqlCleared: (node) => nodeIsSqlCleared(node, facts),
|
|
7489
8858
|
isPathSanitized: (node) => nodeIsPathSanitized(node, facts),
|
|
7490
8859
|
isSanitizedFor: (node, kind) => nodeIsSanitizedFor(node, facts, kind),
|
|
7491
8860
|
isNosqlSanitized: (node) => nodeIsNosqlSanitized(node, facts),
|
|
7492
8861
|
isRequestObject: (node) => nodeIsRequestObject(node, facts),
|
|
8862
|
+
holdsBuiltFilter: (node) => nodeHoldsBuiltFilter(node, facts),
|
|
7493
8863
|
functionHasPasswordName: (node) => factsFor(node, facts).hasPasswordName,
|
|
7494
8864
|
assignedName: (node) => assignedNameOf(node),
|
|
7495
8865
|
projectCsrfProtected
|
|
@@ -7527,10 +8897,12 @@ function findingsFromTemplate(filename, source) {
|
|
|
7527
8897
|
});
|
|
7528
8898
|
},
|
|
7529
8899
|
isTainted: () => false,
|
|
8900
|
+
isSqlCleared: () => false,
|
|
7530
8901
|
isPathSanitized: () => false,
|
|
7531
8902
|
isSanitizedFor: () => false,
|
|
7532
8903
|
isNosqlSanitized: () => false,
|
|
7533
8904
|
isRequestObject: () => false,
|
|
8905
|
+
holdsBuiltFilter: () => false,
|
|
7534
8906
|
functionHasPasswordName: () => false,
|
|
7535
8907
|
assignedName: () => void 0,
|
|
7536
8908
|
projectCsrfProtected: false
|
|
@@ -7538,52 +8910,224 @@ function findingsFromTemplate(filename, source) {
|
|
|
7538
8910
|
xssRule.create(ctx);
|
|
7539
8911
|
return findings;
|
|
7540
8912
|
}
|
|
8913
|
+
function dedupeSastSinks(findings) {
|
|
8914
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8915
|
+
const out = [];
|
|
8916
|
+
for (const finding3 of findings) {
|
|
8917
|
+
if (finding3.module !== "sast" || finding3.line === void 0) {
|
|
8918
|
+
out.push(finding3);
|
|
8919
|
+
continue;
|
|
8920
|
+
}
|
|
8921
|
+
const key = `${finding3.ruleId}\0${finding3.file ?? ""}\0${finding3.line}`;
|
|
8922
|
+
if (seen.has(key)) continue;
|
|
8923
|
+
seen.add(key);
|
|
8924
|
+
out.push(finding3);
|
|
8925
|
+
}
|
|
8926
|
+
return out;
|
|
8927
|
+
}
|
|
8928
|
+
function sastStreamThreshold() {
|
|
8929
|
+
const raw = process.env.FULLSHIELD_SAST_STREAM_AT;
|
|
8930
|
+
if (raw !== void 0 && raw !== "") {
|
|
8931
|
+
const parsed = Number(raw);
|
|
8932
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
8933
|
+
}
|
|
8934
|
+
return 4e3;
|
|
8935
|
+
}
|
|
8936
|
+
function sastBatchSize() {
|
|
8937
|
+
const raw = process.env.FULLSHIELD_SAST_BATCH;
|
|
8938
|
+
if (raw !== void 0 && raw !== "") {
|
|
8939
|
+
const parsed = Number(raw);
|
|
8940
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
8941
|
+
}
|
|
8942
|
+
return 150;
|
|
8943
|
+
}
|
|
8944
|
+
function skipFinding(rel, message) {
|
|
8945
|
+
return {
|
|
8946
|
+
id: makeFindingId("SAST-SKIP-001", rel, message),
|
|
8947
|
+
ruleId: "SAST-SKIP-001",
|
|
8948
|
+
module: "sast",
|
|
8949
|
+
title: "File skipped by SAST",
|
|
8950
|
+
severity: "info",
|
|
8951
|
+
confidence: "high",
|
|
8952
|
+
file: rel,
|
|
8953
|
+
message,
|
|
8954
|
+
remediation: "Raise sast.maxFileBytes in fullshield.config.json to parse this file, or leave it skipped."
|
|
8955
|
+
};
|
|
8956
|
+
}
|
|
8957
|
+
function skipReason(source, maxBytes) {
|
|
8958
|
+
if (source.includes("\0")) return "it contains NUL bytes";
|
|
8959
|
+
const bytes = Buffer.byteLength(source);
|
|
8960
|
+
if (bytes > maxBytes) return `${bytes} bytes exceeds the SAST parse limit of ${maxBytes} bytes`;
|
|
8961
|
+
return void 0;
|
|
8962
|
+
}
|
|
8963
|
+
async function readScanSource(cwd, rel) {
|
|
8964
|
+
try {
|
|
8965
|
+
return await readFile(join(cwd, rel), "utf8");
|
|
8966
|
+
} catch {
|
|
8967
|
+
return void 0;
|
|
8968
|
+
}
|
|
8969
|
+
}
|
|
8970
|
+
function cloneAst(value) {
|
|
8971
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
8972
|
+
try {
|
|
8973
|
+
return structuredClone(value);
|
|
8974
|
+
} catch {
|
|
8975
|
+
return void 0;
|
|
8976
|
+
}
|
|
8977
|
+
}
|
|
8978
|
+
async function parseInProcess(source) {
|
|
8979
|
+
try {
|
|
8980
|
+
return await parseSource(source);
|
|
8981
|
+
} catch {
|
|
8982
|
+
return void 0;
|
|
8983
|
+
}
|
|
8984
|
+
}
|
|
8985
|
+
async function parseCode(source) {
|
|
8986
|
+
if (!parsePoolEnabled()) return parseInProcess(source);
|
|
8987
|
+
try {
|
|
8988
|
+
const cloned = cloneAst((await parseWithPool([source]))[0]);
|
|
8989
|
+
if (cloned !== void 0) return cloned;
|
|
8990
|
+
} catch {
|
|
8991
|
+
}
|
|
8992
|
+
return parseInProcess(source);
|
|
8993
|
+
}
|
|
8994
|
+
function releaseParsed(parsed) {
|
|
8995
|
+
for (const entry of parsed) {
|
|
8996
|
+
entry.source = "";
|
|
8997
|
+
entry.ast = void 0;
|
|
8998
|
+
}
|
|
8999
|
+
parsed.length = 0;
|
|
9000
|
+
}
|
|
9001
|
+
function extraReturners(rel, ast, files, returning) {
|
|
9002
|
+
const ns = collectRequires(rel, ast, files);
|
|
9003
|
+
const extra = new Set(returning.get(rel));
|
|
9004
|
+
for (const [local, spec] of ns) {
|
|
9005
|
+
const hash = spec.indexOf("#");
|
|
9006
|
+
const file = hash >= 0 ? spec.slice(0, hash) : spec;
|
|
9007
|
+
const exp = hash >= 0 ? spec.slice(hash + 1) : local;
|
|
9008
|
+
if (returning.get(file)?.has(exp)) extra.add(local);
|
|
9009
|
+
}
|
|
9010
|
+
return extra;
|
|
9011
|
+
}
|
|
9012
|
+
async function runSastStreaming(ctx) {
|
|
9013
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
9014
|
+
const findings = [];
|
|
9015
|
+
const returning = /* @__PURE__ */ new Map();
|
|
9016
|
+
const parseable = [];
|
|
9017
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
9018
|
+
let projectCsrf = false;
|
|
9019
|
+
for (const rel of ctx.files) {
|
|
9020
|
+
const isSource = SOURCE_EXT.test(rel);
|
|
9021
|
+
const isTemplate = TEMPLATE_EXT.test(rel);
|
|
9022
|
+
if (!isSource && !isTemplate) continue;
|
|
9023
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
9024
|
+
if (source === void 0) continue;
|
|
9025
|
+
const why = skipReason(source, maxBytes);
|
|
9026
|
+
if (why !== void 0) {
|
|
9027
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
9028
|
+
continue;
|
|
9029
|
+
}
|
|
9030
|
+
if (sourceHasCsrfGuard(source)) projectCsrf = true;
|
|
9031
|
+
if (isTemplate) {
|
|
9032
|
+
findings.push(...findingsFromTemplate(rel, source));
|
|
9033
|
+
continue;
|
|
9034
|
+
}
|
|
9035
|
+
const ast = await parseCode(source);
|
|
9036
|
+
if (ast === void 0) continue;
|
|
9037
|
+
returning.set(rel, taintSummariesEnabled() ? cachedReturners(source, ast) : functionsThatReturnTaint(ast));
|
|
9038
|
+
parseable.push(rel);
|
|
9039
|
+
fileSet.add(rel);
|
|
9040
|
+
}
|
|
9041
|
+
for (const rel of parseable) {
|
|
9042
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
9043
|
+
if (source === void 0) continue;
|
|
9044
|
+
const ast = await parseCode(source);
|
|
9045
|
+
if (ast === void 0) continue;
|
|
9046
|
+
findings.push(
|
|
9047
|
+
...findingsFromAst(rel, source, ast, extraReturners(rel, ast, fileSet, returning), projectCsrf)
|
|
9048
|
+
);
|
|
9049
|
+
}
|
|
9050
|
+
const batch = sastBatchSize();
|
|
9051
|
+
for (let i = 0; i < parseable.length; i += batch) {
|
|
9052
|
+
const parsed = [];
|
|
9053
|
+
for (const rel of parseable.slice(i, i + batch)) {
|
|
9054
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
9055
|
+
if (source === void 0) continue;
|
|
9056
|
+
const ast = await parseCode(source);
|
|
9057
|
+
if (ast === void 0) continue;
|
|
9058
|
+
parsed.push({ file: rel, source, ast });
|
|
9059
|
+
}
|
|
9060
|
+
findings.push(...runInterproc(parsed));
|
|
9061
|
+
releaseParsed(parsed);
|
|
9062
|
+
}
|
|
9063
|
+
return dedupeSastSinks(findings);
|
|
9064
|
+
}
|
|
7541
9065
|
var sastModule = {
|
|
7542
9066
|
name: "sast",
|
|
7543
9067
|
async run(ctx) {
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
const
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
try {
|
|
7552
|
-
source = await readFile(join(ctx.cwd, rel), "utf8");
|
|
7553
|
-
} catch {
|
|
7554
|
-
continue;
|
|
7555
|
-
}
|
|
7556
|
-
if (source.length > 1024 * 1024 || source.includes("\0")) continue;
|
|
7557
|
-
if (isTemplate) {
|
|
7558
|
-
findings.push(...findingsFromTemplate(rel, source));
|
|
7559
|
-
continue;
|
|
9068
|
+
if (process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1") {
|
|
9069
|
+
const sources = new Set((ctx.config.sast.sources ?? []).map((row) => row.callee));
|
|
9070
|
+
const sanitizers = /* @__PURE__ */ new Map();
|
|
9071
|
+
for (const row of ctx.config.sast.sanitizers ?? []) {
|
|
9072
|
+
const classes = sanitizers.get(row.callee) ?? /* @__PURE__ */ new Set();
|
|
9073
|
+
for (const name of row.clears) classes.add(name);
|
|
9074
|
+
sanitizers.set(row.callee, classes);
|
|
7560
9075
|
}
|
|
7561
|
-
|
|
7562
|
-
|
|
9076
|
+
setCustomTaint({ sources, sanitizers });
|
|
9077
|
+
} else {
|
|
9078
|
+
setCustomTaint(void 0);
|
|
9079
|
+
}
|
|
9080
|
+
setTaintSummaryConfig(taintSummariesEnabled() ? ctx.config : void 0);
|
|
9081
|
+
try {
|
|
9082
|
+
const candidates = ctx.files.filter((rel) => SOURCE_EXT.test(rel) || TEMPLATE_EXT.test(rel));
|
|
9083
|
+
if (candidates.length > sastStreamThreshold()) return await runSastStreaming(ctx);
|
|
9084
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
9085
|
+
const findings = [];
|
|
9086
|
+
const parsed = [];
|
|
9087
|
+
for (const rel of candidates) {
|
|
9088
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
9089
|
+
if (source === void 0) continue;
|
|
9090
|
+
const why = skipReason(source, maxBytes);
|
|
9091
|
+
if (why !== void 0) {
|
|
9092
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
9093
|
+
continue;
|
|
9094
|
+
}
|
|
9095
|
+
if (TEMPLATE_EXT.test(rel)) {
|
|
9096
|
+
findings.push(...findingsFromTemplate(rel, source));
|
|
9097
|
+
continue;
|
|
9098
|
+
}
|
|
9099
|
+
const ast = await parseCode(source);
|
|
7563
9100
|
if (ast === void 0) continue;
|
|
7564
9101
|
parsed.push({ file: rel, source, ast });
|
|
7565
|
-
} catch {
|
|
7566
9102
|
}
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
const ns = collectRequires(p.file, p.ast, files);
|
|
7576
|
-
const extra = new Set(returning.get(p.file));
|
|
7577
|
-
for (const [local, spec] of ns) {
|
|
7578
|
-
const hash = spec.indexOf("#");
|
|
7579
|
-
const file = hash >= 0 ? spec.slice(0, hash) : spec;
|
|
7580
|
-
const exp = hash >= 0 ? spec.slice(hash + 1) : local;
|
|
7581
|
-
if (returning.get(file)?.has(exp)) extra.add(local);
|
|
9103
|
+
const files = new Set(parsed.map((p) => p.file));
|
|
9104
|
+
const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
|
|
9105
|
+
const returning = /* @__PURE__ */ new Map();
|
|
9106
|
+
for (const entry of parsed) {
|
|
9107
|
+
returning.set(
|
|
9108
|
+
entry.file,
|
|
9109
|
+
taintSummariesEnabled() ? cachedReturners(entry.source, entry.ast) : functionsThatReturnTaint(entry.ast)
|
|
9110
|
+
);
|
|
7582
9111
|
}
|
|
7583
|
-
|
|
9112
|
+
for (const entry of parsed) {
|
|
9113
|
+
findings.push(
|
|
9114
|
+
...findingsFromAst(
|
|
9115
|
+
entry.file,
|
|
9116
|
+
entry.source,
|
|
9117
|
+
entry.ast,
|
|
9118
|
+
extraReturners(entry.file, entry.ast, files, returning),
|
|
9119
|
+
projectCsrf
|
|
9120
|
+
)
|
|
9121
|
+
);
|
|
9122
|
+
}
|
|
9123
|
+
findings.push(...runInterproc(parsed));
|
|
9124
|
+
const out = dedupeSastSinks(findings);
|
|
9125
|
+
releaseParsed(parsed);
|
|
9126
|
+
return out;
|
|
9127
|
+
} finally {
|
|
9128
|
+
setCustomTaint(void 0);
|
|
9129
|
+
setTaintSummaryConfig(void 0);
|
|
7584
9130
|
}
|
|
7585
|
-
findings.push(...runInterproc(parsed));
|
|
7586
|
-
return findings;
|
|
7587
9131
|
}
|
|
7588
9132
|
};
|
|
7589
9133
|
|
|
@@ -7860,12 +9404,12 @@ ${text}`);
|
|
|
7860
9404
|
const seeds = fixtureLogic(prompt);
|
|
7861
9405
|
const findings = [];
|
|
7862
9406
|
for (const seed of seeds) {
|
|
7863
|
-
const
|
|
9407
|
+
const hit2 = fileTexts.find((f) => f.file.includes(seed.fileHint) && f.text.includes(seed.fileHint === "user" ? "findById" : "Object.assign")) ?? fileTexts.find(
|
|
7864
9408
|
(f) => f.text.includes(seed.ruleId === "AI-IDOR-001" ? "findById(req.params.id)" : "Object.assign(user, req.body)")
|
|
7865
9409
|
);
|
|
7866
|
-
if (
|
|
7867
|
-
const line =
|
|
7868
|
-
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));
|
|
7869
9413
|
}
|
|
7870
9414
|
return findings;
|
|
7871
9415
|
}
|
|
@@ -8027,16 +9571,37 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
8027
9571
|
}
|
|
8028
9572
|
};
|
|
8029
9573
|
}
|
|
9574
|
+
function isIsoDate(value) {
|
|
9575
|
+
if (!/^\d{4}-\d{2}-\d{2}(T[\d:.+\-Z]+)?$/.test(value)) return false;
|
|
9576
|
+
return Number.isFinite(Date.parse(value));
|
|
9577
|
+
}
|
|
9578
|
+
function expiresAtMs(expires) {
|
|
9579
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) return Date.parse(`${expires}T23:59:59.999Z`);
|
|
9580
|
+
return Date.parse(expires);
|
|
9581
|
+
}
|
|
9582
|
+
function validSuppression(value) {
|
|
9583
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
9584
|
+
const row = value;
|
|
9585
|
+
if (typeof row.ruleId !== "string" || row.ruleId.length === 0) return void 0;
|
|
9586
|
+
if (typeof row.owner !== "string" || row.owner.length === 0) return void 0;
|
|
9587
|
+
if (typeof row.reason !== "string" || row.reason.length === 0) return void 0;
|
|
9588
|
+
if (typeof row.expires !== "string" || !isIsoDate(row.expires)) return void 0;
|
|
9589
|
+
const file = row.file === void 0 ? void 0 : row.file.replace(/\\/g, "/");
|
|
9590
|
+
if (row.file !== void 0 && (typeof row.file !== "string" || row.file.length === 0)) return void 0;
|
|
9591
|
+
return { ruleId: row.ruleId, file, owner: row.owner, reason: row.reason, expires: row.expires };
|
|
9592
|
+
}
|
|
8030
9593
|
async function readBaseline(path) {
|
|
8031
9594
|
try {
|
|
8032
9595
|
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
8033
9596
|
if (raw.version !== 1 || !Array.isArray(raw.findings)) return void 0;
|
|
9597
|
+
if (raw.suppressions !== void 0 && !Array.isArray(raw.suppressions)) return void 0;
|
|
8034
9598
|
return raw;
|
|
8035
9599
|
} catch {
|
|
8036
9600
|
return void 0;
|
|
8037
9601
|
}
|
|
8038
9602
|
}
|
|
8039
9603
|
async function writeBaseline(path, findings) {
|
|
9604
|
+
const existing = await readBaseline(path);
|
|
8040
9605
|
const doc = {
|
|
8041
9606
|
version: 1,
|
|
8042
9607
|
findings: findings.map((f) => ({
|
|
@@ -8044,7 +9609,8 @@ async function writeBaseline(path, findings) {
|
|
|
8044
9609
|
ruleId: f.ruleId,
|
|
8045
9610
|
file: f.file,
|
|
8046
9611
|
line: f.line
|
|
8047
|
-
}))
|
|
9612
|
+
})),
|
|
9613
|
+
...existing?.suppressions !== void 0 ? { suppressions: existing.suppressions } : {}
|
|
8048
9614
|
};
|
|
8049
9615
|
await writeFile(path, `${JSON.stringify(doc, null, 2)}
|
|
8050
9616
|
`, "utf8");
|
|
@@ -8054,6 +9620,30 @@ function applyBaseline(findings, baseline) {
|
|
|
8054
9620
|
const known = new Set(baseline.findings.map((f) => f.id));
|
|
8055
9621
|
return findings.filter((f) => !known.has(f.id));
|
|
8056
9622
|
}
|
|
9623
|
+
function applySuppressions(findings, rows, now = /* @__PURE__ */ new Date()) {
|
|
9624
|
+
if (rows === void 0 || rows.length === 0) return { findings: [...findings], errors: [] };
|
|
9625
|
+
const errors = [];
|
|
9626
|
+
const active = [];
|
|
9627
|
+
rows.forEach((row, index) => {
|
|
9628
|
+
const parsed = validSuppression(row);
|
|
9629
|
+
if (parsed === void 0) {
|
|
9630
|
+
errors.push(`suppression ${index + 1} needs ruleId, owner, reason, and an ISO expires date`);
|
|
9631
|
+
return;
|
|
9632
|
+
}
|
|
9633
|
+
if (expiresAtMs(parsed.expires) < now.getTime()) {
|
|
9634
|
+
errors.push(
|
|
9635
|
+
`suppression for ${parsed.ruleId} expired on ${parsed.expires} (owner: ${parsed.owner}); renew or remove it`
|
|
9636
|
+
);
|
|
9637
|
+
return;
|
|
9638
|
+
}
|
|
9639
|
+
active.push(parsed);
|
|
9640
|
+
});
|
|
9641
|
+
const kept = findings.filter((finding3) => {
|
|
9642
|
+
const file = (finding3.file ?? "").replace(/\\/g, "/");
|
|
9643
|
+
return !active.some((row) => row.ruleId === finding3.ruleId && (row.file === void 0 || row.file === file));
|
|
9644
|
+
});
|
|
9645
|
+
return { findings: kept, errors };
|
|
9646
|
+
}
|
|
8057
9647
|
|
|
8058
9648
|
// src/core/config.ts
|
|
8059
9649
|
init_finding();
|
|
@@ -8062,14 +9652,77 @@ var DEFAULT_CONFIG = {
|
|
|
8062
9652
|
modules: [...MODULE_NAMES],
|
|
8063
9653
|
failOn: "high",
|
|
8064
9654
|
ignore: ["dist/**", "**/*.test.ts"],
|
|
9655
|
+
defaultExcludes: true,
|
|
8065
9656
|
baseline: ".fullshield-baseline.json",
|
|
8066
9657
|
notify: { slackWebhookEnv: "FULLSHIELD_SLACK_WEBHOOK" },
|
|
8067
9658
|
deps: { offline: false },
|
|
8068
|
-
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 }
|
|
9659
|
+
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 },
|
|
9660
|
+
sast: { maxFileBytes: 1024 * 1024 }
|
|
8069
9661
|
};
|
|
8070
9662
|
var ConfigError = class extends Error {
|
|
8071
9663
|
};
|
|
8072
|
-
var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
9664
|
+
var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
9665
|
+
"modules",
|
|
9666
|
+
"failOn",
|
|
9667
|
+
"ignore",
|
|
9668
|
+
"defaultExcludes",
|
|
9669
|
+
"excludes",
|
|
9670
|
+
"baseline",
|
|
9671
|
+
"notify",
|
|
9672
|
+
"deps",
|
|
9673
|
+
"ai",
|
|
9674
|
+
"sast"
|
|
9675
|
+
]);
|
|
9676
|
+
var SAST_CLASSES = /* @__PURE__ */ new Set([
|
|
9677
|
+
"sqli",
|
|
9678
|
+
"nosqli",
|
|
9679
|
+
"cmdi",
|
|
9680
|
+
"path",
|
|
9681
|
+
"xss",
|
|
9682
|
+
"ssrf",
|
|
9683
|
+
"redirect",
|
|
9684
|
+
"eval",
|
|
9685
|
+
"jwt",
|
|
9686
|
+
"crypto",
|
|
9687
|
+
"proto",
|
|
9688
|
+
"secrets",
|
|
9689
|
+
"deps",
|
|
9690
|
+
"config"
|
|
9691
|
+
]);
|
|
9692
|
+
function parseSastSources(value, source) {
|
|
9693
|
+
if (value === void 0) return void 0;
|
|
9694
|
+
if (!Array.isArray(value)) throw new ConfigError(`${source}: "sast.sources" must be an array`);
|
|
9695
|
+
return value.map((item) => {
|
|
9696
|
+
if (typeof item !== "object" || item === null) {
|
|
9697
|
+
throw new ConfigError(`${source}: "sast.sources" entries must be objects`);
|
|
9698
|
+
}
|
|
9699
|
+
const row = item;
|
|
9700
|
+
if (typeof row.callee !== "string" || row.callee.length === 0) {
|
|
9701
|
+
throw new ConfigError(`${source}: "sast.sources[].callee" must be a string`);
|
|
9702
|
+
}
|
|
9703
|
+
if (row.returns !== "tainted") {
|
|
9704
|
+
throw new ConfigError(`${source}: "sast.sources[].returns" must be "tainted"`);
|
|
9705
|
+
}
|
|
9706
|
+
return { callee: row.callee, returns: "tainted" };
|
|
9707
|
+
});
|
|
9708
|
+
}
|
|
9709
|
+
function parseSastSanitizers(value, source) {
|
|
9710
|
+
if (value === void 0) return void 0;
|
|
9711
|
+
if (!Array.isArray(value)) throw new ConfigError(`${source}: "sast.sanitizers" must be an array`);
|
|
9712
|
+
return value.map((item) => {
|
|
9713
|
+
if (typeof item !== "object" || item === null) {
|
|
9714
|
+
throw new ConfigError(`${source}: "sast.sanitizers" entries must be objects`);
|
|
9715
|
+
}
|
|
9716
|
+
const row = item;
|
|
9717
|
+
if (typeof row.callee !== "string" || row.callee.length === 0) {
|
|
9718
|
+
throw new ConfigError(`${source}: "sast.sanitizers[].callee" must be a string`);
|
|
9719
|
+
}
|
|
9720
|
+
if (!Array.isArray(row.clears) || row.clears.some((name) => typeof name !== "string" || !SAST_CLASSES.has(name))) {
|
|
9721
|
+
throw new ConfigError(`${source}: "sast.sanitizers[].clears" must be B4 class names`);
|
|
9722
|
+
}
|
|
9723
|
+
return { callee: row.callee, clears: row.clears.filter((name) => name !== "secrets" && name !== "deps") };
|
|
9724
|
+
});
|
|
9725
|
+
}
|
|
8073
9726
|
function validate(raw, source) {
|
|
8074
9727
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
8075
9728
|
throw new ConfigError(`${source}: config must be a JSON object`);
|
|
@@ -8171,6 +9824,42 @@ function validate(raw, source) {
|
|
|
8171
9824
|
}
|
|
8172
9825
|
out.ai = ai;
|
|
8173
9826
|
}
|
|
9827
|
+
if (record.defaultExcludes !== void 0) {
|
|
9828
|
+
if (typeof record.defaultExcludes !== "boolean") {
|
|
9829
|
+
throw new ConfigError(`${source}: "defaultExcludes" must be a boolean`);
|
|
9830
|
+
}
|
|
9831
|
+
out.defaultExcludes = record.defaultExcludes;
|
|
9832
|
+
}
|
|
9833
|
+
if (record.excludes !== void 0) {
|
|
9834
|
+
if (!Array.isArray(record.excludes) || record.excludes.some((p) => typeof p !== "string")) {
|
|
9835
|
+
throw new ConfigError(`${source}: "excludes" must be an array of glob strings`);
|
|
9836
|
+
}
|
|
9837
|
+
out.excludes = record.excludes;
|
|
9838
|
+
}
|
|
9839
|
+
if (record.sast !== void 0) {
|
|
9840
|
+
if (typeof record.sast !== "object" || record.sast === null || Array.isArray(record.sast)) {
|
|
9841
|
+
throw new ConfigError(`${source}: "sast" must be an object`);
|
|
9842
|
+
}
|
|
9843
|
+
const rawSast = record.sast;
|
|
9844
|
+
const sast = { maxFileBytes: 1024 * 1024 };
|
|
9845
|
+
if (rawSast.maxFileBytes !== void 0) {
|
|
9846
|
+
if (typeof rawSast.maxFileBytes !== "number" || rawSast.maxFileBytes < 1) {
|
|
9847
|
+
throw new ConfigError(`${source}: "sast.maxFileBytes" must be a positive number`);
|
|
9848
|
+
}
|
|
9849
|
+
sast.maxFileBytes = rawSast.maxFileBytes;
|
|
9850
|
+
}
|
|
9851
|
+
const custom = process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1";
|
|
9852
|
+
if (!custom && (rawSast.sources !== void 0 || rawSast.sanitizers !== void 0)) {
|
|
9853
|
+
throw new ConfigError(
|
|
9854
|
+
`${source}: unknown key "sast.sources" (FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS)`
|
|
9855
|
+
);
|
|
9856
|
+
}
|
|
9857
|
+
if (custom) {
|
|
9858
|
+
sast.sources = parseSastSources(rawSast.sources, source);
|
|
9859
|
+
sast.sanitizers = parseSastSanitizers(rawSast.sanitizers, source);
|
|
9860
|
+
}
|
|
9861
|
+
out.sast = sast;
|
|
9862
|
+
}
|
|
8174
9863
|
return out;
|
|
8175
9864
|
}
|
|
8176
9865
|
async function loadConfig(cwd, configPath) {
|
|
@@ -8197,10 +9886,26 @@ async function loadConfig(cwd, configPath) {
|
|
|
8197
9886
|
return { ...structuredClone(DEFAULT_CONFIG), ...validate(raw, file) };
|
|
8198
9887
|
}
|
|
8199
9888
|
var ignoreFactory = ignoreModule.default;
|
|
8200
|
-
var
|
|
9889
|
+
var HARD_IGNORED = [".git/", ".fullshield/"];
|
|
9890
|
+
var DEFAULT_EXCLUDES = [
|
|
9891
|
+
"node_modules/",
|
|
9892
|
+
"dist/",
|
|
9893
|
+
"build/",
|
|
9894
|
+
".next/",
|
|
9895
|
+
"coverage/",
|
|
9896
|
+
"vendor/",
|
|
9897
|
+
"vendored/",
|
|
9898
|
+
"third_party/",
|
|
9899
|
+
"third-party/",
|
|
9900
|
+
"compiled/",
|
|
9901
|
+
"*.min.js"
|
|
9902
|
+
];
|
|
8201
9903
|
async function walkFiles(options) {
|
|
8202
9904
|
const ig = ignoreFactory();
|
|
8203
|
-
ig.add(
|
|
9905
|
+
ig.add(HARD_IGNORED);
|
|
9906
|
+
if (options.defaultExcludes !== false) {
|
|
9907
|
+
ig.add(options.excludes ?? DEFAULT_EXCLUDES);
|
|
9908
|
+
}
|
|
8204
9909
|
try {
|
|
8205
9910
|
ig.add(await readFile(join(options.cwd, ".gitignore"), "utf8"));
|
|
8206
9911
|
} catch {
|
|
@@ -8382,7 +10087,7 @@ function resolvePolicyBranch(cwd, env = process.env) {
|
|
|
8382
10087
|
return "*";
|
|
8383
10088
|
}
|
|
8384
10089
|
}
|
|
8385
|
-
function
|
|
10090
|
+
function expiresAtMs2(expires) {
|
|
8386
10091
|
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
|
|
8387
10092
|
return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8388
10093
|
}
|
|
@@ -8404,7 +10109,7 @@ function applyPolicy(findings, policy, ctx = {}) {
|
|
|
8404
10109
|
});
|
|
8405
10110
|
const suppressed = /* @__PURE__ */ new Set();
|
|
8406
10111
|
for (const exception of policy.exceptions ?? []) {
|
|
8407
|
-
if (
|
|
10112
|
+
if (expiresAtMs2(exception.expires) < now.getTime()) {
|
|
8408
10113
|
errors.push(
|
|
8409
10114
|
`policy exception for ${exception.ruleId} expired on ${exception.expires} (owner: ${exception.owner}); renew or remove it`
|
|
8410
10115
|
);
|
|
@@ -8452,6 +10157,8 @@ function dedupe2(findings) {
|
|
|
8452
10157
|
async function runScan(options = {}, modules = builtinModules) {
|
|
8453
10158
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8454
10159
|
const t0 = performance.now();
|
|
10160
|
+
depsModule.unusedDirect = void 0;
|
|
10161
|
+
depsModule.upgradePlan = void 0;
|
|
8455
10162
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
8456
10163
|
const stats = await stat(cwd).catch(() => null);
|
|
8457
10164
|
if (stats === null || !stats.isDirectory()) {
|
|
@@ -8473,7 +10180,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8473
10180
|
options.modules ?? config.modules,
|
|
8474
10181
|
loadedPolicy?.requiredModules
|
|
8475
10182
|
);
|
|
8476
|
-
let files = await walkFiles({
|
|
10183
|
+
let files = await walkFiles({
|
|
10184
|
+
cwd,
|
|
10185
|
+
ignore: config.ignore,
|
|
10186
|
+
defaultExcludes: config.defaultExcludes,
|
|
10187
|
+
excludes: config.excludes
|
|
10188
|
+
});
|
|
8477
10189
|
if (options.staged === true) {
|
|
8478
10190
|
const staged = new Set(await listStagedFiles(cwd));
|
|
8479
10191
|
files = files.filter((file) => staged.has(file));
|
|
@@ -8507,6 +10219,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8507
10219
|
);
|
|
8508
10220
|
}
|
|
8509
10221
|
const extraHits = [];
|
|
10222
|
+
let historyNotice;
|
|
8510
10223
|
const secretsWanted = modulesRequested.includes("secrets");
|
|
8511
10224
|
if (secretsAdvEnabled() && secretsWanted) {
|
|
8512
10225
|
if (options.history === true) {
|
|
@@ -8514,6 +10227,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8514
10227
|
const hist = await scanGitHistory2(cwd);
|
|
8515
10228
|
extraHits.push(...hist.hits);
|
|
8516
10229
|
collected.push(...hist.findings);
|
|
10230
|
+
historyNotice = hist.history;
|
|
8517
10231
|
}
|
|
8518
10232
|
if (options.bundle !== void 0 && options.bundle.length > 0) {
|
|
8519
10233
|
const { scanBundle: scanBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
@@ -8534,13 +10248,17 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8534
10248
|
if (options.updateBaseline === true) {
|
|
8535
10249
|
await writeBaseline(baselinePath, unsuppressed);
|
|
8536
10250
|
}
|
|
8537
|
-
|
|
10251
|
+
const baselineDoc = await readBaseline(baselinePath);
|
|
10252
|
+
let findings = sortFindings(applyBaseline(unsuppressed, baselineDoc));
|
|
10253
|
+
const suppressed = applySuppressions(findings, baselineDoc?.suppressions);
|
|
10254
|
+
findings = sortFindings(suppressed.findings);
|
|
10255
|
+
const suppressionErrors = suppressed.errors;
|
|
8538
10256
|
if (secretsAdvEnabled() && secretsWanted && options.verifySecrets === true && options.iOwnTheseCredentials === true) {
|
|
8539
10257
|
const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
8540
10258
|
const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
8541
10259
|
const secretById = /* @__PURE__ */ new Map();
|
|
8542
|
-
for (const
|
|
8543
|
-
if (
|
|
10260
|
+
for (const hit2 of extraHits) {
|
|
10261
|
+
if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
|
|
8544
10262
|
}
|
|
8545
10263
|
for (const finding3 of findings) {
|
|
8546
10264
|
if (finding3.module !== "secrets" || finding3.file === void 0 || secretById.has(finding3.id)) {
|
|
@@ -8554,8 +10272,8 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8554
10272
|
continue;
|
|
8555
10273
|
}
|
|
8556
10274
|
}
|
|
8557
|
-
for (const
|
|
8558
|
-
if (
|
|
10275
|
+
for (const hit2 of scanTextForSecretHits2(finding3.file, text)) {
|
|
10276
|
+
if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
|
|
8559
10277
|
}
|
|
8560
10278
|
}
|
|
8561
10279
|
findings = sortFindings(
|
|
@@ -8606,10 +10324,14 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8606
10324
|
cwd,
|
|
8607
10325
|
filesScanned: files.length,
|
|
8608
10326
|
modulesRequested: [...modulesRequested],
|
|
8609
|
-
modulesRun
|
|
10327
|
+
modulesRun,
|
|
10328
|
+
...historyNotice !== void 0 ? { history: historyNotice } : {},
|
|
10329
|
+
...depsModule.unusedDirect !== void 0 ? { unusedDirect: depsModule.unusedDirect } : {},
|
|
10330
|
+
...depsModule.upgradePlan !== void 0 ? { upgradePlan: depsModule.upgradePlan } : {}
|
|
8610
10331
|
},
|
|
8611
10332
|
config,
|
|
8612
|
-
policy: policyResult
|
|
10333
|
+
policy: policyResult,
|
|
10334
|
+
...suppressionErrors.length > 0 ? { suppressionErrors } : {}
|
|
8613
10335
|
};
|
|
8614
10336
|
}
|
|
8615
10337
|
function scan(options = {}) {
|