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