fullshield 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +4 -4
- package/dist/agent.js +1535 -348
- package/dist/cli/index.js +2175 -387
- package/dist/compliance.js +4 -0
- package/dist/index.d.ts +54 -3
- package/dist/index.js +1890 -426
- package/dist/mcp.js +1546 -359
- package/dist/vex.js +4 -2
- package/package.json +2 -2
- package/rules/README.md +2 -0
- package/rules/SUPPLY-CHAINC2-001.md +3 -0
- package/rules/SUPPLY-PLATFORMGATE-001.md +3 -0
- package/rules/SUPPLY-SECHOLDER-001.md +3 -0
- package/rules/SUPPLY-STEALER-001.md +3 -0
package/dist/cli/index.js
CHANGED
|
@@ -645,6 +645,173 @@ var init_config = __esm({
|
|
|
645
645
|
}
|
|
646
646
|
});
|
|
647
647
|
|
|
648
|
+
// src/modules/deps/lockfile-modern.ts
|
|
649
|
+
function addEdge(edges, from, to) {
|
|
650
|
+
if (from.length === 0 || to.length === 0 || from === to) return;
|
|
651
|
+
const list = edges.get(from) ?? /* @__PURE__ */ new Set();
|
|
652
|
+
list.add(to);
|
|
653
|
+
edges.set(from, list);
|
|
654
|
+
}
|
|
655
|
+
function graph(packages, edges, directProd, directDev, edgesKnown) {
|
|
656
|
+
return { packages, edges, directProd, directDev, edgesKnown };
|
|
657
|
+
}
|
|
658
|
+
function parseYarnBerry(contents) {
|
|
659
|
+
const packages = [];
|
|
660
|
+
const edges = /* @__PURE__ */ new Map();
|
|
661
|
+
const directProd = /* @__PURE__ */ new Set();
|
|
662
|
+
const directDev = /* @__PURE__ */ new Set();
|
|
663
|
+
let current;
|
|
664
|
+
let inDeps = false;
|
|
665
|
+
let devBlock = false;
|
|
666
|
+
for (const raw of contents.split(/\r?\n/)) {
|
|
667
|
+
if (!raw.startsWith(" ") && raw.trimEnd().endsWith(":")) {
|
|
668
|
+
const key = raw.slice(0, -1).trim().replace(/^"|"$/g, "");
|
|
669
|
+
current = key.includes("@workspace:.") ? "" : nameFromBerryKey(key);
|
|
670
|
+
inDeps = false;
|
|
671
|
+
devBlock = false;
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
const version = /^\s+version:\s+(.+)$/.exec(raw)?.[1]?.replace(/["']/g, "").trim();
|
|
675
|
+
if (version !== void 0 && current !== void 0 && current.length > 0 && !inDeps) {
|
|
676
|
+
packages.push({ name: current, version: versionFromProtocol(version), path: `yarn:${current}` });
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (/^\s+dependencies:\s*$/.test(raw)) {
|
|
680
|
+
inDeps = true;
|
|
681
|
+
devBlock = false;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (/^\s+devDependencies:\s*$/.test(raw)) {
|
|
685
|
+
inDeps = true;
|
|
686
|
+
devBlock = true;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!inDeps || current === void 0) continue;
|
|
690
|
+
const dep = /^\s{4}"?([^":]+)"?:\s+(.+)$/.exec(raw);
|
|
691
|
+
if (dep?.[1] === void 0) continue;
|
|
692
|
+
const depName = dep[1];
|
|
693
|
+
if (current.length === 0) {
|
|
694
|
+
if (devBlock) directDev.add(depName);
|
|
695
|
+
else directProd.add(depName);
|
|
696
|
+
} else {
|
|
697
|
+
addEdge(edges, current, depName);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return graph(packages, edges, directProd, directDev, true);
|
|
701
|
+
}
|
|
702
|
+
function nameFromBerryKey(key) {
|
|
703
|
+
const first = key.split(",")[0]?.trim() ?? key;
|
|
704
|
+
if (first.startsWith("@")) {
|
|
705
|
+
const match = /^(@[^@/]+\/[^@/]+)@/.exec(first);
|
|
706
|
+
return match?.[1];
|
|
707
|
+
}
|
|
708
|
+
const at = first.indexOf("@");
|
|
709
|
+
return at <= 0 ? first : first.slice(0, at);
|
|
710
|
+
}
|
|
711
|
+
function versionFromProtocol(raw) {
|
|
712
|
+
const patch = /^patch:(.+)#/.exec(raw);
|
|
713
|
+
const body = patch?.[1] ?? raw;
|
|
714
|
+
const npm = /^(?:npm:)?(@?[^@]+)@(.+)$/.exec(body);
|
|
715
|
+
if (npm?.[2] !== void 0) return npm[2];
|
|
716
|
+
return body;
|
|
717
|
+
}
|
|
718
|
+
function parsePnpmV9(contents) {
|
|
719
|
+
const packages = [];
|
|
720
|
+
const edges = /* @__PURE__ */ new Map();
|
|
721
|
+
const directProd = /* @__PURE__ */ new Set();
|
|
722
|
+
const directDev = /* @__PURE__ */ new Set();
|
|
723
|
+
let block = "none";
|
|
724
|
+
let snapshot;
|
|
725
|
+
let inSnapshotDeps = false;
|
|
726
|
+
for (const raw of contents.split(/\r?\n/)) {
|
|
727
|
+
if (raw === "packages:") {
|
|
728
|
+
block = "packages";
|
|
729
|
+
snapshot = void 0;
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
if (raw === "snapshots:") {
|
|
733
|
+
block = "snapshots";
|
|
734
|
+
snapshot = void 0;
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
if (raw === "importers:" || raw.startsWith("lockfileVersion:")) {
|
|
738
|
+
block = "none";
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (block !== "snapshots" && /^\s{4}dependencies:\s*$/.test(raw)) {
|
|
742
|
+
block = "importer-deps";
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (/^\s{4}devDependencies:\s*$/.test(raw)) {
|
|
746
|
+
block = "importer-dev";
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (block === "packages") {
|
|
750
|
+
const header = /^\s{2}['"]?(@?[^@'\s]+)@([^:'"]+)['"]?:/.exec(raw);
|
|
751
|
+
if (header?.[1] !== void 0 && header[2] !== void 0) {
|
|
752
|
+
packages.push({ name: header[1], version: header[2], path: `pnpm:${header[1]}` });
|
|
753
|
+
}
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (block === "snapshots") {
|
|
757
|
+
const header = /^\s{2}['"]?(@?[^@'\s]+)@([^:'"]+)['"]?:/.exec(raw);
|
|
758
|
+
if (header?.[1] !== void 0) {
|
|
759
|
+
snapshot = header[1];
|
|
760
|
+
inSnapshotDeps = false;
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
if (/^\s+dependencies:\s*$/.test(raw)) {
|
|
764
|
+
inSnapshotDeps = true;
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
if (inSnapshotDeps && snapshot !== void 0) {
|
|
768
|
+
const dep = /^\s{6}['"]?([^'":]+)['"]?:/.exec(raw);
|
|
769
|
+
if (dep?.[1] !== void 0) addEdge(edges, snapshot, dep[1]);
|
|
770
|
+
}
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (block === "importer-deps" || block === "importer-dev") {
|
|
774
|
+
const dep = /^\s{6}['"]?([^'":]+)['"]?:/.exec(raw);
|
|
775
|
+
if (dep?.[1] !== void 0 && !raw.trim().startsWith("specifier") && !raw.trim().startsWith("version")) {
|
|
776
|
+
if (block === "importer-dev") directDev.add(dep[1]);
|
|
777
|
+
else directProd.add(dep[1]);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return graph(packages, edges, directProd, directDev, true);
|
|
782
|
+
}
|
|
783
|
+
function parseBunLock(contents) {
|
|
784
|
+
const stripped = contents.replace(/^\s*\/\/.*$/gm, "");
|
|
785
|
+
const packages = [];
|
|
786
|
+
const edges = /* @__PURE__ */ new Map();
|
|
787
|
+
const directProd = /* @__PURE__ */ new Set();
|
|
788
|
+
const directDev = /* @__PURE__ */ new Set();
|
|
789
|
+
try {
|
|
790
|
+
const json = JSON.parse(stripped);
|
|
791
|
+
for (const [name, value] of Object.entries(json.packages ?? {})) {
|
|
792
|
+
if (!Array.isArray(value)) continue;
|
|
793
|
+
const entry = Array.from(value);
|
|
794
|
+
const spec = typeof entry[0] === "string" ? entry[0] : "";
|
|
795
|
+
const version = spec.includes("@") ? spec.slice(spec.lastIndexOf("@") + 1) : spec;
|
|
796
|
+
packages.push({ name, version, path: `bun:${name}` });
|
|
797
|
+
const meta = entry[1];
|
|
798
|
+
if (meta !== null && typeof meta === "object" && "dependencies" in meta) {
|
|
799
|
+
const deps = meta.dependencies;
|
|
800
|
+
if (deps !== null && typeof deps === "object") {
|
|
801
|
+
for (const dep of Object.keys(deps)) addEdge(edges, name, dep);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
} catch {
|
|
806
|
+
return graph([], /* @__PURE__ */ new Map(), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(), false);
|
|
807
|
+
}
|
|
808
|
+
return graph(packages, edges, directProd, directDev, packages.length > 0);
|
|
809
|
+
}
|
|
810
|
+
var init_lockfile_modern = __esm({
|
|
811
|
+
"src/modules/deps/lockfile-modern.ts"() {
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
648
815
|
// src/modules/deps/lockfile.ts
|
|
649
816
|
function basename2(filename) {
|
|
650
817
|
const parts = filename.replace(/\\/g, "/").split("/");
|
|
@@ -674,7 +841,7 @@ function objectKeys(value) {
|
|
|
674
841
|
}
|
|
675
842
|
return Object.keys(value);
|
|
676
843
|
}
|
|
677
|
-
function
|
|
844
|
+
function addEdge2(edges, from, to) {
|
|
678
845
|
const set = edges.get(from) ?? /* @__PURE__ */ new Set();
|
|
679
846
|
set.add(to);
|
|
680
847
|
edges.set(from, set);
|
|
@@ -754,7 +921,7 @@ function parseYarnGraph(contents) {
|
|
|
754
921
|
const depBerry = /^\s{4}(?:"([^"]+)"|(@?[\w./-]+)):/.exec(raw);
|
|
755
922
|
const depName = depClassic?.[1] ?? depClassic?.[2] ?? depBerry?.[1] ?? depBerry?.[2];
|
|
756
923
|
if (depName !== void 0) {
|
|
757
|
-
|
|
924
|
+
addEdge2(edges, currentName, depName);
|
|
758
925
|
continue;
|
|
759
926
|
}
|
|
760
927
|
if (/^\s{2}\S/.test(raw)) inDeps = false;
|
|
@@ -848,7 +1015,7 @@ function parsePnpmGraph(contents) {
|
|
|
848
1015
|
const dep = /^ (?:'([^']+)'|"([^"]+)"|([^:]+)):/.exec(raw);
|
|
849
1016
|
const depName = dep?.[1] ?? dep?.[2] ?? dep?.[3];
|
|
850
1017
|
if (depName !== void 0) {
|
|
851
|
-
|
|
1018
|
+
addEdge2(edges, currentPkg, depName);
|
|
852
1019
|
continue;
|
|
853
1020
|
}
|
|
854
1021
|
if (/^ \S/.test(raw)) inPkgDeps = false;
|
|
@@ -883,7 +1050,7 @@ function graphFromPackagesTable(packages) {
|
|
|
883
1050
|
const name = key === "" ? "" : nameFromPackagesKey(key);
|
|
884
1051
|
if (name === void 0 || name.length === 0) continue;
|
|
885
1052
|
for (const dep of [...objectKeys(value.dependencies), ...objectKeys(value.optionalDependencies)]) {
|
|
886
|
-
|
|
1053
|
+
addEdge2(edges, name, dep);
|
|
887
1054
|
}
|
|
888
1055
|
}
|
|
889
1056
|
return { edges, directProd, directDev };
|
|
@@ -893,7 +1060,7 @@ function graphFromDependenciesTree(deps, edges) {
|
|
|
893
1060
|
const child = value.dependencies;
|
|
894
1061
|
if (child === void 0 || typeof child !== "object" || child === null) continue;
|
|
895
1062
|
const nested = child;
|
|
896
|
-
for (const childName of Object.keys(nested))
|
|
1063
|
+
for (const childName of Object.keys(nested)) addEdge2(edges, name, childName);
|
|
897
1064
|
graphFromDependenciesTree(nested, edges);
|
|
898
1065
|
}
|
|
899
1066
|
}
|
|
@@ -906,8 +1073,16 @@ function parseLockfileGraph(filename, contents) {
|
|
|
906
1073
|
directDev: /* @__PURE__ */ new Set(),
|
|
907
1074
|
edgesKnown: false
|
|
908
1075
|
};
|
|
909
|
-
|
|
910
|
-
if (name === "
|
|
1076
|
+
const modern = process.env.FULLSHIELD_EXPERIMENTAL_LOCKFILES === "1";
|
|
1077
|
+
if (name === "yarn.lock") {
|
|
1078
|
+
if (modern && contents.includes("__metadata")) return parseYarnBerry(contents);
|
|
1079
|
+
return parseYarnGraph(contents);
|
|
1080
|
+
}
|
|
1081
|
+
if (name === "pnpm-lock.yaml") {
|
|
1082
|
+
if (modern && /lockfileVersion:\s*['"]?9/.test(contents)) return parsePnpmV9(contents);
|
|
1083
|
+
return parsePnpmGraph(contents);
|
|
1084
|
+
}
|
|
1085
|
+
if (modern && name === "bun.lock") return parseBunLock(contents);
|
|
911
1086
|
if (name !== "package-lock.json") return empty;
|
|
912
1087
|
try {
|
|
913
1088
|
const json = JSON.parse(contents);
|
|
@@ -934,6 +1109,7 @@ function parseLockfileGraph(filename, contents) {
|
|
|
934
1109
|
var PNPM_KEY;
|
|
935
1110
|
var init_lockfile = __esm({
|
|
936
1111
|
"src/modules/deps/lockfile.ts"() {
|
|
1112
|
+
init_lockfile_modern();
|
|
937
1113
|
PNPM_KEY = /^ {2}['"]?(?:\/)?(@?[^'"@\s/]+(?:\/[^'"@\s/]+)?)@([^'"(\s:]+)(?:\([^)]*\))?['"]?:\s*$/;
|
|
938
1114
|
}
|
|
939
1115
|
});
|
|
@@ -1092,6 +1268,91 @@ var init_pins = __esm({
|
|
|
1092
1268
|
}
|
|
1093
1269
|
});
|
|
1094
1270
|
|
|
1271
|
+
// src/modules/deps/evidence.ts
|
|
1272
|
+
function packageNameFromSpec(spec) {
|
|
1273
|
+
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
|
|
1274
|
+
if (spec.startsWith("@")) {
|
|
1275
|
+
const slash2 = spec.indexOf("/");
|
|
1276
|
+
if (slash2 < 0) return void 0;
|
|
1277
|
+
const rest = spec.slice(slash2 + 1);
|
|
1278
|
+
const cut = rest.indexOf("/");
|
|
1279
|
+
return cut === -1 ? spec : spec.slice(0, slash2 + 1 + cut);
|
|
1280
|
+
}
|
|
1281
|
+
const slash = spec.indexOf("/");
|
|
1282
|
+
return slash === -1 ? spec : spec.slice(0, slash);
|
|
1283
|
+
}
|
|
1284
|
+
function collectImportSites(sources) {
|
|
1285
|
+
const byPkg = /* @__PURE__ */ new Map();
|
|
1286
|
+
const files = [...sources.keys()].sort();
|
|
1287
|
+
for (const file of files) {
|
|
1288
|
+
const text = sources.get(file) ?? "";
|
|
1289
|
+
const lines = text.split(/\r?\n/);
|
|
1290
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1291
|
+
const line = lines[i] ?? "";
|
|
1292
|
+
IMPORT_RE.lastIndex = 0;
|
|
1293
|
+
let match;
|
|
1294
|
+
while ((match = IMPORT_RE.exec(line)) !== null) {
|
|
1295
|
+
const spec = match[1] ?? match[2] ?? match[3];
|
|
1296
|
+
if (spec === void 0) continue;
|
|
1297
|
+
const pkg = packageNameFromSpec(spec);
|
|
1298
|
+
if (pkg === void 0) continue;
|
|
1299
|
+
const list = byPkg.get(pkg) ?? [];
|
|
1300
|
+
list.push({ file, line: i + 1, specifier: spec, pkg });
|
|
1301
|
+
byPkg.set(pkg, list);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
for (const [pkg, list] of byPkg) {
|
|
1306
|
+
list.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
1307
|
+
byPkg.set(pkg, list.slice(0, 5));
|
|
1308
|
+
}
|
|
1309
|
+
return byPkg;
|
|
1310
|
+
}
|
|
1311
|
+
function evidenceForFinding(finding6, sites, productionFiles, edgesKnown) {
|
|
1312
|
+
if (finding6.ruleId !== "DEPS-VULN-001" && finding6.ruleId !== "DEPS-MALICIOUS-001") return void 0;
|
|
1313
|
+
const snippet = finding6.snippet ?? "";
|
|
1314
|
+
const at = snippet.startsWith("@") ? snippet.indexOf("@", 1) : snippet.indexOf("@");
|
|
1315
|
+
const pkg = at <= 0 ? snippet : snippet.slice(0, at);
|
|
1316
|
+
if (pkg.length === 0) return void 0;
|
|
1317
|
+
const imports = (sites.get(pkg) ?? []).map((site) => ({
|
|
1318
|
+
file: site.file,
|
|
1319
|
+
line: site.line,
|
|
1320
|
+
specifier: site.specifier
|
|
1321
|
+
}));
|
|
1322
|
+
const first = imports[0];
|
|
1323
|
+
const path = finding6.dependencyPath;
|
|
1324
|
+
let reason;
|
|
1325
|
+
switch (finding6.reachability) {
|
|
1326
|
+
case "not-imported":
|
|
1327
|
+
reason = `No production file imports ${pkg}. Checked ${productionFiles.length} production file(s)${productionFiles.length > 0 ? `: ${productionFiles.slice(0, 8).join(", ")}` : ""}.`;
|
|
1328
|
+
return { imports: [], reason };
|
|
1329
|
+
case "reachability-unknown":
|
|
1330
|
+
reason = edgesKnown ? `Reachability for ${pkg} is unknown.` : `Lockfile edges are missing, so ${pkg} is reachability-unknown.`;
|
|
1331
|
+
return { imports, ...first !== void 0 ? {} : {}, reason };
|
|
1332
|
+
case "reachable-via-dependency":
|
|
1333
|
+
reason = path !== void 0 && path.length > 0 ? `Required through ${path.join(" > ")}.` : `Required by an imported package.`;
|
|
1334
|
+
return { imports, ...path !== void 0 ? { dependencyPath: path } : {}, reason };
|
|
1335
|
+
case "imported-not-called":
|
|
1336
|
+
reason = first !== void 0 ? `Imported from ${first.file}:${first.line} and not called.` : `Imported and not called.`;
|
|
1337
|
+
return { imports, reason };
|
|
1338
|
+
case "symbol-reachable":
|
|
1339
|
+
reason = first !== void 0 ? `A named vulnerable export is called from ${first.file}:${first.line}.` : `A named vulnerable export is called.`;
|
|
1340
|
+
return { imports, reason };
|
|
1341
|
+
case "symbol-not-called":
|
|
1342
|
+
reason = `A named vulnerable export is not called.`;
|
|
1343
|
+
return { imports, reason };
|
|
1344
|
+
default:
|
|
1345
|
+
reason = first !== void 0 ? `Imported from ${first.file}:${first.line}.` : `Imported by production code.`;
|
|
1346
|
+
return { imports, ...path !== void 0 ? { dependencyPath: path } : {}, reason };
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
var IMPORT_RE;
|
|
1350
|
+
var init_evidence = __esm({
|
|
1351
|
+
"src/modules/deps/evidence.ts"() {
|
|
1352
|
+
IMPORT_RE = /(?:require\(\s*['"]([^'"]+)['"]\s*\)|from\s+['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\))/g;
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1095
1356
|
// src/modules/deps/symbol-reachability.ts
|
|
1096
1357
|
function affectedFunctionNames(vuln) {
|
|
1097
1358
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -1200,7 +1461,7 @@ function packageNameFromSnippet(snippet) {
|
|
|
1200
1461
|
const at = snippet.indexOf("@");
|
|
1201
1462
|
return at <= 0 ? snippet : snippet.slice(0, at);
|
|
1202
1463
|
}
|
|
1203
|
-
function
|
|
1464
|
+
function packageNameFromSpec2(spec) {
|
|
1204
1465
|
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
|
|
1205
1466
|
if (spec.startsWith("@")) {
|
|
1206
1467
|
const parts = spec.split("/");
|
|
@@ -1236,7 +1497,7 @@ function indexPackageUsage(sources) {
|
|
|
1236
1497
|
const local = m[1];
|
|
1237
1498
|
const spec = m[2];
|
|
1238
1499
|
if (local === void 0 || spec === void 0) continue;
|
|
1239
|
-
const pkg =
|
|
1500
|
+
const pkg = packageNameFromSpec2(spec);
|
|
1240
1501
|
if (pkg === void 0) continue;
|
|
1241
1502
|
bump(pkg, localIsCalled(source, local));
|
|
1242
1503
|
}
|
|
@@ -1246,7 +1507,7 @@ function indexPackageUsage(sources) {
|
|
|
1246
1507
|
while ((named = namedRe.exec(source)) !== null) {
|
|
1247
1508
|
const spec = named[2];
|
|
1248
1509
|
if (spec === void 0) continue;
|
|
1249
|
-
const pkg =
|
|
1510
|
+
const pkg = packageNameFromSpec2(spec);
|
|
1250
1511
|
if (pkg === void 0) continue;
|
|
1251
1512
|
const locals = (named[1] ?? "").split(",").map((s) => s.trim().split(/\s+as\s+/).pop() ?? "");
|
|
1252
1513
|
bump(
|
|
@@ -1305,34 +1566,55 @@ function reachableFrom(target, roots, edges) {
|
|
|
1305
1566
|
if (roots.has(target)) return true;
|
|
1306
1567
|
return shortestDependencyPath(target, roots, edges) !== void 0;
|
|
1307
1568
|
}
|
|
1308
|
-
function importedDirectNames(usage,
|
|
1309
|
-
const directs = /* @__PURE__ */ new Set([...
|
|
1569
|
+
function importedDirectNames(usage, graph2) {
|
|
1570
|
+
const directs = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
|
|
1310
1571
|
const out = /* @__PURE__ */ new Set();
|
|
1311
|
-
for (const [name,
|
|
1312
|
-
if (!
|
|
1572
|
+
for (const [name, hit2] of usage) {
|
|
1573
|
+
if (!hit2.imported) continue;
|
|
1313
1574
|
if (directs.size === 0 || directs.has(name)) out.add(name);
|
|
1314
1575
|
}
|
|
1315
1576
|
return out;
|
|
1316
1577
|
}
|
|
1317
|
-
function classifyReachability(pkgName, productionUsage,
|
|
1318
|
-
const
|
|
1319
|
-
if (
|
|
1320
|
-
return { reachability:
|
|
1578
|
+
function classifyReachability(pkgName, productionUsage, graph2) {
|
|
1579
|
+
const hit2 = productionUsage.get(pkgName);
|
|
1580
|
+
if (hit2?.imported === true) {
|
|
1581
|
+
return { reachability: hit2.called ? "reachable" : "imported-not-called" };
|
|
1321
1582
|
}
|
|
1322
|
-
if (
|
|
1323
|
-
const roots = importedDirectNames(productionUsage,
|
|
1324
|
-
const paths = collectDependencyPaths(pkgName, roots,
|
|
1583
|
+
if (graph2 !== void 0 && graph2.edgesKnown) {
|
|
1584
|
+
const roots = importedDirectNames(productionUsage, graph2);
|
|
1585
|
+
const paths = collectDependencyPaths(pkgName, roots, graph2.edges);
|
|
1325
1586
|
const path = paths[0];
|
|
1326
1587
|
if (path !== void 0 && path.length >= 2) {
|
|
1327
1588
|
return { reachability: "reachable-via-dependency", path, paths };
|
|
1328
1589
|
}
|
|
1329
1590
|
return { reachability: "not-imported" };
|
|
1330
1591
|
}
|
|
1331
|
-
if (
|
|
1592
|
+
if (graph2 !== void 0 && !graph2.edgesKnown) {
|
|
1332
1593
|
return { reachability: "reachability-unknown" };
|
|
1333
1594
|
}
|
|
1334
1595
|
return { reachability: "not-imported" };
|
|
1335
1596
|
}
|
|
1597
|
+
function unusedDirectPackages(ctx) {
|
|
1598
|
+
if (!ctx.graph.edgesKnown) return [];
|
|
1599
|
+
const imported = importedDirectNames(ctx.productionUsage, ctx.graph);
|
|
1600
|
+
return [...ctx.graph.directProd].filter((name) => {
|
|
1601
|
+
if (ctx.productionUsage.get(name)?.imported === true) return false;
|
|
1602
|
+
return !reachableFrom(name, imported, ctx.graph.edges);
|
|
1603
|
+
}).sort();
|
|
1604
|
+
}
|
|
1605
|
+
function unusedDirectPlan(findings, ctx) {
|
|
1606
|
+
const packages = unusedDirectPackages(ctx);
|
|
1607
|
+
if (packages.length === 0) return void 0;
|
|
1608
|
+
const names = new Set(packages);
|
|
1609
|
+
let advisories = 0;
|
|
1610
|
+
for (const finding6 of findings) {
|
|
1611
|
+
if (finding6.ruleId !== "DEPS-VULN-001" && finding6.ruleId !== "DEPS-MALICIOUS-001") continue;
|
|
1612
|
+
if (finding6.snippet === void 0) continue;
|
|
1613
|
+
const pkg = packageNameFromSnippet(finding6.snippet);
|
|
1614
|
+
if (pkg !== void 0 && names.has(pkg)) advisories += 1;
|
|
1615
|
+
}
|
|
1616
|
+
return { packages, advisories };
|
|
1617
|
+
}
|
|
1336
1618
|
function reachabilitySteps(reachability) {
|
|
1337
1619
|
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
|
|
1338
1620
|
return 0;
|
|
@@ -1566,6 +1848,186 @@ var init_semver = __esm({
|
|
|
1566
1848
|
}
|
|
1567
1849
|
});
|
|
1568
1850
|
|
|
1851
|
+
// src/modules/deps/upgrade-plan.ts
|
|
1852
|
+
function sentenceFor(bumps, unresolved) {
|
|
1853
|
+
const parts = [];
|
|
1854
|
+
if (bumps.length === 1) {
|
|
1855
|
+
const bump = bumps[0];
|
|
1856
|
+
if (bump !== void 0) {
|
|
1857
|
+
const noun = bump.findings === 1 ? "finding" : "findings";
|
|
1858
|
+
parts.push(`1 bump (${bump.name} ${bump.from} \u2192 ${bump.to}) resolves ${bump.findings} ${noun}.`);
|
|
1859
|
+
}
|
|
1860
|
+
} else if (bumps.length > 1) {
|
|
1861
|
+
const listed = bumps.map((bump) => `${bump.name} ${bump.from} \u2192 ${bump.to}`).join(", ");
|
|
1862
|
+
const total = bumps.reduce((sum, bump) => sum + bump.findings, 0);
|
|
1863
|
+
const noun = total === 1 ? "finding" : "findings";
|
|
1864
|
+
parts.push(`${bumps.length} bumps (${listed}) resolve ${total} ${noun}.`);
|
|
1865
|
+
}
|
|
1866
|
+
if (unresolved.length > 0) {
|
|
1867
|
+
parts.push(`No fixed version in the OSV cache for ${unresolved.join(", ")}.`);
|
|
1868
|
+
}
|
|
1869
|
+
return parts.join(" ");
|
|
1870
|
+
}
|
|
1871
|
+
function chooseUpgradePlan(advisories) {
|
|
1872
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1873
|
+
for (const advisory of advisories) {
|
|
1874
|
+
const list = byName.get(advisory.name);
|
|
1875
|
+
if (list === void 0) byName.set(advisory.name, [advisory]);
|
|
1876
|
+
else list.push(advisory);
|
|
1877
|
+
}
|
|
1878
|
+
const unresolved = [];
|
|
1879
|
+
const candidates = [];
|
|
1880
|
+
for (const [name, list] of byName) {
|
|
1881
|
+
const from = list[0]?.from ?? "0.0.0";
|
|
1882
|
+
if (list.some((advisory) => advisory.fixed === void 0)) {
|
|
1883
|
+
unresolved.push(name);
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
let to = list[0]?.fixed;
|
|
1887
|
+
for (const advisory of list) {
|
|
1888
|
+
if (advisory.fixed !== void 0 && (to === void 0 || compareSemver(advisory.fixed, to) > 0)) {
|
|
1889
|
+
to = advisory.fixed;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
if (to === void 0 || compareSemver(to, from) <= 0) continue;
|
|
1893
|
+
candidates.push({
|
|
1894
|
+
name,
|
|
1895
|
+
from,
|
|
1896
|
+
to,
|
|
1897
|
+
ids: list.map((advisory) => advisory.id)
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
unresolved.sort();
|
|
1901
|
+
const chosen = smallestMaxCover(candidates);
|
|
1902
|
+
if (chosen.length === 0 && unresolved.length === 0) return void 0;
|
|
1903
|
+
const bumps = chosen.map((candidate) => ({
|
|
1904
|
+
name: candidate.name,
|
|
1905
|
+
from: candidate.from,
|
|
1906
|
+
to: candidate.to,
|
|
1907
|
+
safe: sameMajor(candidate.from, candidate.to),
|
|
1908
|
+
findings: candidate.ids.length
|
|
1909
|
+
}));
|
|
1910
|
+
return { bumps, unresolved, sentence: sentenceFor(bumps, unresolved) };
|
|
1911
|
+
}
|
|
1912
|
+
function smallestMaxCover(candidates) {
|
|
1913
|
+
const limit = Math.min(candidates.length, 12);
|
|
1914
|
+
let best = [];
|
|
1915
|
+
let bestCount = -1;
|
|
1916
|
+
const walk5 = (index, picked, covered) => {
|
|
1917
|
+
if (index === limit) {
|
|
1918
|
+
if (covered.size > bestCount || covered.size === bestCount && betterTie(picked, best)) {
|
|
1919
|
+
best = picked;
|
|
1920
|
+
bestCount = covered.size;
|
|
1921
|
+
}
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
const next = candidates[index];
|
|
1925
|
+
if (next === void 0) return;
|
|
1926
|
+
walk5(index + 1, picked, covered);
|
|
1927
|
+
const grown = new Set(covered);
|
|
1928
|
+
for (const id of next.ids) grown.add(id);
|
|
1929
|
+
walk5(index + 1, [...picked, next], grown);
|
|
1930
|
+
};
|
|
1931
|
+
walk5(0, [], /* @__PURE__ */ new Set());
|
|
1932
|
+
return [...best].sort((a, b) => a.name.localeCompare(b.name));
|
|
1933
|
+
}
|
|
1934
|
+
function betterTie(picked, best) {
|
|
1935
|
+
if (picked.length !== best.length) return picked.length < best.length;
|
|
1936
|
+
const left = picked.map((item) => item.name).sort().join("\0");
|
|
1937
|
+
const right = best.map((item) => item.name).sort().join("\0");
|
|
1938
|
+
return left < right;
|
|
1939
|
+
}
|
|
1940
|
+
var init_upgrade_plan = __esm({
|
|
1941
|
+
"src/modules/deps/upgrade-plan.ts"() {
|
|
1942
|
+
init_semver();
|
|
1943
|
+
}
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
// src/supply/campaign.ts
|
|
1947
|
+
function hit(ruleId, title, message, file, severity) {
|
|
1948
|
+
return {
|
|
1949
|
+
id: makeFindingId(ruleId, file, message),
|
|
1950
|
+
ruleId,
|
|
1951
|
+
module: "deps",
|
|
1952
|
+
title,
|
|
1953
|
+
severity,
|
|
1954
|
+
confidence: "high",
|
|
1955
|
+
file,
|
|
1956
|
+
message,
|
|
1957
|
+
remediation: "Do not install this package. Remove it, delete node_modules, and reinstall from a clean lockfile."
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
function campaignFindings(files) {
|
|
1961
|
+
const findings = [];
|
|
1962
|
+
for (const source of files) {
|
|
1963
|
+
const hasChain = RPC_RE.test(source.text);
|
|
1964
|
+
const hasNet = NET_RE.test(source.text) || EVAL_RE.test(source.text);
|
|
1965
|
+
if (hasChain && hasNet) {
|
|
1966
|
+
findings.push(
|
|
1967
|
+
hit(
|
|
1968
|
+
"SUPPLY-CHAINC2-001",
|
|
1969
|
+
"Package reads a blockchain RPC and then fetches or evaluates the result",
|
|
1970
|
+
"static: RPC method combined with fetch or eval",
|
|
1971
|
+
source.file,
|
|
1972
|
+
"high"
|
|
1973
|
+
)
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
const steals = STEAL_RE.test(source.text);
|
|
1977
|
+
const callsNet = NET_RE.test(source.text);
|
|
1978
|
+
if (steals && callsNet) {
|
|
1979
|
+
findings.push(
|
|
1980
|
+
hit(
|
|
1981
|
+
"SUPPLY-STEALER-001",
|
|
1982
|
+
"Package reads a local credential store and makes a network call",
|
|
1983
|
+
"static: browser, chat, or wallet path combined with a network call",
|
|
1984
|
+
source.file,
|
|
1985
|
+
"critical"
|
|
1986
|
+
)
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
const gated2 = PLATFORM_RE.test(source.text);
|
|
1990
|
+
const downloads = DOWNLOAD_RE.test(source.text);
|
|
1991
|
+
const spawns = SPAWN_RE.test(source.text);
|
|
1992
|
+
if (gated2 && downloads && spawns) {
|
|
1993
|
+
findings.push(
|
|
1994
|
+
hit(
|
|
1995
|
+
"SUPPLY-PLATFORMGATE-001",
|
|
1996
|
+
"Package downloads and launches a file only on one platform",
|
|
1997
|
+
"static: platform check combined with a download and a process spawn",
|
|
1998
|
+
source.file,
|
|
1999
|
+
"high"
|
|
2000
|
+
)
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
return findings;
|
|
2005
|
+
}
|
|
2006
|
+
function securityPlaceholderFinding(name, locked, latest) {
|
|
2007
|
+
if (latest !== "0.0.1-security") return void 0;
|
|
2008
|
+
if (locked === "0.0.1-security") return void 0;
|
|
2009
|
+
return hit(
|
|
2010
|
+
"SUPPLY-SECHOLDER-001",
|
|
2011
|
+
`Locked ${name}@${locked} was replaced on the registry by the security placeholder`,
|
|
2012
|
+
`${name} latest is 0.0.1-security while the lockfile pins ${locked}`,
|
|
2013
|
+
"package-lock.json",
|
|
2014
|
+
"critical"
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
var RPC_RE, NET_RE, EVAL_RE, STEAL_RE, PLATFORM_RE, DOWNLOAD_RE, SPAWN_RE;
|
|
2018
|
+
var init_campaign = __esm({
|
|
2019
|
+
"src/supply/campaign.ts"() {
|
|
2020
|
+
init_finding();
|
|
2021
|
+
RPC_RE = /getSignaturesForAddress|eth_getLogs/;
|
|
2022
|
+
NET_RE = /https\.request|http\.request|fetch\s*\(|axios\./;
|
|
2023
|
+
EVAL_RE = /\beval\s*\(/;
|
|
2024
|
+
STEAL_RE = /Login Data|leveldb|discord[\\/]Local Storage|wallet-extension|Ethereum\/keystore/i;
|
|
2025
|
+
PLATFORM_RE = /process\.platform\s*===\s*['"]win32['"]|os\.platform\s*\(\s*\)/;
|
|
2026
|
+
DOWNLOAD_RE = /https?:\/\/example\.invalid|createWriteStream|https\.get/;
|
|
2027
|
+
SPAWN_RE = /child_process|spawn\s*\(|execFile\s*\(|\.exe['"]/;
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
|
|
1569
2031
|
// src/modules/deps/popular.ts
|
|
1570
2032
|
var POPULAR_PACKAGES;
|
|
1571
2033
|
var init_popular = __esm({
|
|
@@ -3601,7 +4063,7 @@ __export(supply_exports, {
|
|
|
3601
4063
|
checkProvenance: () => checkProvenance,
|
|
3602
4064
|
diffPackageVersions: () => diffPackageVersions,
|
|
3603
4065
|
installGuard: () => installGuard,
|
|
3604
|
-
packageNameFromSpec: () =>
|
|
4066
|
+
packageNameFromSpec: () => packageNameFromSpec3,
|
|
3605
4067
|
provenanceCoverage: () => provenanceCoverage
|
|
3606
4068
|
});
|
|
3607
4069
|
function assertRegistryUrl(url) {
|
|
@@ -3615,7 +4077,7 @@ function assertRegistryUrl(url) {
|
|
|
3615
4077
|
throw new Error(`supply: only ${REGISTRY_HOST} is allowed (got ${parsed.hostname || url})`);
|
|
3616
4078
|
}
|
|
3617
4079
|
}
|
|
3618
|
-
function
|
|
4080
|
+
function packageNameFromSpec3(spec) {
|
|
3619
4081
|
if (spec.startsWith("@")) {
|
|
3620
4082
|
const slash = spec.indexOf("/");
|
|
3621
4083
|
if (slash < 0) return spec;
|
|
@@ -3684,13 +4146,13 @@ function analyzePackageDir(dir) {
|
|
|
3684
4146
|
const readsNpmrc = sources.some((s) => s.text.includes(".npmrc") || s.text.includes(".ssh"));
|
|
3685
4147
|
const hasNet = sources.some((s) => /https\.request|http\.request|fetch\s*\(|axios\./.test(s.text));
|
|
3686
4148
|
if (readsNpmrc && hasNet) {
|
|
3687
|
-
const
|
|
4149
|
+
const hit2 = sources.find((s) => s.text.includes(".npmrc") || s.text.includes(".ssh"));
|
|
3688
4150
|
findings.push(
|
|
3689
4151
|
finding(
|
|
3690
4152
|
"SUPPLY-NPMRC-NET-001",
|
|
3691
4153
|
"Install-time code reads npm/ssh credentials and uses the network",
|
|
3692
4154
|
"static: credential file read combined with http(s)/fetch",
|
|
3693
|
-
|
|
4155
|
+
hit2?.file,
|
|
3694
4156
|
"critical"
|
|
3695
4157
|
)
|
|
3696
4158
|
);
|
|
@@ -3821,7 +4283,7 @@ async function installGuard(spec, opts = {}) {
|
|
|
3821
4283
|
if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY !== "1") {
|
|
3822
4284
|
return { findings: [], skipped: true };
|
|
3823
4285
|
}
|
|
3824
|
-
const name =
|
|
4286
|
+
const name = packageNameFromSpec3(spec);
|
|
3825
4287
|
const url = `https://${REGISTRY_HOST}/${encodeURIComponent(name)}`;
|
|
3826
4288
|
assertRegistryUrl(url);
|
|
3827
4289
|
if (opts.fetchPackument) await opts.fetchPackument(url);
|
|
@@ -3857,7 +4319,7 @@ ${scripts}`;
|
|
|
3857
4319
|
if (!TOKEN_RE.test(text)) return [];
|
|
3858
4320
|
const replicates = PUBLISH_RE.test(text) || REGISTRY_PUT_RE.test(text) || GITHUB_CREATE_RE.test(text) || WORKFLOW_PATH_RE.test(text);
|
|
3859
4321
|
if (!replicates) return [];
|
|
3860
|
-
const
|
|
4322
|
+
const hit2 = sources.find(
|
|
3861
4323
|
(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))
|
|
3862
4324
|
) ?? sources.find((s) => TOKEN_RE.test(s.text));
|
|
3863
4325
|
return [
|
|
@@ -3865,7 +4327,7 @@ ${scripts}`;
|
|
|
3865
4327
|
"SUPPLY-WORM-001",
|
|
3866
4328
|
"Self-replicating worm indicators in dependency code",
|
|
3867
4329
|
"static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",
|
|
3868
|
-
|
|
4330
|
+
hit2?.file,
|
|
3869
4331
|
"critical"
|
|
3870
4332
|
)
|
|
3871
4333
|
];
|
|
@@ -3875,13 +4337,13 @@ function analyzeWorkflowTampering(dir) {
|
|
|
3875
4337
|
const text = `${sources.map((s) => s.text).join("\n")}
|
|
3876
4338
|
${scripts}`;
|
|
3877
4339
|
if (!CI_WRITE_RE.test(text)) return [];
|
|
3878
|
-
const
|
|
4340
|
+
const hit2 = sources.find((s) => CI_WRITE_RE.test(s.text));
|
|
3879
4341
|
return [
|
|
3880
4342
|
finding(
|
|
3881
4343
|
"SUPPLY-WORKFLOW-001",
|
|
3882
4344
|
"Dependency writes GitHub workflow, git hooks, or CI config",
|
|
3883
4345
|
"static: install script or package code writes .github/, .git/hooks/, or CI configuration",
|
|
3884
|
-
|
|
4346
|
+
hit2?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
|
|
3885
4347
|
"critical"
|
|
3886
4348
|
)
|
|
3887
4349
|
];
|
|
@@ -3890,9 +4352,9 @@ function provenanceCoverage(packages, known) {
|
|
|
3890
4352
|
let withProvenance = 0;
|
|
3891
4353
|
let unknown = 0;
|
|
3892
4354
|
for (const pkg of packages) {
|
|
3893
|
-
const
|
|
3894
|
-
if (
|
|
3895
|
-
else if (
|
|
4355
|
+
const hit2 = known.get(`${pkg.name}@${pkg.version}`);
|
|
4356
|
+
if (hit2 === true) withProvenance += 1;
|
|
4357
|
+
else if (hit2 === void 0) unknown += 1;
|
|
3896
4358
|
}
|
|
3897
4359
|
const total = packages.length;
|
|
3898
4360
|
return {
|
|
@@ -3932,8 +4394,8 @@ function parentsOf(name, edges) {
|
|
|
3932
4394
|
}
|
|
3933
4395
|
return out;
|
|
3934
4396
|
}
|
|
3935
|
-
function isDirect(name,
|
|
3936
|
-
return
|
|
4397
|
+
function isDirect(name, graph2) {
|
|
4398
|
+
return graph2.directProd.has(name) || graph2.directDev.has(name);
|
|
3937
4399
|
}
|
|
3938
4400
|
function packagesWithAFixedRelease(findings) {
|
|
3939
4401
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -3951,17 +4413,17 @@ function targetVersion(finding6) {
|
|
|
3951
4413
|
function kindFromLockfile(lockFile) {
|
|
3952
4414
|
return lockfileKind(lockFile) ?? "npm";
|
|
3953
4415
|
}
|
|
3954
|
-
function attachOverrideProposals(findings,
|
|
3955
|
-
if (!
|
|
4416
|
+
function attachOverrideProposals(findings, graph2, lockFile, manifestRaw) {
|
|
4417
|
+
if (!graph2.edgesKnown || manifestRaw === void 0) return findings;
|
|
3956
4418
|
const withFix = packagesWithAFixedRelease(findings);
|
|
3957
4419
|
const kind = kindFromLockfile(lockFile);
|
|
3958
4420
|
return findings.map((finding6) => {
|
|
3959
4421
|
if (finding6.ruleId !== "DEPS-VULN-001" || finding6.snippet === void 0) return finding6;
|
|
3960
4422
|
const name = packageNameFromSnippet2(finding6.snippet);
|
|
3961
|
-
if (name.length === 0 || isDirect(name,
|
|
4423
|
+
if (name.length === 0 || isDirect(name, graph2)) return finding6;
|
|
3962
4424
|
const version = targetVersion(finding6);
|
|
3963
4425
|
if (version === void 0) return finding6;
|
|
3964
|
-
const parentFixed = parentsOf(name,
|
|
4426
|
+
const parentFixed = parentsOf(name, graph2.edges).some((parent) => withFix.has(parent));
|
|
3965
4427
|
if (parentFixed) return finding6;
|
|
3966
4428
|
const after = withManifestOverride(manifestRaw, kind, name, version);
|
|
3967
4429
|
const diff = unifiedDiff("package.json", manifestRaw, after);
|
|
@@ -3982,10 +4444,10 @@ function scanInstalledPackages(cwd, packages) {
|
|
|
3982
4444
|
const dir = packageDir(cwd, pkg);
|
|
3983
4445
|
if (dir === void 0) continue;
|
|
3984
4446
|
const prefix = pkg.path.startsWith("node_modules/") ? pkg.path : `node_modules/${pkg.name}`;
|
|
3985
|
-
for (const
|
|
4447
|
+
for (const hit2 of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
|
|
3986
4448
|
out.push({
|
|
3987
|
-
...
|
|
3988
|
-
file:
|
|
4449
|
+
...hit2,
|
|
4450
|
+
file: hit2.file !== void 0 && hit2.file.length > 0 ? `${prefix}/${hit2.file}` : prefix
|
|
3989
4451
|
});
|
|
3990
4452
|
}
|
|
3991
4453
|
}
|
|
@@ -4066,19 +4528,19 @@ function maliciousFinding(lockFile, pkg, vuln) {
|
|
|
4066
4528
|
references: [vuln.id, ...vuln.aliases ?? []]
|
|
4067
4529
|
};
|
|
4068
4530
|
}
|
|
4069
|
-
function typosquatFinding(lockFile,
|
|
4070
|
-
const snippet = `${
|
|
4531
|
+
function typosquatFinding(lockFile, hit2) {
|
|
4532
|
+
const snippet = `${hit2.name}@${hit2.version}`;
|
|
4071
4533
|
return {
|
|
4072
4534
|
id: makeFindingId("DEPS-TYPOSQUAT-001", lockFile, snippet),
|
|
4073
4535
|
ruleId: "DEPS-TYPOSQUAT-001",
|
|
4074
4536
|
module: "deps",
|
|
4075
|
-
title: `Possible typosquat: ${
|
|
4537
|
+
title: `Possible typosquat: ${hit2.name}`,
|
|
4076
4538
|
severity: "medium",
|
|
4077
4539
|
confidence: "low",
|
|
4078
4540
|
file: lockFile,
|
|
4079
4541
|
snippet,
|
|
4080
|
-
message: `Package "${
|
|
4081
|
-
remediation: `Confirm you intended to install "${
|
|
4542
|
+
message: `Package "${hit2.name}" is 1 character from popular package "${hit2.lookalike}" (possible typosquat).`,
|
|
4543
|
+
remediation: `Confirm you intended to install "${hit2.name}". If not, remove it and depend on "${hit2.lookalike}" instead.`
|
|
4082
4544
|
};
|
|
4083
4545
|
}
|
|
4084
4546
|
function noLockFinding() {
|
|
@@ -4109,6 +4571,76 @@ function deprecatedFinding(lockFile, pkg) {
|
|
|
4109
4571
|
remediation: `Replace ${pkg.name} with a maintained alternative.`
|
|
4110
4572
|
};
|
|
4111
4573
|
}
|
|
4574
|
+
function workspacesEnabled() {
|
|
4575
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_WORKSPACES === "1";
|
|
4576
|
+
}
|
|
4577
|
+
function workspacePatterns(manifestRaw) {
|
|
4578
|
+
try {
|
|
4579
|
+
const manifest = JSON.parse(manifestRaw);
|
|
4580
|
+
const field = manifest.workspaces;
|
|
4581
|
+
if (Array.isArray(field)) return field.filter((item) => typeof item === "string");
|
|
4582
|
+
if (field !== null && typeof field === "object" && Array.isArray(field.packages)) {
|
|
4583
|
+
return field.packages.filter((item) => typeof item === "string");
|
|
4584
|
+
}
|
|
4585
|
+
} catch {
|
|
4586
|
+
return [];
|
|
4587
|
+
}
|
|
4588
|
+
return [];
|
|
4589
|
+
}
|
|
4590
|
+
async function expandWorkspace(cwd, pattern) {
|
|
4591
|
+
const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
4592
|
+
if (!normalized.includes("*")) return [normalized];
|
|
4593
|
+
const slash = normalized.indexOf("/*");
|
|
4594
|
+
if (slash < 0) return [];
|
|
4595
|
+
const parent = normalized.slice(0, slash);
|
|
4596
|
+
try {
|
|
4597
|
+
const entries = await readdir(join(cwd, parent), { withFileTypes: true });
|
|
4598
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => `${parent}/${entry.name}`).sort();
|
|
4599
|
+
} catch {
|
|
4600
|
+
return [];
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
async function workspaceAdvisoryFindings(cwd, manifestRaw, osvOpts) {
|
|
4604
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
4605
|
+
for (const pattern of workspacePatterns(manifestRaw)) {
|
|
4606
|
+
for (const dir of await expandWorkspace(cwd, pattern)) dirs.add(dir);
|
|
4607
|
+
}
|
|
4608
|
+
const findings = [];
|
|
4609
|
+
for (const dir of [...dirs].sort()) {
|
|
4610
|
+
if (dir === "" || dir === ".") continue;
|
|
4611
|
+
const lockName = LOCKFILE_NAMES.find((name) => existsSync(join(cwd, dir, name)));
|
|
4612
|
+
if (lockName === void 0) continue;
|
|
4613
|
+
let text;
|
|
4614
|
+
try {
|
|
4615
|
+
text = await readFile(join(cwd, dir, lockName), "utf8");
|
|
4616
|
+
} catch {
|
|
4617
|
+
continue;
|
|
4618
|
+
}
|
|
4619
|
+
const graph2 = parseLockfileGraph(lockName, text);
|
|
4620
|
+
const hits = await queryOsv(graph2.packages, osvOpts);
|
|
4621
|
+
const file = `${dir}/${lockName}`;
|
|
4622
|
+
for (const hit2 of hits) {
|
|
4623
|
+
for (const vuln of hit2.vulns) {
|
|
4624
|
+
findings.push(
|
|
4625
|
+
isMaliciousAdvisory(vuln) ? maliciousFinding(file, hit2.pkg, vuln) : vulnFinding(file, hit2.pkg, vuln)
|
|
4626
|
+
);
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
return findings;
|
|
4631
|
+
}
|
|
4632
|
+
async function withWorkspaceAdvisories(cwd, manifestRaw, osvOpts, findings) {
|
|
4633
|
+
if (!workspacesEnabled() || manifestRaw === void 0) return findings;
|
|
4634
|
+
return [...findings, ...await workspaceAdvisoryFindings(cwd, manifestRaw, osvOpts)];
|
|
4635
|
+
}
|
|
4636
|
+
async function readPlaceholderLatest(file) {
|
|
4637
|
+
try {
|
|
4638
|
+
const raw = JSON.parse(await readFile(file, "utf8"));
|
|
4639
|
+
return typeof raw.latest === "string" ? raw.latest : void 0;
|
|
4640
|
+
} catch {
|
|
4641
|
+
return void 0;
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4112
4644
|
function graphFromManifest(manifest) {
|
|
4113
4645
|
const keys = (field) => {
|
|
4114
4646
|
const block = manifest[field];
|
|
@@ -4125,7 +4657,7 @@ function graphFromManifest(manifest) {
|
|
|
4125
4657
|
edgesKnown: false
|
|
4126
4658
|
};
|
|
4127
4659
|
}
|
|
4128
|
-
async function reachabilityCtx(cwd, files,
|
|
4660
|
+
async function reachabilityCtx(cwd, files, graph2) {
|
|
4129
4661
|
const prodSources = /* @__PURE__ */ new Map();
|
|
4130
4662
|
for (const rel of files) {
|
|
4131
4663
|
if (!/\.[cm]?[jt]sx?$/.test(rel)) continue;
|
|
@@ -4135,7 +4667,7 @@ async function reachabilityCtx(cwd, files, graph) {
|
|
|
4135
4667
|
} catch {
|
|
4136
4668
|
}
|
|
4137
4669
|
}
|
|
4138
|
-
return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
|
|
4670
|
+
return { productionUsage: indexPackageUsage(prodSources), graph: graph2, sources: prodSources };
|
|
4139
4671
|
}
|
|
4140
4672
|
var LOCKFILE_NAMES, MANIFEST_FIELDS2, MALICIOUS_REMEDIATION, depsModule;
|
|
4141
4673
|
var init_deps = __esm({
|
|
@@ -4144,7 +4676,10 @@ var init_deps = __esm({
|
|
|
4144
4676
|
init_lockfile();
|
|
4145
4677
|
init_osv();
|
|
4146
4678
|
init_pins();
|
|
4679
|
+
init_evidence();
|
|
4147
4680
|
init_reachability();
|
|
4681
|
+
init_upgrade_plan();
|
|
4682
|
+
init_campaign();
|
|
4148
4683
|
init_symbol_reachability();
|
|
4149
4684
|
init_semver();
|
|
4150
4685
|
init_typosquat();
|
|
@@ -4158,6 +4693,8 @@ var init_deps = __esm({
|
|
|
4158
4693
|
depsModule = {
|
|
4159
4694
|
name: "deps",
|
|
4160
4695
|
async run(ctx) {
|
|
4696
|
+
depsModule.unusedDirect = void 0;
|
|
4697
|
+
depsModule.upgradePlan = void 0;
|
|
4161
4698
|
const lockFile = LOCKFILE_NAMES.find((name) => ctx.files.includes(name));
|
|
4162
4699
|
const cacheDir = ctx.config.deps.cacheDir !== void 0 ? resolve(ctx.cwd, ctx.config.deps.cacheDir) : defaultCacheDir();
|
|
4163
4700
|
const osvOpts = { cacheDir, offline: ctx.config.deps.offline };
|
|
@@ -4183,21 +4720,21 @@ var init_deps = __esm({
|
|
|
4183
4720
|
}
|
|
4184
4721
|
const pins = exactPinsFromManifest(manifest);
|
|
4185
4722
|
const directNames2 = await readDirectDependencyNames(ctx.cwd);
|
|
4186
|
-
for (const
|
|
4187
|
-
findings2.push(typosquatFinding("package.json",
|
|
4723
|
+
for (const hit2 of findTyposquats(pins, { directNames: directNames2 })) {
|
|
4724
|
+
findings2.push(typosquatFinding("package.json", hit2));
|
|
4188
4725
|
}
|
|
4189
4726
|
const osvHits2 = await queryOsv(pins, osvOpts);
|
|
4190
4727
|
const symbols2 = /* @__PURE__ */ new Map();
|
|
4191
|
-
for (const
|
|
4192
|
-
for (const vuln of
|
|
4193
|
-
const finding6 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json",
|
|
4728
|
+
for (const hit2 of osvHits2) {
|
|
4729
|
+
for (const vuln of hit2.vulns) {
|
|
4730
|
+
const finding6 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit2.pkg, vuln) : vulnFinding("package.json", hit2.pkg, vuln, depLoc(hit2.pkg.name));
|
|
4194
4731
|
const names = affectedFunctionNames(vuln);
|
|
4195
4732
|
if (names.length > 0 && finding6.ruleId === "DEPS-VULN-001") symbols2.set(finding6.id, names);
|
|
4196
4733
|
findings2.push(finding6);
|
|
4197
4734
|
}
|
|
4198
4735
|
}
|
|
4199
4736
|
const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
|
|
4200
|
-
return applyReachability(findings2, noLockCtx, symbols2);
|
|
4737
|
+
return withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, applyReachability(findings2, noLockCtx, symbols2));
|
|
4201
4738
|
}
|
|
4202
4739
|
let contents;
|
|
4203
4740
|
try {
|
|
@@ -4205,12 +4742,12 @@ var init_deps = __esm({
|
|
|
4205
4742
|
} catch {
|
|
4206
4743
|
return [];
|
|
4207
4744
|
}
|
|
4208
|
-
const
|
|
4209
|
-
const packages =
|
|
4745
|
+
const graph2 = parseLockfileGraph(lockFile, contents);
|
|
4746
|
+
const packages = graph2.packages;
|
|
4210
4747
|
const findings = [];
|
|
4211
4748
|
const directNames = await readDirectDependencyNames(ctx.cwd);
|
|
4212
|
-
for (const
|
|
4213
|
-
findings.push(typosquatFinding(lockFile,
|
|
4749
|
+
for (const hit2 of findTyposquats(packages, { directNames })) {
|
|
4750
|
+
findings.push(typosquatFinding(lockFile, hit2));
|
|
4214
4751
|
}
|
|
4215
4752
|
for (const pkg of packages) {
|
|
4216
4753
|
if (pkg.deprecated !== void 0) {
|
|
@@ -4219,20 +4756,56 @@ var init_deps = __esm({
|
|
|
4219
4756
|
}
|
|
4220
4757
|
const osvHits = await queryOsv(packages, osvOpts);
|
|
4221
4758
|
const symbols = /* @__PURE__ */ new Map();
|
|
4222
|
-
for (const
|
|
4223
|
-
for (const vuln of
|
|
4224
|
-
const finding6 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile,
|
|
4759
|
+
for (const hit2 of osvHits) {
|
|
4760
|
+
for (const vuln of hit2.vulns) {
|
|
4761
|
+
const finding6 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit2.pkg, vuln) : vulnFinding(lockFile, hit2.pkg, vuln, depLoc(hit2.pkg.name));
|
|
4225
4762
|
const names = affectedFunctionNames(vuln);
|
|
4226
4763
|
if (names.length > 0 && finding6.ruleId === "DEPS-VULN-001") symbols.set(finding6.id, names);
|
|
4227
4764
|
findings.push(finding6);
|
|
4228
4765
|
}
|
|
4229
4766
|
}
|
|
4230
|
-
|
|
4767
|
+
const reachCtx = await reachabilityCtx(ctx.cwd, ctx.files, graph2);
|
|
4768
|
+
let reached = applyReachability(findings, reachCtx, symbols);
|
|
4231
4769
|
if (experimentalVex()) {
|
|
4232
|
-
reached = attachOverrideProposals(reached,
|
|
4770
|
+
reached = attachOverrideProposals(reached, graph2, lockFile, manifestRaw);
|
|
4233
4771
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
4234
4772
|
}
|
|
4235
4773
|
if (licenseEnabled()) reached.push(...await scanCopyleftLicenses(ctx.cwd));
|
|
4774
|
+
depsModule.unusedDirect = unusedDirectPlan(reached, reachCtx);
|
|
4775
|
+
const direct = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
|
|
4776
|
+
const advisories = [];
|
|
4777
|
+
for (const hit2 of osvHits) {
|
|
4778
|
+
if (!direct.has(hit2.pkg.name)) continue;
|
|
4779
|
+
for (const vuln of hit2.vulns) {
|
|
4780
|
+
if (isMaliciousAdvisory(vuln)) continue;
|
|
4781
|
+
advisories.push({
|
|
4782
|
+
name: hit2.pkg.name,
|
|
4783
|
+
from: hit2.pkg.version,
|
|
4784
|
+
id: vuln.id,
|
|
4785
|
+
fixed: minFixedVersion(vuln.affected ?? [])
|
|
4786
|
+
});
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
depsModule.upgradePlan = chooseUpgradePlan(advisories);
|
|
4790
|
+
if (process.env.FULLSHIELD_EXPERIMENTAL_REACH_EVIDENCE === "1") {
|
|
4791
|
+
const sites = collectImportSites(reachCtx.sources);
|
|
4792
|
+
const files = [...reachCtx.sources.keys()].sort();
|
|
4793
|
+
reached = reached.map((finding6) => {
|
|
4794
|
+
const evidence = evidenceForFinding(finding6, sites, files, graph2.edgesKnown);
|
|
4795
|
+
return evidence === void 0 ? finding6 : { ...finding6, reachabilityEvidence: evidence };
|
|
4796
|
+
});
|
|
4797
|
+
}
|
|
4798
|
+
reached = await withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, reached);
|
|
4799
|
+
if (process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY === "1") {
|
|
4800
|
+
reached.push(...campaignFindings([...reachCtx.sources.entries()].map(([file, text]) => ({ file, text }))));
|
|
4801
|
+
for (const name of [...direct].sort()) {
|
|
4802
|
+
const locked = packages.find((pkg) => pkg.name === name)?.version;
|
|
4803
|
+
if (locked === void 0) continue;
|
|
4804
|
+
const latest = await readPlaceholderLatest(join(cacheDir, "registry", `${name.replaceAll("/", "__")}.json`));
|
|
4805
|
+
const placeholder = securityPlaceholderFinding(name, locked, latest);
|
|
4806
|
+
if (placeholder !== void 0) reached.push(placeholder);
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4236
4809
|
return reached;
|
|
4237
4810
|
}
|
|
4238
4811
|
};
|
|
@@ -4614,27 +5187,174 @@ var init_mcp = __esm({
|
|
|
4614
5187
|
}
|
|
4615
5188
|
});
|
|
4616
5189
|
|
|
4617
|
-
// src/
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
5190
|
+
// src/runtime/lru.ts
|
|
5191
|
+
var LruMap;
|
|
5192
|
+
var init_lru = __esm({
|
|
5193
|
+
"src/runtime/lru.ts"() {
|
|
5194
|
+
LruMap = class {
|
|
5195
|
+
constructor(max) {
|
|
5196
|
+
this.max = max;
|
|
5197
|
+
if (max < 1) throw new Error("LruMap max must be >= 1");
|
|
5198
|
+
}
|
|
5199
|
+
max;
|
|
5200
|
+
map = /* @__PURE__ */ new Map();
|
|
5201
|
+
get size() {
|
|
5202
|
+
return this.map.size;
|
|
5203
|
+
}
|
|
5204
|
+
has(key) {
|
|
5205
|
+
return this.map.has(key);
|
|
5206
|
+
}
|
|
5207
|
+
get(key) {
|
|
5208
|
+
if (!this.map.has(key)) return void 0;
|
|
5209
|
+
const value = this.map.get(key);
|
|
5210
|
+
this.map.delete(key);
|
|
5211
|
+
this.map.set(key, value);
|
|
5212
|
+
return value;
|
|
5213
|
+
}
|
|
5214
|
+
set(key, value) {
|
|
5215
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
5216
|
+
this.map.set(key, value);
|
|
5217
|
+
while (this.map.size > this.max) {
|
|
5218
|
+
const oldest = this.map.keys().next().value;
|
|
5219
|
+
if (oldest === void 0) break;
|
|
5220
|
+
this.map.delete(oldest);
|
|
5221
|
+
}
|
|
5222
|
+
return this;
|
|
5223
|
+
}
|
|
5224
|
+
};
|
|
5225
|
+
}
|
|
5226
|
+
});
|
|
5227
|
+
function packageVersion() {
|
|
5228
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
5229
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
5230
|
+
try {
|
|
5231
|
+
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
5232
|
+
const pkg = JSON.parse(raw);
|
|
5233
|
+
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
5234
|
+
return pkg.version;
|
|
5235
|
+
}
|
|
5236
|
+
} catch {
|
|
5237
|
+
}
|
|
5238
|
+
const parent = dirname(dir);
|
|
5239
|
+
if (parent === dir) break;
|
|
5240
|
+
dir = parent;
|
|
5241
|
+
}
|
|
5242
|
+
throw new Error("fullshield package.json not found");
|
|
5243
|
+
}
|
|
5244
|
+
var init_version = __esm({
|
|
5245
|
+
"src/core/version.ts"() {
|
|
5246
|
+
}
|
|
5247
|
+
});
|
|
5248
|
+
function parsePoolEnabled() {
|
|
5249
|
+
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
5250
|
+
}
|
|
5251
|
+
function parsePoolSize() {
|
|
5252
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
5253
|
+
if (raw !== void 0 && raw !== "") {
|
|
5254
|
+
const parsed = Number(raw);
|
|
5255
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
|
|
5256
|
+
}
|
|
5257
|
+
return Math.min(2, Math.max(1, availableParallelism() - 1));
|
|
5258
|
+
}
|
|
5259
|
+
function workerMemoryMb() {
|
|
5260
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
|
|
5261
|
+
const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
|
|
5262
|
+
if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
|
|
5263
|
+
return 512;
|
|
5264
|
+
}
|
|
5265
|
+
function parseCacheDir() {
|
|
5266
|
+
return join(homedir(), ".cache", "fullshield", "parse");
|
|
5267
|
+
}
|
|
5268
|
+
function parseCacheKey(code) {
|
|
5269
|
+
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
5270
|
+
}
|
|
5271
|
+
function parseOne(code) {
|
|
5272
|
+
const key = parseCacheKey(code);
|
|
5273
|
+
const hit2 = cache.get(key);
|
|
5274
|
+
if (hit2 !== void 0) return Promise.resolve(hit2);
|
|
5275
|
+
return new Promise((resolve8, reject) => {
|
|
5276
|
+
const worker = new Worker(fileURLToPath(workerUrl), {
|
|
5277
|
+
workerData: { code },
|
|
5278
|
+
resourceLimits: { maxOldGenerationSizeMb: workerMemoryMb() }
|
|
5279
|
+
});
|
|
5280
|
+
let settled = false;
|
|
5281
|
+
const finish = () => {
|
|
5282
|
+
void worker.terminate();
|
|
5283
|
+
};
|
|
5284
|
+
const succeed = (ast) => {
|
|
5285
|
+
if (settled) return;
|
|
5286
|
+
settled = true;
|
|
5287
|
+
cache.set(key, ast);
|
|
5288
|
+
finish();
|
|
5289
|
+
resolve8(ast);
|
|
5290
|
+
};
|
|
5291
|
+
const fail = (error) => {
|
|
5292
|
+
if (settled) return;
|
|
5293
|
+
settled = true;
|
|
5294
|
+
finish();
|
|
5295
|
+
reject(error);
|
|
5296
|
+
};
|
|
5297
|
+
worker.once("message", (ast) => {
|
|
5298
|
+
succeed(ast);
|
|
5299
|
+
});
|
|
5300
|
+
worker.once("error", (error) => {
|
|
5301
|
+
fail(error);
|
|
5302
|
+
});
|
|
5303
|
+
worker.once("exit", (code2) => {
|
|
5304
|
+
if (settled) return;
|
|
5305
|
+
fail(new Error(`parse worker exited ${code2 ?? "null"}`));
|
|
5306
|
+
});
|
|
5307
|
+
});
|
|
5308
|
+
}
|
|
5309
|
+
async function parseWithPool(codes) {
|
|
5310
|
+
const size = parsePoolSize();
|
|
5311
|
+
const out = new Array(codes.length);
|
|
5312
|
+
let next = 0;
|
|
5313
|
+
async function run() {
|
|
5314
|
+
for (; ; ) {
|
|
5315
|
+
const index = next;
|
|
5316
|
+
next += 1;
|
|
5317
|
+
if (index >= codes.length) return;
|
|
5318
|
+
const code = codes[index];
|
|
5319
|
+
if (code === void 0) return;
|
|
5320
|
+
out[index] = await parseOne(code);
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
const workers = Math.min(size, Math.max(1, codes.length));
|
|
5324
|
+
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
5325
|
+
return out;
|
|
5326
|
+
}
|
|
5327
|
+
var workerUrl, cache;
|
|
5328
|
+
var init_parse_pool = __esm({
|
|
5329
|
+
"src/core/parse-pool.ts"() {
|
|
5330
|
+
init_lru();
|
|
5331
|
+
init_version();
|
|
5332
|
+
workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
5333
|
+
cache = new LruMap(64);
|
|
5334
|
+
}
|
|
5335
|
+
});
|
|
5336
|
+
|
|
5337
|
+
// src/modules/sast/frameworks.ts
|
|
5338
|
+
function decoratorExpr(dec) {
|
|
5339
|
+
if (dec.type !== "Decorator") return void 0;
|
|
5340
|
+
return dec.expression;
|
|
5341
|
+
}
|
|
5342
|
+
function decoratorName(dec) {
|
|
5343
|
+
const expr = decoratorExpr(dec);
|
|
5344
|
+
if (expr === void 0) return void 0;
|
|
5345
|
+
if (expr.type === "Identifier") return expr.name;
|
|
5346
|
+
if (expr.type === "CallExpression") return calleeName(expr);
|
|
5347
|
+
return void 0;
|
|
5348
|
+
}
|
|
5349
|
+
function nodeDecorators(node) {
|
|
5350
|
+
const decs = node.decorators;
|
|
5351
|
+
return Array.isArray(decs) ? decs : [];
|
|
5352
|
+
}
|
|
5353
|
+
function nestInputDecorator(param) {
|
|
5354
|
+
if (!frameworksEnabled()) return void 0;
|
|
5355
|
+
for (const dec of nodeDecorators(param)) {
|
|
5356
|
+
const name = decoratorName(dec);
|
|
5357
|
+
if (name !== void 0 && NEST_INPUT.test(name)) return name;
|
|
4638
5358
|
}
|
|
4639
5359
|
return void 0;
|
|
4640
5360
|
}
|
|
@@ -4762,6 +5482,9 @@ var init_mcp2 = __esm({
|
|
|
4762
5482
|
});
|
|
4763
5483
|
|
|
4764
5484
|
// src/modules/sast/taint.ts
|
|
5485
|
+
function setCustomTaint(config) {
|
|
5486
|
+
customTaint = config;
|
|
5487
|
+
}
|
|
4765
5488
|
function objectChainMentions(node, pattern) {
|
|
4766
5489
|
let current = node;
|
|
4767
5490
|
for (let i = 0; i < 8 && current !== void 0; i += 1) {
|
|
@@ -4893,6 +5616,61 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
|
4893
5616
|
return false;
|
|
4894
5617
|
}
|
|
4895
5618
|
}
|
|
5619
|
+
function valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners) {
|
|
5620
|
+
if (node === void 0) return false;
|
|
5621
|
+
if (isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) return false;
|
|
5622
|
+
if (node.type === "Identifier") {
|
|
5623
|
+
if (nosqlSanitized.has(node.name)) return false;
|
|
5624
|
+
return built.has(node.name) || tainted.has(node.name);
|
|
5625
|
+
}
|
|
5626
|
+
if (isRequestObjectNode(node)) return true;
|
|
5627
|
+
if (node.type === "AwaitExpression") {
|
|
5628
|
+
return valueFeedsFilter(node.argument, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
5629
|
+
}
|
|
5630
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && filterReturners.has(node.callee.name)) {
|
|
5631
|
+
return true;
|
|
5632
|
+
}
|
|
5633
|
+
if (node.type === "ObjectExpression") {
|
|
5634
|
+
return node.properties.some((prop) => {
|
|
5635
|
+
if (prop.type === "SpreadElement") {
|
|
5636
|
+
return valueFeedsFilter(prop.argument, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
5637
|
+
}
|
|
5638
|
+
if (prop.type !== "Property") return false;
|
|
5639
|
+
return valueFeedsFilter(prop.value, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5642
|
+
if (node.type === "ArrayExpression") {
|
|
5643
|
+
return node.elements.some((el) => {
|
|
5644
|
+
if (el === null) return false;
|
|
5645
|
+
if (el.type === "SpreadElement") {
|
|
5646
|
+
return valueFeedsFilter(el.argument, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
5647
|
+
}
|
|
5648
|
+
return valueFeedsFilter(el, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
5649
|
+
});
|
|
5650
|
+
}
|
|
5651
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
|
|
5652
|
+
return node.arguments.some(
|
|
5653
|
+
(arg) => arg.type !== "SpreadElement" && valueFeedsFilter(arg, tainted, returners, nosqlSanitized, built, filterReturners)
|
|
5654
|
+
);
|
|
5655
|
+
}
|
|
5656
|
+
if (node.type === "ChainExpression" || node.type === "TSAsExpression" || node.type === "TSNonNullExpression" || node.type === "TSTypeAssertion" || node.type === "MemberExpression") {
|
|
5657
|
+
return exprIsTainted(node, tainted, returners);
|
|
5658
|
+
}
|
|
5659
|
+
return false;
|
|
5660
|
+
}
|
|
5661
|
+
function initIsBuiltFilter(node, tainted, returners, nosqlSanitized, built, filterReturners) {
|
|
5662
|
+
if (node.type === "AwaitExpression") {
|
|
5663
|
+
return initIsBuiltFilter(node.argument, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
5664
|
+
}
|
|
5665
|
+
if (node.type === "Identifier") return built.has(node.name) && !nosqlSanitized.has(node.name);
|
|
5666
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier" && filterReturners.has(node.callee.name)) {
|
|
5667
|
+
return true;
|
|
5668
|
+
}
|
|
5669
|
+
if (node.type === "ObjectExpression" || node.type === "ArrayExpression") {
|
|
5670
|
+
return valueFeedsFilter(node, tainted, returners, nosqlSanitized, built, filterReturners);
|
|
5671
|
+
}
|
|
5672
|
+
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);
|
|
5673
|
+
}
|
|
4896
5674
|
function exprIsSanitized(node, tainted, names, isCall, returners = NO_RETURNERS) {
|
|
4897
5675
|
if (node === void 0) return true;
|
|
4898
5676
|
if (isCall(node)) return true;
|
|
@@ -4956,7 +5734,7 @@ function seedMcpToolParams(scope, tainted) {
|
|
|
4956
5734
|
for (const name of boundNames(param)) tainted.add(name);
|
|
4957
5735
|
}
|
|
4958
5736
|
}
|
|
4959
|
-
function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
5737
|
+
function analyzeScope(scope, seed = [], returners = NO_RETURNERS, filterReturners = NO_RETURNERS, initialBuilt = NO_RETURNERS) {
|
|
4960
5738
|
const tainted = new Set(seed);
|
|
4961
5739
|
seedDestructuredRequestParams(scope, tainted);
|
|
4962
5740
|
seedFrameworkParams(scope, tainted);
|
|
@@ -4964,6 +5742,8 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
4964
5742
|
const urlSanitized = /* @__PURE__ */ new Set();
|
|
4965
5743
|
const xssSanitized = /* @__PURE__ */ new Set();
|
|
4966
5744
|
const nosqlSanitized = /* @__PURE__ */ new Set();
|
|
5745
|
+
const sqlCleared = /* @__PURE__ */ new Set();
|
|
5746
|
+
const customPath = /* @__PURE__ */ new Set();
|
|
4967
5747
|
let changed = true;
|
|
4968
5748
|
while (changed) {
|
|
4969
5749
|
changed = false;
|
|
@@ -4974,6 +5754,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
4974
5754
|
for (const name of boundNames(node.id)) nosqlSanitized.add(name);
|
|
4975
5755
|
}
|
|
4976
5756
|
if (isSchemaValidatorCall(init)) return;
|
|
5757
|
+
applyCustomCall(init, boundNames(node.id), tainted, sqlCleared, customPath);
|
|
4977
5758
|
if (isUrlSanitizerCall(init)) {
|
|
4978
5759
|
for (const name of boundNames(node.id)) urlSanitized.add(name);
|
|
4979
5760
|
}
|
|
@@ -4994,6 +5775,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
4994
5775
|
for (const name of boundNames(node.left)) nosqlSanitized.add(name);
|
|
4995
5776
|
}
|
|
4996
5777
|
if (isSchemaValidatorCall(node.right)) return;
|
|
5778
|
+
applyCustomCall(node.right, boundNames(node.left), tainted, sqlCleared, customPath);
|
|
4997
5779
|
if (isUrlSanitizerCall(node.right)) {
|
|
4998
5780
|
for (const name of boundNames(node.left)) urlSanitized.add(name);
|
|
4999
5781
|
}
|
|
@@ -5020,7 +5802,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5020
5802
|
for (const name of boundNames(node.left)) resolved.add(name);
|
|
5021
5803
|
}
|
|
5022
5804
|
});
|
|
5023
|
-
const pathSanitized =
|
|
5805
|
+
const pathSanitized = new Set(customPath);
|
|
5024
5806
|
walkScope(scope, (node) => {
|
|
5025
5807
|
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
5026
5808
|
if (propName(node.callee) !== "startsWith") return;
|
|
@@ -5062,17 +5844,92 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
5062
5844
|
for (const name of boundNames(node.left)) requestObjects.add(name);
|
|
5063
5845
|
}
|
|
5064
5846
|
});
|
|
5847
|
+
const builtFilters = new Set(initialBuilt);
|
|
5848
|
+
let grew = true;
|
|
5849
|
+
while (grew) {
|
|
5850
|
+
grew = false;
|
|
5851
|
+
const add = (names) => {
|
|
5852
|
+
for (const name of names) {
|
|
5853
|
+
if (nosqlSanitized.has(name) || builtFilters.has(name)) continue;
|
|
5854
|
+
builtFilters.add(name);
|
|
5855
|
+
grew = true;
|
|
5856
|
+
}
|
|
5857
|
+
};
|
|
5858
|
+
walkScope(scope, (node) => {
|
|
5859
|
+
if (node.type === "VariableDeclarator" && node.init !== null) {
|
|
5860
|
+
if (initIsBuiltFilter(node.init, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
|
|
5861
|
+
add(boundNames(node.id));
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
if (node.type !== "AssignmentExpression") return;
|
|
5865
|
+
if (node.left.type === "Identifier" && initIsBuiltFilter(node.right, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
|
|
5866
|
+
add([node.left.name]);
|
|
5867
|
+
}
|
|
5868
|
+
if (node.left.type === "MemberExpression" && node.left.object.type === "Identifier" && valueFeedsFilter(node.right, tainted, returners, nosqlSanitized, builtFilters, filterReturners)) {
|
|
5869
|
+
add([node.left.object.name]);
|
|
5870
|
+
}
|
|
5871
|
+
});
|
|
5872
|
+
}
|
|
5065
5873
|
return {
|
|
5066
5874
|
tainted,
|
|
5067
5875
|
pathSanitized,
|
|
5876
|
+
sqlCleared,
|
|
5068
5877
|
urlSanitized,
|
|
5069
5878
|
xssSanitized,
|
|
5070
5879
|
nosqlSanitized,
|
|
5071
5880
|
hasPasswordName,
|
|
5072
5881
|
requestObjects,
|
|
5882
|
+
builtFilters,
|
|
5883
|
+
filterReturners: new Set(filterReturners),
|
|
5073
5884
|
returners: new Set(returners)
|
|
5074
5885
|
};
|
|
5075
5886
|
}
|
|
5887
|
+
function locallyDeclared(scope) {
|
|
5888
|
+
const names = /* @__PURE__ */ new Set();
|
|
5889
|
+
if (isFunctionNode(scope)) {
|
|
5890
|
+
for (const param of scope.params) {
|
|
5891
|
+
for (const name of boundNames(param)) names.add(name);
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5894
|
+
walkScope(scope, (node) => {
|
|
5895
|
+
if (node.type === "VariableDeclarator") {
|
|
5896
|
+
for (const name of boundNames(node.id)) names.add(name);
|
|
5897
|
+
}
|
|
5898
|
+
if (node.type === "FunctionDeclaration" && node.id !== null) names.add(node.id.name);
|
|
5899
|
+
});
|
|
5900
|
+
return names;
|
|
5901
|
+
}
|
|
5902
|
+
function inheritedBuiltFilters(fn, facts) {
|
|
5903
|
+
const local = locallyDeclared(fn);
|
|
5904
|
+
const inherited = /* @__PURE__ */ new Set();
|
|
5905
|
+
let parent = fn.parent;
|
|
5906
|
+
while (parent !== void 0 && !isFunctionNode(parent)) {
|
|
5907
|
+
parent = parent.parent;
|
|
5908
|
+
}
|
|
5909
|
+
const outer = parent === void 0 ? void 0 : facts.get(parent);
|
|
5910
|
+
if (outer === void 0) return inherited;
|
|
5911
|
+
for (const name of outer.builtFilters) {
|
|
5912
|
+
if (!local.has(name)) inherited.add(name);
|
|
5913
|
+
}
|
|
5914
|
+
return inherited;
|
|
5915
|
+
}
|
|
5916
|
+
function scopeReturnsBuiltFilter(scope, facts) {
|
|
5917
|
+
let yes = false;
|
|
5918
|
+
walkScope(scope, (node) => {
|
|
5919
|
+
if (node.type !== "ReturnStatement" || node.argument === null) return;
|
|
5920
|
+
if (initIsBuiltFilter(
|
|
5921
|
+
node.argument,
|
|
5922
|
+
facts.tainted,
|
|
5923
|
+
facts.returners,
|
|
5924
|
+
facts.nosqlSanitized,
|
|
5925
|
+
facts.builtFilters,
|
|
5926
|
+
facts.filterReturners
|
|
5927
|
+
)) {
|
|
5928
|
+
yes = true;
|
|
5929
|
+
}
|
|
5930
|
+
});
|
|
5931
|
+
return yes;
|
|
5932
|
+
}
|
|
5076
5933
|
function functionName(fn) {
|
|
5077
5934
|
if (fn.type === "FunctionDeclaration" && fn.id !== null) return fn.id.name;
|
|
5078
5935
|
if (isFunctionNode(fn) && fn.id !== null && fn.id.type === "Identifier") return fn.id.name;
|
|
@@ -5161,11 +6018,12 @@ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
|
5161
6018
|
const seeds = /* @__PURE__ */ new Map();
|
|
5162
6019
|
for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
|
|
5163
6020
|
const returners = new Set(extraReturners2);
|
|
6021
|
+
const filterReturners = /* @__PURE__ */ new Set();
|
|
5164
6022
|
for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
|
|
5165
6023
|
let changed = false;
|
|
5166
|
-
facts.set(ast, analyzeScope(ast, [], returners));
|
|
6024
|
+
facts.set(ast, analyzeScope(ast, [], returners, filterReturners));
|
|
5167
6025
|
for (const fn of functions) {
|
|
5168
|
-
facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners));
|
|
6026
|
+
facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners, filterReturners, inheritedBuiltFilters(fn, facts)));
|
|
5169
6027
|
}
|
|
5170
6028
|
for (const fn of functions) {
|
|
5171
6029
|
const name = functionName(fn);
|
|
@@ -5175,6 +6033,10 @@ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
|
5175
6033
|
returners.add(name);
|
|
5176
6034
|
changed = true;
|
|
5177
6035
|
}
|
|
6036
|
+
if (scopeReturnsBuiltFilter(fn, f) && !filterReturners.has(name)) {
|
|
6037
|
+
filterReturners.add(name);
|
|
6038
|
+
changed = true;
|
|
6039
|
+
}
|
|
5178
6040
|
}
|
|
5179
6041
|
for (const fn of functions) {
|
|
5180
6042
|
const f = facts.get(fn);
|
|
@@ -5207,6 +6069,51 @@ function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
|
5207
6069
|
}
|
|
5208
6070
|
return facts;
|
|
5209
6071
|
}
|
|
6072
|
+
function sinkClasses(scope, tainted, returners) {
|
|
6073
|
+
const found = /* @__PURE__ */ new Set();
|
|
6074
|
+
walkScope(scope, (node) => {
|
|
6075
|
+
if (node.type !== "CallExpression") return;
|
|
6076
|
+
const name = calleeName(node);
|
|
6077
|
+
const first = node.arguments[0];
|
|
6078
|
+
if (name === void 0 || first === void 0 || first.type === "SpreadElement") return;
|
|
6079
|
+
if (!exprIsTainted(first, tainted, returners)) return;
|
|
6080
|
+
if (SQL_SINK_NAMES.has(name)) found.add("sqli");
|
|
6081
|
+
if (CMD_SINK_NAMES.has(name)) found.add("cmdi");
|
|
6082
|
+
if (PATH_SINK_NAMES.has(name)) found.add("path");
|
|
6083
|
+
});
|
|
6084
|
+
return [...found].sort();
|
|
6085
|
+
}
|
|
6086
|
+
function fileTaintSummary(ast) {
|
|
6087
|
+
attachParents(ast);
|
|
6088
|
+
const facts = analyzeFile(ast);
|
|
6089
|
+
const returnsTaint = [];
|
|
6090
|
+
const functions = [];
|
|
6091
|
+
walk(ast, (node) => {
|
|
6092
|
+
if (!isFunctionNode(node)) return;
|
|
6093
|
+
const name = functionName(node);
|
|
6094
|
+
const own = facts.get(node);
|
|
6095
|
+
if (name === void 0 || own === void 0) return;
|
|
6096
|
+
if (scopeReturnsTaint(node, own.tainted, own.returners)) returnsTaint.push(name);
|
|
6097
|
+
const paramsToReturn = [];
|
|
6098
|
+
const sinks = [];
|
|
6099
|
+
node.params.forEach((param, index) => {
|
|
6100
|
+
const pname = boundNames(param)[0];
|
|
6101
|
+
if (pname === void 0) return;
|
|
6102
|
+
const seeded = analyzeScope(node, [pname]);
|
|
6103
|
+
if (scopeReturnsTaint(node, seeded.tainted, seeded.returners)) paramsToReturn.push(index);
|
|
6104
|
+
for (const cls of sinkClasses(node, seeded.tainted, seeded.returners)) sinks.push({ param: index, class: cls });
|
|
6105
|
+
});
|
|
6106
|
+
functions.push({
|
|
6107
|
+
name,
|
|
6108
|
+
line: node.loc?.start.line ?? 0,
|
|
6109
|
+
paramsToReturn,
|
|
6110
|
+
sinks
|
|
6111
|
+
});
|
|
6112
|
+
});
|
|
6113
|
+
returnsTaint.sort();
|
|
6114
|
+
functions.sort((a, b) => a.line - b.line || a.name.localeCompare(b.name));
|
|
6115
|
+
return { returnsTaint, functions };
|
|
6116
|
+
}
|
|
5210
6117
|
function functionsThatReturnTaint(ast) {
|
|
5211
6118
|
attachParents(ast);
|
|
5212
6119
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -5228,18 +6135,50 @@ function factsFor(node, map) {
|
|
|
5228
6135
|
return map.get(enclosingScope(node)) ?? {
|
|
5229
6136
|
tainted: /* @__PURE__ */ new Set(),
|
|
5230
6137
|
pathSanitized: /* @__PURE__ */ new Set(),
|
|
6138
|
+
sqlCleared: /* @__PURE__ */ new Set(),
|
|
5231
6139
|
urlSanitized: /* @__PURE__ */ new Set(),
|
|
5232
6140
|
xssSanitized: /* @__PURE__ */ new Set(),
|
|
5233
6141
|
nosqlSanitized: /* @__PURE__ */ new Set(),
|
|
5234
6142
|
hasPasswordName: false,
|
|
5235
6143
|
requestObjects: /* @__PURE__ */ new Set(),
|
|
6144
|
+
builtFilters: /* @__PURE__ */ new Set(),
|
|
6145
|
+
filterReturners: /* @__PURE__ */ new Set(),
|
|
5236
6146
|
returners: /* @__PURE__ */ new Set()
|
|
5237
6147
|
};
|
|
5238
6148
|
}
|
|
6149
|
+
function nodeHoldsBuiltFilter(node, map) {
|
|
6150
|
+
const facts = factsFor(node, map);
|
|
6151
|
+
if (node.type === "Identifier") return facts.builtFilters.has(node.name);
|
|
6152
|
+
if (node.type === "AwaitExpression") return nodeHoldsBuiltFilter(node.argument, map);
|
|
6153
|
+
if (node.type === "CallExpression" && node.callee.type === "Identifier") {
|
|
6154
|
+
return facts.filterReturners.has(node.callee.name);
|
|
6155
|
+
}
|
|
6156
|
+
return false;
|
|
6157
|
+
}
|
|
5239
6158
|
function nodeIsTainted(node, map) {
|
|
5240
6159
|
const facts = factsFor(node, map);
|
|
5241
6160
|
return exprIsTainted(node, facts.tainted, facts.returners);
|
|
5242
6161
|
}
|
|
6162
|
+
function nodeIsSqlCleared(node, map) {
|
|
6163
|
+
if (node.type === "Identifier") return factsFor(node, map).sqlCleared.has(node.name);
|
|
6164
|
+
return false;
|
|
6165
|
+
}
|
|
6166
|
+
function applyCustomCall(init, names, tainted, sqlCleared, customPath) {
|
|
6167
|
+
if (customTaint === void 0 || init === void 0 || init.type !== "CallExpression") return;
|
|
6168
|
+
const called2 = calleeName(init);
|
|
6169
|
+
if (called2 === void 0) return;
|
|
6170
|
+
if (customTaint.sources.has(called2)) {
|
|
6171
|
+
for (const name of names) tainted.add(name);
|
|
6172
|
+
}
|
|
6173
|
+
const clears = customTaint.sanitizers.get(called2);
|
|
6174
|
+
if (clears === void 0) return;
|
|
6175
|
+
if (clears.has("sqli")) {
|
|
6176
|
+
for (const name of names) sqlCleared.add(name);
|
|
6177
|
+
}
|
|
6178
|
+
if (clears.has("path")) {
|
|
6179
|
+
for (const name of names) customPath.add(name);
|
|
6180
|
+
}
|
|
6181
|
+
}
|
|
5243
6182
|
function nodeIsPathSanitized(node, map) {
|
|
5244
6183
|
if (node.type !== "Identifier") return false;
|
|
5245
6184
|
return factsFor(node, map).pathSanitized.has(node.name);
|
|
@@ -5261,7 +6200,7 @@ function nodeIsNosqlSanitized(node, map) {
|
|
|
5261
6200
|
if (node.type === "Identifier") return factsFor(node, map).nosqlSanitized.has(node.name);
|
|
5262
6201
|
return false;
|
|
5263
6202
|
}
|
|
5264
|
-
var SCHEMA_ROOT, NO_RETURNERS, CALLBACK_METHODS, taintedThisFields, MAX_TAINT_HOPS;
|
|
6203
|
+
var customTaint, SCHEMA_ROOT, NO_RETURNERS, CALLBACK_METHODS, taintedThisFields, MAX_TAINT_HOPS, SQL_SINK_NAMES, CMD_SINK_NAMES, PATH_SINK_NAMES;
|
|
5265
6204
|
var init_taint = __esm({
|
|
5266
6205
|
"src/modules/sast/taint.ts"() {
|
|
5267
6206
|
init_ast();
|
|
@@ -5283,6 +6222,157 @@ var init_taint = __esm({
|
|
|
5283
6222
|
]);
|
|
5284
6223
|
taintedThisFields = /* @__PURE__ */ new WeakMap();
|
|
5285
6224
|
MAX_TAINT_HOPS = 5;
|
|
6225
|
+
SQL_SINK_NAMES = /* @__PURE__ */ new Set(["query", "execute"]);
|
|
6226
|
+
CMD_SINK_NAMES = /* @__PURE__ */ new Set(["exec", "execSync", "spawn", "spawnSync"]);
|
|
6227
|
+
PATH_SINK_NAMES = /* @__PURE__ */ new Set(["readFile", "readFileSync", "writeFile", "writeFileSync"]);
|
|
6228
|
+
}
|
|
6229
|
+
});
|
|
6230
|
+
function taintSummariesEnabled() {
|
|
6231
|
+
return process.env.FULLSHIELD_TAINT_SUMMARIES === "1";
|
|
6232
|
+
}
|
|
6233
|
+
function setTaintSummaryConfig(config) {
|
|
6234
|
+
fileMemo.clear();
|
|
6235
|
+
if (config === void 0) {
|
|
6236
|
+
configHash = "";
|
|
6237
|
+
return;
|
|
6238
|
+
}
|
|
6239
|
+
configHash = createHash("sha256").update(
|
|
6240
|
+
JSON.stringify({
|
|
6241
|
+
maxFileBytes: config.sast.maxFileBytes,
|
|
6242
|
+
sources: config.sast.sources ?? [],
|
|
6243
|
+
sanitizers: config.sast.sanitizers ?? [],
|
|
6244
|
+
taintV2: taintV2Enabled(),
|
|
6245
|
+
frameworks: frameworksEnabled(),
|
|
6246
|
+
gaps: sastGapsEnabled(),
|
|
6247
|
+
classes: classesEnabled(),
|
|
6248
|
+
custom: process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1"
|
|
6249
|
+
})
|
|
6250
|
+
).digest("hex");
|
|
6251
|
+
}
|
|
6252
|
+
function summaryDir() {
|
|
6253
|
+
return process.env.FULLSHIELD_TAINT_SUMMARY_DIR ?? join(parseCacheDir(), "taint");
|
|
6254
|
+
}
|
|
6255
|
+
function taintSummaryKey(source, extra = "") {
|
|
6256
|
+
return createHash("sha256").update(packageVersion()).update("\0").update(configHash).update("\0").update(extra).update("\0").update(source).digest("hex");
|
|
6257
|
+
}
|
|
6258
|
+
function summaryPath(key) {
|
|
6259
|
+
return join(summaryDir(), `${key}.json`);
|
|
6260
|
+
}
|
|
6261
|
+
function isStringArray(value) {
|
|
6262
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
6263
|
+
}
|
|
6264
|
+
function readJson3(key) {
|
|
6265
|
+
let text;
|
|
6266
|
+
try {
|
|
6267
|
+
text = readFileSync(summaryPath(key), "utf8");
|
|
6268
|
+
} catch {
|
|
6269
|
+
return void 0;
|
|
6270
|
+
}
|
|
6271
|
+
try {
|
|
6272
|
+
return JSON.parse(text);
|
|
6273
|
+
} catch {
|
|
6274
|
+
discard(key);
|
|
6275
|
+
return void 0;
|
|
6276
|
+
}
|
|
6277
|
+
}
|
|
6278
|
+
function discard(key) {
|
|
6279
|
+
rmSync(summaryPath(key), { force: true });
|
|
6280
|
+
}
|
|
6281
|
+
function writeJson(key, value) {
|
|
6282
|
+
const dir = summaryDir();
|
|
6283
|
+
mkdirSync(dir, { recursive: true });
|
|
6284
|
+
writeFileSync(summaryPath(key), JSON.stringify(value));
|
|
6285
|
+
}
|
|
6286
|
+
function readFileSummary(key) {
|
|
6287
|
+
const raw = readJson3(key);
|
|
6288
|
+
if (typeof raw !== "object" || raw === null) {
|
|
6289
|
+
if (raw !== void 0) discard(key);
|
|
6290
|
+
return void 0;
|
|
6291
|
+
}
|
|
6292
|
+
const row = raw;
|
|
6293
|
+
if (row.v !== 1 || !isStringArray(row.returnsTaint) || !Array.isArray(row.functions)) {
|
|
6294
|
+
discard(key);
|
|
6295
|
+
return void 0;
|
|
6296
|
+
}
|
|
6297
|
+
return row;
|
|
6298
|
+
}
|
|
6299
|
+
function cachedReturners(source, ast) {
|
|
6300
|
+
const key = taintSummaryKey(source, "file");
|
|
6301
|
+
const memo = fileMemo.get(key);
|
|
6302
|
+
if (memo !== void 0) return new Set(memo.returnsTaint);
|
|
6303
|
+
const cached = readFileSummary(key);
|
|
6304
|
+
if (cached !== void 0) {
|
|
6305
|
+
fileMemo.set(key, cached);
|
|
6306
|
+
return new Set(cached.returnsTaint);
|
|
6307
|
+
}
|
|
6308
|
+
const summary = fileTaintSummary(ast);
|
|
6309
|
+
const stored = { v: 1, returnsTaint: summary.returnsTaint, functions: summary.functions };
|
|
6310
|
+
fileMemo.set(key, stored);
|
|
6311
|
+
writeJson(key, stored);
|
|
6312
|
+
return new Set(stored.returnsTaint);
|
|
6313
|
+
}
|
|
6314
|
+
function lists(facts) {
|
|
6315
|
+
const names = (set) => [...set].sort();
|
|
6316
|
+
return {
|
|
6317
|
+
v: 1,
|
|
6318
|
+
tainted: names(facts.tainted),
|
|
6319
|
+
pathSanitized: names(facts.pathSanitized),
|
|
6320
|
+
sqlCleared: names(facts.sqlCleared),
|
|
6321
|
+
urlSanitized: names(facts.urlSanitized),
|
|
6322
|
+
xssSanitized: names(facts.xssSanitized),
|
|
6323
|
+
nosqlSanitized: names(facts.nosqlSanitized),
|
|
6324
|
+
requestObjects: names(facts.requestObjects),
|
|
6325
|
+
builtFilters: names(facts.builtFilters),
|
|
6326
|
+
filterReturners: names(facts.filterReturners),
|
|
6327
|
+
returners: names(facts.returners),
|
|
6328
|
+
hasPasswordName: facts.hasPasswordName
|
|
6329
|
+
};
|
|
6330
|
+
}
|
|
6331
|
+
function factsFrom(row) {
|
|
6332
|
+
return {
|
|
6333
|
+
tainted: new Set(row.tainted),
|
|
6334
|
+
pathSanitized: new Set(row.pathSanitized),
|
|
6335
|
+
sqlCleared: new Set(row.sqlCleared),
|
|
6336
|
+
urlSanitized: new Set(row.urlSanitized),
|
|
6337
|
+
xssSanitized: new Set(row.xssSanitized),
|
|
6338
|
+
nosqlSanitized: new Set(row.nosqlSanitized),
|
|
6339
|
+
requestObjects: new Set(row.requestObjects),
|
|
6340
|
+
builtFilters: new Set(row.builtFilters),
|
|
6341
|
+
filterReturners: new Set(row.filterReturners),
|
|
6342
|
+
returners: new Set(row.returners),
|
|
6343
|
+
hasPasswordName: row.hasPasswordName
|
|
6344
|
+
};
|
|
6345
|
+
}
|
|
6346
|
+
function readFacts(key) {
|
|
6347
|
+
const raw = readJson3(key);
|
|
6348
|
+
if (typeof raw !== "object" || raw === null) {
|
|
6349
|
+
if (raw !== void 0) discard(key);
|
|
6350
|
+
return void 0;
|
|
6351
|
+
}
|
|
6352
|
+
const row = raw;
|
|
6353
|
+
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") {
|
|
6354
|
+
discard(key);
|
|
6355
|
+
return void 0;
|
|
6356
|
+
}
|
|
6357
|
+
return factsFrom(row);
|
|
6358
|
+
}
|
|
6359
|
+
function cachedFunctionFacts(fileSource, fn, seed) {
|
|
6360
|
+
const key = taintSummaryKey(fileSource, `seed:${fn.loc?.start.line ?? 0}:${seed.join(",")}`);
|
|
6361
|
+
const cached = readFacts(key);
|
|
6362
|
+
if (cached !== void 0) return cached;
|
|
6363
|
+
const facts = analyzeFunctionWithSeed(fn, seed);
|
|
6364
|
+
writeJson(key, lists(facts));
|
|
6365
|
+
return facts;
|
|
6366
|
+
}
|
|
6367
|
+
var configHash, fileMemo;
|
|
6368
|
+
var init_summaries = __esm({
|
|
6369
|
+
"src/modules/sast/summaries.ts"() {
|
|
6370
|
+
init_parse_pool();
|
|
6371
|
+
init_version();
|
|
6372
|
+
init_taint();
|
|
6373
|
+
init_experimental();
|
|
6374
|
+
configHash = "";
|
|
6375
|
+
fileMemo = /* @__PURE__ */ new Map();
|
|
5286
6376
|
}
|
|
5287
6377
|
});
|
|
5288
6378
|
function collectFunctions(file, ast) {
|
|
@@ -5393,22 +6483,22 @@ function collectInstances(ast, ns) {
|
|
|
5393
6483
|
if (node.type !== "VariableDeclarator" || node.init === null) return;
|
|
5394
6484
|
if (node.id.type !== "Identifier" || node.init.type !== "NewExpression") return;
|
|
5395
6485
|
if (node.init.callee.type !== "Identifier") return;
|
|
5396
|
-
const
|
|
5397
|
-
if (
|
|
5398
|
-
instances.set(node.id.name,
|
|
6486
|
+
const hit2 = ns.get(node.init.callee.name);
|
|
6487
|
+
if (hit2 === void 0) return;
|
|
6488
|
+
instances.set(node.id.name, hit2.includes("#") ? hit2.split("#")[0] ?? hit2 : hit2);
|
|
5399
6489
|
});
|
|
5400
6490
|
return instances;
|
|
5401
6491
|
}
|
|
5402
6492
|
function resolveCallee(node, ns, instances, currentFile, functions) {
|
|
5403
6493
|
if (node.type !== "CallExpression") return void 0;
|
|
5404
6494
|
if (node.callee.type === "Identifier") {
|
|
5405
|
-
const
|
|
5406
|
-
if (
|
|
5407
|
-
if (
|
|
5408
|
-
const [file, name] =
|
|
6495
|
+
const hit2 = ns.get(node.callee.name);
|
|
6496
|
+
if (hit2 !== void 0) {
|
|
6497
|
+
if (hit2.includes("#")) {
|
|
6498
|
+
const [file, name] = hit2.split("#");
|
|
5409
6499
|
if (file !== void 0 && name !== void 0) return { file, name };
|
|
5410
6500
|
}
|
|
5411
|
-
return { file:
|
|
6501
|
+
return { file: hit2, name: node.callee.name };
|
|
5412
6502
|
}
|
|
5413
6503
|
if (functions.has(`${currentFile}#${node.callee.name}`)) {
|
|
5414
6504
|
return { file: currentFile, name: node.callee.name };
|
|
@@ -5515,9 +6605,9 @@ function runInterproc(parsed) {
|
|
|
5515
6605
|
if (fn === void 0) continue;
|
|
5516
6606
|
const param = fn.params[seed.paramIndex];
|
|
5517
6607
|
if (param === void 0) continue;
|
|
5518
|
-
const facts = analyzeFunctionWithSeed(fn.node, [param]);
|
|
5519
6608
|
const p = byFile.get(fn.file);
|
|
5520
6609
|
if (p === void 0) continue;
|
|
6610
|
+
const facts = taintSummariesEnabled() ? cachedFunctionFacts(p.source, fn.node, [param]) : analyzeFunctionWithSeed(fn.node, [param]);
|
|
5521
6611
|
const ns = requires.get(fn.file) ?? /* @__PURE__ */ new Map();
|
|
5522
6612
|
const inst = instances.get(fn.file) ?? /* @__PURE__ */ new Map();
|
|
5523
6613
|
walk(fn.node, (node) => {
|
|
@@ -5611,6 +6701,7 @@ var init_interproc = __esm({
|
|
|
5611
6701
|
init_finding();
|
|
5612
6702
|
init_ast();
|
|
5613
6703
|
init_experimental();
|
|
6704
|
+
init_summaries();
|
|
5614
6705
|
init_taint();
|
|
5615
6706
|
MAX_HOPS = 5;
|
|
5616
6707
|
}
|
|
@@ -5982,6 +7073,184 @@ var init_jwt = __esm({
|
|
|
5982
7073
|
}
|
|
5983
7074
|
});
|
|
5984
7075
|
|
|
7076
|
+
// src/modules/sast/validation.ts
|
|
7077
|
+
function validationEnabled() {
|
|
7078
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_VALIDATION === "1";
|
|
7079
|
+
}
|
|
7080
|
+
function parentOf(node) {
|
|
7081
|
+
return node.parent;
|
|
7082
|
+
}
|
|
7083
|
+
function mentions(node, names) {
|
|
7084
|
+
let current = node;
|
|
7085
|
+
for (let i = 0; i < 12 && current !== void 0; i += 1) {
|
|
7086
|
+
if (current.type === "CallExpression") {
|
|
7087
|
+
const name = calleeName(current);
|
|
7088
|
+
if (name !== void 0 && names.has(name)) return true;
|
|
7089
|
+
current = current.callee.type === "MemberExpression" ? current.callee.object : void 0;
|
|
7090
|
+
continue;
|
|
7091
|
+
}
|
|
7092
|
+
if (current.type === "MemberExpression") {
|
|
7093
|
+
const name = propName(current);
|
|
7094
|
+
if (name !== void 0 && names.has(name)) return true;
|
|
7095
|
+
current = current.object;
|
|
7096
|
+
continue;
|
|
7097
|
+
}
|
|
7098
|
+
if (current.type === "Identifier" && names.has(current.name)) return true;
|
|
7099
|
+
break;
|
|
7100
|
+
}
|
|
7101
|
+
return false;
|
|
7102
|
+
}
|
|
7103
|
+
function rootName(node) {
|
|
7104
|
+
let current = node;
|
|
7105
|
+
for (let i = 0; i < 8 && current !== void 0; i += 1) {
|
|
7106
|
+
if (current.type === "Identifier") return current.name;
|
|
7107
|
+
if (current.type === "CallExpression") {
|
|
7108
|
+
current = current.callee;
|
|
7109
|
+
continue;
|
|
7110
|
+
}
|
|
7111
|
+
if (current.type === "MemberExpression") {
|
|
7112
|
+
current = current.object;
|
|
7113
|
+
continue;
|
|
7114
|
+
}
|
|
7115
|
+
break;
|
|
7116
|
+
}
|
|
7117
|
+
return void 0;
|
|
7118
|
+
}
|
|
7119
|
+
function propertyName(prop) {
|
|
7120
|
+
if (prop.type !== "Property") return void 0;
|
|
7121
|
+
const key = prop.key;
|
|
7122
|
+
if (key.type === "Identifier") return key.name;
|
|
7123
|
+
if (key.type === "Literal" && typeof key.value === "string") return key.value;
|
|
7124
|
+
return void 0;
|
|
7125
|
+
}
|
|
7126
|
+
function fieldsOfObjectSchema(node) {
|
|
7127
|
+
if (node.type !== "CallExpression" || calleeName(node) !== "object") return /* @__PURE__ */ new Set();
|
|
7128
|
+
const root = rootName(node.callee);
|
|
7129
|
+
if (root === void 0 || !SCHEMA_ROOT2.has(root)) return /* @__PURE__ */ new Set();
|
|
7130
|
+
const arg = node.arguments[0];
|
|
7131
|
+
if (arg === void 0 || arg.type !== "ObjectExpression") return /* @__PURE__ */ new Set();
|
|
7132
|
+
const fields = /* @__PURE__ */ new Set();
|
|
7133
|
+
for (const prop of arg.properties) {
|
|
7134
|
+
if (prop.type !== "Property") continue;
|
|
7135
|
+
const name = propertyName(prop);
|
|
7136
|
+
if (name === void 0) continue;
|
|
7137
|
+
if (mentions(prop.value, OPEN)) continue;
|
|
7138
|
+
if (mentions(prop.value, PRIMITIVE)) fields.add(name);
|
|
7139
|
+
}
|
|
7140
|
+
return fields;
|
|
7141
|
+
}
|
|
7142
|
+
function decoratorName2(decorator) {
|
|
7143
|
+
const expr = decorator.type === "Decorator" ? decorator.expression : decorator;
|
|
7144
|
+
if (expr === void 0) return void 0;
|
|
7145
|
+
if (expr.type === "CallExpression") return calleeName(expr);
|
|
7146
|
+
if (expr.type === "Identifier") return expr.name;
|
|
7147
|
+
return void 0;
|
|
7148
|
+
}
|
|
7149
|
+
function fieldsOfClass(node) {
|
|
7150
|
+
if (node.type !== "ClassDeclaration" && node.type !== "ClassExpression") return /* @__PURE__ */ new Set();
|
|
7151
|
+
if (node.body.type !== "ClassBody") return /* @__PURE__ */ new Set();
|
|
7152
|
+
const fields = /* @__PURE__ */ new Set();
|
|
7153
|
+
for (const element of node.body.body) {
|
|
7154
|
+
if (element.type !== "PropertyDefinition") continue;
|
|
7155
|
+
const key = element.key;
|
|
7156
|
+
const name = key.type === "Identifier" ? key.name : void 0;
|
|
7157
|
+
if (name === void 0) continue;
|
|
7158
|
+
const decorators = element.decorators ?? [];
|
|
7159
|
+
if (decorators.some((decorator) => PRIMITIVE.has(decoratorName2(decorator) ?? ""))) fields.add(name);
|
|
7160
|
+
}
|
|
7161
|
+
return fields;
|
|
7162
|
+
}
|
|
7163
|
+
function programOf(node) {
|
|
7164
|
+
let current = node;
|
|
7165
|
+
while (current !== void 0 && current.type !== "Program") current = parentOf(current);
|
|
7166
|
+
return current;
|
|
7167
|
+
}
|
|
7168
|
+
function bindings(program) {
|
|
7169
|
+
const map = /* @__PURE__ */ new Map();
|
|
7170
|
+
walk(program, (node) => {
|
|
7171
|
+
if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.init !== null && node.init !== void 0) {
|
|
7172
|
+
const fields = fieldsOfObjectSchema(node.init);
|
|
7173
|
+
if (fields.size > 0) map.set(node.id.name, fields);
|
|
7174
|
+
}
|
|
7175
|
+
if (node.type === "ClassDeclaration" && node.id !== null) {
|
|
7176
|
+
const fields = fieldsOfClass(node);
|
|
7177
|
+
if (fields.size > 0) map.set(node.id.name, fields);
|
|
7178
|
+
}
|
|
7179
|
+
});
|
|
7180
|
+
return map;
|
|
7181
|
+
}
|
|
7182
|
+
function fieldsOfExpr(expr, known) {
|
|
7183
|
+
if (expr.type === "Identifier") return new Set(known.get(expr.name) ?? []);
|
|
7184
|
+
if (expr.type === "ClassExpression" || expr.type === "ClassDeclaration") return fieldsOfClass(expr);
|
|
7185
|
+
const direct = fieldsOfObjectSchema(expr);
|
|
7186
|
+
if (expr.type !== "CallExpression") return direct;
|
|
7187
|
+
for (const arg of expr.arguments) {
|
|
7188
|
+
if (arg.type === "SpreadElement") continue;
|
|
7189
|
+
for (const name of fieldsOfExpr(arg, known)) direct.add(name);
|
|
7190
|
+
}
|
|
7191
|
+
return direct;
|
|
7192
|
+
}
|
|
7193
|
+
function isRouteCall2(node) {
|
|
7194
|
+
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return false;
|
|
7195
|
+
const method = propName(node.callee);
|
|
7196
|
+
if (method === void 0 || !ROUTE_METHODS.has(method)) return false;
|
|
7197
|
+
return node.callee.object.type === "Identifier" && /^(app|router)$/i.test(node.callee.object.name);
|
|
7198
|
+
}
|
|
7199
|
+
function routeOf(node) {
|
|
7200
|
+
let current = node;
|
|
7201
|
+
while (current !== void 0) {
|
|
7202
|
+
const parent = parentOf(current);
|
|
7203
|
+
if (isFunctionNode(current) && parent !== void 0 && isRouteCall2(parent)) return parent;
|
|
7204
|
+
current = parent;
|
|
7205
|
+
}
|
|
7206
|
+
return void 0;
|
|
7207
|
+
}
|
|
7208
|
+
function requestField(node) {
|
|
7209
|
+
if (node.type !== "MemberExpression" || node.computed === true) return void 0;
|
|
7210
|
+
const field = node.property.type === "Identifier" ? node.property.name : void 0;
|
|
7211
|
+
if (field === void 0 || node.object.type !== "MemberExpression" || node.object.computed === true) return void 0;
|
|
7212
|
+
const bag = node.object.property.type === "Identifier" ? node.object.property.name : void 0;
|
|
7213
|
+
if (bag !== "body" && bag !== "query" && bag !== "params") return void 0;
|
|
7214
|
+
if (node.object.object.type !== "Identifier" || node.object.object.name !== "req") return void 0;
|
|
7215
|
+
return field;
|
|
7216
|
+
}
|
|
7217
|
+
function fieldsForRoute(route) {
|
|
7218
|
+
const cached = routeCache.get(route);
|
|
7219
|
+
if (cached !== void 0) return cached;
|
|
7220
|
+
const program = programOf(route);
|
|
7221
|
+
const known = program === void 0 ? /* @__PURE__ */ new Map() : bindingCache.get(program) ?? bindings(program);
|
|
7222
|
+
if (program !== void 0) bindingCache.set(program, known);
|
|
7223
|
+
const fields = /* @__PURE__ */ new Set();
|
|
7224
|
+
if (route.type === "CallExpression") {
|
|
7225
|
+
for (const arg of route.arguments) {
|
|
7226
|
+
if (arg.type === "SpreadElement" || isFunctionNode(arg)) continue;
|
|
7227
|
+
for (const name of fieldsOfExpr(arg, known)) fields.add(name);
|
|
7228
|
+
}
|
|
7229
|
+
}
|
|
7230
|
+
routeCache.set(route, fields);
|
|
7231
|
+
return fields;
|
|
7232
|
+
}
|
|
7233
|
+
function isValidatedRouteField(node) {
|
|
7234
|
+
if (!validationEnabled()) return false;
|
|
7235
|
+
const field = requestField(node);
|
|
7236
|
+
if (field === void 0) return false;
|
|
7237
|
+
const route = routeOf(node);
|
|
7238
|
+
if (route === void 0) return false;
|
|
7239
|
+
return fieldsForRoute(route).has(field);
|
|
7240
|
+
}
|
|
7241
|
+
var PRIMITIVE, OPEN, SCHEMA_ROOT2, ROUTE_METHODS, bindingCache, routeCache;
|
|
7242
|
+
var init_validation = __esm({
|
|
7243
|
+
"src/modules/sast/validation.ts"() {
|
|
7244
|
+
init_ast();
|
|
7245
|
+
PRIMITIVE = /* @__PURE__ */ new Set(["string", "number", "boolean", "IsString", "IsNumber", "IsInt", "IsBoolean"]);
|
|
7246
|
+
OPEN = /* @__PURE__ */ new Set(["any", "unknown", "object", "array", "record", "lazy", "union", "tuple"]);
|
|
7247
|
+
SCHEMA_ROOT2 = /* @__PURE__ */ new Set(["z", "zod", "Joi", "joi"]);
|
|
7248
|
+
ROUTE_METHODS = /* @__PURE__ */ new Set(["post", "put", "patch", "delete"]);
|
|
7249
|
+
bindingCache = /* @__PURE__ */ new WeakMap();
|
|
7250
|
+
routeCache = /* @__PURE__ */ new WeakMap();
|
|
7251
|
+
}
|
|
7252
|
+
});
|
|
7253
|
+
|
|
5985
7254
|
// src/modules/sast/rules/nosqli.ts
|
|
5986
7255
|
function objectKey(prop) {
|
|
5987
7256
|
if (prop.type !== "Property" || prop.key === void 0) return void 0;
|
|
@@ -6043,6 +7312,7 @@ function isNosqlSafeValue(node, source, isNosqlSanitized) {
|
|
|
6043
7312
|
if (isNosqlSanitized(node) || isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) {
|
|
6044
7313
|
return true;
|
|
6045
7314
|
}
|
|
7315
|
+
if (isValidatedRouteField(node)) return true;
|
|
6046
7316
|
return isSanitizeWrap(node, source);
|
|
6047
7317
|
}
|
|
6048
7318
|
function objectHasUnsafeTaintedValue(node, isTainted, isSafe) {
|
|
@@ -6064,6 +7334,7 @@ var init_nosqli = __esm({
|
|
|
6064
7334
|
"src/modules/sast/rules/nosqli.ts"() {
|
|
6065
7335
|
init_ast();
|
|
6066
7336
|
init_taint();
|
|
7337
|
+
init_validation();
|
|
6067
7338
|
MONGO_SINKS = /* @__PURE__ */ new Set([
|
|
6068
7339
|
"find",
|
|
6069
7340
|
"findOne",
|
|
@@ -6134,6 +7405,14 @@ var init_nosqli = __esm({
|
|
|
6134
7405
|
return;
|
|
6135
7406
|
}
|
|
6136
7407
|
if (middleware) return;
|
|
7408
|
+
if (ctx.holdsBuiltFilter(filter)) {
|
|
7409
|
+
ctx.report({
|
|
7410
|
+
node,
|
|
7411
|
+
message: "A Mongo filter built from request input is passed to the query.",
|
|
7412
|
+
confidence: "medium"
|
|
7413
|
+
});
|
|
7414
|
+
return;
|
|
7415
|
+
}
|
|
6137
7416
|
if (objectHasUnsafeTaintedValue(filter, (n) => ctx.isTainted(n), isSafe)) {
|
|
6138
7417
|
ctx.report({
|
|
6139
7418
|
node,
|
|
@@ -6335,7 +7614,7 @@ var init_sqli = __esm({
|
|
|
6335
7614
|
CallExpression(node) {
|
|
6336
7615
|
if (node.type !== "CallExpression" || !isSqlSink2(node)) return;
|
|
6337
7616
|
const sql = node.arguments[0];
|
|
6338
|
-
if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql)) {
|
|
7617
|
+
if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql) && !ctx.isSqlCleared(sql)) {
|
|
6339
7618
|
ctx.report({
|
|
6340
7619
|
node,
|
|
6341
7620
|
message: "Tainted input is concatenated into a SQL string passed to query/execute."
|
|
@@ -6499,27 +7778,27 @@ function numberLiteral(node) {
|
|
|
6499
7778
|
return node.value;
|
|
6500
7779
|
}
|
|
6501
7780
|
function containsIdentMember(node, objectName) {
|
|
6502
|
-
let
|
|
7781
|
+
let hit2 = false;
|
|
6503
7782
|
walk(node, (child) => {
|
|
6504
7783
|
if (child.type === "MemberExpression" && child.object.type === "Identifier" && child.object.name === objectName) {
|
|
6505
|
-
|
|
7784
|
+
hit2 = true;
|
|
6506
7785
|
}
|
|
6507
7786
|
});
|
|
6508
|
-
return
|
|
7787
|
+
return hit2;
|
|
6509
7788
|
}
|
|
6510
7789
|
function containsCallNamed(node, name) {
|
|
6511
|
-
let
|
|
7790
|
+
let hit2 = false;
|
|
6512
7791
|
walk(node, (child) => {
|
|
6513
|
-
if (child.type === "CallExpression" && calleeName(child) === name)
|
|
7792
|
+
if (child.type === "CallExpression" && calleeName(child) === name) hit2 = true;
|
|
6514
7793
|
});
|
|
6515
|
-
return
|
|
7794
|
+
return hit2;
|
|
6516
7795
|
}
|
|
6517
7796
|
function containsOriginalName(node) {
|
|
6518
|
-
let
|
|
7797
|
+
let hit2 = false;
|
|
6519
7798
|
walk(node, (child) => {
|
|
6520
|
-
if (child.type === "MemberExpression" && propName(child) === "originalname")
|
|
7799
|
+
if (child.type === "MemberExpression" && propName(child) === "originalname") hit2 = true;
|
|
6521
7800
|
});
|
|
6522
|
-
return
|
|
7801
|
+
return hit2;
|
|
6523
7802
|
}
|
|
6524
7803
|
function schemaLooksSafe(schema) {
|
|
6525
7804
|
if (schema === void 0) return false;
|
|
@@ -7137,162 +8416,18 @@ function scanPromptInjection(file, source) {
|
|
|
7137
8416
|
line: index + 1,
|
|
7138
8417
|
snippet: line.trim().slice(0, 120),
|
|
7139
8418
|
message: "Code contains text attempting to manipulate AI reviewers.",
|
|
7140
|
-
remediation: "Remove comments that try to instruct an AI reviewer.",
|
|
7141
|
-
references: ["AI-INJECT-001"],
|
|
7142
|
-
source: "deterministic"
|
|
7143
|
-
});
|
|
7144
|
-
});
|
|
7145
|
-
return findings;
|
|
7146
|
-
}
|
|
7147
|
-
var PHRASES;
|
|
7148
|
-
var init_injection = __esm({
|
|
7149
|
-
"src/ai/injection.ts"() {
|
|
7150
|
-
init_finding();
|
|
7151
|
-
PHRASES = [/ignore previous/i, /mark as safe/i, /ai reviewer/i];
|
|
7152
|
-
}
|
|
7153
|
-
});
|
|
7154
|
-
|
|
7155
|
-
// src/runtime/lru.ts
|
|
7156
|
-
var LruMap;
|
|
7157
|
-
var init_lru = __esm({
|
|
7158
|
-
"src/runtime/lru.ts"() {
|
|
7159
|
-
LruMap = class {
|
|
7160
|
-
constructor(max) {
|
|
7161
|
-
this.max = max;
|
|
7162
|
-
if (max < 1) throw new Error("LruMap max must be >= 1");
|
|
7163
|
-
}
|
|
7164
|
-
max;
|
|
7165
|
-
map = /* @__PURE__ */ new Map();
|
|
7166
|
-
get size() {
|
|
7167
|
-
return this.map.size;
|
|
7168
|
-
}
|
|
7169
|
-
has(key) {
|
|
7170
|
-
return this.map.has(key);
|
|
7171
|
-
}
|
|
7172
|
-
get(key) {
|
|
7173
|
-
if (!this.map.has(key)) return void 0;
|
|
7174
|
-
const value = this.map.get(key);
|
|
7175
|
-
this.map.delete(key);
|
|
7176
|
-
this.map.set(key, value);
|
|
7177
|
-
return value;
|
|
7178
|
-
}
|
|
7179
|
-
set(key, value) {
|
|
7180
|
-
if (this.map.has(key)) this.map.delete(key);
|
|
7181
|
-
this.map.set(key, value);
|
|
7182
|
-
while (this.map.size > this.max) {
|
|
7183
|
-
const oldest = this.map.keys().next().value;
|
|
7184
|
-
if (oldest === void 0) break;
|
|
7185
|
-
this.map.delete(oldest);
|
|
7186
|
-
}
|
|
7187
|
-
return this;
|
|
7188
|
-
}
|
|
7189
|
-
};
|
|
7190
|
-
}
|
|
7191
|
-
});
|
|
7192
|
-
function packageVersion() {
|
|
7193
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7194
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
7195
|
-
try {
|
|
7196
|
-
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
7197
|
-
const pkg = JSON.parse(raw);
|
|
7198
|
-
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
7199
|
-
return pkg.version;
|
|
7200
|
-
}
|
|
7201
|
-
} catch {
|
|
7202
|
-
}
|
|
7203
|
-
const parent = dirname(dir);
|
|
7204
|
-
if (parent === dir) break;
|
|
7205
|
-
dir = parent;
|
|
7206
|
-
}
|
|
7207
|
-
throw new Error("fullshield package.json not found");
|
|
7208
|
-
}
|
|
7209
|
-
var init_version = __esm({
|
|
7210
|
-
"src/core/version.ts"() {
|
|
7211
|
-
}
|
|
7212
|
-
});
|
|
7213
|
-
function parsePoolEnabled() {
|
|
7214
|
-
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
7215
|
-
}
|
|
7216
|
-
function parsePoolSize() {
|
|
7217
|
-
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
7218
|
-
if (raw !== void 0 && raw !== "") {
|
|
7219
|
-
const parsed = Number(raw);
|
|
7220
|
-
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
|
|
7221
|
-
}
|
|
7222
|
-
return Math.min(2, Math.max(1, availableParallelism() - 1));
|
|
7223
|
-
}
|
|
7224
|
-
function workerMemoryMb() {
|
|
7225
|
-
const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
|
|
7226
|
-
const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
|
|
7227
|
-
if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
|
|
7228
|
-
return 512;
|
|
7229
|
-
}
|
|
7230
|
-
function parseCacheKey(code) {
|
|
7231
|
-
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
7232
|
-
}
|
|
7233
|
-
function parseOne(code) {
|
|
7234
|
-
const key = parseCacheKey(code);
|
|
7235
|
-
const hit = cache.get(key);
|
|
7236
|
-
if (hit !== void 0) return Promise.resolve(hit);
|
|
7237
|
-
return new Promise((resolve8, reject) => {
|
|
7238
|
-
const worker = new Worker(fileURLToPath(workerUrl), {
|
|
7239
|
-
workerData: { code },
|
|
7240
|
-
resourceLimits: { maxOldGenerationSizeMb: workerMemoryMb() }
|
|
7241
|
-
});
|
|
7242
|
-
let settled = false;
|
|
7243
|
-
const finish = () => {
|
|
7244
|
-
void worker.terminate();
|
|
7245
|
-
};
|
|
7246
|
-
const succeed = (ast) => {
|
|
7247
|
-
if (settled) return;
|
|
7248
|
-
settled = true;
|
|
7249
|
-
cache.set(key, ast);
|
|
7250
|
-
finish();
|
|
7251
|
-
resolve8(ast);
|
|
7252
|
-
};
|
|
7253
|
-
const fail = (error) => {
|
|
7254
|
-
if (settled) return;
|
|
7255
|
-
settled = true;
|
|
7256
|
-
finish();
|
|
7257
|
-
reject(error);
|
|
7258
|
-
};
|
|
7259
|
-
worker.once("message", (ast) => {
|
|
7260
|
-
succeed(ast);
|
|
7261
|
-
});
|
|
7262
|
-
worker.once("error", (error) => {
|
|
7263
|
-
fail(error);
|
|
7264
|
-
});
|
|
7265
|
-
worker.once("exit", (code2) => {
|
|
7266
|
-
if (settled) return;
|
|
7267
|
-
fail(new Error(`parse worker exited ${code2 ?? "null"}`));
|
|
8419
|
+
remediation: "Remove comments that try to instruct an AI reviewer.",
|
|
8420
|
+
references: ["AI-INJECT-001"],
|
|
8421
|
+
source: "deterministic"
|
|
7268
8422
|
});
|
|
7269
8423
|
});
|
|
8424
|
+
return findings;
|
|
7270
8425
|
}
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
for (; ; ) {
|
|
7277
|
-
const index = next;
|
|
7278
|
-
next += 1;
|
|
7279
|
-
if (index >= codes.length) return;
|
|
7280
|
-
const code = codes[index];
|
|
7281
|
-
if (code === void 0) return;
|
|
7282
|
-
out[index] = await parseOne(code);
|
|
7283
|
-
}
|
|
7284
|
-
}
|
|
7285
|
-
const workers = Math.min(size, Math.max(1, codes.length));
|
|
7286
|
-
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
7287
|
-
return out;
|
|
7288
|
-
}
|
|
7289
|
-
var workerUrl, cache;
|
|
7290
|
-
var init_parse_pool = __esm({
|
|
7291
|
-
"src/core/parse-pool.ts"() {
|
|
7292
|
-
init_lru();
|
|
7293
|
-
init_version();
|
|
7294
|
-
workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7295
|
-
cache = new LruMap(64);
|
|
8426
|
+
var PHRASES;
|
|
8427
|
+
var init_injection = __esm({
|
|
8428
|
+
"src/ai/injection.ts"() {
|
|
8429
|
+
init_finding();
|
|
8430
|
+
PHRASES = [/ignore previous/i, /mark as safe/i, /ai reviewer/i];
|
|
7296
8431
|
}
|
|
7297
8432
|
});
|
|
7298
8433
|
async function parseSource(code) {
|
|
@@ -7337,10 +8472,12 @@ function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ *
|
|
|
7337
8472
|
});
|
|
7338
8473
|
},
|
|
7339
8474
|
isTainted: (node) => nodeIsTainted(node, facts),
|
|
8475
|
+
isSqlCleared: (node) => nodeIsSqlCleared(node, facts),
|
|
7340
8476
|
isPathSanitized: (node) => nodeIsPathSanitized(node, facts),
|
|
7341
8477
|
isSanitizedFor: (node, kind) => nodeIsSanitizedFor(node, facts, kind),
|
|
7342
8478
|
isNosqlSanitized: (node) => nodeIsNosqlSanitized(node, facts),
|
|
7343
8479
|
isRequestObject: (node) => nodeIsRequestObject(node, facts),
|
|
8480
|
+
holdsBuiltFilter: (node) => nodeHoldsBuiltFilter(node, facts),
|
|
7344
8481
|
functionHasPasswordName: (node) => factsFor(node, facts).hasPasswordName,
|
|
7345
8482
|
assignedName: (node) => assignedNameOf(node),
|
|
7346
8483
|
projectCsrfProtected
|
|
@@ -7378,10 +8515,12 @@ function findingsFromTemplate(filename, source) {
|
|
|
7378
8515
|
});
|
|
7379
8516
|
},
|
|
7380
8517
|
isTainted: () => false,
|
|
8518
|
+
isSqlCleared: () => false,
|
|
7381
8519
|
isPathSanitized: () => false,
|
|
7382
8520
|
isSanitizedFor: () => false,
|
|
7383
8521
|
isNosqlSanitized: () => false,
|
|
7384
8522
|
isRequestObject: () => false,
|
|
8523
|
+
holdsBuiltFilter: () => false,
|
|
7385
8524
|
functionHasPasswordName: () => false,
|
|
7386
8525
|
assignedName: () => void 0,
|
|
7387
8526
|
projectCsrfProtected: false
|
|
@@ -7513,7 +8652,7 @@ async function runSastStreaming(ctx) {
|
|
|
7513
8652
|
}
|
|
7514
8653
|
const ast = await parseCode(source);
|
|
7515
8654
|
if (ast === void 0) continue;
|
|
7516
|
-
returning.set(rel, functionsThatReturnTaint(ast));
|
|
8655
|
+
returning.set(rel, taintSummariesEnabled() ? cachedReturners(source, ast) : functionsThatReturnTaint(ast));
|
|
7517
8656
|
parseable.push(rel);
|
|
7518
8657
|
fileSet.add(rel);
|
|
7519
8658
|
}
|
|
@@ -7547,6 +8686,7 @@ var init_engine = __esm({
|
|
|
7547
8686
|
init_finding();
|
|
7548
8687
|
init_ast();
|
|
7549
8688
|
init_interproc();
|
|
8689
|
+
init_summaries();
|
|
7550
8690
|
init_taint();
|
|
7551
8691
|
init_auth();
|
|
7552
8692
|
init_cmdi();
|
|
@@ -7582,48 +8722,69 @@ var init_engine = __esm({
|
|
|
7582
8722
|
sastModule = {
|
|
7583
8723
|
name: "sast",
|
|
7584
8724
|
async run(ctx) {
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
if (source === void 0) continue;
|
|
7593
|
-
const why = skipReason(source, maxBytes);
|
|
7594
|
-
if (why !== void 0) {
|
|
7595
|
-
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
7596
|
-
continue;
|
|
8725
|
+
if (process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1") {
|
|
8726
|
+
const sources = new Set((ctx.config.sast.sources ?? []).map((row) => row.callee));
|
|
8727
|
+
const sanitizers = /* @__PURE__ */ new Map();
|
|
8728
|
+
for (const row of ctx.config.sast.sanitizers ?? []) {
|
|
8729
|
+
const classes = sanitizers.get(row.callee) ?? /* @__PURE__ */ new Set();
|
|
8730
|
+
for (const name of row.clears) classes.add(name);
|
|
8731
|
+
sanitizers.set(row.callee, classes);
|
|
7597
8732
|
}
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
}
|
|
7602
|
-
const ast = await parseCode(source);
|
|
7603
|
-
if (ast === void 0) continue;
|
|
7604
|
-
parsed.push({ file: rel, source, ast });
|
|
7605
|
-
}
|
|
7606
|
-
const files = new Set(parsed.map((p) => p.file));
|
|
7607
|
-
const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
|
|
7608
|
-
const returning = /* @__PURE__ */ new Map();
|
|
7609
|
-
for (const entry of parsed) {
|
|
7610
|
-
returning.set(entry.file, functionsThatReturnTaint(entry.ast));
|
|
8733
|
+
setCustomTaint({ sources, sanitizers });
|
|
8734
|
+
} else {
|
|
8735
|
+
setCustomTaint(void 0);
|
|
7611
8736
|
}
|
|
7612
|
-
|
|
7613
|
-
|
|
7614
|
-
|
|
8737
|
+
setTaintSummaryConfig(taintSummariesEnabled() ? ctx.config : void 0);
|
|
8738
|
+
try {
|
|
8739
|
+
const candidates = ctx.files.filter((rel) => SOURCE_EXT.test(rel) || TEMPLATE_EXT.test(rel));
|
|
8740
|
+
if (candidates.length > sastStreamThreshold()) return await runSastStreaming(ctx);
|
|
8741
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
8742
|
+
const findings = [];
|
|
8743
|
+
const parsed = [];
|
|
8744
|
+
for (const rel of candidates) {
|
|
8745
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
8746
|
+
if (source === void 0) continue;
|
|
8747
|
+
const why = skipReason(source, maxBytes);
|
|
8748
|
+
if (why !== void 0) {
|
|
8749
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
8750
|
+
continue;
|
|
8751
|
+
}
|
|
8752
|
+
if (TEMPLATE_EXT.test(rel)) {
|
|
8753
|
+
findings.push(...findingsFromTemplate(rel, source));
|
|
8754
|
+
continue;
|
|
8755
|
+
}
|
|
8756
|
+
const ast = await parseCode(source);
|
|
8757
|
+
if (ast === void 0) continue;
|
|
8758
|
+
parsed.push({ file: rel, source, ast });
|
|
8759
|
+
}
|
|
8760
|
+
const files = new Set(parsed.map((p) => p.file));
|
|
8761
|
+
const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
|
|
8762
|
+
const returning = /* @__PURE__ */ new Map();
|
|
8763
|
+
for (const entry of parsed) {
|
|
8764
|
+
returning.set(
|
|
7615
8765
|
entry.file,
|
|
7616
|
-
entry.source,
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
|
|
7621
|
-
|
|
8766
|
+
taintSummariesEnabled() ? cachedReturners(entry.source, entry.ast) : functionsThatReturnTaint(entry.ast)
|
|
8767
|
+
);
|
|
8768
|
+
}
|
|
8769
|
+
for (const entry of parsed) {
|
|
8770
|
+
findings.push(
|
|
8771
|
+
...findingsFromAst(
|
|
8772
|
+
entry.file,
|
|
8773
|
+
entry.source,
|
|
8774
|
+
entry.ast,
|
|
8775
|
+
extraReturners(entry.file, entry.ast, files, returning),
|
|
8776
|
+
projectCsrf
|
|
8777
|
+
)
|
|
8778
|
+
);
|
|
8779
|
+
}
|
|
8780
|
+
findings.push(...runInterproc(parsed));
|
|
8781
|
+
const out = dedupeSastSinks(findings);
|
|
8782
|
+
releaseParsed(parsed);
|
|
8783
|
+
return out;
|
|
8784
|
+
} finally {
|
|
8785
|
+
setCustomTaint(void 0);
|
|
8786
|
+
setTaintSummaryConfig(void 0);
|
|
7622
8787
|
}
|
|
7623
|
-
findings.push(...runInterproc(parsed));
|
|
7624
|
-
const out = dedupeSastSinks(findings);
|
|
7625
|
-
releaseParsed(parsed);
|
|
7626
|
-
return out;
|
|
7627
8788
|
}
|
|
7628
8789
|
};
|
|
7629
8790
|
}
|
|
@@ -8007,7 +9168,7 @@ function scanTextForSecretHits(path, text) {
|
|
|
8007
9168
|
return hits;
|
|
8008
9169
|
}
|
|
8009
9170
|
function scanTextForSecrets(path, text) {
|
|
8010
|
-
return scanTextForSecretHits(path, text).map((
|
|
9171
|
+
return scanTextForSecretHits(path, text).map((hit2) => hit2.finding);
|
|
8011
9172
|
}
|
|
8012
9173
|
var secretsRules, REMEDIATION, MAX_FILE_SIZE, secretsModule;
|
|
8013
9174
|
var init_secrets = __esm({
|
|
@@ -8363,12 +9524,12 @@ ${text}`);
|
|
|
8363
9524
|
const seeds = fixtureLogic(prompt);
|
|
8364
9525
|
const findings = [];
|
|
8365
9526
|
for (const seed of seeds) {
|
|
8366
|
-
const
|
|
9527
|
+
const hit2 = fileTexts.find((f) => f.file.includes(seed.fileHint) && f.text.includes(seed.fileHint === "user" ? "findById" : "Object.assign")) ?? fileTexts.find(
|
|
8367
9528
|
(f) => f.text.includes(seed.ruleId === "AI-IDOR-001" ? "findById(req.params.id)" : "Object.assign(user, req.body)")
|
|
8368
9529
|
);
|
|
8369
|
-
if (
|
|
8370
|
-
const line =
|
|
8371
|
-
findings.push(asAiFinding(seed,
|
|
9530
|
+
if (hit2 === void 0) continue;
|
|
9531
|
+
const line = hit2.text.split(/\r?\n/).findIndex((l) => l.includes(seed.ruleId === "AI-IDOR-001" ? "findById" : "Object.assign")) + 1;
|
|
9532
|
+
findings.push(asAiFinding(seed, hit2.file, line > 0 ? line : 1));
|
|
8372
9533
|
}
|
|
8373
9534
|
return findings;
|
|
8374
9535
|
}
|
|
@@ -8626,6 +9787,40 @@ var init_baseline = __esm({
|
|
|
8626
9787
|
"src/core/baseline.ts"() {
|
|
8627
9788
|
}
|
|
8628
9789
|
});
|
|
9790
|
+
function parseSastSources(value, source) {
|
|
9791
|
+
if (value === void 0) return void 0;
|
|
9792
|
+
if (!Array.isArray(value)) throw new ConfigError(`${source}: "sast.sources" must be an array`);
|
|
9793
|
+
return value.map((item) => {
|
|
9794
|
+
if (typeof item !== "object" || item === null) {
|
|
9795
|
+
throw new ConfigError(`${source}: "sast.sources" entries must be objects`);
|
|
9796
|
+
}
|
|
9797
|
+
const row = item;
|
|
9798
|
+
if (typeof row.callee !== "string" || row.callee.length === 0) {
|
|
9799
|
+
throw new ConfigError(`${source}: "sast.sources[].callee" must be a string`);
|
|
9800
|
+
}
|
|
9801
|
+
if (row.returns !== "tainted") {
|
|
9802
|
+
throw new ConfigError(`${source}: "sast.sources[].returns" must be "tainted"`);
|
|
9803
|
+
}
|
|
9804
|
+
return { callee: row.callee, returns: "tainted" };
|
|
9805
|
+
});
|
|
9806
|
+
}
|
|
9807
|
+
function parseSastSanitizers(value, source) {
|
|
9808
|
+
if (value === void 0) return void 0;
|
|
9809
|
+
if (!Array.isArray(value)) throw new ConfigError(`${source}: "sast.sanitizers" must be an array`);
|
|
9810
|
+
return value.map((item) => {
|
|
9811
|
+
if (typeof item !== "object" || item === null) {
|
|
9812
|
+
throw new ConfigError(`${source}: "sast.sanitizers" entries must be objects`);
|
|
9813
|
+
}
|
|
9814
|
+
const row = item;
|
|
9815
|
+
if (typeof row.callee !== "string" || row.callee.length === 0) {
|
|
9816
|
+
throw new ConfigError(`${source}: "sast.sanitizers[].callee" must be a string`);
|
|
9817
|
+
}
|
|
9818
|
+
if (!Array.isArray(row.clears) || row.clears.some((name) => typeof name !== "string" || !SAST_CLASSES.has(name))) {
|
|
9819
|
+
throw new ConfigError(`${source}: "sast.sanitizers[].clears" must be B4 class names`);
|
|
9820
|
+
}
|
|
9821
|
+
return { callee: row.callee, clears: row.clears.filter((name) => name !== "secrets" && name !== "deps") };
|
|
9822
|
+
});
|
|
9823
|
+
}
|
|
8629
9824
|
function validate(raw, source) {
|
|
8630
9825
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
8631
9826
|
throw new ConfigError(`${source}: config must be a JSON object`);
|
|
@@ -8751,6 +9946,16 @@ function validate(raw, source) {
|
|
|
8751
9946
|
}
|
|
8752
9947
|
sast.maxFileBytes = rawSast.maxFileBytes;
|
|
8753
9948
|
}
|
|
9949
|
+
const custom = process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS === "1";
|
|
9950
|
+
if (!custom && (rawSast.sources !== void 0 || rawSast.sanitizers !== void 0)) {
|
|
9951
|
+
throw new ConfigError(
|
|
9952
|
+
`${source}: unknown key "sast.sources" (FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS)`
|
|
9953
|
+
);
|
|
9954
|
+
}
|
|
9955
|
+
if (custom) {
|
|
9956
|
+
sast.sources = parseSastSources(rawSast.sources, source);
|
|
9957
|
+
sast.sanitizers = parseSastSanitizers(rawSast.sanitizers, source);
|
|
9958
|
+
}
|
|
8754
9959
|
out.sast = sast;
|
|
8755
9960
|
}
|
|
8756
9961
|
return out;
|
|
@@ -8778,7 +9983,7 @@ async function loadConfig(cwd, configPath) {
|
|
|
8778
9983
|
}
|
|
8779
9984
|
return { ...structuredClone(DEFAULT_CONFIG), ...validate(raw, file) };
|
|
8780
9985
|
}
|
|
8781
|
-
var CONFIG_FILE_NAME, DEFAULT_CONFIG, ConfigError, KNOWN_KEYS;
|
|
9986
|
+
var CONFIG_FILE_NAME, DEFAULT_CONFIG, ConfigError, KNOWN_KEYS, SAST_CLASSES;
|
|
8782
9987
|
var init_config2 = __esm({
|
|
8783
9988
|
"src/core/config.ts"() {
|
|
8784
9989
|
init_finding();
|
|
@@ -8808,6 +10013,22 @@ var init_config2 = __esm({
|
|
|
8808
10013
|
"ai",
|
|
8809
10014
|
"sast"
|
|
8810
10015
|
]);
|
|
10016
|
+
SAST_CLASSES = /* @__PURE__ */ new Set([
|
|
10017
|
+
"sqli",
|
|
10018
|
+
"nosqli",
|
|
10019
|
+
"cmdi",
|
|
10020
|
+
"path",
|
|
10021
|
+
"xss",
|
|
10022
|
+
"ssrf",
|
|
10023
|
+
"redirect",
|
|
10024
|
+
"eval",
|
|
10025
|
+
"jwt",
|
|
10026
|
+
"crypto",
|
|
10027
|
+
"proto",
|
|
10028
|
+
"secrets",
|
|
10029
|
+
"deps",
|
|
10030
|
+
"config"
|
|
10031
|
+
]);
|
|
8811
10032
|
}
|
|
8812
10033
|
});
|
|
8813
10034
|
async function walkFiles(options) {
|
|
@@ -9197,13 +10418,13 @@ async function readBlob(cwd, hash) {
|
|
|
9197
10418
|
return void 0;
|
|
9198
10419
|
}
|
|
9199
10420
|
}
|
|
9200
|
-
function remapHit(
|
|
9201
|
-
const snippet =
|
|
9202
|
-
const line =
|
|
9203
|
-
const id = makeFindingId(
|
|
10421
|
+
function remapHit(hit2, file) {
|
|
10422
|
+
const snippet = hit2.finding.snippet;
|
|
10423
|
+
const line = hit2.finding.line ?? 0;
|
|
10424
|
+
const id = makeFindingId(hit2.finding.ruleId, file, `${line}|${snippet ?? "file"}`);
|
|
9204
10425
|
return {
|
|
9205
|
-
finding: { ...
|
|
9206
|
-
secret:
|
|
10426
|
+
finding: { ...hit2.finding, id, file },
|
|
10427
|
+
secret: hit2.secret
|
|
9207
10428
|
};
|
|
9208
10429
|
}
|
|
9209
10430
|
function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
|
|
@@ -9293,8 +10514,8 @@ async function headSecretKeys(cwd) {
|
|
|
9293
10514
|
hits = scanTextForSecretHits(path, text);
|
|
9294
10515
|
seen.set(hash, hits);
|
|
9295
10516
|
}
|
|
9296
|
-
for (const
|
|
9297
|
-
keys.add(`${
|
|
10517
|
+
for (const hit2 of hits) {
|
|
10518
|
+
keys.add(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
|
|
9298
10519
|
}
|
|
9299
10520
|
}
|
|
9300
10521
|
return keys;
|
|
@@ -9327,8 +10548,8 @@ async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
|
9327
10548
|
hits2 = rawHits.filter((h) => allowed.has(h.finding.id));
|
|
9328
10549
|
blobHits.set(change.hash, hits2);
|
|
9329
10550
|
}
|
|
9330
|
-
for (const
|
|
9331
|
-
const mapped = remapHit(
|
|
10551
|
+
for (const hit2 of hits2) {
|
|
10552
|
+
const mapped = remapHit(hit2, change.path);
|
|
9332
10553
|
if (!first.has(mapped.finding.id)) {
|
|
9333
10554
|
first.set(mapped.finding.id, { hit: mapped, commit: change.commit });
|
|
9334
10555
|
}
|
|
@@ -9336,11 +10557,11 @@ async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
|
9336
10557
|
}
|
|
9337
10558
|
const atHead = await headSecretKeys(cwd);
|
|
9338
10559
|
const hits = [];
|
|
9339
|
-
for (const { hit, commit } of first.values()) {
|
|
9340
|
-
const stillAtHead = atHead.has(`${
|
|
10560
|
+
for (const { hit: hit2, commit } of first.values()) {
|
|
10561
|
+
const stillAtHead = atHead.has(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
|
|
9341
10562
|
hits.push({
|
|
9342
|
-
finding: { ...
|
|
9343
|
-
secret:
|
|
10563
|
+
finding: { ...hit2.finding, introducedIn: commit, stillAtHead },
|
|
10564
|
+
secret: hit2.secret
|
|
9344
10565
|
});
|
|
9345
10566
|
}
|
|
9346
10567
|
const scanned = Math.min(maxCommits, total);
|
|
@@ -9582,15 +10803,15 @@ function providerFor(ruleId, secret) {
|
|
|
9582
10803
|
async function verifySecretHits(hits, options = {}) {
|
|
9583
10804
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
9584
10805
|
const out = [];
|
|
9585
|
-
for (const
|
|
9586
|
-
const secret =
|
|
10806
|
+
for (const hit2 of hits) {
|
|
10807
|
+
const secret = hit2.secret;
|
|
9587
10808
|
if (secret === void 0 || secret.length === 0) {
|
|
9588
|
-
out.push({ finding: { ...
|
|
10809
|
+
out.push({ finding: { ...hit2.finding, verification: "unknown" }, secret: hit2.secret });
|
|
9589
10810
|
continue;
|
|
9590
10811
|
}
|
|
9591
|
-
const run = providerFor(
|
|
10812
|
+
const run = providerFor(hit2.finding.ruleId, secret);
|
|
9592
10813
|
const verification = run === void 0 ? "unknown" : await run(secret, fetchImpl);
|
|
9593
|
-
out.push({ finding: { ...
|
|
10814
|
+
out.push({ finding: { ...hit2.finding, verification }, secret: hit2.secret });
|
|
9594
10815
|
}
|
|
9595
10816
|
return out;
|
|
9596
10817
|
}
|
|
@@ -9623,6 +10844,8 @@ function dedupe2(findings) {
|
|
|
9623
10844
|
async function runScan(options = {}, modules = builtinModules) {
|
|
9624
10845
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9625
10846
|
const t0 = performance.now();
|
|
10847
|
+
depsModule.unusedDirect = void 0;
|
|
10848
|
+
depsModule.upgradePlan = void 0;
|
|
9626
10849
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
9627
10850
|
const stats = await stat(cwd).catch(() => null);
|
|
9628
10851
|
if (stats === null || !stats.isDirectory()) {
|
|
@@ -9721,8 +10944,8 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9721
10944
|
const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
9722
10945
|
const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
9723
10946
|
const secretById = /* @__PURE__ */ new Map();
|
|
9724
|
-
for (const
|
|
9725
|
-
if (
|
|
10947
|
+
for (const hit2 of extraHits) {
|
|
10948
|
+
if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
|
|
9726
10949
|
}
|
|
9727
10950
|
for (const finding6 of findings) {
|
|
9728
10951
|
if (finding6.module !== "secrets" || finding6.file === void 0 || secretById.has(finding6.id)) {
|
|
@@ -9736,8 +10959,8 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9736
10959
|
continue;
|
|
9737
10960
|
}
|
|
9738
10961
|
}
|
|
9739
|
-
for (const
|
|
9740
|
-
if (
|
|
10962
|
+
for (const hit2 of scanTextForSecretHits2(finding6.file, text)) {
|
|
10963
|
+
if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
|
|
9741
10964
|
}
|
|
9742
10965
|
}
|
|
9743
10966
|
findings = sortFindings(
|
|
@@ -9789,7 +11012,9 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9789
11012
|
filesScanned: files.length,
|
|
9790
11013
|
modulesRequested: [...modulesRequested],
|
|
9791
11014
|
modulesRun,
|
|
9792
|
-
...historyNotice !== void 0 ? { history: historyNotice } : {}
|
|
11015
|
+
...historyNotice !== void 0 ? { history: historyNotice } : {},
|
|
11016
|
+
...depsModule.unusedDirect !== void 0 ? { unusedDirect: depsModule.unusedDirect } : {},
|
|
11017
|
+
...depsModule.upgradePlan !== void 0 ? { upgradePlan: depsModule.upgradePlan } : {}
|
|
9793
11018
|
},
|
|
9794
11019
|
config,
|
|
9795
11020
|
policy: policyResult,
|
|
@@ -9934,6 +11159,16 @@ function validateCycloneDx15(doc) {
|
|
|
9934
11159
|
if (!Array.isArray(root.components)) errors.push("components must be an array");
|
|
9935
11160
|
return errors;
|
|
9936
11161
|
}
|
|
11162
|
+
function reachProperties(finding6) {
|
|
11163
|
+
const properties = [
|
|
11164
|
+
{ name: "fullshield:reachability", value: finding6.reachability ?? "unspecified" },
|
|
11165
|
+
{ name: "fullshield:devOnly", value: finding6.message.includes("(dev-only)") ? "true" : "false" }
|
|
11166
|
+
];
|
|
11167
|
+
if (finding6.dependencyPath !== void 0 && finding6.dependencyPath.length > 0) {
|
|
11168
|
+
properties.splice(1, 0, { name: "fullshield:dependencyPath", value: finding6.dependencyPath.join(" > ") });
|
|
11169
|
+
}
|
|
11170
|
+
return properties;
|
|
11171
|
+
}
|
|
9937
11172
|
function renderCycloneDx(input) {
|
|
9938
11173
|
const vulns = (input.findings ?? []).filter((f) => f.ruleId === "DEPS-VULN-001");
|
|
9939
11174
|
return {
|
|
@@ -9944,13 +11179,18 @@ function renderCycloneDx(input) {
|
|
|
9944
11179
|
metadata: {
|
|
9945
11180
|
component: { type: "application", name: input.name, version: input.version }
|
|
9946
11181
|
},
|
|
9947
|
-
components: input.components.map((c) =>
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
11182
|
+
components: input.components.map((c) => {
|
|
11183
|
+
const match = (input.findings ?? []).find((finding6) => finding6.snippet?.startsWith(`${c.name}@`));
|
|
11184
|
+
const properties = input.includeReach === true && match !== void 0 ? reachProperties(match) : void 0;
|
|
11185
|
+
return {
|
|
11186
|
+
type: "library",
|
|
11187
|
+
name: c.name,
|
|
11188
|
+
version: c.version,
|
|
11189
|
+
purl: c.purl ?? `pkg:npm/${encodeURIComponent(c.name)}@${c.version}`,
|
|
11190
|
+
...c.license !== void 0 ? { licenses: [{ license: { id: c.license } }] } : {},
|
|
11191
|
+
...properties !== void 0 ? { properties } : {}
|
|
11192
|
+
};
|
|
11193
|
+
}),
|
|
9954
11194
|
vulnerabilities: vulns.map((f) => ({
|
|
9955
11195
|
id: f.references?.find((r) => r.startsWith("GHSA-") || r.startsWith("CVE-")) ?? f.id,
|
|
9956
11196
|
description: f.message,
|
|
@@ -10339,6 +11579,10 @@ var init_compliance = __esm({
|
|
|
10339
11579
|
ROW("SUPPLY-PROVENANCE-001", "A08:2021 Integrity Failures", "CWE-494", "V14.2.3", "PCI DSS 6.3.3"),
|
|
10340
11580
|
ROW("SUPPLY-WORM-001", "A06:2021 Vulnerable Components", "CWE-506", "V14.2.1", "PCI DSS 6.3.3 / 12.10"),
|
|
10341
11581
|
ROW("SUPPLY-WORKFLOW-001", "A08:2021 Integrity Failures", "CWE-829", "V14.2.3", "PCI DSS 6.3.3"),
|
|
11582
|
+
ROW("SUPPLY-CHAINC2-001", "A06:2021 Vulnerable Components", "CWE-506", "V14.2.1", "PCI DSS 6.3.3"),
|
|
11583
|
+
ROW("SUPPLY-STEALER-001", "A06:2021 Vulnerable Components", "CWE-506", "V14.2.1", "PCI DSS 12.10"),
|
|
11584
|
+
ROW("SUPPLY-PLATFORMGATE-001", "A06:2021 Vulnerable Components", "CWE-829", "V14.2.1", "PCI DSS 6.3.3"),
|
|
11585
|
+
ROW("SUPPLY-SECHOLDER-001", "A06:2021 Vulnerable Components", "CWE-506", "V14.2.1", "PCI DSS 6.3.3"),
|
|
10342
11586
|
ROW("IAC-GHA-UNPINNED-001", "A08:2021 Integrity Failures", "CWE-494", "V14.2.3", "PCI DSS 6.3.3"),
|
|
10343
11587
|
ROW("IAC-GHA-PRTARGET-001", "A01:2021 Broken Access Control", "CWE-269", "V4.1.1", "PCI DSS 7.2"),
|
|
10344
11588
|
ROW("IAC-GHA-PERMS-001", "A01:2021 Broken Access Control", "CWE-250", "V4.1.1", "PCI DSS 7.2"),
|
|
@@ -10723,9 +11967,9 @@ async function callMcpTool(session, name, args = {}) {
|
|
|
10723
11967
|
return { findings: session.findings };
|
|
10724
11968
|
}
|
|
10725
11969
|
if (name === "explain_finding") {
|
|
10726
|
-
const
|
|
10727
|
-
if (!
|
|
10728
|
-
return { text: `${
|
|
11970
|
+
const hit2 = session.findings.find((f) => f.ruleId === args.ruleId);
|
|
11971
|
+
if (!hit2) return { text: `no finding ${args.ruleId}` };
|
|
11972
|
+
return { text: `${hit2.title}: ${hit2.message} ${hit2.remediation}` };
|
|
10729
11973
|
}
|
|
10730
11974
|
if (name === "propose_fix") {
|
|
10731
11975
|
return proposeOnly(session, args.ruleId);
|
|
@@ -10736,25 +11980,25 @@ async function callMcpTool(session, name, args = {}) {
|
|
|
10736
11980
|
return { error: `unknown tool ${name}` };
|
|
10737
11981
|
}
|
|
10738
11982
|
function proposeOnly(session, ruleId) {
|
|
10739
|
-
const
|
|
10740
|
-
if (!
|
|
10741
|
-
const abs = join(session.cwd,
|
|
11983
|
+
const hit2 = session.findings.find((f) => f.ruleId === ruleId);
|
|
11984
|
+
if (!hit2?.file || !session.cwd) return { diff: "", wrote: false };
|
|
11985
|
+
const abs = join(session.cwd, hit2.file);
|
|
10742
11986
|
const source = readFileSync(abs, "utf8");
|
|
10743
|
-
const proposal = proposeFix(
|
|
11987
|
+
const proposal = proposeFix(hit2, source);
|
|
10744
11988
|
return { diff: proposal?.diff ?? "", wrote: false };
|
|
10745
11989
|
}
|
|
10746
11990
|
function verifyInTemp(session, ruleId) {
|
|
10747
|
-
const
|
|
10748
|
-
if (!
|
|
10749
|
-
const original = join(session.cwd,
|
|
11991
|
+
const hit2 = session.findings.find((f) => f.ruleId === ruleId);
|
|
11992
|
+
if (!hit2?.file || !session.cwd) return { originalUntouched: true, tempApplied: false };
|
|
11993
|
+
const original = join(session.cwd, hit2.file);
|
|
10750
11994
|
const before = readFileSync(original, "utf8");
|
|
10751
11995
|
const mtime = statSync(original).mtimeMs;
|
|
10752
|
-
const proposal = proposeFix(
|
|
11996
|
+
const proposal = proposeFix(hit2, before);
|
|
10753
11997
|
if (!proposal?.diff) return { originalUntouched: true, tempApplied: false };
|
|
10754
11998
|
const tmp = mkdtempSync(join(tmpdir(), "fullshield-mcp-verify-"));
|
|
10755
11999
|
try {
|
|
10756
12000
|
cpSync(session.cwd, tmp, { recursive: true });
|
|
10757
|
-
const copy = join(tmp,
|
|
12001
|
+
const copy = join(tmp, hit2.file);
|
|
10758
12002
|
const next = applyUnifiedDiff(readFileSync(copy, "utf8"), proposal.diff);
|
|
10759
12003
|
writeFileSync(copy, next);
|
|
10760
12004
|
const untouched = readFileSync(original, "utf8") === before && statSync(original).mtimeMs === mtime;
|
|
@@ -10978,16 +12222,16 @@ async function confirmTarget(baseUrl, opts) {
|
|
|
10978
12222
|
try {
|
|
10979
12223
|
if (route.kind === "idor") {
|
|
10980
12224
|
const url2 = `${origin}${route.path}`;
|
|
10981
|
-
const
|
|
12225
|
+
const hit3 = await probe(fetchImpl, url2, route.method, void 0, {
|
|
10982
12226
|
"x-user": route.asUser ?? opts.users?.b ?? "bob"
|
|
10983
12227
|
});
|
|
10984
12228
|
const owner = route.expectOwner ?? opts.users?.a ?? "alice";
|
|
10985
|
-
const confirmed2 =
|
|
12229
|
+
const confirmed2 = hit3.ok && hit3.text.includes(owner);
|
|
10986
12230
|
confirmations.push({
|
|
10987
12231
|
kind: "idor",
|
|
10988
12232
|
status: confirmed2 ? "confirmed" : "unconfirmed",
|
|
10989
12233
|
proof: curlProof(route.method, url2, `-H 'x-user: ${route.asUser ?? "bob"}' `),
|
|
10990
|
-
detail:
|
|
12234
|
+
detail: hit3.text.slice(0, 200)
|
|
10991
12235
|
});
|
|
10992
12236
|
continue;
|
|
10993
12237
|
}
|
|
@@ -11000,13 +12244,13 @@ async function confirmTarget(baseUrl, opts) {
|
|
|
11000
12244
|
} else {
|
|
11001
12245
|
body = { [param]: payload };
|
|
11002
12246
|
}
|
|
11003
|
-
const
|
|
11004
|
-
const confirmed =
|
|
12247
|
+
const hit2 = await probe(fetchImpl, url, route.method, body);
|
|
12248
|
+
const confirmed = hit2.text.includes(token) || route.kind === "path" && hit2.text.includes("FULLSHIELD_CANARY");
|
|
11005
12249
|
confirmations.push({
|
|
11006
12250
|
kind: route.kind,
|
|
11007
12251
|
status: confirmed ? "confirmed" : "unconfirmed",
|
|
11008
12252
|
proof: curlProof(route.method, url),
|
|
11009
|
-
detail:
|
|
12253
|
+
detail: hit2.text.slice(0, 200)
|
|
11010
12254
|
});
|
|
11011
12255
|
} catch (err) {
|
|
11012
12256
|
confirmations.push({
|
|
@@ -11284,14 +12528,16 @@ function openVexStatement(finding6) {
|
|
|
11284
12528
|
vulnerability: { name: vulnName(finding6) },
|
|
11285
12529
|
products: [{ "@id": npmPurl(finding6.snippet) }]
|
|
11286
12530
|
};
|
|
12531
|
+
const detail = process.env.FULLSHIELD_EXPERIMENTAL_SBOM_REACH === "1" && finding6.reachabilityEvidence?.reason !== void 0 ? { detail: finding6.reachabilityEvidence.reason } : {};
|
|
11287
12532
|
if (notImported(finding6)) {
|
|
11288
12533
|
return {
|
|
11289
12534
|
...base,
|
|
11290
12535
|
status: "not_affected",
|
|
11291
|
-
justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION
|
|
12536
|
+
justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION,
|
|
12537
|
+
...detail
|
|
11292
12538
|
};
|
|
11293
12539
|
}
|
|
11294
|
-
return { ...base, status: "under_investigation" };
|
|
12540
|
+
return { ...base, status: "under_investigation", ...detail };
|
|
11295
12541
|
}
|
|
11296
12542
|
function buildOpenVex(findings) {
|
|
11297
12543
|
const statements = depFindings(findings).map(openVexStatement);
|
|
@@ -11579,6 +12825,28 @@ async function runFix(options = {}) {
|
|
|
11579
12825
|
const after = applyUnifiedDiff(source, proposed.diff);
|
|
11580
12826
|
byFile.set(sourceFile, after);
|
|
11581
12827
|
}
|
|
12828
|
+
const unused = result.metadata.unusedDirect?.packages ?? [];
|
|
12829
|
+
if (unused.length > 0) {
|
|
12830
|
+
let manifest = byFile.get("package.json");
|
|
12831
|
+
if (manifest === void 0) {
|
|
12832
|
+
try {
|
|
12833
|
+
manifest = await readFile(join(cwd, "package.json"), "utf8");
|
|
12834
|
+
} catch {
|
|
12835
|
+
manifest = void 0;
|
|
12836
|
+
}
|
|
12837
|
+
}
|
|
12838
|
+
if (manifest !== void 0) {
|
|
12839
|
+
const next = removeDirectDependencies(manifest, unused);
|
|
12840
|
+
const diff = dependencyRemovalDiff(manifest, unused);
|
|
12841
|
+
if (diff.length > 0) {
|
|
12842
|
+
patches.push({ file: "package.json", ruleId: "DEPS-VULN-001", safe: false, diff });
|
|
12843
|
+
if (options.unsafe === true) {
|
|
12844
|
+
byFile.set("package.json", next);
|
|
12845
|
+
appliedOverride = true;
|
|
12846
|
+
}
|
|
12847
|
+
}
|
|
12848
|
+
}
|
|
12849
|
+
}
|
|
11582
12850
|
const notice = appliedOverride ? "run npm install" : void 0;
|
|
11583
12851
|
if (options.apply !== true) {
|
|
11584
12852
|
return { applied: false, reverted: false, patches, findings: result.findings, notice };
|
|
@@ -11626,6 +12894,29 @@ async function restoreSnapshots(cwd, snapshots) {
|
|
|
11626
12894
|
}
|
|
11627
12895
|
}
|
|
11628
12896
|
}
|
|
12897
|
+
function dependencyRemovalDiff(manifest, names) {
|
|
12898
|
+
const drop = names.map((name) => `"${name}"`);
|
|
12899
|
+
const removed = manifest.split("\n").filter((line) => drop.some((token) => line.includes(token)));
|
|
12900
|
+
if (removed.length === 0) return "";
|
|
12901
|
+
return ["--- a/package.json", "+++ b/package.json", ...removed.map((line) => `-${line}`)].join("\n");
|
|
12902
|
+
}
|
|
12903
|
+
function removeDirectDependencies(manifest, names) {
|
|
12904
|
+
let parsed;
|
|
12905
|
+
try {
|
|
12906
|
+
parsed = JSON.parse(manifest);
|
|
12907
|
+
} catch {
|
|
12908
|
+
return manifest;
|
|
12909
|
+
}
|
|
12910
|
+
if (parsed.dependencies === void 0 || typeof parsed.dependencies !== "object") return manifest;
|
|
12911
|
+
const drop = new Set(names);
|
|
12912
|
+
const next = {};
|
|
12913
|
+
for (const [name, version] of Object.entries(parsed.dependencies)) {
|
|
12914
|
+
if (!drop.has(name)) next[name] = version;
|
|
12915
|
+
}
|
|
12916
|
+
parsed.dependencies = next;
|
|
12917
|
+
const trailing = manifest.endsWith("\n") ? "\n" : "";
|
|
12918
|
+
return `${JSON.stringify(parsed, null, 2)}${trailing}`;
|
|
12919
|
+
}
|
|
11629
12920
|
function runProjectTests(cwd) {
|
|
11630
12921
|
let pkg;
|
|
11631
12922
|
try {
|
|
@@ -11994,7 +13285,159 @@ function renderFinding(finding6) {
|
|
|
11994
13285
|
}
|
|
11995
13286
|
return lines;
|
|
11996
13287
|
}
|
|
11997
|
-
|
|
13288
|
+
var SEVERITY_RANK = {
|
|
13289
|
+
critical: 0,
|
|
13290
|
+
high: 1,
|
|
13291
|
+
medium: 2,
|
|
13292
|
+
low: 3,
|
|
13293
|
+
info: 4
|
|
13294
|
+
};
|
|
13295
|
+
var SHOWN_VULN_REACH = /* @__PURE__ */ new Set([
|
|
13296
|
+
"reachable",
|
|
13297
|
+
"symbol-reachable",
|
|
13298
|
+
"reachable-via-dependency",
|
|
13299
|
+
"reachability-unknown"
|
|
13300
|
+
]);
|
|
13301
|
+
function packageNameFromSnippet3(snippet) {
|
|
13302
|
+
if (snippet === void 0 || snippet.length === 0) return void 0;
|
|
13303
|
+
if (snippet.startsWith("@")) {
|
|
13304
|
+
const second = snippet.indexOf("@", 1);
|
|
13305
|
+
return second === -1 ? snippet : snippet.slice(0, second);
|
|
13306
|
+
}
|
|
13307
|
+
const at = snippet.indexOf("@");
|
|
13308
|
+
return at <= 0 ? snippet : snippet.slice(0, at);
|
|
13309
|
+
}
|
|
13310
|
+
function versionsFromSnippet(snippet) {
|
|
13311
|
+
const name = packageNameFromSnippet3(snippet);
|
|
13312
|
+
if (name === void 0 || snippet === void 0) return [];
|
|
13313
|
+
const rest = snippet.slice(name.length + 1);
|
|
13314
|
+
return rest.length === 0 ? [] : rest.split(",").filter((version) => version.length > 0);
|
|
13315
|
+
}
|
|
13316
|
+
function isDevOnly2(finding6) {
|
|
13317
|
+
return finding6.message.includes("(dev-only)");
|
|
13318
|
+
}
|
|
13319
|
+
function isActionable(finding6) {
|
|
13320
|
+
if (finding6.ruleId === "DEPS-MALICIOUS-001") return true;
|
|
13321
|
+
if (finding6.ruleId === "DEPS-VULN-001") {
|
|
13322
|
+
if (isDevOnly2(finding6)) return false;
|
|
13323
|
+
return finding6.reachability !== void 0 && SHOWN_VULN_REACH.has(finding6.reachability);
|
|
13324
|
+
}
|
|
13325
|
+
if (finding6.module === "sast" || finding6.module === "secrets" || finding6.module === "config") {
|
|
13326
|
+
return finding6.severity === "high" || finding6.severity === "critical" || finding6.confidence === "high";
|
|
13327
|
+
}
|
|
13328
|
+
return false;
|
|
13329
|
+
}
|
|
13330
|
+
function reachRank(finding6) {
|
|
13331
|
+
const reachability = finding6.reachability;
|
|
13332
|
+
if (reachability === "reachable" || reachability === "symbol-reachable") return 0;
|
|
13333
|
+
if (reachability === "reachable-via-dependency") return 1;
|
|
13334
|
+
if (reachability === "reachability-unknown") return 2;
|
|
13335
|
+
if (reachability === void 0) return 4;
|
|
13336
|
+
return 3;
|
|
13337
|
+
}
|
|
13338
|
+
function compareActionable(a, b) {
|
|
13339
|
+
const maliciousA = a.ruleId === "DEPS-MALICIOUS-001" ? 0 : 1;
|
|
13340
|
+
const maliciousB = b.ruleId === "DEPS-MALICIOUS-001" ? 0 : 1;
|
|
13341
|
+
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);
|
|
13342
|
+
}
|
|
13343
|
+
function highestSeverity(findings) {
|
|
13344
|
+
return findings.reduce(
|
|
13345
|
+
(best, finding6) => SEVERITY_RANK[finding6.severity] < SEVERITY_RANK[best] ? finding6.severity : best,
|
|
13346
|
+
findings[0]?.severity ?? "info"
|
|
13347
|
+
);
|
|
13348
|
+
}
|
|
13349
|
+
function bestReachability(findings) {
|
|
13350
|
+
let best;
|
|
13351
|
+
let bestRank = 5;
|
|
13352
|
+
for (const finding6 of findings) {
|
|
13353
|
+
const rank = reachRank(finding6);
|
|
13354
|
+
if (finding6.reachability !== void 0 && rank < bestRank) {
|
|
13355
|
+
best = finding6.reachability;
|
|
13356
|
+
bestRank = rank;
|
|
13357
|
+
}
|
|
13358
|
+
}
|
|
13359
|
+
return best;
|
|
13360
|
+
}
|
|
13361
|
+
function shownConsoleRows(findings) {
|
|
13362
|
+
return dependencyRows(findings.filter(isActionable));
|
|
13363
|
+
}
|
|
13364
|
+
function dependencyRows(findings) {
|
|
13365
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13366
|
+
const singles = [];
|
|
13367
|
+
for (const finding6 of findings) {
|
|
13368
|
+
const name = packageNameFromSnippet3(finding6.snippet);
|
|
13369
|
+
if ((finding6.ruleId === "DEPS-VULN-001" || finding6.ruleId === "DEPS-MALICIOUS-001") && name !== void 0) {
|
|
13370
|
+
const list = groups.get(name);
|
|
13371
|
+
if (list === void 0) groups.set(name, [finding6]);
|
|
13372
|
+
else list.push(finding6);
|
|
13373
|
+
} else {
|
|
13374
|
+
singles.push(finding6);
|
|
13375
|
+
}
|
|
13376
|
+
}
|
|
13377
|
+
const rows = singles.map((finding6) => ({
|
|
13378
|
+
findings: [finding6],
|
|
13379
|
+
severity: finding6.severity,
|
|
13380
|
+
reachability: finding6.reachability,
|
|
13381
|
+
ruleId: finding6.ruleId,
|
|
13382
|
+
file: finding6.file,
|
|
13383
|
+
line: finding6.line,
|
|
13384
|
+
versions: [],
|
|
13385
|
+
grouped: false
|
|
13386
|
+
}));
|
|
13387
|
+
for (const [packageName, list] of groups) {
|
|
13388
|
+
const ordered = [...list].sort(compareActionable);
|
|
13389
|
+
const first = ordered[0];
|
|
13390
|
+
if (first === void 0) continue;
|
|
13391
|
+
const versions = [...new Set(list.flatMap((finding6) => versionsFromSnippet(finding6.snippet)))].sort();
|
|
13392
|
+
const malicious = list.find((finding6) => finding6.ruleId === "DEPS-MALICIOUS-001");
|
|
13393
|
+
rows.push({
|
|
13394
|
+
findings: ordered,
|
|
13395
|
+
severity: highestSeverity(list),
|
|
13396
|
+
reachability: bestReachability(list),
|
|
13397
|
+
ruleId: malicious?.ruleId ?? "DEPS-VULN-001",
|
|
13398
|
+
file: first.file,
|
|
13399
|
+
line: first.line,
|
|
13400
|
+
packageName,
|
|
13401
|
+
versions,
|
|
13402
|
+
path: first.dependencyPath,
|
|
13403
|
+
grouped: true
|
|
13404
|
+
});
|
|
13405
|
+
}
|
|
13406
|
+
rows.sort((a, b) => {
|
|
13407
|
+
const probeA = a.findings[0];
|
|
13408
|
+
const probeB = b.findings[0];
|
|
13409
|
+
if (probeA === void 0 || probeB === void 0) return 0;
|
|
13410
|
+
const maliciousA = a.ruleId === "DEPS-MALICIOUS-001" ? 0 : 1;
|
|
13411
|
+
const maliciousB = b.ruleId === "DEPS-MALICIOUS-001" ? 0 : 1;
|
|
13412
|
+
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);
|
|
13413
|
+
});
|
|
13414
|
+
return rows;
|
|
13415
|
+
}
|
|
13416
|
+
function renderRow(row) {
|
|
13417
|
+
if (!row.grouped) {
|
|
13418
|
+
const only = row.findings[0];
|
|
13419
|
+
if (only === void 0) return [];
|
|
13420
|
+
return [...renderFinding(only), ` docs: rules/${only.ruleId}.md`];
|
|
13421
|
+
}
|
|
13422
|
+
const reach = row.reachability ?? "unspecified";
|
|
13423
|
+
const lines = [
|
|
13424
|
+
` ${pc.bold(`[${row.ruleId}]`)} ${row.packageName} \u2014 ${row.findings.length} advisories, ${row.severity}, ${reach}`,
|
|
13425
|
+
` versions: ${row.versions.join(", ") || "unknown"}`
|
|
13426
|
+
];
|
|
13427
|
+
if (row.path !== void 0 && row.path.length > 0) {
|
|
13428
|
+
lines.push(` path: ${row.path.join(" > ")}`);
|
|
13429
|
+
}
|
|
13430
|
+
if (row.file !== void 0) {
|
|
13431
|
+
lines.push(` ${row.file}${row.line !== void 0 ? `:${row.line}` : ""}`);
|
|
13432
|
+
}
|
|
13433
|
+
const evidenceImport = row.findings[0]?.reachabilityEvidence?.imports[0];
|
|
13434
|
+
if (evidenceImport !== void 0) {
|
|
13435
|
+
lines.push(` import: ${evidenceImport.file}:${evidenceImport.line} ${evidenceImport.specifier}`);
|
|
13436
|
+
}
|
|
13437
|
+
lines.push(` docs: rules/${row.ruleId}.md`);
|
|
13438
|
+
return lines;
|
|
13439
|
+
}
|
|
13440
|
+
function renderConsoleReport(input, options = {}) {
|
|
11998
13441
|
const result = redactScan(input);
|
|
11999
13442
|
const { findings, summary, metadata } = result;
|
|
12000
13443
|
const lines = [];
|
|
@@ -12004,34 +13447,63 @@ function renderConsoleReport(input) {
|
|
|
12004
13447
|
)
|
|
12005
13448
|
);
|
|
12006
13449
|
if (metadata.history !== void 0) lines.push(metadata.history);
|
|
13450
|
+
if (metadata.upgradePlan !== void 0 && metadata.upgradePlan.sentence.length > 0) {
|
|
13451
|
+
lines.push(metadata.upgradePlan.sentence);
|
|
13452
|
+
}
|
|
13453
|
+
if (metadata.unusedDirect !== void 0) {
|
|
13454
|
+
const count = metadata.unusedDirect.packages.length;
|
|
13455
|
+
const advisories = metadata.unusedDirect.advisories;
|
|
13456
|
+
const packageWord = count === 1 ? "package is" : "packages are";
|
|
13457
|
+
const advisoryWord = advisories === 1 ? "advisory" : "advisories";
|
|
13458
|
+
lines.push(
|
|
13459
|
+
`${count} ${packageWord} never imported; removing them clears ${advisories} ${advisoryWord}.`
|
|
13460
|
+
);
|
|
13461
|
+
}
|
|
12007
13462
|
if (metadata.modulesRun.length < metadata.modulesRequested.length) {
|
|
12008
13463
|
const pending = metadata.modulesRequested.filter((m) => !metadata.modulesRun.includes(m));
|
|
12009
13464
|
lines.push(
|
|
12010
13465
|
pc.dim(`note: requested modules not implemented yet: ${pending.join(", ")}`)
|
|
12011
13466
|
);
|
|
12012
13467
|
}
|
|
12013
|
-
const
|
|
12014
|
-
const
|
|
12015
|
-
const
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
13468
|
+
const showAll = options.all === true;
|
|
13469
|
+
const folded = findings.filter((finding6) => !isActionable(finding6));
|
|
13470
|
+
const rows = showAll ? [...findings].sort(compareActionable).map((finding6) => ({
|
|
13471
|
+
findings: [finding6],
|
|
13472
|
+
severity: finding6.severity,
|
|
13473
|
+
reachability: finding6.reachability,
|
|
13474
|
+
ruleId: finding6.ruleId,
|
|
13475
|
+
file: finding6.file,
|
|
13476
|
+
line: finding6.line,
|
|
13477
|
+
versions: [],
|
|
13478
|
+
grouped: false
|
|
13479
|
+
})) : shownConsoleRows(findings);
|
|
13480
|
+
const printed = showAll ? rows : rows.slice(0, 20);
|
|
13481
|
+
const hiddenActionable = showAll ? 0 : rows.length - printed.length;
|
|
13482
|
+
let previous;
|
|
13483
|
+
for (const row of printed) {
|
|
13484
|
+
if (row.severity !== previous) {
|
|
13485
|
+
const count = printed.filter((item) => item.severity === row.severity).length;
|
|
13486
|
+
lines.push("");
|
|
13487
|
+
lines.push(SEVERITY_COLOR[row.severity](`${row.severity.toUpperCase()} (${count})`));
|
|
13488
|
+
previous = row.severity;
|
|
12025
13489
|
}
|
|
13490
|
+
lines.push(...renderRow(row));
|
|
13491
|
+
}
|
|
13492
|
+
if (hiddenActionable > 0) {
|
|
13493
|
+
lines.push("");
|
|
13494
|
+
lines.push(`${hiddenActionable} more actionable \u2014 use --json`);
|
|
12026
13495
|
}
|
|
12027
|
-
if (
|
|
13496
|
+
if (!showAll && folded.length > 0) {
|
|
12028
13497
|
lines.push("");
|
|
12029
|
-
lines.push(
|
|
13498
|
+
lines.push(
|
|
13499
|
+
`${folded.length} more (not directly reachable, or below high confidence) \u2014 --all or see JSON`
|
|
13500
|
+
);
|
|
12030
13501
|
}
|
|
12031
13502
|
lines.push("");
|
|
12032
13503
|
if (summary.total === 0) {
|
|
12033
13504
|
lines.push(pc.green("\u2714 No findings."));
|
|
12034
13505
|
} else {
|
|
13506
|
+
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
12035
13507
|
const counts = bySeverityDesc.filter((s) => summary.bySeverity[s] > 0).map((s) => `${summary.bySeverity[s]} ${s}`).join(", ");
|
|
12036
13508
|
lines.push(pc.red(`\u2716 ${summary.total} finding(s): ${counts}`));
|
|
12037
13509
|
}
|
|
@@ -12161,8 +13633,6 @@ function renderMarkdownReport(input) {
|
|
|
12161
13633
|
}
|
|
12162
13634
|
return lines.join("\n");
|
|
12163
13635
|
}
|
|
12164
|
-
|
|
12165
|
-
// src/reporters/sarif.ts
|
|
12166
13636
|
var SCHEMA_URI = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
12167
13637
|
function sarifLevel(severity) {
|
|
12168
13638
|
if (severity === "critical" || severity === "high") return "error";
|
|
@@ -12172,15 +13642,37 @@ function sarifLevel(severity) {
|
|
|
12172
13642
|
function uri(file) {
|
|
12173
13643
|
return (file ?? "").replace(/\\/g, "/");
|
|
12174
13644
|
}
|
|
13645
|
+
function sarifV2() {
|
|
13646
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_SARIF_V2 === "1";
|
|
13647
|
+
}
|
|
13648
|
+
function ruleMarkdown(ruleId) {
|
|
13649
|
+
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "rules", `${ruleId}.md`);
|
|
13650
|
+
if (!existsSync(path)) return void 0;
|
|
13651
|
+
return readFileSync(path, "utf8").slice(0, 2048);
|
|
13652
|
+
}
|
|
13653
|
+
function relatedLocations(finding6) {
|
|
13654
|
+
if (finding6.dependencyPath === void 0 || finding6.dependencyPath.length === 0) return void 0;
|
|
13655
|
+
return finding6.dependencyPath.map((step) => ({
|
|
13656
|
+
message: { text: step },
|
|
13657
|
+
physicalLocation: {
|
|
13658
|
+
artifactLocation: { uri: uri(finding6.file) },
|
|
13659
|
+
...finding6.line !== void 0 ? { region: { startLine: finding6.line } } : {}
|
|
13660
|
+
}
|
|
13661
|
+
}));
|
|
13662
|
+
}
|
|
12175
13663
|
function renderSarifReport(input) {
|
|
12176
13664
|
const result = redactScan(input);
|
|
13665
|
+
const v2 = sarifV2();
|
|
12177
13666
|
const rules = /* @__PURE__ */ new Map();
|
|
12178
13667
|
for (const finding6 of result.findings) {
|
|
12179
13668
|
if (!rules.has(finding6.ruleId)) {
|
|
13669
|
+
const markdown = v2 ? ruleMarkdown(finding6.ruleId) : void 0;
|
|
12180
13670
|
rules.set(finding6.ruleId, {
|
|
12181
13671
|
id: finding6.ruleId,
|
|
12182
13672
|
name: finding6.ruleId,
|
|
12183
|
-
shortDescription: { text: finding6.title }
|
|
13673
|
+
shortDescription: { text: finding6.title },
|
|
13674
|
+
...markdown !== void 0 ? { help: { markdown } } : {},
|
|
13675
|
+
...v2 ? { properties: { precision: "unreviewed" } } : {}
|
|
12184
13676
|
});
|
|
12185
13677
|
}
|
|
12186
13678
|
}
|
|
@@ -12209,6 +13701,7 @@ function renderSarifReport(input) {
|
|
|
12209
13701
|
}
|
|
12210
13702
|
}
|
|
12211
13703
|
] : [];
|
|
13704
|
+
const related = v2 ? relatedLocations(finding6) : void 0;
|
|
12212
13705
|
const codeFlows = finding6.trace !== void 0 && finding6.trace.length > 0 ? [
|
|
12213
13706
|
{
|
|
12214
13707
|
threadFlows: [
|
|
@@ -12231,7 +13724,12 @@ function renderSarifReport(input) {
|
|
|
12231
13724
|
level: sarifLevel(finding6.severity),
|
|
12232
13725
|
message: { text: finding6.message },
|
|
12233
13726
|
locations: location4,
|
|
12234
|
-
...codeFlows !== void 0 ? { codeFlows } : {}
|
|
13727
|
+
...codeFlows !== void 0 ? { codeFlows } : {},
|
|
13728
|
+
...v2 ? {
|
|
13729
|
+
partialFingerprints: { "fullshield/v1": lineIndependentFingerprint(finding6) },
|
|
13730
|
+
...related !== void 0 ? { relatedLocations: related } : {},
|
|
13731
|
+
...finding6.module === "deps" && finding6.reachability !== void 0 ? { properties: { reachability: finding6.reachability } } : {}
|
|
13732
|
+
} : {}
|
|
12235
13733
|
};
|
|
12236
13734
|
})
|
|
12237
13735
|
}
|
|
@@ -12373,13 +13871,152 @@ function renderDiffText(diff) {
|
|
|
12373
13871
|
init_experimental();
|
|
12374
13872
|
init_policy();
|
|
12375
13873
|
|
|
13874
|
+
// src/deps-diff/index.ts
|
|
13875
|
+
init_semver();
|
|
13876
|
+
init_lockfile();
|
|
13877
|
+
var execFileAsync4 = promisify(execFile);
|
|
13878
|
+
var LOCK_NAMES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock"];
|
|
13879
|
+
function diffLockedPackages(before, after, direct) {
|
|
13880
|
+
const oldMap = new Map(before.map((pkg) => [pkg.name, pkg.version]));
|
|
13881
|
+
const newMap = new Map(after.map((pkg) => [pkg.name, pkg.version]));
|
|
13882
|
+
const names = [.../* @__PURE__ */ new Set([...oldMap.keys(), ...newMap.keys()])].sort();
|
|
13883
|
+
const changes = [];
|
|
13884
|
+
for (const name of names) {
|
|
13885
|
+
const oldVersion = oldMap.get(name);
|
|
13886
|
+
const newVersion = newMap.get(name);
|
|
13887
|
+
const isDirect2 = direct.has(name);
|
|
13888
|
+
if (oldVersion === void 0 && newVersion !== void 0) {
|
|
13889
|
+
changes.push({ name, change: "added", newVersion, direct: isDirect2 });
|
|
13890
|
+
} else if (oldVersion !== void 0 && newVersion === void 0) {
|
|
13891
|
+
changes.push({ name, change: "removed", oldVersion, direct: isDirect2 });
|
|
13892
|
+
} else if (oldVersion !== void 0 && newVersion !== void 0 && oldVersion !== newVersion) {
|
|
13893
|
+
const order = compareSemver(newVersion, oldVersion);
|
|
13894
|
+
changes.push({
|
|
13895
|
+
name,
|
|
13896
|
+
change: order >= 0 ? "upgraded" : "downgraded",
|
|
13897
|
+
oldVersion,
|
|
13898
|
+
newVersion,
|
|
13899
|
+
direct: isDirect2
|
|
13900
|
+
});
|
|
13901
|
+
}
|
|
13902
|
+
}
|
|
13903
|
+
return changes;
|
|
13904
|
+
}
|
|
13905
|
+
function packagesFromLock(filename, contents) {
|
|
13906
|
+
const graph2 = parseLockfileGraph(filename, contents);
|
|
13907
|
+
return { packages: graph2.packages, direct: /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]) };
|
|
13908
|
+
}
|
|
13909
|
+
async function gitShow(cwd, spec, run = defaultGit) {
|
|
13910
|
+
return run(cwd, ["show", spec]);
|
|
13911
|
+
}
|
|
13912
|
+
async function defaultGit(cwd, args) {
|
|
13913
|
+
if (args[0] !== "show") throw new Error("deps-diff only runs git show");
|
|
13914
|
+
const { stdout } = await execFileAsync4("git", args, { cwd, encoding: "utf8" });
|
|
13915
|
+
return stdout;
|
|
13916
|
+
}
|
|
13917
|
+
async function lockAtRef(cwd, ref, run = defaultGit) {
|
|
13918
|
+
for (const file of LOCK_NAMES) {
|
|
13919
|
+
try {
|
|
13920
|
+
const text = await gitShow(cwd, `${ref}:${file}`, run);
|
|
13921
|
+
if (text.length > 0) return { file, text };
|
|
13922
|
+
} catch {
|
|
13923
|
+
}
|
|
13924
|
+
}
|
|
13925
|
+
throw new Error(`deps-diff cannot read a lockfile at ${ref} (FULLSHIELD_EXPERIMENTAL_DEPS_DIFF)`);
|
|
13926
|
+
}
|
|
13927
|
+
function releaseAgeLabel(publishedAt, nowMs, minHours = 72) {
|
|
13928
|
+
if (publishedAt === void 0) return "unknown";
|
|
13929
|
+
const published = Date.parse(publishedAt);
|
|
13930
|
+
if (Number.isNaN(published)) return "unknown";
|
|
13931
|
+
const hours = (nowMs - published) / 36e5;
|
|
13932
|
+
return hours < minHours ? "new-release" : "aged";
|
|
13933
|
+
}
|
|
13934
|
+
var ExplainError = class extends Error {
|
|
13935
|
+
constructor(message) {
|
|
13936
|
+
super(message);
|
|
13937
|
+
this.name = "ExplainError";
|
|
13938
|
+
}
|
|
13939
|
+
};
|
|
13940
|
+
function matchFinding(findings, rawId) {
|
|
13941
|
+
const id = rawId.trim();
|
|
13942
|
+
if (id.length < 6) {
|
|
13943
|
+
throw new ExplainError(`id prefix must be at least 6 characters (FULLSHIELD_EXPERIMENTAL_EXPLAIN)`);
|
|
13944
|
+
}
|
|
13945
|
+
const exact = findings.find((finding6) => finding6.id === id);
|
|
13946
|
+
if (exact !== void 0) return exact;
|
|
13947
|
+
const matches = findings.filter((finding6) => finding6.id.startsWith(id));
|
|
13948
|
+
if (matches.length === 1) {
|
|
13949
|
+
const only = matches[0];
|
|
13950
|
+
if (only !== void 0) return only;
|
|
13951
|
+
}
|
|
13952
|
+
if (matches.length === 0) {
|
|
13953
|
+
const candidates = findings.slice(0, 5).map((finding6) => finding6.id);
|
|
13954
|
+
throw new ExplainError(
|
|
13955
|
+
`no finding matches ${id}${candidates.length > 0 ? `; candidates: ${candidates.join(", ")}` : ""} (FULLSHIELD_EXPERIMENTAL_EXPLAIN)`
|
|
13956
|
+
);
|
|
13957
|
+
}
|
|
13958
|
+
throw new ExplainError(
|
|
13959
|
+
`ambiguous id ${id}; candidates: ${matches.slice(0, 5).map((finding6) => finding6.id).join(", ")} (FULLSHIELD_EXPERIMENTAL_EXPLAIN)`
|
|
13960
|
+
);
|
|
13961
|
+
}
|
|
13962
|
+
function contextLines(text, line) {
|
|
13963
|
+
if (line === void 0) return [];
|
|
13964
|
+
const rows = text.split(/\r?\n/);
|
|
13965
|
+
const start = Math.max(0, line - 3);
|
|
13966
|
+
const end = Math.min(rows.length, line + 2);
|
|
13967
|
+
return rows.slice(start, end).map((row, index) => `${start + index + 1}|${row}`);
|
|
13968
|
+
}
|
|
13969
|
+
function renderExplain(finding6, fileText = "") {
|
|
13970
|
+
const location4 = finding6.file !== void 0 ? `${finding6.file}${finding6.line !== void 0 ? `:${finding6.line}` : ""}` : "(project)";
|
|
13971
|
+
const lines = [
|
|
13972
|
+
`${finding6.ruleId} ${finding6.title}`,
|
|
13973
|
+
`severity: ${finding6.severity}`,
|
|
13974
|
+
`confidence: ${finding6.confidence}`,
|
|
13975
|
+
`location: ${location4}`
|
|
13976
|
+
];
|
|
13977
|
+
const context = contextLines(fileText, finding6.line);
|
|
13978
|
+
if (context.length > 0) {
|
|
13979
|
+
lines.push("context:");
|
|
13980
|
+
lines.push(...context.map((row) => ` ${row}`));
|
|
13981
|
+
}
|
|
13982
|
+
if (finding6.snippet !== void 0) lines.push(`snippet: ${finding6.snippet}`);
|
|
13983
|
+
if (finding6.trace !== void 0 && finding6.trace.length > 0) {
|
|
13984
|
+
lines.push(
|
|
13985
|
+
`trace: ${finding6.trace.map((step) => `${step.file}${step.line !== void 0 ? `:${step.line}` : ""} ${step.note}`).join(" \u2192 ")}`
|
|
13986
|
+
);
|
|
13987
|
+
}
|
|
13988
|
+
if (finding6.reachability !== void 0) {
|
|
13989
|
+
const path = finding6.dependencyPath !== void 0 ? ` (${finding6.dependencyPath.join(" > ")})` : "";
|
|
13990
|
+
lines.push(`reachability: ${finding6.reachability}${path}`);
|
|
13991
|
+
}
|
|
13992
|
+
if (finding6.fix !== void 0) lines.push(`fix: ${finding6.fix.diff}`);
|
|
13993
|
+
lines.push(`docs: rules/${finding6.ruleId}.md`);
|
|
13994
|
+
return lines.join("\n");
|
|
13995
|
+
}
|
|
13996
|
+
async function explainFinding(options) {
|
|
13997
|
+
const finding6 = matchFinding(options.findings, options.id);
|
|
13998
|
+
let text = "";
|
|
13999
|
+
if (finding6.file !== void 0) {
|
|
14000
|
+
try {
|
|
14001
|
+
text = await readFile(join(options.cwd, finding6.file), "utf8");
|
|
14002
|
+
} catch {
|
|
14003
|
+
text = "";
|
|
14004
|
+
}
|
|
14005
|
+
}
|
|
14006
|
+
return renderExplain(finding6, text);
|
|
14007
|
+
}
|
|
14008
|
+
|
|
14009
|
+
// src/cli/program.ts
|
|
14010
|
+
init_osv();
|
|
14011
|
+
init_semver();
|
|
14012
|
+
|
|
12376
14013
|
// src/sbom/index.ts
|
|
12377
14014
|
init_engine2();
|
|
12378
14015
|
init_lockfile();
|
|
12379
14016
|
init_pins();
|
|
12380
14017
|
init_cyclonedx();
|
|
12381
14018
|
var LOCKFILE_NAMES2 = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
12382
|
-
async function buildSbom(cwd) {
|
|
14019
|
+
async function buildSbom(cwd, options = {}) {
|
|
12383
14020
|
const result = await scan({ cwd, modules: ["deps"] });
|
|
12384
14021
|
let name = "app";
|
|
12385
14022
|
let version = "0.0.0";
|
|
@@ -12405,7 +14042,13 @@ async function buildSbom(cwd) {
|
|
|
12405
14042
|
if (components.length === 0) {
|
|
12406
14043
|
components = exactPinsFromManifest(manifest).map((p) => ({ name: p.name, version: p.version }));
|
|
12407
14044
|
}
|
|
12408
|
-
return renderCycloneDx({
|
|
14045
|
+
return renderCycloneDx({
|
|
14046
|
+
name,
|
|
14047
|
+
version,
|
|
14048
|
+
components,
|
|
14049
|
+
findings: result.findings,
|
|
14050
|
+
includeReach: options.includeReach === true
|
|
14051
|
+
});
|
|
12409
14052
|
}
|
|
12410
14053
|
|
|
12411
14054
|
// src/cli/program.ts
|
|
@@ -13045,7 +14688,7 @@ function stateForGrace(sshd) {
|
|
|
13045
14688
|
|
|
13046
14689
|
// src/cli/commands/doctor.ts
|
|
13047
14690
|
init_config2();
|
|
13048
|
-
var MIN_NODE = [
|
|
14691
|
+
var MIN_NODE = [22, 13, 0];
|
|
13049
14692
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
13050
14693
|
var FLAGS = [
|
|
13051
14694
|
"FULLSHIELD_EXPERIMENTAL_SAST_GAPS",
|
|
@@ -13076,7 +14719,7 @@ function nodeOk(version) {
|
|
|
13076
14719
|
async function runDoctor(cwd, options = {}) {
|
|
13077
14720
|
const lines = [];
|
|
13078
14721
|
const version = process.versions.node;
|
|
13079
|
-
lines.push(`node: ${version} ${nodeOk(version) ? "ok" : "too old (need >=
|
|
14722
|
+
lines.push(`node: ${version} ${nodeOk(version) ? "ok" : "too old (need >= 22.13.0)"}`);
|
|
13080
14723
|
const locks = LOCKFILES.filter((name) => existsSync(join(cwd, name)));
|
|
13081
14724
|
lines.push(locks.length > 0 ? `lockfile: ${locks.join(", ")}` : "lockfile: none");
|
|
13082
14725
|
const cacheDir = join(homedir(), ".cache", "fullshield", "osv");
|
|
@@ -13132,6 +14775,21 @@ var KNOWN = [
|
|
|
13132
14775
|
test: /FULLSHIELD_EXPERIMENTAL_DIFF|scan JSON|cannot read diff/,
|
|
13133
14776
|
code: "FS-E006",
|
|
13134
14777
|
hint: "Set FULLSHIELD_EXPERIMENTAL_DIFF=1 or pass --experimental-diff. Both inputs must be scan --json files."
|
|
14778
|
+
},
|
|
14779
|
+
{
|
|
14780
|
+
test: /FULLSHIELD_EXPERIMENTAL_EXPLAIN|id prefix|no finding matches|ambiguous id/,
|
|
14781
|
+
code: "FS-E007",
|
|
14782
|
+
hint: "Set FULLSHIELD_EXPERIMENTAL_EXPLAIN=1. Pass an id prefix of at least 6 characters from a scan JSON file."
|
|
14783
|
+
},
|
|
14784
|
+
{
|
|
14785
|
+
test: /FULLSHIELD_EXPERIMENTAL_DEPS_DIFF|deps-diff cannot/,
|
|
14786
|
+
code: "FS-E008",
|
|
14787
|
+
hint: "Set FULLSHIELD_EXPERIMENTAL_DEPS_DIFF=1. Pass a git ref that contains a lockfile."
|
|
14788
|
+
},
|
|
14789
|
+
{
|
|
14790
|
+
test: /FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS|sast\.sources|sast\.sanitizers/,
|
|
14791
|
+
code: "FS-E009",
|
|
14792
|
+
hint: "Set FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS=1 before naming sast.sources or sast.sanitizers."
|
|
13135
14793
|
}
|
|
13136
14794
|
];
|
|
13137
14795
|
var DOCS = "https://github.com/kathanp/fullshield/blob/main/docs/errors.md";
|
|
@@ -13183,7 +14841,7 @@ function parseFailOn(value) {
|
|
|
13183
14841
|
function createProgram(io = defaultIO) {
|
|
13184
14842
|
const program = new Command();
|
|
13185
14843
|
program.name("fullshield").description("Full-cycle security scanner for Node.js/TypeScript backends").version(packageVersion()).option("--debug", "print stack traces on CLI errors");
|
|
13186
|
-
program.command("scan").description("Scan a project directory for security findings").argument("[path]", "directory to scan", ".").option("--json", "machine-readable JSON output").option("--fail-on <severity>", "exit 1 if any finding is at or above this severity").option("--modules <modules>", "comma-separated subset of modules to run").option("--staged", "scan only files staged in git (pre-commit mode)").option("--offline", "use the OSV cache only (no network)").option("--offline-safe", "alias of --offline").option("--config <path>", "path to fullshield.config.json").option("--sarif <file>", "write a SARIF 2.1.0 report to this path").option("--markdown <file>", "write a Markdown report to this path").option("--html <file>", "write an HTML report to this path").option(
|
|
14844
|
+
program.command("scan").description("Scan a project directory for security findings").argument("[path]", "directory to scan", ".").option("--json", "machine-readable JSON output").option("--all", "print every finding in the console, with no 20-row cap").option("--fail-on <severity>", "exit 1 if any finding is at or above this severity").option("--modules <modules>", "comma-separated subset of modules to run").option("--staged", "scan only files staged in git (pre-commit mode)").option("--offline", "use the OSV cache only (no network)").option("--offline-safe", "alias of --offline").option("--config <path>", "path to fullshield.config.json").option("--sarif <file>", "write a SARIF 2.1.0 report to this path").option("--markdown <file>", "write a Markdown report to this path").option("--html <file>", "write an HTML report to this path").option(
|
|
13187
14845
|
"--experimental-ci-reports",
|
|
13188
14846
|
"experimental: GitHub annotations, JUnit, and GitLab Code Quality (FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1)"
|
|
13189
14847
|
).option(
|
|
@@ -13217,6 +14875,27 @@ function createProgram(io = defaultIO) {
|
|
|
13217
14875
|
"--experimental-vex",
|
|
13218
14876
|
"experimental: worm/workflow indicators, overrides, provenance, VEX (FULLSHIELD_EXPERIMENTAL_VEX=1)"
|
|
13219
14877
|
).option(
|
|
14878
|
+
"--experimental-reach-evidence",
|
|
14879
|
+
"experimental: record why a dependency reachability class was chosen (FULLSHIELD_EXPERIMENTAL_REACH_EVIDENCE=1)"
|
|
14880
|
+
).option(
|
|
14881
|
+
"--experimental-lockfiles",
|
|
14882
|
+
"experimental: yarn berry, pnpm v9, and bun.lock (FULLSHIELD_EXPERIMENTAL_LOCKFILES=1)"
|
|
14883
|
+
).option(
|
|
14884
|
+
"--experimental-supply",
|
|
14885
|
+
"experimental: supply-chain campaign heuristics (FULLSHIELD_EXPERIMENTAL_SUPPLY=1)"
|
|
14886
|
+
).option(
|
|
14887
|
+
"--experimental-workspaces",
|
|
14888
|
+
"experimental: scan each workspace lockfile (FULLSHIELD_EXPERIMENTAL_WORKSPACES=1)"
|
|
14889
|
+
).option(
|
|
14890
|
+
"--experimental-sarif-v2",
|
|
14891
|
+
"experimental: extra SARIF fields (FULLSHIELD_EXPERIMENTAL_SARIF_V2=1)"
|
|
14892
|
+
).option(
|
|
14893
|
+
"--experimental-custom-sanitizers",
|
|
14894
|
+
"experimental: config-declared sources and sanitizers (FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS=1)"
|
|
14895
|
+
).option(
|
|
14896
|
+
"--experimental-validation",
|
|
14897
|
+
"experimental: route schema validation inference (FULLSHIELD_EXPERIMENTAL_VALIDATION=1)"
|
|
14898
|
+
).option("--taint-summaries", "cache per-function taint summaries (FULLSHIELD_TAINT_SUMMARIES=1)").option(
|
|
13220
14899
|
"--experimental-mcpcfg",
|
|
13221
14900
|
"experimental: MCP client/server config rules (FULLSHIELD_EXPERIMENTAL_MCPCFG=1)"
|
|
13222
14901
|
).option(
|
|
@@ -13245,6 +14924,30 @@ function createProgram(io = defaultIO) {
|
|
|
13245
14924
|
if (opts.experimentalVex === true) {
|
|
13246
14925
|
process.env.FULLSHIELD_EXPERIMENTAL_VEX = "1";
|
|
13247
14926
|
}
|
|
14927
|
+
if (opts.experimentalReachEvidence === true) {
|
|
14928
|
+
process.env.FULLSHIELD_EXPERIMENTAL_REACH_EVIDENCE = "1";
|
|
14929
|
+
}
|
|
14930
|
+
if (opts.experimentalLockfiles === true) {
|
|
14931
|
+
process.env.FULLSHIELD_EXPERIMENTAL_LOCKFILES = "1";
|
|
14932
|
+
}
|
|
14933
|
+
if (opts.experimentalSupply === true) {
|
|
14934
|
+
process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY = "1";
|
|
14935
|
+
}
|
|
14936
|
+
if (opts.experimentalWorkspaces === true) {
|
|
14937
|
+
process.env.FULLSHIELD_EXPERIMENTAL_WORKSPACES = "1";
|
|
14938
|
+
}
|
|
14939
|
+
if (opts.experimentalSarifV2 === true) {
|
|
14940
|
+
process.env.FULLSHIELD_EXPERIMENTAL_SARIF_V2 = "1";
|
|
14941
|
+
}
|
|
14942
|
+
if (opts.experimentalCustomSanitizers === true) {
|
|
14943
|
+
process.env.FULLSHIELD_EXPERIMENTAL_CUSTOM_SANITIZERS = "1";
|
|
14944
|
+
}
|
|
14945
|
+
if (opts.experimentalValidation === true) {
|
|
14946
|
+
process.env.FULLSHIELD_EXPERIMENTAL_VALIDATION = "1";
|
|
14947
|
+
}
|
|
14948
|
+
if (opts.taintSummaries === true) {
|
|
14949
|
+
process.env.FULLSHIELD_TAINT_SUMMARIES = "1";
|
|
14950
|
+
}
|
|
13248
14951
|
if (opts.experimentalMcpcfg === true) {
|
|
13249
14952
|
process.env.FULLSHIELD_EXPERIMENTAL_MCPCFG = "1";
|
|
13250
14953
|
}
|
|
@@ -13309,7 +15012,9 @@ function createProgram(io = defaultIO) {
|
|
|
13309
15012
|
if (opts.prSummary === true) {
|
|
13310
15013
|
io.out(renderPrSummary(result.findings));
|
|
13311
15014
|
} else {
|
|
13312
|
-
io.out(
|
|
15015
|
+
io.out(
|
|
15016
|
+
opts.json === true ? renderJsonReport(result) : renderConsoleReport(result, { all: opts.all === true })
|
|
15017
|
+
);
|
|
13313
15018
|
}
|
|
13314
15019
|
if (result.policy !== void 0 && result.policy.errors.length > 0) {
|
|
13315
15020
|
for (const message of result.policy.errors) {
|
|
@@ -13424,7 +15129,9 @@ function createProgram(io = defaultIO) {
|
|
|
13424
15129
|
if (opts.format !== void 0 && opts.format !== "cyclonedx") {
|
|
13425
15130
|
throw new Error(`unsupported SBOM format "${opts.format}" (valid: cyclonedx)`);
|
|
13426
15131
|
}
|
|
13427
|
-
const doc = await buildSbom(resolve(path)
|
|
15132
|
+
const doc = await buildSbom(resolve(path), {
|
|
15133
|
+
includeReach: process.env.FULLSHIELD_EXPERIMENTAL_SBOM_REACH === "1"
|
|
15134
|
+
});
|
|
13428
15135
|
const errors = validateCycloneDx15(doc);
|
|
13429
15136
|
if (errors.length > 0) throw new Error(`invalid SBOM: ${errors.join("; ")}`);
|
|
13430
15137
|
const json = JSON.stringify(doc, null, 2);
|
|
@@ -13578,6 +15285,87 @@ function createProgram(io = defaultIO) {
|
|
|
13578
15285
|
process.exitCode = 2;
|
|
13579
15286
|
}
|
|
13580
15287
|
});
|
|
15288
|
+
program.command("explain").description("Print one finding from a scan JSON file (requires FULLSHIELD_EXPERIMENTAL_EXPLAIN=1)").argument("<id>", "finding id or a prefix of at least 6 characters").option("--from <file>", "scan --json file").action(async (id, opts) => {
|
|
15289
|
+
try {
|
|
15290
|
+
if (process.env.FULLSHIELD_EXPERIMENTAL_EXPLAIN !== "1") {
|
|
15291
|
+
throw new Error("explain requires FULLSHIELD_EXPERIMENTAL_EXPLAIN=1");
|
|
15292
|
+
}
|
|
15293
|
+
if (opts.from === void 0) throw new Error("explain needs --from <scan.json> (FULLSHIELD_EXPERIMENTAL_EXPLAIN)");
|
|
15294
|
+
const raw = JSON.parse(await readFile(opts.from, "utf8"));
|
|
15295
|
+
if (!Array.isArray(raw.findings)) throw new Error("explain --from is not scan JSON (FULLSHIELD_EXPERIMENTAL_EXPLAIN)");
|
|
15296
|
+
const text = await explainFinding({
|
|
15297
|
+
cwd: process.cwd(),
|
|
15298
|
+
id,
|
|
15299
|
+
findings: raw.findings
|
|
15300
|
+
});
|
|
15301
|
+
io.out(text);
|
|
15302
|
+
} catch (error) {
|
|
15303
|
+
io.err(formatCliError(error));
|
|
15304
|
+
process.exitCode = 2;
|
|
15305
|
+
}
|
|
15306
|
+
});
|
|
15307
|
+
program.command("deps-diff").description("Compare a lockfile at a git ref with the working tree (requires FULLSHIELD_EXPERIMENTAL_DEPS_DIFF=1)").argument("<ref>", "git ref to read with git show").argument("[path]", "project directory", ".").option("--json", "print JSON").option("--fail-on <severity>", "exit 1 when an introduced advisory is at or above this severity").option("--offline", "use the OSV cache only").action(async (ref, path, opts) => {
|
|
15308
|
+
try {
|
|
15309
|
+
if (process.env.FULLSHIELD_EXPERIMENTAL_DEPS_DIFF !== "1") {
|
|
15310
|
+
throw new Error("deps-diff requires FULLSHIELD_EXPERIMENTAL_DEPS_DIFF=1");
|
|
15311
|
+
}
|
|
15312
|
+
const cwd = resolve(path);
|
|
15313
|
+
const previous = await lockAtRef(cwd, ref);
|
|
15314
|
+
let currentText = "";
|
|
15315
|
+
try {
|
|
15316
|
+
currentText = await readFile(join(cwd, previous.file), "utf8");
|
|
15317
|
+
} catch {
|
|
15318
|
+
currentText = "";
|
|
15319
|
+
}
|
|
15320
|
+
const before = packagesFromLock(previous.file, previous.text);
|
|
15321
|
+
const after = packagesFromLock(previous.file, currentText);
|
|
15322
|
+
const changes = diffLockedPackages(before.packages, after.packages, after.direct);
|
|
15323
|
+
const moved = after.packages.filter(
|
|
15324
|
+
(pkg) => changes.some((change) => change.name === pkg.name && change.change !== "removed" && change.newVersion === pkg.version)
|
|
15325
|
+
);
|
|
15326
|
+
const hits = await queryOsv(moved, {
|
|
15327
|
+
cacheDir: join(cwd, "osv-cache"),
|
|
15328
|
+
offline: opts.offline === true
|
|
15329
|
+
});
|
|
15330
|
+
const introduced = hits.flatMap(
|
|
15331
|
+
(hit2) => hit2.vulns.map((vuln) => ({
|
|
15332
|
+
name: hit2.pkg.name,
|
|
15333
|
+
version: hit2.pkg.version,
|
|
15334
|
+
id: vuln.id,
|
|
15335
|
+
severity: osvSeverity(vuln)
|
|
15336
|
+
}))
|
|
15337
|
+
);
|
|
15338
|
+
const added = changes.filter((change) => change.change === "added");
|
|
15339
|
+
const ages = [];
|
|
15340
|
+
for (const change of added) {
|
|
15341
|
+
let published;
|
|
15342
|
+
let installScripts = "unknown";
|
|
15343
|
+
try {
|
|
15344
|
+
const raw = JSON.parse(await readFile(join(cwd, "osv-cache", "registry", `${change.name}.json`), "utf8"));
|
|
15345
|
+
published = raw.time;
|
|
15346
|
+
if (raw.hasInstallScript === true) installScripts = "yes";
|
|
15347
|
+
if (raw.hasInstallScript === false) installScripts = "no";
|
|
15348
|
+
} catch {
|
|
15349
|
+
published = void 0;
|
|
15350
|
+
}
|
|
15351
|
+
ages.push({
|
|
15352
|
+
name: change.name,
|
|
15353
|
+
version: change.newVersion,
|
|
15354
|
+
age: releaseAgeLabel(published, Date.now()),
|
|
15355
|
+
installScripts
|
|
15356
|
+
});
|
|
15357
|
+
}
|
|
15358
|
+
const report = { changes, introduced, ages };
|
|
15359
|
+
io.out(JSON.stringify(report, null, 2));
|
|
15360
|
+
if (opts.failOn !== void 0) {
|
|
15361
|
+
const threshold = parseFailOn(opts.failOn);
|
|
15362
|
+
if (introduced.some((item) => meetsThreshold(item.severity, threshold))) process.exitCode = 1;
|
|
15363
|
+
}
|
|
15364
|
+
} catch (error) {
|
|
15365
|
+
io.err(formatCliError(error));
|
|
15366
|
+
process.exitCode = 2;
|
|
15367
|
+
}
|
|
15368
|
+
});
|
|
13581
15369
|
program.command("confirm").description("Experimental exploit confirm (localhost/127.0.0.1/FullShield container only)").argument("<url>", "target base URL").action(async (url) => {
|
|
13582
15370
|
try {
|
|
13583
15371
|
if (process.env.FULLSHIELD_EXPERIMENTAL_CONFIRM !== "1") {
|
|
@@ -13713,8 +15501,8 @@ function createProgram(io = defaultIO) {
|
|
|
13713
15501
|
let name = "app";
|
|
13714
15502
|
let version = "0.0.0";
|
|
13715
15503
|
try {
|
|
13716
|
-
const { readFile:
|
|
13717
|
-
const pkg = JSON.parse(await
|
|
15504
|
+
const { readFile: readFile22 } = await import('fs/promises');
|
|
15505
|
+
const pkg = JSON.parse(await readFile22(join(cwd, "package.json"), "utf8"));
|
|
13718
15506
|
if (typeof pkg.name === "string") name = pkg.name;
|
|
13719
15507
|
if (typeof pkg.version === "string") version = pkg.version;
|
|
13720
15508
|
} catch {
|