fullshield 0.2.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +99 -0
- package/README.md +10 -5
- package/dist/agent.js +1966 -244
- package/dist/cli/index.js +3159 -381
- package/dist/compliance.js +9 -0
- package/dist/dashboard.js +18 -1
- package/dist/iac.js +48 -0
- package/dist/index.d.ts +121 -7
- package/dist/index.js +2557 -280
- package/dist/mcp.js +1977 -255
- package/dist/supply.js +13 -3
- package/dist/vex.js +4 -2
- package/package.json +1 -1
- package/rules/README.md +2 -0
- package/rules/SAST-SKIP-001.md +9 -0
- package/rules/SUPPLY-CHAINC2-001.md +3 -0
- package/rules/SUPPLY-PLATFORMGATE-001.md +3 -0
- package/rules/SUPPLY-SECHOLDER-001.md +3 -0
- package/rules/SUPPLY-STEALER-001.md +3 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
-
import { writeFile,
|
|
3
|
+
import { writeFile, readFile, mkdir, stat, mkdtemp, unlink, readdir } from 'fs/promises';
|
|
4
4
|
import { resolve, join, dirname, relative, basename, posix } from 'path';
|
|
5
5
|
import { readFileSync, existsSync, readdirSync, cpSync, mkdirSync, rmSync, statSync, writeFileSync, mkdtempSync, unlinkSync } from 'fs';
|
|
6
6
|
import { homedir, tmpdir, availableParallelism } from 'os';
|
|
@@ -113,7 +113,8 @@ function walk(node, visit) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
function attachParents(node, parent) {
|
|
116
|
-
|
|
116
|
+
const current = node;
|
|
117
|
+
if (parent !== void 0 || current.parent === void 0) current.parent = parent;
|
|
117
118
|
for (const key of Object.keys(node)) {
|
|
118
119
|
if (SKIP_KEYS.has(String(key))) continue;
|
|
119
120
|
const value = node[key];
|
|
@@ -192,8 +193,18 @@ function mcpCfgEnabled() {
|
|
|
192
193
|
function taintV2Enabled() {
|
|
193
194
|
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
194
195
|
}
|
|
196
|
+
function licenseEnabled() {
|
|
197
|
+
return process.env[LICENSE_FLAG] === "1";
|
|
198
|
+
}
|
|
199
|
+
function diffEnabled() {
|
|
200
|
+
return process.env[DIFF_FLAG] === "1";
|
|
201
|
+
}
|
|
202
|
+
var CI_REPORTS_FLAG, DIFF_FLAG, LICENSE_FLAG;
|
|
195
203
|
var init_experimental = __esm({
|
|
196
204
|
"src/modules/sast/experimental.ts"() {
|
|
205
|
+
CI_REPORTS_FLAG = "FULLSHIELD_EXPERIMENTAL_CI_REPORTS";
|
|
206
|
+
DIFF_FLAG = "FULLSHIELD_EXPERIMENTAL_DIFF";
|
|
207
|
+
LICENSE_FLAG = "FULLSHIELD_EXPERIMENTAL_LICENSE";
|
|
197
208
|
}
|
|
198
209
|
});
|
|
199
210
|
function isSeverity(value) {
|
|
@@ -634,6 +645,173 @@ var init_config = __esm({
|
|
|
634
645
|
}
|
|
635
646
|
});
|
|
636
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
|
+
|
|
637
815
|
// src/modules/deps/lockfile.ts
|
|
638
816
|
function basename2(filename) {
|
|
639
817
|
const parts = filename.replace(/\\/g, "/").split("/");
|
|
@@ -663,7 +841,7 @@ function objectKeys(value) {
|
|
|
663
841
|
}
|
|
664
842
|
return Object.keys(value);
|
|
665
843
|
}
|
|
666
|
-
function
|
|
844
|
+
function addEdge2(edges, from, to) {
|
|
667
845
|
const set = edges.get(from) ?? /* @__PURE__ */ new Set();
|
|
668
846
|
set.add(to);
|
|
669
847
|
edges.set(from, set);
|
|
@@ -743,7 +921,7 @@ function parseYarnGraph(contents) {
|
|
|
743
921
|
const depBerry = /^\s{4}(?:"([^"]+)"|(@?[\w./-]+)):/.exec(raw);
|
|
744
922
|
const depName = depClassic?.[1] ?? depClassic?.[2] ?? depBerry?.[1] ?? depBerry?.[2];
|
|
745
923
|
if (depName !== void 0) {
|
|
746
|
-
|
|
924
|
+
addEdge2(edges, currentName, depName);
|
|
747
925
|
continue;
|
|
748
926
|
}
|
|
749
927
|
if (/^\s{2}\S/.test(raw)) inDeps = false;
|
|
@@ -837,7 +1015,7 @@ function parsePnpmGraph(contents) {
|
|
|
837
1015
|
const dep = /^ (?:'([^']+)'|"([^"]+)"|([^:]+)):/.exec(raw);
|
|
838
1016
|
const depName = dep?.[1] ?? dep?.[2] ?? dep?.[3];
|
|
839
1017
|
if (depName !== void 0) {
|
|
840
|
-
|
|
1018
|
+
addEdge2(edges, currentPkg, depName);
|
|
841
1019
|
continue;
|
|
842
1020
|
}
|
|
843
1021
|
if (/^ \S/.test(raw)) inPkgDeps = false;
|
|
@@ -872,7 +1050,7 @@ function graphFromPackagesTable(packages) {
|
|
|
872
1050
|
const name = key === "" ? "" : nameFromPackagesKey(key);
|
|
873
1051
|
if (name === void 0 || name.length === 0) continue;
|
|
874
1052
|
for (const dep of [...objectKeys(value.dependencies), ...objectKeys(value.optionalDependencies)]) {
|
|
875
|
-
|
|
1053
|
+
addEdge2(edges, name, dep);
|
|
876
1054
|
}
|
|
877
1055
|
}
|
|
878
1056
|
return { edges, directProd, directDev };
|
|
@@ -882,7 +1060,7 @@ function graphFromDependenciesTree(deps, edges) {
|
|
|
882
1060
|
const child = value.dependencies;
|
|
883
1061
|
if (child === void 0 || typeof child !== "object" || child === null) continue;
|
|
884
1062
|
const nested = child;
|
|
885
|
-
for (const childName of Object.keys(nested))
|
|
1063
|
+
for (const childName of Object.keys(nested)) addEdge2(edges, name, childName);
|
|
886
1064
|
graphFromDependenciesTree(nested, edges);
|
|
887
1065
|
}
|
|
888
1066
|
}
|
|
@@ -895,8 +1073,16 @@ function parseLockfileGraph(filename, contents) {
|
|
|
895
1073
|
directDev: /* @__PURE__ */ new Set(),
|
|
896
1074
|
edgesKnown: false
|
|
897
1075
|
};
|
|
898
|
-
|
|
899
|
-
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);
|
|
900
1086
|
if (name !== "package-lock.json") return empty;
|
|
901
1087
|
try {
|
|
902
1088
|
const json = JSON.parse(contents);
|
|
@@ -923,6 +1109,7 @@ function parseLockfileGraph(filename, contents) {
|
|
|
923
1109
|
var PNPM_KEY;
|
|
924
1110
|
var init_lockfile = __esm({
|
|
925
1111
|
"src/modules/deps/lockfile.ts"() {
|
|
1112
|
+
init_lockfile_modern();
|
|
926
1113
|
PNPM_KEY = /^ {2}['"]?(?:\/)?(@?[^'"@\s/]+(?:\/[^'"@\s/]+)?)@([^'"(\s:]+)(?:\([^)]*\))?['"]?:\s*$/;
|
|
927
1114
|
}
|
|
928
1115
|
});
|
|
@@ -1081,6 +1268,91 @@ var init_pins = __esm({
|
|
|
1081
1268
|
}
|
|
1082
1269
|
});
|
|
1083
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
|
+
|
|
1084
1356
|
// src/modules/deps/symbol-reachability.ts
|
|
1085
1357
|
function affectedFunctionNames(vuln) {
|
|
1086
1358
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -1189,7 +1461,7 @@ function packageNameFromSnippet(snippet) {
|
|
|
1189
1461
|
const at = snippet.indexOf("@");
|
|
1190
1462
|
return at <= 0 ? snippet : snippet.slice(0, at);
|
|
1191
1463
|
}
|
|
1192
|
-
function
|
|
1464
|
+
function packageNameFromSpec2(spec) {
|
|
1193
1465
|
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:")) return void 0;
|
|
1194
1466
|
if (spec.startsWith("@")) {
|
|
1195
1467
|
const parts = spec.split("/");
|
|
@@ -1225,7 +1497,7 @@ function indexPackageUsage(sources) {
|
|
|
1225
1497
|
const local = m[1];
|
|
1226
1498
|
const spec = m[2];
|
|
1227
1499
|
if (local === void 0 || spec === void 0) continue;
|
|
1228
|
-
const pkg =
|
|
1500
|
+
const pkg = packageNameFromSpec2(spec);
|
|
1229
1501
|
if (pkg === void 0) continue;
|
|
1230
1502
|
bump(pkg, localIsCalled(source, local));
|
|
1231
1503
|
}
|
|
@@ -1235,7 +1507,7 @@ function indexPackageUsage(sources) {
|
|
|
1235
1507
|
while ((named = namedRe.exec(source)) !== null) {
|
|
1236
1508
|
const spec = named[2];
|
|
1237
1509
|
if (spec === void 0) continue;
|
|
1238
|
-
const pkg =
|
|
1510
|
+
const pkg = packageNameFromSpec2(spec);
|
|
1239
1511
|
if (pkg === void 0) continue;
|
|
1240
1512
|
const locals = (named[1] ?? "").split(",").map((s) => s.trim().split(/\s+as\s+/).pop() ?? "");
|
|
1241
1513
|
bump(
|
|
@@ -1267,37 +1539,82 @@ function shortestDependencyPath(target, starts, edges) {
|
|
|
1267
1539
|
}
|
|
1268
1540
|
return void 0;
|
|
1269
1541
|
}
|
|
1542
|
+
function collectDependencyPaths(target, starts, edges, limit = 8) {
|
|
1543
|
+
const out = [];
|
|
1544
|
+
const queue = [];
|
|
1545
|
+
let steps = 0;
|
|
1546
|
+
for (const start of starts) queue.push([start]);
|
|
1547
|
+
while (queue.length > 0 && out.length < limit && steps < 4e3) {
|
|
1548
|
+
steps += 1;
|
|
1549
|
+
const path = queue.shift();
|
|
1550
|
+
if (path === void 0) continue;
|
|
1551
|
+
const last = path[path.length - 1];
|
|
1552
|
+
if (last === void 0) continue;
|
|
1553
|
+
if (last === target) {
|
|
1554
|
+
if (path.length >= 2) out.push(path);
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
if (path.length >= 12) continue;
|
|
1558
|
+
for (const next of edges.get(last) ?? []) {
|
|
1559
|
+
if (path.includes(next)) continue;
|
|
1560
|
+
queue.push([...path, next]);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
return out;
|
|
1564
|
+
}
|
|
1270
1565
|
function reachableFrom(target, roots, edges) {
|
|
1271
1566
|
if (roots.has(target)) return true;
|
|
1272
1567
|
return shortestDependencyPath(target, roots, edges) !== void 0;
|
|
1273
1568
|
}
|
|
1274
|
-
function importedDirectNames(usage,
|
|
1275
|
-
const directs = /* @__PURE__ */ new Set([...
|
|
1569
|
+
function importedDirectNames(usage, graph2) {
|
|
1570
|
+
const directs = /* @__PURE__ */ new Set([...graph2.directProd, ...graph2.directDev]);
|
|
1276
1571
|
const out = /* @__PURE__ */ new Set();
|
|
1277
|
-
for (const [name,
|
|
1278
|
-
if (!
|
|
1572
|
+
for (const [name, hit2] of usage) {
|
|
1573
|
+
if (!hit2.imported) continue;
|
|
1279
1574
|
if (directs.size === 0 || directs.has(name)) out.add(name);
|
|
1280
1575
|
}
|
|
1281
1576
|
return out;
|
|
1282
1577
|
}
|
|
1283
|
-
function classifyReachability(pkgName, productionUsage,
|
|
1284
|
-
const
|
|
1285
|
-
if (
|
|
1286
|
-
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" };
|
|
1287
1582
|
}
|
|
1288
|
-
if (
|
|
1289
|
-
const roots = importedDirectNames(productionUsage,
|
|
1290
|
-
const
|
|
1583
|
+
if (graph2 !== void 0 && graph2.edgesKnown) {
|
|
1584
|
+
const roots = importedDirectNames(productionUsage, graph2);
|
|
1585
|
+
const paths = collectDependencyPaths(pkgName, roots, graph2.edges);
|
|
1586
|
+
const path = paths[0];
|
|
1291
1587
|
if (path !== void 0 && path.length >= 2) {
|
|
1292
|
-
return { reachability: "reachable-via-dependency", path };
|
|
1588
|
+
return { reachability: "reachable-via-dependency", path, paths };
|
|
1293
1589
|
}
|
|
1294
1590
|
return { reachability: "not-imported" };
|
|
1295
1591
|
}
|
|
1296
|
-
if (
|
|
1592
|
+
if (graph2 !== void 0 && !graph2.edgesKnown) {
|
|
1297
1593
|
return { reachability: "reachability-unknown" };
|
|
1298
1594
|
}
|
|
1299
1595
|
return { reachability: "not-imported" };
|
|
1300
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
|
+
}
|
|
1301
1618
|
function reachabilitySteps(reachability) {
|
|
1302
1619
|
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
|
|
1303
1620
|
return 0;
|
|
@@ -1319,8 +1636,99 @@ function isDevOnly(pkgName, ctx) {
|
|
|
1319
1636
|
if (inProd) return false;
|
|
1320
1637
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
1321
1638
|
}
|
|
1639
|
+
function versionFromSnippet(snippet) {
|
|
1640
|
+
const name = packageNameFromSnippet(snippet);
|
|
1641
|
+
if (name === void 0) return snippet;
|
|
1642
|
+
return snippet.slice(name.length + 1);
|
|
1643
|
+
}
|
|
1644
|
+
function pathFields(classified) {
|
|
1645
|
+
const paths = classified.paths ?? (classified.path !== void 0 ? [classified.path] : []);
|
|
1646
|
+
if (paths.length === 0) return {};
|
|
1647
|
+
return {
|
|
1648
|
+
dependencyPath: paths[0],
|
|
1649
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
function appendOtherPaths(message, paths) {
|
|
1653
|
+
if (paths === void 0 || paths.length < 2) return message;
|
|
1654
|
+
const rest = paths.slice(1).map((p) => p.join(" > ")).join("; ");
|
|
1655
|
+
return `${message} Other paths: ${rest}.`;
|
|
1656
|
+
}
|
|
1657
|
+
function groupPackageAdvisories(findings) {
|
|
1658
|
+
const passthrough = [];
|
|
1659
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1660
|
+
for (const finding6 of findings) {
|
|
1661
|
+
if (finding6.ruleId !== "DEPS-VULN-001" && finding6.ruleId !== "DEPS-MALICIOUS-001") {
|
|
1662
|
+
passthrough.push(finding6);
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
const name = packageNameFromSnippet(finding6.snippet ?? "") ?? finding6.snippet ?? finding6.id;
|
|
1666
|
+
const advisory = finding6.references?.[0] ?? finding6.id;
|
|
1667
|
+
const key = `${finding6.ruleId}\0${name}\0${advisory}`;
|
|
1668
|
+
const list = groups.get(key);
|
|
1669
|
+
if (list === void 0) groups.set(key, [finding6]);
|
|
1670
|
+
else list.push(finding6);
|
|
1671
|
+
}
|
|
1672
|
+
const merged = [];
|
|
1673
|
+
for (const list of groups.values()) {
|
|
1674
|
+
const only = list[0];
|
|
1675
|
+
if (list.length === 1 && only !== void 0) {
|
|
1676
|
+
merged.push(only);
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
merged.push(mergeAdvisory(list));
|
|
1680
|
+
}
|
|
1681
|
+
return [...passthrough, ...merged];
|
|
1682
|
+
}
|
|
1683
|
+
function mergeAdvisory(list) {
|
|
1684
|
+
const ranked = [...list].sort(
|
|
1685
|
+
(a, b) => severityRank(b.severity) - severityRank(a.severity) || (REACH_RANK[a.reachability ?? "not-imported"] ?? 9) - (REACH_RANK[b.reachability ?? "not-imported"] ?? 9)
|
|
1686
|
+
);
|
|
1687
|
+
const primary = ranked[0];
|
|
1688
|
+
if (primary === void 0) {
|
|
1689
|
+
throw new Error("empty advisory group");
|
|
1690
|
+
}
|
|
1691
|
+
const name = packageNameFromSnippet(primary.snippet ?? "") ?? "package";
|
|
1692
|
+
const versions = [...new Set(list.map((f) => versionFromSnippet(f.snippet ?? "")))].filter((v) => v.length > 0).sort();
|
|
1693
|
+
const paths = [];
|
|
1694
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
1695
|
+
for (const finding6 of list) {
|
|
1696
|
+
const candidates = finding6.dependencyPaths ?? (finding6.dependencyPath !== void 0 ? [finding6.dependencyPath] : []);
|
|
1697
|
+
for (const path of candidates) {
|
|
1698
|
+
const key = path.join("\0");
|
|
1699
|
+
if (seenPath.has(key)) continue;
|
|
1700
|
+
seenPath.add(key);
|
|
1701
|
+
paths.push(path);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
paths.sort((a, b) => a.length - b.length || a.join(">").localeCompare(b.join(">")));
|
|
1705
|
+
const advisory = primary.references?.[0] ?? "";
|
|
1706
|
+
const onManifest = list.find((f) => f.file === "package.json");
|
|
1707
|
+
const file = onManifest?.file ?? primary.file;
|
|
1708
|
+
const line = onManifest?.line ?? primary.line;
|
|
1709
|
+
const snippet = versions.length > 0 ? `${name}@${versions.join(",")}` : primary.snippet ?? name;
|
|
1710
|
+
let message = primary.message;
|
|
1711
|
+
if (versions.length > 1) {
|
|
1712
|
+
message = `${message} Versions: ${versions.join(", ")}.`;
|
|
1713
|
+
}
|
|
1714
|
+
if (paths.length > 1) {
|
|
1715
|
+
message = `${message} Dependency paths: ${paths.map((p) => p.join(" > ")).join("; ")}.`;
|
|
1716
|
+
}
|
|
1717
|
+
return {
|
|
1718
|
+
...primary,
|
|
1719
|
+
id: makeFindingId(primary.ruleId, file, `${name}|${advisory}|${versions.join(",")}`),
|
|
1720
|
+
file,
|
|
1721
|
+
line,
|
|
1722
|
+
snippet,
|
|
1723
|
+
message,
|
|
1724
|
+
severity: primary.severity,
|
|
1725
|
+
reachability: primary.reachability,
|
|
1726
|
+
...paths[0] !== void 0 ? { dependencyPath: paths[0] } : {},
|
|
1727
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1322
1730
|
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
1323
|
-
|
|
1731
|
+
const mapped = findings.map((finding6) => {
|
|
1324
1732
|
if (finding6.ruleId === "DEPS-MALICIOUS-001") {
|
|
1325
1733
|
if (finding6.snippet === void 0) return finding6;
|
|
1326
1734
|
const pkg2 = packageNameFromSnippet(finding6.snippet);
|
|
@@ -1330,7 +1738,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
1330
1738
|
...finding6,
|
|
1331
1739
|
severity: "critical",
|
|
1332
1740
|
reachability: classified2.reachability,
|
|
1333
|
-
...classified2
|
|
1741
|
+
...pathFields(classified2)
|
|
1334
1742
|
};
|
|
1335
1743
|
}
|
|
1336
1744
|
if (finding6.ruleId !== "DEPS-VULN-001" || finding6.snippet === void 0) return finding6;
|
|
@@ -1340,7 +1748,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
1340
1748
|
const named = symbols.get(finding6.id) ?? [];
|
|
1341
1749
|
if (named.length > 0 && ctx.sources !== void 0) {
|
|
1342
1750
|
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
1343
|
-
classified = { reachability: symbolClass, path: classified.path };
|
|
1751
|
+
classified = { reachability: symbolClass, path: classified.path, paths: classified.paths };
|
|
1344
1752
|
}
|
|
1345
1753
|
const steps = reachabilitySteps(classified.reachability);
|
|
1346
1754
|
let severity = steps === 0 ? finding6.severity : downgradeSeverity(finding6.severity, steps);
|
|
@@ -1348,6 +1756,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
1348
1756
|
if (classified.path !== void 0) {
|
|
1349
1757
|
const shown = classified.path.join(" > ");
|
|
1350
1758
|
message = `${message} Reachable via ${shown}.`;
|
|
1759
|
+
message = appendOtherPaths(message, classified.paths);
|
|
1351
1760
|
}
|
|
1352
1761
|
const devOnly = isDevOnly(pkg, ctx);
|
|
1353
1762
|
if (devOnly) {
|
|
@@ -1357,16 +1766,27 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
1357
1766
|
return {
|
|
1358
1767
|
...finding6,
|
|
1359
1768
|
reachability: classified.reachability,
|
|
1360
|
-
...classified
|
|
1769
|
+
...pathFields(classified),
|
|
1361
1770
|
severity,
|
|
1362
1771
|
message
|
|
1363
1772
|
};
|
|
1364
1773
|
});
|
|
1774
|
+
return groupPackageAdvisories(mapped);
|
|
1365
1775
|
}
|
|
1776
|
+
var REACH_RANK;
|
|
1366
1777
|
var init_reachability = __esm({
|
|
1367
1778
|
"src/modules/deps/reachability.ts"() {
|
|
1368
1779
|
init_finding();
|
|
1369
1780
|
init_symbol_reachability();
|
|
1781
|
+
REACH_RANK = {
|
|
1782
|
+
reachable: 0,
|
|
1783
|
+
"symbol-reachable": 1,
|
|
1784
|
+
"reachable-via-dependency": 2,
|
|
1785
|
+
"imported-not-called": 3,
|
|
1786
|
+
"symbol-not-called": 4,
|
|
1787
|
+
"reachability-unknown": 5,
|
|
1788
|
+
"not-imported": 6
|
|
1789
|
+
};
|
|
1370
1790
|
}
|
|
1371
1791
|
});
|
|
1372
1792
|
|
|
@@ -1428,6 +1848,186 @@ var init_semver = __esm({
|
|
|
1428
1848
|
}
|
|
1429
1849
|
});
|
|
1430
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
|
+
|
|
1431
2031
|
// src/modules/deps/popular.ts
|
|
1432
2032
|
var POPULAR_PACKAGES;
|
|
1433
2033
|
var init_popular = __esm({
|
|
@@ -3294,6 +3894,62 @@ var init_experimental2 = __esm({
|
|
|
3294
3894
|
"src/supply/experimental.ts"() {
|
|
3295
3895
|
}
|
|
3296
3896
|
});
|
|
3897
|
+
function isCopyleftLicense(license) {
|
|
3898
|
+
return COPYLEFT.test(license);
|
|
3899
|
+
}
|
|
3900
|
+
function licenseText(value) {
|
|
3901
|
+
if (typeof value === "string") return value;
|
|
3902
|
+
if (typeof value === "object" && value !== null && "type" in value) {
|
|
3903
|
+
const type = value.type;
|
|
3904
|
+
if (typeof type === "string") return type;
|
|
3905
|
+
}
|
|
3906
|
+
return void 0;
|
|
3907
|
+
}
|
|
3908
|
+
async function readJson(path) {
|
|
3909
|
+
try {
|
|
3910
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
3911
|
+
} catch {
|
|
3912
|
+
return void 0;
|
|
3913
|
+
}
|
|
3914
|
+
}
|
|
3915
|
+
async function scanCopyleftLicenses(cwd) {
|
|
3916
|
+
const manifest = await readJson(join(cwd, "package.json"));
|
|
3917
|
+
if (typeof manifest !== "object" || manifest === null) return [];
|
|
3918
|
+
const names = /* @__PURE__ */ new Set();
|
|
3919
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
3920
|
+
const block = manifest[field];
|
|
3921
|
+
if (typeof block === "object" && block !== null && !Array.isArray(block)) {
|
|
3922
|
+
for (const name of Object.keys(block)) names.add(name);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
const findings = [];
|
|
3926
|
+
for (const name of names) {
|
|
3927
|
+
const pkg = await readJson(join(cwd, "node_modules", name, "package.json"));
|
|
3928
|
+
if (typeof pkg !== "object" || pkg === null) continue;
|
|
3929
|
+
const license = licenseText(pkg.license);
|
|
3930
|
+
if (license === void 0 || !isCopyleftLicense(license)) continue;
|
|
3931
|
+
findings.push({
|
|
3932
|
+
id: makeFindingId("DEPS-LICENSE-001", "package.json", `${name}|${license}`),
|
|
3933
|
+
ruleId: "DEPS-LICENSE-001",
|
|
3934
|
+
module: "deps",
|
|
3935
|
+
title: `Copyleft dependency ${name}`,
|
|
3936
|
+
severity: "medium",
|
|
3937
|
+
confidence: "high",
|
|
3938
|
+
file: "package.json",
|
|
3939
|
+
snippet: `${name} ${license}`,
|
|
3940
|
+
message: `${name} is licensed ${license}.`,
|
|
3941
|
+
remediation: "Replace the dependency or get a legal review before shipping it in a commercial product."
|
|
3942
|
+
});
|
|
3943
|
+
}
|
|
3944
|
+
return findings;
|
|
3945
|
+
}
|
|
3946
|
+
var COPYLEFT;
|
|
3947
|
+
var init_license = __esm({
|
|
3948
|
+
"src/modules/deps/license.ts"() {
|
|
3949
|
+
init_finding();
|
|
3950
|
+
COPYLEFT = /(?:^|[^\w])(?:AGPL|GPL)(?:-|[^\w]|$)/i;
|
|
3951
|
+
}
|
|
3952
|
+
});
|
|
3297
3953
|
|
|
3298
3954
|
// src/fixer/eol.ts
|
|
3299
3955
|
function detectEol(text) {
|
|
@@ -3407,7 +4063,7 @@ __export(supply_exports, {
|
|
|
3407
4063
|
checkProvenance: () => checkProvenance,
|
|
3408
4064
|
diffPackageVersions: () => diffPackageVersions,
|
|
3409
4065
|
installGuard: () => installGuard,
|
|
3410
|
-
packageNameFromSpec: () =>
|
|
4066
|
+
packageNameFromSpec: () => packageNameFromSpec3,
|
|
3411
4067
|
provenanceCoverage: () => provenanceCoverage
|
|
3412
4068
|
});
|
|
3413
4069
|
function assertRegistryUrl(url) {
|
|
@@ -3421,7 +4077,7 @@ function assertRegistryUrl(url) {
|
|
|
3421
4077
|
throw new Error(`supply: only ${REGISTRY_HOST} is allowed (got ${parsed.hostname || url})`);
|
|
3422
4078
|
}
|
|
3423
4079
|
}
|
|
3424
|
-
function
|
|
4080
|
+
function packageNameFromSpec3(spec) {
|
|
3425
4081
|
if (spec.startsWith("@")) {
|
|
3426
4082
|
const slash = spec.indexOf("/");
|
|
3427
4083
|
if (slash < 0) return spec;
|
|
@@ -3459,7 +4115,7 @@ function walkJs(dir, out = []) {
|
|
|
3459
4115
|
}
|
|
3460
4116
|
return out;
|
|
3461
4117
|
}
|
|
3462
|
-
function
|
|
4118
|
+
function readJson2(path) {
|
|
3463
4119
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
3464
4120
|
}
|
|
3465
4121
|
function installScriptKeys(scripts) {
|
|
@@ -3470,7 +4126,7 @@ function analyzePackageDir(dir) {
|
|
|
3470
4126
|
const findings = [];
|
|
3471
4127
|
const manifestPath = join(dir, "package.json");
|
|
3472
4128
|
if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) return findings;
|
|
3473
|
-
const pkg =
|
|
4129
|
+
const pkg = readJson2(manifestPath);
|
|
3474
4130
|
const scripts = pkg.scripts ?? {};
|
|
3475
4131
|
const keys = installScriptKeys(scripts);
|
|
3476
4132
|
if (keys.length > 0) {
|
|
@@ -3490,13 +4146,13 @@ function analyzePackageDir(dir) {
|
|
|
3490
4146
|
const readsNpmrc = sources.some((s) => s.text.includes(".npmrc") || s.text.includes(".ssh"));
|
|
3491
4147
|
const hasNet = sources.some((s) => /https\.request|http\.request|fetch\s*\(|axios\./.test(s.text));
|
|
3492
4148
|
if (readsNpmrc && hasNet) {
|
|
3493
|
-
const
|
|
4149
|
+
const hit2 = sources.find((s) => s.text.includes(".npmrc") || s.text.includes(".ssh"));
|
|
3494
4150
|
findings.push(
|
|
3495
4151
|
finding(
|
|
3496
4152
|
"SUPPLY-NPMRC-NET-001",
|
|
3497
4153
|
"Install-time code reads npm/ssh credentials and uses the network",
|
|
3498
4154
|
"static: credential file read combined with http(s)/fetch",
|
|
3499
|
-
|
|
4155
|
+
hit2?.file,
|
|
3500
4156
|
"critical"
|
|
3501
4157
|
)
|
|
3502
4158
|
);
|
|
@@ -3520,8 +4176,8 @@ function analyzePackageDir(dir) {
|
|
|
3520
4176
|
return findings;
|
|
3521
4177
|
}
|
|
3522
4178
|
function diffPackageVersions(oldDir, newDir) {
|
|
3523
|
-
const oldManifest = existsSync(join(oldDir, "package.json")) ?
|
|
3524
|
-
const nextManifest = existsSync(join(newDir, "package.json")) ?
|
|
4179
|
+
const oldManifest = existsSync(join(oldDir, "package.json")) ? readJson2(join(oldDir, "package.json")) : {};
|
|
4180
|
+
const nextManifest = existsSync(join(newDir, "package.json")) ? readJson2(join(newDir, "package.json")) : {};
|
|
3525
4181
|
const oldScripts = oldManifest.scripts ?? {};
|
|
3526
4182
|
const nextScripts = nextManifest.scripts ?? {};
|
|
3527
4183
|
const added = INSTALL_SCRIPT_KEYS.filter((k) => typeof nextScripts[k] === "string" && oldScripts[k] === void 0);
|
|
@@ -3562,14 +4218,24 @@ function checkLockfileIntegrity(lock) {
|
|
|
3562
4218
|
if (name === "") continue;
|
|
3563
4219
|
const resolved = entry.resolved;
|
|
3564
4220
|
if (!resolved) continue;
|
|
4221
|
+
if (entry.integrity === void 0 || entry.integrity.length === 0) {
|
|
4222
|
+
findings.push(
|
|
4223
|
+
finding(
|
|
4224
|
+
"SUPPLY-LOCKINTEGRITY-001",
|
|
4225
|
+
"Lockfile entry has no integrity hash",
|
|
4226
|
+
`${name} resolved without an integrity hash`,
|
|
4227
|
+
"package-lock.json"
|
|
4228
|
+
)
|
|
4229
|
+
);
|
|
4230
|
+
}
|
|
3565
4231
|
try {
|
|
3566
|
-
const
|
|
3567
|
-
if (
|
|
4232
|
+
const url = new URL(resolved);
|
|
4233
|
+
if (url.protocol !== "https:" || url.hostname !== REGISTRY_HOST) {
|
|
3568
4234
|
findings.push(
|
|
3569
4235
|
finding(
|
|
3570
4236
|
"SUPPLY-LOCKURL-001",
|
|
3571
4237
|
"Lockfile entry resolves from a non-npm registry host",
|
|
3572
|
-
`${name} resolved host ${
|
|
4238
|
+
`${name} resolved host ${url.hostname || resolved}`,
|
|
3573
4239
|
"package-lock.json"
|
|
3574
4240
|
)
|
|
3575
4241
|
);
|
|
@@ -3617,7 +4283,7 @@ async function installGuard(spec, opts = {}) {
|
|
|
3617
4283
|
if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY !== "1") {
|
|
3618
4284
|
return { findings: [], skipped: true };
|
|
3619
4285
|
}
|
|
3620
|
-
const name =
|
|
4286
|
+
const name = packageNameFromSpec3(spec);
|
|
3621
4287
|
const url = `https://${REGISTRY_HOST}/${encodeURIComponent(name)}`;
|
|
3622
4288
|
assertRegistryUrl(url);
|
|
3623
4289
|
if (opts.fetchPackument) await opts.fetchPackument(url);
|
|
@@ -3632,7 +4298,7 @@ function packageSources(dir) {
|
|
|
3632
4298
|
let scripts = "";
|
|
3633
4299
|
if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
|
|
3634
4300
|
try {
|
|
3635
|
-
const pkg =
|
|
4301
|
+
const pkg = readJson2(manifestPath);
|
|
3636
4302
|
const block = pkg.scripts ?? {};
|
|
3637
4303
|
scripts = Object.values(block).join("\n");
|
|
3638
4304
|
} catch {
|
|
@@ -3653,7 +4319,7 @@ ${scripts}`;
|
|
|
3653
4319
|
if (!TOKEN_RE.test(text)) return [];
|
|
3654
4320
|
const replicates = PUBLISH_RE.test(text) || REGISTRY_PUT_RE.test(text) || GITHUB_CREATE_RE.test(text) || WORKFLOW_PATH_RE.test(text);
|
|
3655
4321
|
if (!replicates) return [];
|
|
3656
|
-
const
|
|
4322
|
+
const hit2 = sources.find(
|
|
3657
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))
|
|
3658
4324
|
) ?? sources.find((s) => TOKEN_RE.test(s.text));
|
|
3659
4325
|
return [
|
|
@@ -3661,7 +4327,7 @@ ${scripts}`;
|
|
|
3661
4327
|
"SUPPLY-WORM-001",
|
|
3662
4328
|
"Self-replicating worm indicators in dependency code",
|
|
3663
4329
|
"static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",
|
|
3664
|
-
|
|
4330
|
+
hit2?.file,
|
|
3665
4331
|
"critical"
|
|
3666
4332
|
)
|
|
3667
4333
|
];
|
|
@@ -3671,13 +4337,13 @@ function analyzeWorkflowTampering(dir) {
|
|
|
3671
4337
|
const text = `${sources.map((s) => s.text).join("\n")}
|
|
3672
4338
|
${scripts}`;
|
|
3673
4339
|
if (!CI_WRITE_RE.test(text)) return [];
|
|
3674
|
-
const
|
|
4340
|
+
const hit2 = sources.find((s) => CI_WRITE_RE.test(s.text));
|
|
3675
4341
|
return [
|
|
3676
4342
|
finding(
|
|
3677
4343
|
"SUPPLY-WORKFLOW-001",
|
|
3678
4344
|
"Dependency writes GitHub workflow, git hooks, or CI config",
|
|
3679
4345
|
"static: install script or package code writes .github/, .git/hooks/, or CI configuration",
|
|
3680
|
-
|
|
4346
|
+
hit2?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
|
|
3681
4347
|
"critical"
|
|
3682
4348
|
)
|
|
3683
4349
|
];
|
|
@@ -3686,9 +4352,9 @@ function provenanceCoverage(packages, known) {
|
|
|
3686
4352
|
let withProvenance = 0;
|
|
3687
4353
|
let unknown = 0;
|
|
3688
4354
|
for (const pkg of packages) {
|
|
3689
|
-
const
|
|
3690
|
-
if (
|
|
3691
|
-
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;
|
|
3692
4358
|
}
|
|
3693
4359
|
const total = packages.length;
|
|
3694
4360
|
return {
|
|
@@ -3728,8 +4394,8 @@ function parentsOf(name, edges) {
|
|
|
3728
4394
|
}
|
|
3729
4395
|
return out;
|
|
3730
4396
|
}
|
|
3731
|
-
function isDirect(name,
|
|
3732
|
-
return
|
|
4397
|
+
function isDirect(name, graph2) {
|
|
4398
|
+
return graph2.directProd.has(name) || graph2.directDev.has(name);
|
|
3733
4399
|
}
|
|
3734
4400
|
function packagesWithAFixedRelease(findings) {
|
|
3735
4401
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -3747,17 +4413,17 @@ function targetVersion(finding6) {
|
|
|
3747
4413
|
function kindFromLockfile(lockFile) {
|
|
3748
4414
|
return lockfileKind(lockFile) ?? "npm";
|
|
3749
4415
|
}
|
|
3750
|
-
function attachOverrideProposals(findings,
|
|
3751
|
-
if (!
|
|
4416
|
+
function attachOverrideProposals(findings, graph2, lockFile, manifestRaw) {
|
|
4417
|
+
if (!graph2.edgesKnown || manifestRaw === void 0) return findings;
|
|
3752
4418
|
const withFix = packagesWithAFixedRelease(findings);
|
|
3753
4419
|
const kind = kindFromLockfile(lockFile);
|
|
3754
4420
|
return findings.map((finding6) => {
|
|
3755
4421
|
if (finding6.ruleId !== "DEPS-VULN-001" || finding6.snippet === void 0) return finding6;
|
|
3756
4422
|
const name = packageNameFromSnippet2(finding6.snippet);
|
|
3757
|
-
if (name.length === 0 || isDirect(name,
|
|
4423
|
+
if (name.length === 0 || isDirect(name, graph2)) return finding6;
|
|
3758
4424
|
const version = targetVersion(finding6);
|
|
3759
4425
|
if (version === void 0) return finding6;
|
|
3760
|
-
const parentFixed = parentsOf(name,
|
|
4426
|
+
const parentFixed = parentsOf(name, graph2.edges).some((parent) => withFix.has(parent));
|
|
3761
4427
|
if (parentFixed) return finding6;
|
|
3762
4428
|
const after = withManifestOverride(manifestRaw, kind, name, version);
|
|
3763
4429
|
const diff = unifiedDiff("package.json", manifestRaw, after);
|
|
@@ -3778,10 +4444,10 @@ function scanInstalledPackages(cwd, packages) {
|
|
|
3778
4444
|
const dir = packageDir(cwd, pkg);
|
|
3779
4445
|
if (dir === void 0) continue;
|
|
3780
4446
|
const prefix = pkg.path.startsWith("node_modules/") ? pkg.path : `node_modules/${pkg.name}`;
|
|
3781
|
-
for (const
|
|
4447
|
+
for (const hit2 of [...analyzeWormIndicators(dir), ...analyzeWorkflowTampering(dir)]) {
|
|
3782
4448
|
out.push({
|
|
3783
|
-
...
|
|
3784
|
-
file:
|
|
4449
|
+
...hit2,
|
|
4450
|
+
file: hit2.file !== void 0 && hit2.file.length > 0 ? `${prefix}/${hit2.file}` : prefix
|
|
3785
4451
|
});
|
|
3786
4452
|
}
|
|
3787
4453
|
}
|
|
@@ -3862,19 +4528,19 @@ function maliciousFinding(lockFile, pkg, vuln) {
|
|
|
3862
4528
|
references: [vuln.id, ...vuln.aliases ?? []]
|
|
3863
4529
|
};
|
|
3864
4530
|
}
|
|
3865
|
-
function typosquatFinding(lockFile,
|
|
3866
|
-
const snippet = `${
|
|
4531
|
+
function typosquatFinding(lockFile, hit2) {
|
|
4532
|
+
const snippet = `${hit2.name}@${hit2.version}`;
|
|
3867
4533
|
return {
|
|
3868
4534
|
id: makeFindingId("DEPS-TYPOSQUAT-001", lockFile, snippet),
|
|
3869
4535
|
ruleId: "DEPS-TYPOSQUAT-001",
|
|
3870
4536
|
module: "deps",
|
|
3871
|
-
title: `Possible typosquat: ${
|
|
4537
|
+
title: `Possible typosquat: ${hit2.name}`,
|
|
3872
4538
|
severity: "medium",
|
|
3873
4539
|
confidence: "low",
|
|
3874
4540
|
file: lockFile,
|
|
3875
4541
|
snippet,
|
|
3876
|
-
message: `Package "${
|
|
3877
|
-
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.`
|
|
3878
4544
|
};
|
|
3879
4545
|
}
|
|
3880
4546
|
function noLockFinding() {
|
|
@@ -3905,46 +4571,121 @@ function deprecatedFinding(lockFile, pkg) {
|
|
|
3905
4571
|
remediation: `Replace ${pkg.name} with a maintained alternative.`
|
|
3906
4572
|
};
|
|
3907
4573
|
}
|
|
3908
|
-
function
|
|
3909
|
-
|
|
3910
|
-
const block = manifest[field];
|
|
3911
|
-
if (block !== null && typeof block === "object" && !Array.isArray(block)) {
|
|
3912
|
-
return new Set(Object.keys(block));
|
|
3913
|
-
}
|
|
3914
|
-
return /* @__PURE__ */ new Set();
|
|
3915
|
-
};
|
|
3916
|
-
return {
|
|
3917
|
-
packages: [],
|
|
3918
|
-
edges: /* @__PURE__ */ new Map(),
|
|
3919
|
-
directProd: keys("dependencies"),
|
|
3920
|
-
directDev: keys("devDependencies"),
|
|
3921
|
-
edgesKnown: false
|
|
3922
|
-
};
|
|
4574
|
+
function workspacesEnabled() {
|
|
4575
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_WORKSPACES === "1";
|
|
3923
4576
|
}
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
if (
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
} catch {
|
|
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");
|
|
3932
4584
|
}
|
|
4585
|
+
} catch {
|
|
4586
|
+
return [];
|
|
3933
4587
|
}
|
|
3934
|
-
return
|
|
4588
|
+
return [];
|
|
3935
4589
|
}
|
|
3936
|
-
|
|
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
|
+
}
|
|
4644
|
+
function graphFromManifest(manifest) {
|
|
4645
|
+
const keys = (field) => {
|
|
4646
|
+
const block = manifest[field];
|
|
4647
|
+
if (block !== null && typeof block === "object" && !Array.isArray(block)) {
|
|
4648
|
+
return new Set(Object.keys(block));
|
|
4649
|
+
}
|
|
4650
|
+
return /* @__PURE__ */ new Set();
|
|
4651
|
+
};
|
|
4652
|
+
return {
|
|
4653
|
+
packages: [],
|
|
4654
|
+
edges: /* @__PURE__ */ new Map(),
|
|
4655
|
+
directProd: keys("dependencies"),
|
|
4656
|
+
directDev: keys("devDependencies"),
|
|
4657
|
+
edgesKnown: false
|
|
4658
|
+
};
|
|
4659
|
+
}
|
|
4660
|
+
async function reachabilityCtx(cwd, files, graph2) {
|
|
4661
|
+
const prodSources = /* @__PURE__ */ new Map();
|
|
4662
|
+
for (const rel of files) {
|
|
4663
|
+
if (!/\.[cm]?[jt]sx?$/.test(rel)) continue;
|
|
4664
|
+
if (!isProductionSource(rel)) continue;
|
|
4665
|
+
try {
|
|
4666
|
+
prodSources.set(rel, await readFile(join(cwd, rel), "utf8"));
|
|
4667
|
+
} catch {
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
return { productionUsage: indexPackageUsage(prodSources), graph: graph2, sources: prodSources };
|
|
4671
|
+
}
|
|
4672
|
+
var LOCKFILE_NAMES, MANIFEST_FIELDS2, MALICIOUS_REMEDIATION, depsModule;
|
|
3937
4673
|
var init_deps = __esm({
|
|
3938
4674
|
"src/modules/deps/index.ts"() {
|
|
3939
4675
|
init_finding();
|
|
3940
4676
|
init_lockfile();
|
|
3941
4677
|
init_osv();
|
|
3942
4678
|
init_pins();
|
|
4679
|
+
init_evidence();
|
|
3943
4680
|
init_reachability();
|
|
4681
|
+
init_upgrade_plan();
|
|
4682
|
+
init_campaign();
|
|
3944
4683
|
init_symbol_reachability();
|
|
3945
4684
|
init_semver();
|
|
3946
4685
|
init_typosquat();
|
|
3947
4686
|
init_experimental2();
|
|
4687
|
+
init_experimental();
|
|
4688
|
+
init_license();
|
|
3948
4689
|
init_phase31();
|
|
3949
4690
|
LOCKFILE_NAMES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
3950
4691
|
MANIFEST_FIELDS2 = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
@@ -3952,6 +4693,8 @@ var init_deps = __esm({
|
|
|
3952
4693
|
depsModule = {
|
|
3953
4694
|
name: "deps",
|
|
3954
4695
|
async run(ctx) {
|
|
4696
|
+
depsModule.unusedDirect = void 0;
|
|
4697
|
+
depsModule.upgradePlan = void 0;
|
|
3955
4698
|
const lockFile = LOCKFILE_NAMES.find((name) => ctx.files.includes(name));
|
|
3956
4699
|
const cacheDir = ctx.config.deps.cacheDir !== void 0 ? resolve(ctx.cwd, ctx.config.deps.cacheDir) : defaultCacheDir();
|
|
3957
4700
|
const osvOpts = { cacheDir, offline: ctx.config.deps.offline };
|
|
@@ -3977,21 +4720,21 @@ var init_deps = __esm({
|
|
|
3977
4720
|
}
|
|
3978
4721
|
const pins = exactPinsFromManifest(manifest);
|
|
3979
4722
|
const directNames2 = await readDirectDependencyNames(ctx.cwd);
|
|
3980
|
-
for (const
|
|
3981
|
-
findings2.push(typosquatFinding("package.json",
|
|
4723
|
+
for (const hit2 of findTyposquats(pins, { directNames: directNames2 })) {
|
|
4724
|
+
findings2.push(typosquatFinding("package.json", hit2));
|
|
3982
4725
|
}
|
|
3983
4726
|
const osvHits2 = await queryOsv(pins, osvOpts);
|
|
3984
4727
|
const symbols2 = /* @__PURE__ */ new Map();
|
|
3985
|
-
for (const
|
|
3986
|
-
for (const vuln of
|
|
3987
|
-
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));
|
|
3988
4731
|
const names = affectedFunctionNames(vuln);
|
|
3989
4732
|
if (names.length > 0 && finding6.ruleId === "DEPS-VULN-001") symbols2.set(finding6.id, names);
|
|
3990
4733
|
findings2.push(finding6);
|
|
3991
4734
|
}
|
|
3992
4735
|
}
|
|
3993
4736
|
const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
|
|
3994
|
-
return applyReachability(findings2, noLockCtx, symbols2);
|
|
4737
|
+
return withWorkspaceAdvisories(ctx.cwd, manifestRaw, osvOpts, applyReachability(findings2, noLockCtx, symbols2));
|
|
3995
4738
|
}
|
|
3996
4739
|
let contents;
|
|
3997
4740
|
try {
|
|
@@ -3999,12 +4742,12 @@ var init_deps = __esm({
|
|
|
3999
4742
|
} catch {
|
|
4000
4743
|
return [];
|
|
4001
4744
|
}
|
|
4002
|
-
const
|
|
4003
|
-
const packages =
|
|
4745
|
+
const graph2 = parseLockfileGraph(lockFile, contents);
|
|
4746
|
+
const packages = graph2.packages;
|
|
4004
4747
|
const findings = [];
|
|
4005
4748
|
const directNames = await readDirectDependencyNames(ctx.cwd);
|
|
4006
|
-
for (const
|
|
4007
|
-
findings.push(typosquatFinding(lockFile,
|
|
4749
|
+
for (const hit2 of findTyposquats(packages, { directNames })) {
|
|
4750
|
+
findings.push(typosquatFinding(lockFile, hit2));
|
|
4008
4751
|
}
|
|
4009
4752
|
for (const pkg of packages) {
|
|
4010
4753
|
if (pkg.deprecated !== void 0) {
|
|
@@ -4013,19 +4756,56 @@ var init_deps = __esm({
|
|
|
4013
4756
|
}
|
|
4014
4757
|
const osvHits = await queryOsv(packages, osvOpts);
|
|
4015
4758
|
const symbols = /* @__PURE__ */ new Map();
|
|
4016
|
-
for (const
|
|
4017
|
-
for (const vuln of
|
|
4018
|
-
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));
|
|
4019
4762
|
const names = affectedFunctionNames(vuln);
|
|
4020
4763
|
if (names.length > 0 && finding6.ruleId === "DEPS-VULN-001") symbols.set(finding6.id, names);
|
|
4021
4764
|
findings.push(finding6);
|
|
4022
4765
|
}
|
|
4023
4766
|
}
|
|
4024
|
-
|
|
4767
|
+
const reachCtx = await reachabilityCtx(ctx.cwd, ctx.files, graph2);
|
|
4768
|
+
let reached = applyReachability(findings, reachCtx, symbols);
|
|
4025
4769
|
if (experimentalVex()) {
|
|
4026
|
-
reached = attachOverrideProposals(reached,
|
|
4770
|
+
reached = attachOverrideProposals(reached, graph2, lockFile, manifestRaw);
|
|
4027
4771
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
4028
4772
|
}
|
|
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
|
+
}
|
|
4029
4809
|
return reached;
|
|
4030
4810
|
}
|
|
4031
4811
|
};
|
|
@@ -4407,6 +5187,153 @@ var init_mcp = __esm({
|
|
|
4407
5187
|
}
|
|
4408
5188
|
});
|
|
4409
5189
|
|
|
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
|
+
|
|
4410
5337
|
// src/modules/sast/frameworks.ts
|
|
4411
5338
|
function decoratorExpr(dec) {
|
|
4412
5339
|
if (dec.type !== "Decorator") return void 0;
|
|
@@ -4555,6 +5482,9 @@ var init_mcp2 = __esm({
|
|
|
4555
5482
|
});
|
|
4556
5483
|
|
|
4557
5484
|
// src/modules/sast/taint.ts
|
|
5485
|
+
function setCustomTaint(config) {
|
|
5486
|
+
customTaint = config;
|
|
5487
|
+
}
|
|
4558
5488
|
function objectChainMentions(node, pattern) {
|
|
4559
5489
|
let current = node;
|
|
4560
5490
|
for (let i = 0; i < 8 && current !== void 0; i += 1) {
|
|
@@ -4686,6 +5616,61 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
|
4686
5616
|
return false;
|
|
4687
5617
|
}
|
|
4688
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
|
+
}
|
|
4689
5674
|
function exprIsSanitized(node, tainted, names, isCall, returners = NO_RETURNERS) {
|
|
4690
5675
|
if (node === void 0) return true;
|
|
4691
5676
|
if (isCall(node)) return true;
|
|
@@ -4749,7 +5734,7 @@ function seedMcpToolParams(scope, tainted) {
|
|
|
4749
5734
|
for (const name of boundNames(param)) tainted.add(name);
|
|
4750
5735
|
}
|
|
4751
5736
|
}
|
|
4752
|
-
function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
5737
|
+
function analyzeScope(scope, seed = [], returners = NO_RETURNERS, filterReturners = NO_RETURNERS, initialBuilt = NO_RETURNERS) {
|
|
4753
5738
|
const tainted = new Set(seed);
|
|
4754
5739
|
seedDestructuredRequestParams(scope, tainted);
|
|
4755
5740
|
seedFrameworkParams(scope, tainted);
|
|
@@ -4757,6 +5742,8 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
4757
5742
|
const urlSanitized = /* @__PURE__ */ new Set();
|
|
4758
5743
|
const xssSanitized = /* @__PURE__ */ new Set();
|
|
4759
5744
|
const nosqlSanitized = /* @__PURE__ */ new Set();
|
|
5745
|
+
const sqlCleared = /* @__PURE__ */ new Set();
|
|
5746
|
+
const customPath = /* @__PURE__ */ new Set();
|
|
4760
5747
|
let changed = true;
|
|
4761
5748
|
while (changed) {
|
|
4762
5749
|
changed = false;
|
|
@@ -4767,6 +5754,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
4767
5754
|
for (const name of boundNames(node.id)) nosqlSanitized.add(name);
|
|
4768
5755
|
}
|
|
4769
5756
|
if (isSchemaValidatorCall(init)) return;
|
|
5757
|
+
applyCustomCall(init, boundNames(node.id), tainted, sqlCleared, customPath);
|
|
4770
5758
|
if (isUrlSanitizerCall(init)) {
|
|
4771
5759
|
for (const name of boundNames(node.id)) urlSanitized.add(name);
|
|
4772
5760
|
}
|
|
@@ -4787,6 +5775,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
4787
5775
|
for (const name of boundNames(node.left)) nosqlSanitized.add(name);
|
|
4788
5776
|
}
|
|
4789
5777
|
if (isSchemaValidatorCall(node.right)) return;
|
|
5778
|
+
applyCustomCall(node.right, boundNames(node.left), tainted, sqlCleared, customPath);
|
|
4790
5779
|
if (isUrlSanitizerCall(node.right)) {
|
|
4791
5780
|
for (const name of boundNames(node.left)) urlSanitized.add(name);
|
|
4792
5781
|
}
|
|
@@ -4813,7 +5802,7 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
4813
5802
|
for (const name of boundNames(node.left)) resolved.add(name);
|
|
4814
5803
|
}
|
|
4815
5804
|
});
|
|
4816
|
-
const pathSanitized =
|
|
5805
|
+
const pathSanitized = new Set(customPath);
|
|
4817
5806
|
walkScope(scope, (node) => {
|
|
4818
5807
|
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
4819
5808
|
if (propName(node.callee) !== "startsWith") return;
|
|
@@ -4855,17 +5844,92 @@ function analyzeScope(scope, seed = [], returners = NO_RETURNERS) {
|
|
|
4855
5844
|
for (const name of boundNames(node.left)) requestObjects.add(name);
|
|
4856
5845
|
}
|
|
4857
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
|
+
}
|
|
4858
5873
|
return {
|
|
4859
5874
|
tainted,
|
|
4860
5875
|
pathSanitized,
|
|
5876
|
+
sqlCleared,
|
|
4861
5877
|
urlSanitized,
|
|
4862
5878
|
xssSanitized,
|
|
4863
5879
|
nosqlSanitized,
|
|
4864
5880
|
hasPasswordName,
|
|
4865
5881
|
requestObjects,
|
|
5882
|
+
builtFilters,
|
|
5883
|
+
filterReturners: new Set(filterReturners),
|
|
4866
5884
|
returners: new Set(returners)
|
|
4867
5885
|
};
|
|
4868
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
|
+
}
|
|
4869
5933
|
function functionName(fn) {
|
|
4870
5934
|
if (fn.type === "FunctionDeclaration" && fn.id !== null) return fn.id.name;
|
|
4871
5935
|
if (isFunctionNode(fn) && fn.id !== null && fn.id.type === "Identifier") return fn.id.name;
|
|
@@ -4939,7 +6003,7 @@ function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
|
4939
6003
|
}
|
|
4940
6004
|
return changed;
|
|
4941
6005
|
}
|
|
4942
|
-
function analyzeFile(ast,
|
|
6006
|
+
function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
4943
6007
|
attachParents(ast);
|
|
4944
6008
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
4945
6009
|
const functions = [];
|
|
@@ -4953,12 +6017,13 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
4953
6017
|
}
|
|
4954
6018
|
const seeds = /* @__PURE__ */ new Map();
|
|
4955
6019
|
for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
|
|
4956
|
-
const returners = new Set(
|
|
6020
|
+
const returners = new Set(extraReturners2);
|
|
6021
|
+
const filterReturners = /* @__PURE__ */ new Set();
|
|
4957
6022
|
for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
|
|
4958
6023
|
let changed = false;
|
|
4959
|
-
facts.set(ast, analyzeScope(ast, [], returners));
|
|
6024
|
+
facts.set(ast, analyzeScope(ast, [], returners, filterReturners));
|
|
4960
6025
|
for (const fn of functions) {
|
|
4961
|
-
facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners));
|
|
6026
|
+
facts.set(fn, analyzeScope(fn, seeds.get(fn) ?? [], returners, filterReturners, inheritedBuiltFilters(fn, facts)));
|
|
4962
6027
|
}
|
|
4963
6028
|
for (const fn of functions) {
|
|
4964
6029
|
const name = functionName(fn);
|
|
@@ -4968,6 +6033,10 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
4968
6033
|
returners.add(name);
|
|
4969
6034
|
changed = true;
|
|
4970
6035
|
}
|
|
6036
|
+
if (scopeReturnsBuiltFilter(fn, f) && !filterReturners.has(name)) {
|
|
6037
|
+
filterReturners.add(name);
|
|
6038
|
+
changed = true;
|
|
6039
|
+
}
|
|
4971
6040
|
}
|
|
4972
6041
|
for (const fn of functions) {
|
|
4973
6042
|
const f = facts.get(fn);
|
|
@@ -5000,6 +6069,51 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5000
6069
|
}
|
|
5001
6070
|
return facts;
|
|
5002
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
|
+
}
|
|
5003
6117
|
function functionsThatReturnTaint(ast) {
|
|
5004
6118
|
attachParents(ast);
|
|
5005
6119
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -5021,18 +6135,50 @@ function factsFor(node, map) {
|
|
|
5021
6135
|
return map.get(enclosingScope(node)) ?? {
|
|
5022
6136
|
tainted: /* @__PURE__ */ new Set(),
|
|
5023
6137
|
pathSanitized: /* @__PURE__ */ new Set(),
|
|
6138
|
+
sqlCleared: /* @__PURE__ */ new Set(),
|
|
5024
6139
|
urlSanitized: /* @__PURE__ */ new Set(),
|
|
5025
6140
|
xssSanitized: /* @__PURE__ */ new Set(),
|
|
5026
6141
|
nosqlSanitized: /* @__PURE__ */ new Set(),
|
|
5027
6142
|
hasPasswordName: false,
|
|
5028
6143
|
requestObjects: /* @__PURE__ */ new Set(),
|
|
6144
|
+
builtFilters: /* @__PURE__ */ new Set(),
|
|
6145
|
+
filterReturners: /* @__PURE__ */ new Set(),
|
|
5029
6146
|
returners: /* @__PURE__ */ new Set()
|
|
5030
6147
|
};
|
|
5031
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
|
+
}
|
|
5032
6158
|
function nodeIsTainted(node, map) {
|
|
5033
6159
|
const facts = factsFor(node, map);
|
|
5034
6160
|
return exprIsTainted(node, facts.tainted, facts.returners);
|
|
5035
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
|
+
}
|
|
5036
6182
|
function nodeIsPathSanitized(node, map) {
|
|
5037
6183
|
if (node.type !== "Identifier") return false;
|
|
5038
6184
|
return factsFor(node, map).pathSanitized.has(node.name);
|
|
@@ -5054,7 +6200,7 @@ function nodeIsNosqlSanitized(node, map) {
|
|
|
5054
6200
|
if (node.type === "Identifier") return factsFor(node, map).nosqlSanitized.has(node.name);
|
|
5055
6201
|
return false;
|
|
5056
6202
|
}
|
|
5057
|
-
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;
|
|
5058
6204
|
var init_taint = __esm({
|
|
5059
6205
|
"src/modules/sast/taint.ts"() {
|
|
5060
6206
|
init_ast();
|
|
@@ -5076,6 +6222,157 @@ var init_taint = __esm({
|
|
|
5076
6222
|
]);
|
|
5077
6223
|
taintedThisFields = /* @__PURE__ */ new WeakMap();
|
|
5078
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();
|
|
5079
6376
|
}
|
|
5080
6377
|
});
|
|
5081
6378
|
function collectFunctions(file, ast) {
|
|
@@ -5186,22 +6483,22 @@ function collectInstances(ast, ns) {
|
|
|
5186
6483
|
if (node.type !== "VariableDeclarator" || node.init === null) return;
|
|
5187
6484
|
if (node.id.type !== "Identifier" || node.init.type !== "NewExpression") return;
|
|
5188
6485
|
if (node.init.callee.type !== "Identifier") return;
|
|
5189
|
-
const
|
|
5190
|
-
if (
|
|
5191
|
-
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);
|
|
5192
6489
|
});
|
|
5193
6490
|
return instances;
|
|
5194
6491
|
}
|
|
5195
6492
|
function resolveCallee(node, ns, instances, currentFile, functions) {
|
|
5196
6493
|
if (node.type !== "CallExpression") return void 0;
|
|
5197
6494
|
if (node.callee.type === "Identifier") {
|
|
5198
|
-
const
|
|
5199
|
-
if (
|
|
5200
|
-
if (
|
|
5201
|
-
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("#");
|
|
5202
6499
|
if (file !== void 0 && name !== void 0) return { file, name };
|
|
5203
6500
|
}
|
|
5204
|
-
return { file:
|
|
6501
|
+
return { file: hit2, name: node.callee.name };
|
|
5205
6502
|
}
|
|
5206
6503
|
if (functions.has(`${currentFile}#${node.callee.name}`)) {
|
|
5207
6504
|
return { file: currentFile, name: node.callee.name };
|
|
@@ -5308,9 +6605,9 @@ function runInterproc(parsed) {
|
|
|
5308
6605
|
if (fn === void 0) continue;
|
|
5309
6606
|
const param = fn.params[seed.paramIndex];
|
|
5310
6607
|
if (param === void 0) continue;
|
|
5311
|
-
const facts = analyzeFunctionWithSeed(fn.node, [param]);
|
|
5312
6608
|
const p = byFile.get(fn.file);
|
|
5313
6609
|
if (p === void 0) continue;
|
|
6610
|
+
const facts = taintSummariesEnabled() ? cachedFunctionFacts(p.source, fn.node, [param]) : analyzeFunctionWithSeed(fn.node, [param]);
|
|
5314
6611
|
const ns = requires.get(fn.file) ?? /* @__PURE__ */ new Map();
|
|
5315
6612
|
const inst = instances.get(fn.file) ?? /* @__PURE__ */ new Map();
|
|
5316
6613
|
walk(fn.node, (node) => {
|
|
@@ -5404,6 +6701,7 @@ var init_interproc = __esm({
|
|
|
5404
6701
|
init_finding();
|
|
5405
6702
|
init_ast();
|
|
5406
6703
|
init_experimental();
|
|
6704
|
+
init_summaries();
|
|
5407
6705
|
init_taint();
|
|
5408
6706
|
MAX_HOPS = 5;
|
|
5409
6707
|
}
|
|
@@ -5775,6 +7073,184 @@ var init_jwt = __esm({
|
|
|
5775
7073
|
}
|
|
5776
7074
|
});
|
|
5777
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
|
+
|
|
5778
7254
|
// src/modules/sast/rules/nosqli.ts
|
|
5779
7255
|
function objectKey(prop) {
|
|
5780
7256
|
if (prop.type !== "Property" || prop.key === void 0) return void 0;
|
|
@@ -5836,6 +7312,7 @@ function isNosqlSafeValue(node, source, isNosqlSanitized) {
|
|
|
5836
7312
|
if (isNosqlSanitized(node) || isCoercionCall(node) || isSchemaValidatorCall(node) || isMongoSanitizeCall(node)) {
|
|
5837
7313
|
return true;
|
|
5838
7314
|
}
|
|
7315
|
+
if (isValidatedRouteField(node)) return true;
|
|
5839
7316
|
return isSanitizeWrap(node, source);
|
|
5840
7317
|
}
|
|
5841
7318
|
function objectHasUnsafeTaintedValue(node, isTainted, isSafe) {
|
|
@@ -5857,6 +7334,7 @@ var init_nosqli = __esm({
|
|
|
5857
7334
|
"src/modules/sast/rules/nosqli.ts"() {
|
|
5858
7335
|
init_ast();
|
|
5859
7336
|
init_taint();
|
|
7337
|
+
init_validation();
|
|
5860
7338
|
MONGO_SINKS = /* @__PURE__ */ new Set([
|
|
5861
7339
|
"find",
|
|
5862
7340
|
"findOne",
|
|
@@ -5927,6 +7405,14 @@ var init_nosqli = __esm({
|
|
|
5927
7405
|
return;
|
|
5928
7406
|
}
|
|
5929
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
|
+
}
|
|
5930
7416
|
if (objectHasUnsafeTaintedValue(filter, (n) => ctx.isTainted(n), isSafe)) {
|
|
5931
7417
|
ctx.report({
|
|
5932
7418
|
node,
|
|
@@ -6128,7 +7614,7 @@ var init_sqli = __esm({
|
|
|
6128
7614
|
CallExpression(node) {
|
|
6129
7615
|
if (node.type !== "CallExpression" || !isSqlSink2(node)) return;
|
|
6130
7616
|
const sql = node.arguments[0];
|
|
6131
|
-
if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql)) {
|
|
7617
|
+
if (sql !== void 0 && sql.type !== "SpreadElement" && ctx.isTainted(sql) && !ctx.isSqlCleared(sql)) {
|
|
6132
7618
|
ctx.report({
|
|
6133
7619
|
node,
|
|
6134
7620
|
message: "Tainted input is concatenated into a SQL string passed to query/execute."
|
|
@@ -6292,27 +7778,27 @@ function numberLiteral(node) {
|
|
|
6292
7778
|
return node.value;
|
|
6293
7779
|
}
|
|
6294
7780
|
function containsIdentMember(node, objectName) {
|
|
6295
|
-
let
|
|
7781
|
+
let hit2 = false;
|
|
6296
7782
|
walk(node, (child) => {
|
|
6297
7783
|
if (child.type === "MemberExpression" && child.object.type === "Identifier" && child.object.name === objectName) {
|
|
6298
|
-
|
|
7784
|
+
hit2 = true;
|
|
6299
7785
|
}
|
|
6300
7786
|
});
|
|
6301
|
-
return
|
|
7787
|
+
return hit2;
|
|
6302
7788
|
}
|
|
6303
7789
|
function containsCallNamed(node, name) {
|
|
6304
|
-
let
|
|
7790
|
+
let hit2 = false;
|
|
6305
7791
|
walk(node, (child) => {
|
|
6306
|
-
if (child.type === "CallExpression" && calleeName(child) === name)
|
|
7792
|
+
if (child.type === "CallExpression" && calleeName(child) === name) hit2 = true;
|
|
6307
7793
|
});
|
|
6308
|
-
return
|
|
7794
|
+
return hit2;
|
|
6309
7795
|
}
|
|
6310
7796
|
function containsOriginalName(node) {
|
|
6311
|
-
let
|
|
7797
|
+
let hit2 = false;
|
|
6312
7798
|
walk(node, (child) => {
|
|
6313
|
-
if (child.type === "MemberExpression" && propName(child) === "originalname")
|
|
7799
|
+
if (child.type === "MemberExpression" && propName(child) === "originalname") hit2 = true;
|
|
6314
7800
|
});
|
|
6315
|
-
return
|
|
7801
|
+
return hit2;
|
|
6316
7802
|
}
|
|
6317
7803
|
function schemaLooksSafe(schema) {
|
|
6318
7804
|
if (schema === void 0) return false;
|
|
@@ -6904,118 +8390,44 @@ var init_phase29 = __esm({
|
|
|
6904
8390
|
gated(timingRule),
|
|
6905
8391
|
gated(hashRule),
|
|
6906
8392
|
gated(gqlRule)
|
|
6907
|
-
];
|
|
6908
|
-
}
|
|
6909
|
-
});
|
|
6910
|
-
|
|
6911
|
-
// src/ai/injection.ts
|
|
6912
|
-
function isCommentLine(line) {
|
|
6913
|
-
const trimmed = line.trim();
|
|
6914
|
-
return trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*");
|
|
6915
|
-
}
|
|
6916
|
-
function scanPromptInjection(file, source) {
|
|
6917
|
-
const findings = [];
|
|
6918
|
-
const lines = source.split(/\r?\n/);
|
|
6919
|
-
lines.forEach((line, index) => {
|
|
6920
|
-
if (!isCommentLine(line)) return;
|
|
6921
|
-
if (!PHRASES.some((p) => p.test(line))) return;
|
|
6922
|
-
findings.push({
|
|
6923
|
-
id: makeFindingId("AI-INJECT-001", file, `${index + 1}|${line.trim()}`),
|
|
6924
|
-
ruleId: "AI-INJECT-001",
|
|
6925
|
-
module: "sast",
|
|
6926
|
-
title: "Prompt-injection comment aimed at AI reviewers",
|
|
6927
|
-
severity: "medium",
|
|
6928
|
-
confidence: "high",
|
|
6929
|
-
file,
|
|
6930
|
-
line: index + 1,
|
|
6931
|
-
snippet: line.trim().slice(0, 120),
|
|
6932
|
-
message: "Code contains text attempting to manipulate AI reviewers.",
|
|
6933
|
-
remediation: "Remove comments that try to instruct an AI reviewer.",
|
|
6934
|
-
references: ["AI-INJECT-001"],
|
|
6935
|
-
source: "deterministic"
|
|
6936
|
-
});
|
|
6937
|
-
});
|
|
6938
|
-
return findings;
|
|
6939
|
-
}
|
|
6940
|
-
var PHRASES;
|
|
6941
|
-
var init_injection = __esm({
|
|
6942
|
-
"src/ai/injection.ts"() {
|
|
6943
|
-
init_finding();
|
|
6944
|
-
PHRASES = [/ignore previous/i, /mark as safe/i, /ai reviewer/i];
|
|
6945
|
-
}
|
|
6946
|
-
});
|
|
6947
|
-
function packageVersion() {
|
|
6948
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
6949
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
6950
|
-
try {
|
|
6951
|
-
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
6952
|
-
const pkg = JSON.parse(raw);
|
|
6953
|
-
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
6954
|
-
return pkg.version;
|
|
6955
|
-
}
|
|
6956
|
-
} catch {
|
|
6957
|
-
}
|
|
6958
|
-
const parent = dirname(dir);
|
|
6959
|
-
if (parent === dir) break;
|
|
6960
|
-
dir = parent;
|
|
6961
|
-
}
|
|
6962
|
-
throw new Error("fullshield package.json not found");
|
|
6963
|
-
}
|
|
6964
|
-
var init_version = __esm({
|
|
6965
|
-
"src/core/version.ts"() {
|
|
6966
|
-
}
|
|
6967
|
-
});
|
|
6968
|
-
function parsePoolEnabled() {
|
|
6969
|
-
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
6970
|
-
}
|
|
6971
|
-
function parsePoolSize() {
|
|
6972
|
-
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
6973
|
-
if (raw !== void 0 && raw !== "") {
|
|
6974
|
-
const parsed = Number(raw);
|
|
6975
|
-
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
6976
|
-
}
|
|
6977
|
-
return Math.max(1, availableParallelism() - 1);
|
|
6978
|
-
}
|
|
6979
|
-
function parseCacheKey(code) {
|
|
6980
|
-
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
8393
|
+
];
|
|
8394
|
+
}
|
|
8395
|
+
});
|
|
8396
|
+
|
|
8397
|
+
// src/ai/injection.ts
|
|
8398
|
+
function isCommentLine(line) {
|
|
8399
|
+
const trimmed = line.trim();
|
|
8400
|
+
return trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*");
|
|
6981
8401
|
}
|
|
6982
|
-
function
|
|
6983
|
-
const
|
|
6984
|
-
const
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
8402
|
+
function scanPromptInjection(file, source) {
|
|
8403
|
+
const findings = [];
|
|
8404
|
+
const lines = source.split(/\r?\n/);
|
|
8405
|
+
lines.forEach((line, index) => {
|
|
8406
|
+
if (!isCommentLine(line)) return;
|
|
8407
|
+
if (!PHRASES.some((p) => p.test(line))) return;
|
|
8408
|
+
findings.push({
|
|
8409
|
+
id: makeFindingId("AI-INJECT-001", file, `${index + 1}|${line.trim()}`),
|
|
8410
|
+
ruleId: "AI-INJECT-001",
|
|
8411
|
+
module: "sast",
|
|
8412
|
+
title: "Prompt-injection comment aimed at AI reviewers",
|
|
8413
|
+
severity: "medium",
|
|
8414
|
+
confidence: "high",
|
|
8415
|
+
file,
|
|
8416
|
+
line: index + 1,
|
|
8417
|
+
snippet: line.trim().slice(0, 120),
|
|
8418
|
+
message: "Code contains text attempting to manipulate AI reviewers.",
|
|
8419
|
+
remediation: "Remove comments that try to instruct an AI reviewer.",
|
|
8420
|
+
references: ["AI-INJECT-001"],
|
|
8421
|
+
source: "deterministic"
|
|
6991
8422
|
});
|
|
6992
|
-
worker.once("error", reject);
|
|
6993
8423
|
});
|
|
8424
|
+
return findings;
|
|
6994
8425
|
}
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
for (; ; ) {
|
|
7001
|
-
const index = next;
|
|
7002
|
-
next += 1;
|
|
7003
|
-
if (index >= codes.length) return;
|
|
7004
|
-
const code = codes[index];
|
|
7005
|
-
if (code === void 0) return;
|
|
7006
|
-
out[index] = await parseOne(code);
|
|
7007
|
-
}
|
|
7008
|
-
}
|
|
7009
|
-
const workers = Math.min(size, Math.max(1, codes.length));
|
|
7010
|
-
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
7011
|
-
return out;
|
|
7012
|
-
}
|
|
7013
|
-
var workerUrl, cache;
|
|
7014
|
-
var init_parse_pool = __esm({
|
|
7015
|
-
"src/core/parse-pool.ts"() {
|
|
7016
|
-
init_version();
|
|
7017
|
-
workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7018
|
-
cache = /* @__PURE__ */ new Map();
|
|
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];
|
|
7019
8431
|
}
|
|
7020
8432
|
});
|
|
7021
8433
|
async function parseSource(code) {
|
|
@@ -7033,8 +8445,8 @@ function snippetOf(source, node) {
|
|
|
7033
8445
|
if (node.range === void 0) return "";
|
|
7034
8446
|
return source.slice(node.range[0], node.range[1]).replace(/\s+/g, " ").trim().slice(0, 120);
|
|
7035
8447
|
}
|
|
7036
|
-
function findingsFromAst(filename, source, ast,
|
|
7037
|
-
const facts = analyzeFile(ast,
|
|
8448
|
+
function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
|
|
8449
|
+
const facts = analyzeFile(ast, extraReturners2);
|
|
7038
8450
|
const findings = [];
|
|
7039
8451
|
for (const rule of sastRules) {
|
|
7040
8452
|
const ctx = {
|
|
@@ -7060,10 +8472,12 @@ function findingsFromAst(filename, source, ast, extraReturners = /* @__PURE__ */
|
|
|
7060
8472
|
});
|
|
7061
8473
|
},
|
|
7062
8474
|
isTainted: (node) => nodeIsTainted(node, facts),
|
|
8475
|
+
isSqlCleared: (node) => nodeIsSqlCleared(node, facts),
|
|
7063
8476
|
isPathSanitized: (node) => nodeIsPathSanitized(node, facts),
|
|
7064
8477
|
isSanitizedFor: (node, kind) => nodeIsSanitizedFor(node, facts, kind),
|
|
7065
8478
|
isNosqlSanitized: (node) => nodeIsNosqlSanitized(node, facts),
|
|
7066
8479
|
isRequestObject: (node) => nodeIsRequestObject(node, facts),
|
|
8480
|
+
holdsBuiltFilter: (node) => nodeHoldsBuiltFilter(node, facts),
|
|
7067
8481
|
functionHasPasswordName: (node) => factsFor(node, facts).hasPasswordName,
|
|
7068
8482
|
assignedName: (node) => assignedNameOf(node),
|
|
7069
8483
|
projectCsrfProtected
|
|
@@ -7101,10 +8515,12 @@ function findingsFromTemplate(filename, source) {
|
|
|
7101
8515
|
});
|
|
7102
8516
|
},
|
|
7103
8517
|
isTainted: () => false,
|
|
8518
|
+
isSqlCleared: () => false,
|
|
7104
8519
|
isPathSanitized: () => false,
|
|
7105
8520
|
isSanitizedFor: () => false,
|
|
7106
8521
|
isNosqlSanitized: () => false,
|
|
7107
8522
|
isRequestObject: () => false,
|
|
8523
|
+
holdsBuiltFilter: () => false,
|
|
7108
8524
|
functionHasPasswordName: () => false,
|
|
7109
8525
|
assignedName: () => void 0,
|
|
7110
8526
|
projectCsrfProtected: false
|
|
@@ -7112,12 +8528,165 @@ function findingsFromTemplate(filename, source) {
|
|
|
7112
8528
|
xssRule.create(ctx);
|
|
7113
8529
|
return findings;
|
|
7114
8530
|
}
|
|
8531
|
+
function dedupeSastSinks(findings) {
|
|
8532
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8533
|
+
const out = [];
|
|
8534
|
+
for (const finding6 of findings) {
|
|
8535
|
+
if (finding6.module !== "sast" || finding6.line === void 0) {
|
|
8536
|
+
out.push(finding6);
|
|
8537
|
+
continue;
|
|
8538
|
+
}
|
|
8539
|
+
const key = `${finding6.ruleId}\0${finding6.file ?? ""}\0${finding6.line}`;
|
|
8540
|
+
if (seen.has(key)) continue;
|
|
8541
|
+
seen.add(key);
|
|
8542
|
+
out.push(finding6);
|
|
8543
|
+
}
|
|
8544
|
+
return out;
|
|
8545
|
+
}
|
|
8546
|
+
function sastStreamThreshold() {
|
|
8547
|
+
const raw = process.env.FULLSHIELD_SAST_STREAM_AT;
|
|
8548
|
+
if (raw !== void 0 && raw !== "") {
|
|
8549
|
+
const parsed = Number(raw);
|
|
8550
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
8551
|
+
}
|
|
8552
|
+
return 4e3;
|
|
8553
|
+
}
|
|
8554
|
+
function sastBatchSize() {
|
|
8555
|
+
const raw = process.env.FULLSHIELD_SAST_BATCH;
|
|
8556
|
+
if (raw !== void 0 && raw !== "") {
|
|
8557
|
+
const parsed = Number(raw);
|
|
8558
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
8559
|
+
}
|
|
8560
|
+
return 150;
|
|
8561
|
+
}
|
|
8562
|
+
function skipFinding(rel, message) {
|
|
8563
|
+
return {
|
|
8564
|
+
id: makeFindingId("SAST-SKIP-001", rel, message),
|
|
8565
|
+
ruleId: "SAST-SKIP-001",
|
|
8566
|
+
module: "sast",
|
|
8567
|
+
title: "File skipped by SAST",
|
|
8568
|
+
severity: "info",
|
|
8569
|
+
confidence: "high",
|
|
8570
|
+
file: rel,
|
|
8571
|
+
message,
|
|
8572
|
+
remediation: "Raise sast.maxFileBytes in fullshield.config.json to parse this file, or leave it skipped."
|
|
8573
|
+
};
|
|
8574
|
+
}
|
|
8575
|
+
function skipReason(source, maxBytes) {
|
|
8576
|
+
if (source.includes("\0")) return "it contains NUL bytes";
|
|
8577
|
+
const bytes = Buffer.byteLength(source);
|
|
8578
|
+
if (bytes > maxBytes) return `${bytes} bytes exceeds the SAST parse limit of ${maxBytes} bytes`;
|
|
8579
|
+
return void 0;
|
|
8580
|
+
}
|
|
8581
|
+
async function readScanSource(cwd, rel) {
|
|
8582
|
+
try {
|
|
8583
|
+
return await readFile(join(cwd, rel), "utf8");
|
|
8584
|
+
} catch {
|
|
8585
|
+
return void 0;
|
|
8586
|
+
}
|
|
8587
|
+
}
|
|
8588
|
+
function cloneAst(value) {
|
|
8589
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
8590
|
+
try {
|
|
8591
|
+
return structuredClone(value);
|
|
8592
|
+
} catch {
|
|
8593
|
+
return void 0;
|
|
8594
|
+
}
|
|
8595
|
+
}
|
|
8596
|
+
async function parseInProcess(source) {
|
|
8597
|
+
try {
|
|
8598
|
+
return await parseSource(source);
|
|
8599
|
+
} catch {
|
|
8600
|
+
return void 0;
|
|
8601
|
+
}
|
|
8602
|
+
}
|
|
8603
|
+
async function parseCode(source) {
|
|
8604
|
+
if (!parsePoolEnabled()) return parseInProcess(source);
|
|
8605
|
+
try {
|
|
8606
|
+
const cloned = cloneAst((await parseWithPool([source]))[0]);
|
|
8607
|
+
if (cloned !== void 0) return cloned;
|
|
8608
|
+
} catch {
|
|
8609
|
+
}
|
|
8610
|
+
return parseInProcess(source);
|
|
8611
|
+
}
|
|
8612
|
+
function releaseParsed(parsed) {
|
|
8613
|
+
for (const entry of parsed) {
|
|
8614
|
+
entry.source = "";
|
|
8615
|
+
entry.ast = void 0;
|
|
8616
|
+
}
|
|
8617
|
+
parsed.length = 0;
|
|
8618
|
+
}
|
|
8619
|
+
function extraReturners(rel, ast, files, returning) {
|
|
8620
|
+
const ns = collectRequires(rel, ast, files);
|
|
8621
|
+
const extra = new Set(returning.get(rel));
|
|
8622
|
+
for (const [local, spec] of ns) {
|
|
8623
|
+
const hash = spec.indexOf("#");
|
|
8624
|
+
const file = hash >= 0 ? spec.slice(0, hash) : spec;
|
|
8625
|
+
const exp = hash >= 0 ? spec.slice(hash + 1) : local;
|
|
8626
|
+
if (returning.get(file)?.has(exp)) extra.add(local);
|
|
8627
|
+
}
|
|
8628
|
+
return extra;
|
|
8629
|
+
}
|
|
8630
|
+
async function runSastStreaming(ctx) {
|
|
8631
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
8632
|
+
const findings = [];
|
|
8633
|
+
const returning = /* @__PURE__ */ new Map();
|
|
8634
|
+
const parseable = [];
|
|
8635
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
8636
|
+
let projectCsrf = false;
|
|
8637
|
+
for (const rel of ctx.files) {
|
|
8638
|
+
const isSource = SOURCE_EXT.test(rel);
|
|
8639
|
+
const isTemplate = TEMPLATE_EXT.test(rel);
|
|
8640
|
+
if (!isSource && !isTemplate) continue;
|
|
8641
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
8642
|
+
if (source === void 0) continue;
|
|
8643
|
+
const why = skipReason(source, maxBytes);
|
|
8644
|
+
if (why !== void 0) {
|
|
8645
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
8646
|
+
continue;
|
|
8647
|
+
}
|
|
8648
|
+
if (sourceHasCsrfGuard(source)) projectCsrf = true;
|
|
8649
|
+
if (isTemplate) {
|
|
8650
|
+
findings.push(...findingsFromTemplate(rel, source));
|
|
8651
|
+
continue;
|
|
8652
|
+
}
|
|
8653
|
+
const ast = await parseCode(source);
|
|
8654
|
+
if (ast === void 0) continue;
|
|
8655
|
+
returning.set(rel, taintSummariesEnabled() ? cachedReturners(source, ast) : functionsThatReturnTaint(ast));
|
|
8656
|
+
parseable.push(rel);
|
|
8657
|
+
fileSet.add(rel);
|
|
8658
|
+
}
|
|
8659
|
+
for (const rel of parseable) {
|
|
8660
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
8661
|
+
if (source === void 0) continue;
|
|
8662
|
+
const ast = await parseCode(source);
|
|
8663
|
+
if (ast === void 0) continue;
|
|
8664
|
+
findings.push(
|
|
8665
|
+
...findingsFromAst(rel, source, ast, extraReturners(rel, ast, fileSet, returning), projectCsrf)
|
|
8666
|
+
);
|
|
8667
|
+
}
|
|
8668
|
+
const batch = sastBatchSize();
|
|
8669
|
+
for (let i = 0; i < parseable.length; i += batch) {
|
|
8670
|
+
const parsed = [];
|
|
8671
|
+
for (const rel of parseable.slice(i, i + batch)) {
|
|
8672
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
8673
|
+
if (source === void 0) continue;
|
|
8674
|
+
const ast = await parseCode(source);
|
|
8675
|
+
if (ast === void 0) continue;
|
|
8676
|
+
parsed.push({ file: rel, source, ast });
|
|
8677
|
+
}
|
|
8678
|
+
findings.push(...runInterproc(parsed));
|
|
8679
|
+
releaseParsed(parsed);
|
|
8680
|
+
}
|
|
8681
|
+
return dedupeSastSinks(findings);
|
|
8682
|
+
}
|
|
7115
8683
|
var sastRules, SOURCE_EXT, sastModule;
|
|
7116
8684
|
var init_engine = __esm({
|
|
7117
8685
|
"src/modules/sast/engine.ts"() {
|
|
7118
8686
|
init_finding();
|
|
7119
8687
|
init_ast();
|
|
7120
8688
|
init_interproc();
|
|
8689
|
+
init_summaries();
|
|
7121
8690
|
init_taint();
|
|
7122
8691
|
init_auth();
|
|
7123
8692
|
init_cmdi();
|
|
@@ -7153,49 +8722,69 @@ var init_engine = __esm({
|
|
|
7153
8722
|
sastModule = {
|
|
7154
8723
|
name: "sast",
|
|
7155
8724
|
async run(ctx) {
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
const
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
try {
|
|
7164
|
-
source = await readFile(join(ctx.cwd, rel), "utf8");
|
|
7165
|
-
} catch {
|
|
7166
|
-
continue;
|
|
7167
|
-
}
|
|
7168
|
-
if (source.length > 1024 * 1024 || source.includes("\0")) continue;
|
|
7169
|
-
if (isTemplate) {
|
|
7170
|
-
findings.push(...findingsFromTemplate(rel, source));
|
|
7171
|
-
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);
|
|
7172
8732
|
}
|
|
7173
|
-
|
|
7174
|
-
|
|
8733
|
+
setCustomTaint({ sources, sanitizers });
|
|
8734
|
+
} else {
|
|
8735
|
+
setCustomTaint(void 0);
|
|
8736
|
+
}
|
|
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);
|
|
7175
8757
|
if (ast === void 0) continue;
|
|
7176
8758
|
parsed.push({ file: rel, source, ast });
|
|
7177
|
-
} catch {
|
|
7178
8759
|
}
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
const
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
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(
|
|
8765
|
+
entry.file,
|
|
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
|
+
);
|
|
7194
8779
|
}
|
|
7195
|
-
findings.push(...
|
|
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);
|
|
7196
8787
|
}
|
|
7197
|
-
findings.push(...runInterproc(parsed));
|
|
7198
|
-
return findings;
|
|
7199
8788
|
}
|
|
7200
8789
|
};
|
|
7201
8790
|
}
|
|
@@ -7579,7 +9168,7 @@ function scanTextForSecretHits(path, text) {
|
|
|
7579
9168
|
return hits;
|
|
7580
9169
|
}
|
|
7581
9170
|
function scanTextForSecrets(path, text) {
|
|
7582
|
-
return scanTextForSecretHits(path, text).map((
|
|
9171
|
+
return scanTextForSecretHits(path, text).map((hit2) => hit2.finding);
|
|
7583
9172
|
}
|
|
7584
9173
|
var secretsRules, REMEDIATION, MAX_FILE_SIZE, secretsModule;
|
|
7585
9174
|
var init_secrets = __esm({
|
|
@@ -7935,12 +9524,12 @@ ${text}`);
|
|
|
7935
9524
|
const seeds = fixtureLogic(prompt);
|
|
7936
9525
|
const findings = [];
|
|
7937
9526
|
for (const seed of seeds) {
|
|
7938
|
-
const
|
|
9527
|
+
const hit2 = fileTexts.find((f) => f.file.includes(seed.fileHint) && f.text.includes(seed.fileHint === "user" ? "findById" : "Object.assign")) ?? fileTexts.find(
|
|
7939
9528
|
(f) => f.text.includes(seed.ruleId === "AI-IDOR-001" ? "findById(req.params.id)" : "Object.assign(user, req.body)")
|
|
7940
9529
|
);
|
|
7941
|
-
if (
|
|
7942
|
-
const line =
|
|
7943
|
-
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));
|
|
7944
9533
|
}
|
|
7945
9534
|
return findings;
|
|
7946
9535
|
}
|
|
@@ -8121,16 +9710,37 @@ var init_provider = __esm({
|
|
|
8121
9710
|
FIXTURE_COST = 0.01;
|
|
8122
9711
|
}
|
|
8123
9712
|
});
|
|
9713
|
+
function isIsoDate(value) {
|
|
9714
|
+
if (!/^\d{4}-\d{2}-\d{2}(T[\d:.+\-Z]+)?$/.test(value)) return false;
|
|
9715
|
+
return Number.isFinite(Date.parse(value));
|
|
9716
|
+
}
|
|
9717
|
+
function expiresAtMs(expires) {
|
|
9718
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) return Date.parse(`${expires}T23:59:59.999Z`);
|
|
9719
|
+
return Date.parse(expires);
|
|
9720
|
+
}
|
|
9721
|
+
function validSuppression(value) {
|
|
9722
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
9723
|
+
const row = value;
|
|
9724
|
+
if (typeof row.ruleId !== "string" || row.ruleId.length === 0) return void 0;
|
|
9725
|
+
if (typeof row.owner !== "string" || row.owner.length === 0) return void 0;
|
|
9726
|
+
if (typeof row.reason !== "string" || row.reason.length === 0) return void 0;
|
|
9727
|
+
if (typeof row.expires !== "string" || !isIsoDate(row.expires)) return void 0;
|
|
9728
|
+
const file = row.file === void 0 ? void 0 : row.file.replace(/\\/g, "/");
|
|
9729
|
+
if (row.file !== void 0 && (typeof row.file !== "string" || row.file.length === 0)) return void 0;
|
|
9730
|
+
return { ruleId: row.ruleId, file, owner: row.owner, reason: row.reason, expires: row.expires };
|
|
9731
|
+
}
|
|
8124
9732
|
async function readBaseline(path) {
|
|
8125
9733
|
try {
|
|
8126
9734
|
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
8127
9735
|
if (raw.version !== 1 || !Array.isArray(raw.findings)) return void 0;
|
|
9736
|
+
if (raw.suppressions !== void 0 && !Array.isArray(raw.suppressions)) return void 0;
|
|
8128
9737
|
return raw;
|
|
8129
9738
|
} catch {
|
|
8130
9739
|
return void 0;
|
|
8131
9740
|
}
|
|
8132
9741
|
}
|
|
8133
9742
|
async function writeBaseline(path, findings) {
|
|
9743
|
+
const existing = await readBaseline(path);
|
|
8134
9744
|
const doc = {
|
|
8135
9745
|
version: 1,
|
|
8136
9746
|
findings: findings.map((f) => ({
|
|
@@ -8138,7 +9748,8 @@ async function writeBaseline(path, findings) {
|
|
|
8138
9748
|
ruleId: f.ruleId,
|
|
8139
9749
|
file: f.file,
|
|
8140
9750
|
line: f.line
|
|
8141
|
-
}))
|
|
9751
|
+
})),
|
|
9752
|
+
...existing?.suppressions !== void 0 ? { suppressions: existing.suppressions } : {}
|
|
8142
9753
|
};
|
|
8143
9754
|
await writeFile(path, `${JSON.stringify(doc, null, 2)}
|
|
8144
9755
|
`, "utf8");
|
|
@@ -8148,10 +9759,68 @@ function applyBaseline(findings, baseline) {
|
|
|
8148
9759
|
const known = new Set(baseline.findings.map((f) => f.id));
|
|
8149
9760
|
return findings.filter((f) => !known.has(f.id));
|
|
8150
9761
|
}
|
|
9762
|
+
function applySuppressions(findings, rows, now = /* @__PURE__ */ new Date()) {
|
|
9763
|
+
if (rows === void 0 || rows.length === 0) return { findings: [...findings], errors: [] };
|
|
9764
|
+
const errors = [];
|
|
9765
|
+
const active = [];
|
|
9766
|
+
rows.forEach((row, index) => {
|
|
9767
|
+
const parsed = validSuppression(row);
|
|
9768
|
+
if (parsed === void 0) {
|
|
9769
|
+
errors.push(`suppression ${index + 1} needs ruleId, owner, reason, and an ISO expires date`);
|
|
9770
|
+
return;
|
|
9771
|
+
}
|
|
9772
|
+
if (expiresAtMs(parsed.expires) < now.getTime()) {
|
|
9773
|
+
errors.push(
|
|
9774
|
+
`suppression for ${parsed.ruleId} expired on ${parsed.expires} (owner: ${parsed.owner}); renew or remove it`
|
|
9775
|
+
);
|
|
9776
|
+
return;
|
|
9777
|
+
}
|
|
9778
|
+
active.push(parsed);
|
|
9779
|
+
});
|
|
9780
|
+
const kept = findings.filter((finding6) => {
|
|
9781
|
+
const file = (finding6.file ?? "").replace(/\\/g, "/");
|
|
9782
|
+
return !active.some((row) => row.ruleId === finding6.ruleId && (row.file === void 0 || row.file === file));
|
|
9783
|
+
});
|
|
9784
|
+
return { findings: kept, errors };
|
|
9785
|
+
}
|
|
8151
9786
|
var init_baseline = __esm({
|
|
8152
9787
|
"src/core/baseline.ts"() {
|
|
8153
9788
|
}
|
|
8154
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
|
+
}
|
|
8155
9824
|
function validate(raw, source) {
|
|
8156
9825
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
8157
9826
|
throw new ConfigError(`${source}: config must be a JSON object`);
|
|
@@ -8253,6 +9922,42 @@ function validate(raw, source) {
|
|
|
8253
9922
|
}
|
|
8254
9923
|
out.ai = ai;
|
|
8255
9924
|
}
|
|
9925
|
+
if (record.defaultExcludes !== void 0) {
|
|
9926
|
+
if (typeof record.defaultExcludes !== "boolean") {
|
|
9927
|
+
throw new ConfigError(`${source}: "defaultExcludes" must be a boolean`);
|
|
9928
|
+
}
|
|
9929
|
+
out.defaultExcludes = record.defaultExcludes;
|
|
9930
|
+
}
|
|
9931
|
+
if (record.excludes !== void 0) {
|
|
9932
|
+
if (!Array.isArray(record.excludes) || record.excludes.some((p) => typeof p !== "string")) {
|
|
9933
|
+
throw new ConfigError(`${source}: "excludes" must be an array of glob strings`);
|
|
9934
|
+
}
|
|
9935
|
+
out.excludes = record.excludes;
|
|
9936
|
+
}
|
|
9937
|
+
if (record.sast !== void 0) {
|
|
9938
|
+
if (typeof record.sast !== "object" || record.sast === null || Array.isArray(record.sast)) {
|
|
9939
|
+
throw new ConfigError(`${source}: "sast" must be an object`);
|
|
9940
|
+
}
|
|
9941
|
+
const rawSast = record.sast;
|
|
9942
|
+
const sast = { maxFileBytes: 1024 * 1024 };
|
|
9943
|
+
if (rawSast.maxFileBytes !== void 0) {
|
|
9944
|
+
if (typeof rawSast.maxFileBytes !== "number" || rawSast.maxFileBytes < 1) {
|
|
9945
|
+
throw new ConfigError(`${source}: "sast.maxFileBytes" must be a positive number`);
|
|
9946
|
+
}
|
|
9947
|
+
sast.maxFileBytes = rawSast.maxFileBytes;
|
|
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
|
+
}
|
|
9959
|
+
out.sast = sast;
|
|
9960
|
+
}
|
|
8256
9961
|
return out;
|
|
8257
9962
|
}
|
|
8258
9963
|
async function loadConfig(cwd, configPath) {
|
|
@@ -8278,7 +9983,7 @@ async function loadConfig(cwd, configPath) {
|
|
|
8278
9983
|
}
|
|
8279
9984
|
return { ...structuredClone(DEFAULT_CONFIG), ...validate(raw, file) };
|
|
8280
9985
|
}
|
|
8281
|
-
var CONFIG_FILE_NAME, DEFAULT_CONFIG, ConfigError, KNOWN_KEYS;
|
|
9986
|
+
var CONFIG_FILE_NAME, DEFAULT_CONFIG, ConfigError, KNOWN_KEYS, SAST_CLASSES;
|
|
8282
9987
|
var init_config2 = __esm({
|
|
8283
9988
|
"src/core/config.ts"() {
|
|
8284
9989
|
init_finding();
|
|
@@ -8287,19 +9992,51 @@ var init_config2 = __esm({
|
|
|
8287
9992
|
modules: [...MODULE_NAMES],
|
|
8288
9993
|
failOn: "high",
|
|
8289
9994
|
ignore: ["dist/**", "**/*.test.ts"],
|
|
9995
|
+
defaultExcludes: true,
|
|
8290
9996
|
baseline: ".fullshield-baseline.json",
|
|
8291
9997
|
notify: { slackWebhookEnv: "FULLSHIELD_SLACK_WEBHOOK" },
|
|
8292
9998
|
deps: { offline: false },
|
|
8293
|
-
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 }
|
|
9999
|
+
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 },
|
|
10000
|
+
sast: { maxFileBytes: 1024 * 1024 }
|
|
8294
10001
|
};
|
|
8295
10002
|
ConfigError = class extends Error {
|
|
8296
10003
|
};
|
|
8297
|
-
KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
10004
|
+
KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
10005
|
+
"modules",
|
|
10006
|
+
"failOn",
|
|
10007
|
+
"ignore",
|
|
10008
|
+
"defaultExcludes",
|
|
10009
|
+
"excludes",
|
|
10010
|
+
"baseline",
|
|
10011
|
+
"notify",
|
|
10012
|
+
"deps",
|
|
10013
|
+
"ai",
|
|
10014
|
+
"sast"
|
|
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
|
+
]);
|
|
8298
10032
|
}
|
|
8299
10033
|
});
|
|
8300
10034
|
async function walkFiles(options) {
|
|
8301
10035
|
const ig = ignoreFactory();
|
|
8302
|
-
ig.add(
|
|
10036
|
+
ig.add(HARD_IGNORED);
|
|
10037
|
+
if (options.defaultExcludes !== false) {
|
|
10038
|
+
ig.add(options.excludes ?? DEFAULT_EXCLUDES);
|
|
10039
|
+
}
|
|
8303
10040
|
try {
|
|
8304
10041
|
ig.add(await readFile(join(options.cwd, ".gitignore"), "utf8"));
|
|
8305
10042
|
} catch {
|
|
@@ -8325,11 +10062,24 @@ async function walkFiles(options) {
|
|
|
8325
10062
|
await walk5("");
|
|
8326
10063
|
return files.sort();
|
|
8327
10064
|
}
|
|
8328
|
-
var ignoreFactory,
|
|
10065
|
+
var ignoreFactory, HARD_IGNORED, DEFAULT_EXCLUDES;
|
|
8329
10066
|
var init_fileWalker = __esm({
|
|
8330
10067
|
"src/core/fileWalker.ts"() {
|
|
8331
10068
|
ignoreFactory = ignoreModule.default;
|
|
8332
|
-
|
|
10069
|
+
HARD_IGNORED = [".git/", ".fullshield/"];
|
|
10070
|
+
DEFAULT_EXCLUDES = [
|
|
10071
|
+
"node_modules/",
|
|
10072
|
+
"dist/",
|
|
10073
|
+
"build/",
|
|
10074
|
+
".next/",
|
|
10075
|
+
"coverage/",
|
|
10076
|
+
"vendor/",
|
|
10077
|
+
"vendored/",
|
|
10078
|
+
"third_party/",
|
|
10079
|
+
"third-party/",
|
|
10080
|
+
"compiled/",
|
|
10081
|
+
"*.min.js"
|
|
10082
|
+
];
|
|
8333
10083
|
}
|
|
8334
10084
|
});
|
|
8335
10085
|
async function listStagedFiles(cwd) {
|
|
@@ -8543,7 +10293,7 @@ function resolvePolicyBranch(cwd, env = process.env) {
|
|
|
8543
10293
|
return "*";
|
|
8544
10294
|
}
|
|
8545
10295
|
}
|
|
8546
|
-
function
|
|
10296
|
+
function expiresAtMs2(expires) {
|
|
8547
10297
|
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
|
|
8548
10298
|
return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8549
10299
|
}
|
|
@@ -8565,7 +10315,7 @@ function applyPolicy(findings, policy, ctx = {}) {
|
|
|
8565
10315
|
});
|
|
8566
10316
|
const suppressed = /* @__PURE__ */ new Set();
|
|
8567
10317
|
for (const exception of policy.exceptions ?? []) {
|
|
8568
|
-
if (
|
|
10318
|
+
if (expiresAtMs2(exception.expires) < now.getTime()) {
|
|
8569
10319
|
errors.push(
|
|
8570
10320
|
`policy exception for ${exception.ruleId} expired on ${exception.expires} (owner: ${exception.owner}); renew or remove it`
|
|
8571
10321
|
);
|
|
@@ -8634,6 +10384,7 @@ var init_incremental = __esm({
|
|
|
8634
10384
|
// src/modules/secrets/history.ts
|
|
8635
10385
|
var history_exports = {};
|
|
8636
10386
|
__export(history_exports, {
|
|
10387
|
+
historyCommitLimit: () => historyCommitLimit,
|
|
8637
10388
|
scanGitHistory: () => scanGitHistory
|
|
8638
10389
|
});
|
|
8639
10390
|
function gitError(error) {
|
|
@@ -8667,16 +10418,22 @@ async function readBlob(cwd, hash) {
|
|
|
8667
10418
|
return void 0;
|
|
8668
10419
|
}
|
|
8669
10420
|
}
|
|
8670
|
-
function remapHit(
|
|
8671
|
-
const snippet =
|
|
8672
|
-
const line =
|
|
8673
|
-
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"}`);
|
|
8674
10425
|
return {
|
|
8675
|
-
finding: { ...
|
|
8676
|
-
secret:
|
|
10426
|
+
finding: { ...hit2.finding, id, file },
|
|
10427
|
+
secret: hit2.secret
|
|
8677
10428
|
};
|
|
8678
10429
|
}
|
|
8679
|
-
|
|
10430
|
+
function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
|
|
10431
|
+
if (raw === void 0 || raw.trim() === "") return 500;
|
|
10432
|
+
const parsed = Number(raw);
|
|
10433
|
+
if (!Number.isInteger(parsed) || parsed < 1) return 500;
|
|
10434
|
+
return Math.min(parsed, 5e3);
|
|
10435
|
+
}
|
|
10436
|
+
async function streamLogChanges(cwd, maxCommits) {
|
|
8680
10437
|
const changes = [];
|
|
8681
10438
|
await gitOk(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
8682
10439
|
const child = spawn(
|
|
@@ -8693,6 +10450,8 @@ async function streamLogChanges(cwd) {
|
|
|
8693
10450
|
"--format=FSCOMMIT:%H",
|
|
8694
10451
|
"--no-textconv",
|
|
8695
10452
|
"--no-ext-diff",
|
|
10453
|
+
"-n",
|
|
10454
|
+
String(maxCommits),
|
|
8696
10455
|
"HEAD"
|
|
8697
10456
|
],
|
|
8698
10457
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -8755,14 +10514,20 @@ async function headSecretKeys(cwd) {
|
|
|
8755
10514
|
hits = scanTextForSecretHits(path, text);
|
|
8756
10515
|
seen.set(hash, hits);
|
|
8757
10516
|
}
|
|
8758
|
-
for (const
|
|
8759
|
-
keys.add(`${
|
|
10517
|
+
for (const hit2 of hits) {
|
|
10518
|
+
keys.add(`${hit2.finding.ruleId}\0${hit2.finding.snippet ?? ""}`);
|
|
8760
10519
|
}
|
|
8761
10520
|
}
|
|
8762
10521
|
return keys;
|
|
8763
10522
|
}
|
|
8764
|
-
async function
|
|
8765
|
-
const
|
|
10523
|
+
async function commitCount(cwd) {
|
|
10524
|
+
const raw = await gitOk(cwd, ["rev-list", "--count", "HEAD"]);
|
|
10525
|
+
const total = Number(raw.trim());
|
|
10526
|
+
return Number.isInteger(total) && total >= 0 ? total : 0;
|
|
10527
|
+
}
|
|
10528
|
+
async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
10529
|
+
const total = await commitCount(cwd);
|
|
10530
|
+
const changes = await streamLogChanges(cwd, maxCommits);
|
|
8766
10531
|
const blobHits = /* @__PURE__ */ new Map();
|
|
8767
10532
|
const first = /* @__PURE__ */ new Map();
|
|
8768
10533
|
for (const change of changes) {
|
|
@@ -8783,8 +10548,8 @@ async function scanGitHistory(cwd) {
|
|
|
8783
10548
|
hits2 = rawHits.filter((h) => allowed.has(h.finding.id));
|
|
8784
10549
|
blobHits.set(change.hash, hits2);
|
|
8785
10550
|
}
|
|
8786
|
-
for (const
|
|
8787
|
-
const mapped = remapHit(
|
|
10551
|
+
for (const hit2 of hits2) {
|
|
10552
|
+
const mapped = remapHit(hit2, change.path);
|
|
8788
10553
|
if (!first.has(mapped.finding.id)) {
|
|
8789
10554
|
first.set(mapped.finding.id, { hit: mapped, commit: change.commit });
|
|
8790
10555
|
}
|
|
@@ -8792,14 +10557,20 @@ async function scanGitHistory(cwd) {
|
|
|
8792
10557
|
}
|
|
8793
10558
|
const atHead = await headSecretKeys(cwd);
|
|
8794
10559
|
const hits = [];
|
|
8795
|
-
for (const { hit, commit } of first.values()) {
|
|
8796
|
-
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 ?? ""}`);
|
|
8797
10562
|
hits.push({
|
|
8798
|
-
finding: { ...
|
|
8799
|
-
secret:
|
|
10563
|
+
finding: { ...hit2.finding, introducedIn: commit, stillAtHead },
|
|
10564
|
+
secret: hit2.secret
|
|
8800
10565
|
});
|
|
8801
10566
|
}
|
|
8802
|
-
|
|
10567
|
+
const scanned = Math.min(maxCommits, total);
|
|
10568
|
+
const history = total > maxCommits ? `scanned ${scanned} of ${total} commits` : void 0;
|
|
10569
|
+
return {
|
|
10570
|
+
findings: hits.map((h) => h.finding),
|
|
10571
|
+
hits,
|
|
10572
|
+
...history !== void 0 ? { history } : {}
|
|
10573
|
+
};
|
|
8803
10574
|
}
|
|
8804
10575
|
var execFileAsync2, ZERO_HASH, MAX_BLOB, RAW_LINE;
|
|
8805
10576
|
var init_history = __esm({
|
|
@@ -9032,15 +10803,15 @@ function providerFor(ruleId, secret) {
|
|
|
9032
10803
|
async function verifySecretHits(hits, options = {}) {
|
|
9033
10804
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
9034
10805
|
const out = [];
|
|
9035
|
-
for (const
|
|
9036
|
-
const secret =
|
|
10806
|
+
for (const hit2 of hits) {
|
|
10807
|
+
const secret = hit2.secret;
|
|
9037
10808
|
if (secret === void 0 || secret.length === 0) {
|
|
9038
|
-
out.push({ finding: { ...
|
|
10809
|
+
out.push({ finding: { ...hit2.finding, verification: "unknown" }, secret: hit2.secret });
|
|
9039
10810
|
continue;
|
|
9040
10811
|
}
|
|
9041
|
-
const run = providerFor(
|
|
10812
|
+
const run = providerFor(hit2.finding.ruleId, secret);
|
|
9042
10813
|
const verification = run === void 0 ? "unknown" : await run(secret, fetchImpl);
|
|
9043
|
-
out.push({ finding: { ...
|
|
10814
|
+
out.push({ finding: { ...hit2.finding, verification }, secret: hit2.secret });
|
|
9044
10815
|
}
|
|
9045
10816
|
return out;
|
|
9046
10817
|
}
|
|
@@ -9073,6 +10844,8 @@ function dedupe2(findings) {
|
|
|
9073
10844
|
async function runScan(options = {}, modules = builtinModules) {
|
|
9074
10845
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
9075
10846
|
const t0 = performance.now();
|
|
10847
|
+
depsModule.unusedDirect = void 0;
|
|
10848
|
+
depsModule.upgradePlan = void 0;
|
|
9076
10849
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
9077
10850
|
const stats = await stat(cwd).catch(() => null);
|
|
9078
10851
|
if (stats === null || !stats.isDirectory()) {
|
|
@@ -9094,7 +10867,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9094
10867
|
options.modules ?? config.modules,
|
|
9095
10868
|
loadedPolicy?.requiredModules
|
|
9096
10869
|
);
|
|
9097
|
-
let files = await walkFiles({
|
|
10870
|
+
let files = await walkFiles({
|
|
10871
|
+
cwd,
|
|
10872
|
+
ignore: config.ignore,
|
|
10873
|
+
defaultExcludes: config.defaultExcludes,
|
|
10874
|
+
excludes: config.excludes
|
|
10875
|
+
});
|
|
9098
10876
|
if (options.staged === true) {
|
|
9099
10877
|
const staged = new Set(await listStagedFiles(cwd));
|
|
9100
10878
|
files = files.filter((file) => staged.has(file));
|
|
@@ -9128,6 +10906,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9128
10906
|
);
|
|
9129
10907
|
}
|
|
9130
10908
|
const extraHits = [];
|
|
10909
|
+
let historyNotice;
|
|
9131
10910
|
const secretsWanted = modulesRequested.includes("secrets");
|
|
9132
10911
|
if (secretsAdvEnabled() && secretsWanted) {
|
|
9133
10912
|
if (options.history === true) {
|
|
@@ -9135,6 +10914,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9135
10914
|
const hist = await scanGitHistory2(cwd);
|
|
9136
10915
|
extraHits.push(...hist.hits);
|
|
9137
10916
|
collected.push(...hist.findings);
|
|
10917
|
+
historyNotice = hist.history;
|
|
9138
10918
|
}
|
|
9139
10919
|
if (options.bundle !== void 0 && options.bundle.length > 0) {
|
|
9140
10920
|
const { scanBundle: scanBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
@@ -9155,13 +10935,17 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9155
10935
|
if (options.updateBaseline === true) {
|
|
9156
10936
|
await writeBaseline(baselinePath, unsuppressed);
|
|
9157
10937
|
}
|
|
9158
|
-
|
|
10938
|
+
const baselineDoc = await readBaseline(baselinePath);
|
|
10939
|
+
let findings = sortFindings(applyBaseline(unsuppressed, baselineDoc));
|
|
10940
|
+
const suppressed = applySuppressions(findings, baselineDoc?.suppressions);
|
|
10941
|
+
findings = sortFindings(suppressed.findings);
|
|
10942
|
+
const suppressionErrors = suppressed.errors;
|
|
9159
10943
|
if (secretsAdvEnabled() && secretsWanted && options.verifySecrets === true && options.iOwnTheseCredentials === true) {
|
|
9160
10944
|
const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
9161
10945
|
const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
9162
10946
|
const secretById = /* @__PURE__ */ new Map();
|
|
9163
|
-
for (const
|
|
9164
|
-
if (
|
|
10947
|
+
for (const hit2 of extraHits) {
|
|
10948
|
+
if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
|
|
9165
10949
|
}
|
|
9166
10950
|
for (const finding6 of findings) {
|
|
9167
10951
|
if (finding6.module !== "secrets" || finding6.file === void 0 || secretById.has(finding6.id)) {
|
|
@@ -9175,8 +10959,8 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9175
10959
|
continue;
|
|
9176
10960
|
}
|
|
9177
10961
|
}
|
|
9178
|
-
for (const
|
|
9179
|
-
if (
|
|
10962
|
+
for (const hit2 of scanTextForSecretHits2(finding6.file, text)) {
|
|
10963
|
+
if (hit2.secret !== void 0) secretById.set(hit2.finding.id, hit2.secret);
|
|
9180
10964
|
}
|
|
9181
10965
|
}
|
|
9182
10966
|
findings = sortFindings(
|
|
@@ -9227,10 +11011,14 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9227
11011
|
cwd,
|
|
9228
11012
|
filesScanned: files.length,
|
|
9229
11013
|
modulesRequested: [...modulesRequested],
|
|
9230
|
-
modulesRun
|
|
11014
|
+
modulesRun,
|
|
11015
|
+
...historyNotice !== void 0 ? { history: historyNotice } : {},
|
|
11016
|
+
...depsModule.unusedDirect !== void 0 ? { unusedDirect: depsModule.unusedDirect } : {},
|
|
11017
|
+
...depsModule.upgradePlan !== void 0 ? { upgradePlan: depsModule.upgradePlan } : {}
|
|
9231
11018
|
},
|
|
9232
11019
|
config,
|
|
9233
|
-
policy: policyResult
|
|
11020
|
+
policy: policyResult,
|
|
11021
|
+
...suppressionErrors.length > 0 ? { suppressionErrors } : {}
|
|
9234
11022
|
};
|
|
9235
11023
|
}
|
|
9236
11024
|
function scan(options = {}) {
|
|
@@ -9371,6 +11159,16 @@ function validateCycloneDx15(doc) {
|
|
|
9371
11159
|
if (!Array.isArray(root.components)) errors.push("components must be an array");
|
|
9372
11160
|
return errors;
|
|
9373
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
|
+
}
|
|
9374
11172
|
function renderCycloneDx(input) {
|
|
9375
11173
|
const vulns = (input.findings ?? []).filter((f) => f.ruleId === "DEPS-VULN-001");
|
|
9376
11174
|
return {
|
|
@@ -9381,13 +11179,18 @@ function renderCycloneDx(input) {
|
|
|
9381
11179
|
metadata: {
|
|
9382
11180
|
component: { type: "application", name: input.name, version: input.version }
|
|
9383
11181
|
},
|
|
9384
|
-
components: input.components.map((c) =>
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
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
|
+
}),
|
|
9391
11194
|
vulnerabilities: vulns.map((f) => ({
|
|
9392
11195
|
id: f.references?.find((r) => r.startsWith("GHSA-") || r.startsWith("CVE-")) ?? f.id,
|
|
9393
11196
|
description: f.message,
|
|
@@ -9546,6 +11349,32 @@ function finding4(ruleId, title, message, file, severity = "high") {
|
|
|
9546
11349
|
remediation: "Pin actions by SHA, drop pull_request_target, tighten permissions, close 0.0.0.0/0, encrypt storage, drop privileged."
|
|
9547
11350
|
};
|
|
9548
11351
|
}
|
|
11352
|
+
function runBodies(text) {
|
|
11353
|
+
const bodies = [];
|
|
11354
|
+
const lines = text.split(/\r?\n/);
|
|
11355
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
11356
|
+
const match = /^(\s*)(?:-\s+)?run:\s*(.*)$/.exec(lines[i] ?? "");
|
|
11357
|
+
if (match === null) continue;
|
|
11358
|
+
const indent = match[1]?.length ?? 0;
|
|
11359
|
+
let body = match[2] ?? "";
|
|
11360
|
+
if (body === "|" || body === ">") {
|
|
11361
|
+
const block = [];
|
|
11362
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
11363
|
+
const line = lines[j] ?? "";
|
|
11364
|
+
if (line.trim() === "") {
|
|
11365
|
+
block.push(line);
|
|
11366
|
+
continue;
|
|
11367
|
+
}
|
|
11368
|
+
const lead = line.match(/^\s*/)?.[0].length ?? 0;
|
|
11369
|
+
if (lead <= indent) break;
|
|
11370
|
+
block.push(line);
|
|
11371
|
+
}
|
|
11372
|
+
body = block.join("\n");
|
|
11373
|
+
}
|
|
11374
|
+
bodies.push(body);
|
|
11375
|
+
}
|
|
11376
|
+
return bodies;
|
|
11377
|
+
}
|
|
9549
11378
|
function walk3(dir, out = []) {
|
|
9550
11379
|
if (!existsSync(dir)) return out;
|
|
9551
11380
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -9573,6 +11402,28 @@ function scanIac(dir, opts = {}) {
|
|
|
9573
11402
|
if (/pull_request_target/.test(text)) {
|
|
9574
11403
|
findings.push(finding4("IAC-GHA-PRTARGET-001", "pull_request_target workflow", "untrusted PR can run with base secrets", rel, "critical"));
|
|
9575
11404
|
}
|
|
11405
|
+
if (/pull_request_target/.test(text) && /actions\/checkout/.test(text) && /github\.event\.pull_request/.test(text)) {
|
|
11406
|
+
findings.push(
|
|
11407
|
+
finding4(
|
|
11408
|
+
"IAC-GHA-PRCHECKOUT-001",
|
|
11409
|
+
"pull_request_target checks out pull-request code",
|
|
11410
|
+
"untrusted head can run with the base repository token",
|
|
11411
|
+
rel,
|
|
11412
|
+
"critical"
|
|
11413
|
+
)
|
|
11414
|
+
);
|
|
11415
|
+
}
|
|
11416
|
+
if (runBodies(text).some((body) => /\$\{\{\s*github\.event\./.test(body))) {
|
|
11417
|
+
findings.push(
|
|
11418
|
+
finding4(
|
|
11419
|
+
"IAC-GHA-INJECT-001",
|
|
11420
|
+
"Workflow run step interpolates github.event",
|
|
11421
|
+
"untrusted event text is passed to the shell",
|
|
11422
|
+
rel,
|
|
11423
|
+
"critical"
|
|
11424
|
+
)
|
|
11425
|
+
);
|
|
11426
|
+
}
|
|
9576
11427
|
if (/permissions:\s*\n\s+contents:\s*write/.test(text) || /permissions:\s+write-all/.test(text)) {
|
|
9577
11428
|
findings.push(finding4("IAC-GHA-PERMS-001", "Overly broad GitHub Actions permissions", "write-all or contents: write", rel));
|
|
9578
11429
|
}
|
|
@@ -9723,14 +11574,22 @@ var init_compliance = __esm({
|
|
|
9723
11574
|
ROW("SUPPLY-BEHAVIOR-001", "A06:2021 Vulnerable Components", "CWE-829", "V14.2.1", "PCI DSS 6.3.3"),
|
|
9724
11575
|
ROW("SUPPLY-COOLDOWN-001", "A06:2021 Vulnerable Components", "CWE-1104", "V14.2.1", "PCI DSS 6.3.3"),
|
|
9725
11576
|
ROW("SUPPLY-LOCKURL-001", "A08:2021 Integrity Failures", "CWE-494", "V14.2.3", "PCI DSS 6.3.3"),
|
|
11577
|
+
ROW("SUPPLY-LOCKINTEGRITY-001", "A08:2021 Integrity Failures", "CWE-353", "V14.2.3", "PCI DSS 6.3.3"),
|
|
9726
11578
|
ROW("SUPPLY-CONFUSION-001", "A08:2021 Integrity Failures", "CWE-494", "V14.2.3", "PCI DSS 6.3.3"),
|
|
9727
11579
|
ROW("SUPPLY-PROVENANCE-001", "A08:2021 Integrity Failures", "CWE-494", "V14.2.3", "PCI DSS 6.3.3"),
|
|
9728
11580
|
ROW("SUPPLY-WORM-001", "A06:2021 Vulnerable Components", "CWE-506", "V14.2.1", "PCI DSS 6.3.3 / 12.10"),
|
|
9729
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"),
|
|
9730
11586
|
ROW("IAC-GHA-UNPINNED-001", "A08:2021 Integrity Failures", "CWE-494", "V14.2.3", "PCI DSS 6.3.3"),
|
|
9731
11587
|
ROW("IAC-GHA-PRTARGET-001", "A01:2021 Broken Access Control", "CWE-269", "V4.1.1", "PCI DSS 7.2"),
|
|
9732
11588
|
ROW("IAC-GHA-PERMS-001", "A01:2021 Broken Access Control", "CWE-250", "V4.1.1", "PCI DSS 7.2"),
|
|
9733
11589
|
ROW("IAC-GHA-SECRETLOG-001", "A09:2021 Security Logging Failures", "CWE-532", "V7.1.1", "PCI DSS 3.2 / 10.2"),
|
|
11590
|
+
ROW("IAC-GHA-INJECT-001", "A03:2021 Injection", "CWE-94", "V5.2.4", "PCI DSS 6.2.4"),
|
|
11591
|
+
ROW("IAC-GHA-PRCHECKOUT-001", "A01:2021 Broken Access Control", "CWE-829", "V4.1.1", "PCI DSS 6.2.4"),
|
|
11592
|
+
ROW("DEPS-LICENSE-001", "A06:2021 Vulnerable Components", "CWE-1104", "V14.2.1", "PCI DSS 6.3.3"),
|
|
9734
11593
|
ROW("IAC-TF-S3PUBLIC-001", "A01:2021 Broken Access Control", "CWE-284", "V12.1.1", "PCI DSS 1.3 / 3.2"),
|
|
9735
11594
|
ROW("IAC-TF-SGOPEN-001", "A05:2021 Security Misconfiguration", "CWE-284", "V1.4.1", "PCI DSS 1.2 / 1.3"),
|
|
9736
11595
|
ROW("IAC-TF-UNENCRYPTED-001", "A02:2021 Cryptographic Failures", "CWE-311", "V6.2.1", "PCI DSS 3.4 / 4.2"),
|
|
@@ -9757,6 +11616,7 @@ var init_compliance = __esm({
|
|
|
9757
11616
|
ROW("SAST-TIMING-001", "A07:2021 Identification Failures", "CWE-208", "V2.10.3", "PCI DSS 8.3"),
|
|
9758
11617
|
ROW("SAST-HASH-001", "A02:2021 Cryptographic Failures", "CWE-916", "V6.2.2", "PCI DSS 8.3.1"),
|
|
9759
11618
|
ROW("SAST-GQL-001", "A04:2021 Insecure Design", "CWE-770/200", "V13.1.1", "PCI DSS 6.2.4"),
|
|
11619
|
+
ROW("SAST-SKIP-001", "A05:2021 Security Misconfiguration", "CWE-693", "V1.1.1", "PCI DSS 6.2.4"),
|
|
9760
11620
|
ROW("CONFIG-TRUSTPROXY-001", "A07:2021 Identification Failures", "CWE-290", "V2.2.1", "PCI DSS 8.3.4"),
|
|
9761
11621
|
ROW("SECRET-BUNDLE-001", "A02:2021 Cryptographic Failures", "CWE-798", "V6.4.1", "PCI DSS 3.2 / 8.3"),
|
|
9762
11622
|
ROW("DLP-SECRET-001", "A02:2021 Cryptographic Failures", "CWE-200", "V8.3.4", "PCI DSS 3.2 / 8.3"),
|
|
@@ -9776,6 +11636,23 @@ var init_compliance = __esm({
|
|
|
9776
11636
|
byId = new Map(RULE_MAP.map((r) => [r.ruleId, r]));
|
|
9777
11637
|
}
|
|
9778
11638
|
});
|
|
11639
|
+
function nodeSqliteAvailable() {
|
|
11640
|
+
try {
|
|
11641
|
+
require2("node:sqlite");
|
|
11642
|
+
return true;
|
|
11643
|
+
} catch {
|
|
11644
|
+
return false;
|
|
11645
|
+
}
|
|
11646
|
+
}
|
|
11647
|
+
function nodeSqliteUnavailableMessage(feature) {
|
|
11648
|
+
return `${feature} requires Node 22.13+ (node:sqlite is not available on Node ${process.versions.node})`;
|
|
11649
|
+
}
|
|
11650
|
+
var require2;
|
|
11651
|
+
var init_sqlite = __esm({
|
|
11652
|
+
"src/core/sqlite.ts"() {
|
|
11653
|
+
require2 = createRequire(import.meta.url);
|
|
11654
|
+
}
|
|
11655
|
+
});
|
|
9779
11656
|
|
|
9780
11657
|
// src/dashboard/index.ts
|
|
9781
11658
|
var dashboard_exports = {};
|
|
@@ -9789,7 +11666,7 @@ __export(dashboard_exports, {
|
|
|
9789
11666
|
startDashboard: () => startDashboard
|
|
9790
11667
|
});
|
|
9791
11668
|
function sqliteDb(file) {
|
|
9792
|
-
const { DatabaseSync } =
|
|
11669
|
+
const { DatabaseSync } = require3("node:sqlite");
|
|
9793
11670
|
let db;
|
|
9794
11671
|
try {
|
|
9795
11672
|
db = new DatabaseSync(file);
|
|
@@ -9840,6 +11717,9 @@ function sqliteDb(file) {
|
|
|
9840
11717
|
};
|
|
9841
11718
|
}
|
|
9842
11719
|
function openHistory(dir) {
|
|
11720
|
+
if (!nodeSqliteAvailable()) {
|
|
11721
|
+
throw new Error(nodeSqliteUnavailableMessage("history"));
|
|
11722
|
+
}
|
|
9843
11723
|
const root = join(dir, ".fullshield");
|
|
9844
11724
|
mkdirSync(root, { recursive: true });
|
|
9845
11725
|
return sqliteDb(join(root, "history.db"));
|
|
@@ -9941,10 +11821,11 @@ function startDashboard(opts) {
|
|
|
9941
11821
|
});
|
|
9942
11822
|
});
|
|
9943
11823
|
}
|
|
9944
|
-
var
|
|
11824
|
+
var require3;
|
|
9945
11825
|
var init_dashboard = __esm({
|
|
9946
11826
|
"src/dashboard/index.ts"() {
|
|
9947
|
-
|
|
11827
|
+
init_sqlite();
|
|
11828
|
+
require3 = createRequire(import.meta.url);
|
|
9948
11829
|
}
|
|
9949
11830
|
});
|
|
9950
11831
|
function finding5(ruleId, title, message, file, severity = "high") {
|
|
@@ -10086,9 +11967,9 @@ async function callMcpTool(session, name, args = {}) {
|
|
|
10086
11967
|
return { findings: session.findings };
|
|
10087
11968
|
}
|
|
10088
11969
|
if (name === "explain_finding") {
|
|
10089
|
-
const
|
|
10090
|
-
if (!
|
|
10091
|
-
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}` };
|
|
10092
11973
|
}
|
|
10093
11974
|
if (name === "propose_fix") {
|
|
10094
11975
|
return proposeOnly(session, args.ruleId);
|
|
@@ -10099,25 +11980,25 @@ async function callMcpTool(session, name, args = {}) {
|
|
|
10099
11980
|
return { error: `unknown tool ${name}` };
|
|
10100
11981
|
}
|
|
10101
11982
|
function proposeOnly(session, ruleId) {
|
|
10102
|
-
const
|
|
10103
|
-
if (!
|
|
10104
|
-
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);
|
|
10105
11986
|
const source = readFileSync(abs, "utf8");
|
|
10106
|
-
const proposal = proposeFix(
|
|
11987
|
+
const proposal = proposeFix(hit2, source);
|
|
10107
11988
|
return { diff: proposal?.diff ?? "", wrote: false };
|
|
10108
11989
|
}
|
|
10109
11990
|
function verifyInTemp(session, ruleId) {
|
|
10110
|
-
const
|
|
10111
|
-
if (!
|
|
10112
|
-
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);
|
|
10113
11994
|
const before = readFileSync(original, "utf8");
|
|
10114
11995
|
const mtime = statSync(original).mtimeMs;
|
|
10115
|
-
const proposal = proposeFix(
|
|
11996
|
+
const proposal = proposeFix(hit2, before);
|
|
10116
11997
|
if (!proposal?.diff) return { originalUntouched: true, tempApplied: false };
|
|
10117
11998
|
const tmp = mkdtempSync(join(tmpdir(), "fullshield-mcp-verify-"));
|
|
10118
11999
|
try {
|
|
10119
12000
|
cpSync(session.cwd, tmp, { recursive: true });
|
|
10120
|
-
const copy = join(tmp,
|
|
12001
|
+
const copy = join(tmp, hit2.file);
|
|
10121
12002
|
const next = applyUnifiedDiff(readFileSync(copy, "utf8"), proposal.diff);
|
|
10122
12003
|
writeFileSync(copy, next);
|
|
10123
12004
|
const untouched = readFileSync(original, "utf8") === before && statSync(original).mtimeMs === mtime;
|
|
@@ -10341,16 +12222,16 @@ async function confirmTarget(baseUrl, opts) {
|
|
|
10341
12222
|
try {
|
|
10342
12223
|
if (route.kind === "idor") {
|
|
10343
12224
|
const url2 = `${origin}${route.path}`;
|
|
10344
|
-
const
|
|
12225
|
+
const hit3 = await probe(fetchImpl, url2, route.method, void 0, {
|
|
10345
12226
|
"x-user": route.asUser ?? opts.users?.b ?? "bob"
|
|
10346
12227
|
});
|
|
10347
12228
|
const owner = route.expectOwner ?? opts.users?.a ?? "alice";
|
|
10348
|
-
const confirmed2 =
|
|
12229
|
+
const confirmed2 = hit3.ok && hit3.text.includes(owner);
|
|
10349
12230
|
confirmations.push({
|
|
10350
12231
|
kind: "idor",
|
|
10351
12232
|
status: confirmed2 ? "confirmed" : "unconfirmed",
|
|
10352
12233
|
proof: curlProof(route.method, url2, `-H 'x-user: ${route.asUser ?? "bob"}' `),
|
|
10353
|
-
detail:
|
|
12234
|
+
detail: hit3.text.slice(0, 200)
|
|
10354
12235
|
});
|
|
10355
12236
|
continue;
|
|
10356
12237
|
}
|
|
@@ -10363,13 +12244,13 @@ async function confirmTarget(baseUrl, opts) {
|
|
|
10363
12244
|
} else {
|
|
10364
12245
|
body = { [param]: payload };
|
|
10365
12246
|
}
|
|
10366
|
-
const
|
|
10367
|
-
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");
|
|
10368
12249
|
confirmations.push({
|
|
10369
12250
|
kind: route.kind,
|
|
10370
12251
|
status: confirmed ? "confirmed" : "unconfirmed",
|
|
10371
12252
|
proof: curlProof(route.method, url),
|
|
10372
|
-
detail:
|
|
12253
|
+
detail: hit2.text.slice(0, 200)
|
|
10373
12254
|
});
|
|
10374
12255
|
} catch (err) {
|
|
10375
12256
|
confirmations.push({
|
|
@@ -10647,14 +12528,16 @@ function openVexStatement(finding6) {
|
|
|
10647
12528
|
vulnerability: { name: vulnName(finding6) },
|
|
10648
12529
|
products: [{ "@id": npmPurl(finding6.snippet) }]
|
|
10649
12530
|
};
|
|
12531
|
+
const detail = process.env.FULLSHIELD_EXPERIMENTAL_SBOM_REACH === "1" && finding6.reachabilityEvidence?.reason !== void 0 ? { detail: finding6.reachabilityEvidence.reason } : {};
|
|
10650
12532
|
if (notImported(finding6)) {
|
|
10651
12533
|
return {
|
|
10652
12534
|
...base,
|
|
10653
12535
|
status: "not_affected",
|
|
10654
|
-
justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION
|
|
12536
|
+
justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION,
|
|
12537
|
+
...detail
|
|
10655
12538
|
};
|
|
10656
12539
|
}
|
|
10657
|
-
return { ...base, status: "under_investigation" };
|
|
12540
|
+
return { ...base, status: "under_investigation", ...detail };
|
|
10658
12541
|
}
|
|
10659
12542
|
function buildOpenVex(findings) {
|
|
10660
12543
|
const statements = depFindings(findings).map(openVexStatement);
|
|
@@ -10942,6 +12825,28 @@ async function runFix(options = {}) {
|
|
|
10942
12825
|
const after = applyUnifiedDiff(source, proposed.diff);
|
|
10943
12826
|
byFile.set(sourceFile, after);
|
|
10944
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
|
+
}
|
|
10945
12850
|
const notice = appliedOverride ? "run npm install" : void 0;
|
|
10946
12851
|
if (options.apply !== true) {
|
|
10947
12852
|
return { applied: false, reverted: false, patches, findings: result.findings, notice };
|
|
@@ -10989,6 +12894,29 @@ async function restoreSnapshots(cwd, snapshots) {
|
|
|
10989
12894
|
}
|
|
10990
12895
|
}
|
|
10991
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
|
+
}
|
|
10992
12920
|
function runProjectTests(cwd) {
|
|
10993
12921
|
let pkg;
|
|
10994
12922
|
try {
|
|
@@ -11169,6 +13097,153 @@ ${target.message}`
|
|
|
11169
13097
|
// src/cli/program.ts
|
|
11170
13098
|
init_version();
|
|
11171
13099
|
|
|
13100
|
+
// src/core/fingerprint.ts
|
|
13101
|
+
init_finding();
|
|
13102
|
+
function lineIndependentFingerprint(finding6) {
|
|
13103
|
+
const file = (finding6.file ?? "").replace(/\\/g, "/");
|
|
13104
|
+
const text = (finding6.snippet ?? finding6.message ?? "").replace(/\s+/g, " ").trim();
|
|
13105
|
+
return makeFindingId(finding6.ruleId, file, `${finding6.severity}\0${text}`);
|
|
13106
|
+
}
|
|
13107
|
+
|
|
13108
|
+
// src/reporters/ci.ts
|
|
13109
|
+
init_finding();
|
|
13110
|
+
|
|
13111
|
+
// src/reporters/redact.ts
|
|
13112
|
+
init_mask();
|
|
13113
|
+
var SECRET = /\b(?:AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk_live_[A-Za-z0-9]{10,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g;
|
|
13114
|
+
function redactSecrets2(text) {
|
|
13115
|
+
return text.replace(SECRET, (match) => maskSecret(match));
|
|
13116
|
+
}
|
|
13117
|
+
function redactFinding(finding6) {
|
|
13118
|
+
return {
|
|
13119
|
+
...finding6,
|
|
13120
|
+
title: redactSecrets2(finding6.title),
|
|
13121
|
+
message: redactSecrets2(finding6.message),
|
|
13122
|
+
remediation: redactSecrets2(finding6.remediation),
|
|
13123
|
+
snippet: finding6.snippet === void 0 ? void 0 : redactSecrets2(finding6.snippet)
|
|
13124
|
+
};
|
|
13125
|
+
}
|
|
13126
|
+
function redactScan(result) {
|
|
13127
|
+
return { ...result, findings: result.findings.map(redactFinding) };
|
|
13128
|
+
}
|
|
13129
|
+
|
|
13130
|
+
// src/reporters/ci.ts
|
|
13131
|
+
var MESSAGE_CAP = 4e3;
|
|
13132
|
+
function posix2(file) {
|
|
13133
|
+
return file.replace(/\\/g, "/");
|
|
13134
|
+
}
|
|
13135
|
+
function clip(text) {
|
|
13136
|
+
return text.length <= MESSAGE_CAP ? text : `${text.slice(0, MESSAGE_CAP)}\u2026`;
|
|
13137
|
+
}
|
|
13138
|
+
function githubEscape(value, property) {
|
|
13139
|
+
let out = value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A");
|
|
13140
|
+
if (property) out = out.replace(/,/g, "%2C");
|
|
13141
|
+
return out;
|
|
13142
|
+
}
|
|
13143
|
+
function githubLevel(severity) {
|
|
13144
|
+
if (severity === "critical" || severity === "high") return "error";
|
|
13145
|
+
if (severity === "medium") return "warning";
|
|
13146
|
+
return "notice";
|
|
13147
|
+
}
|
|
13148
|
+
function gitlabSeverity(severity) {
|
|
13149
|
+
if (severity === "info") return "info";
|
|
13150
|
+
if (severity === "low") return "minor";
|
|
13151
|
+
if (severity === "medium") return "major";
|
|
13152
|
+
if (severity === "high") return "critical";
|
|
13153
|
+
return "blocker";
|
|
13154
|
+
}
|
|
13155
|
+
function xmlEscape(value) {
|
|
13156
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
13157
|
+
}
|
|
13158
|
+
function oneLine(value) {
|
|
13159
|
+
return value.replace(/\s+/g, " ").trim();
|
|
13160
|
+
}
|
|
13161
|
+
function renderGithubAnnotations(input) {
|
|
13162
|
+
const result = redactScan(input);
|
|
13163
|
+
if (result.findings.length === 0) {
|
|
13164
|
+
return "::notice title=FullShield::No findings\n";
|
|
13165
|
+
}
|
|
13166
|
+
const lines = result.findings.map((finding6) => {
|
|
13167
|
+
const props = [];
|
|
13168
|
+
if (finding6.file !== void 0) props.push(`file=${githubEscape(posix2(finding6.file), true)}`);
|
|
13169
|
+
if (finding6.line !== void 0) props.push(`line=${finding6.line}`);
|
|
13170
|
+
if (finding6.column !== void 0) props.push(`col=${finding6.column + 1}`);
|
|
13171
|
+
props.push(`title=${githubEscape(finding6.ruleId, true)}`);
|
|
13172
|
+
const message = githubEscape(clip(finding6.message), false);
|
|
13173
|
+
return `::${githubLevel(finding6.severity)} ${props.join(",")}::${message}`;
|
|
13174
|
+
});
|
|
13175
|
+
return `${lines.join("\n")}
|
|
13176
|
+
`;
|
|
13177
|
+
}
|
|
13178
|
+
function junitCase(finding6) {
|
|
13179
|
+
const file = finding6.file !== void 0 ? posix2(finding6.file) : "fullshield";
|
|
13180
|
+
const attrs = [
|
|
13181
|
+
`name="${xmlEscape(finding6.ruleId)}"`,
|
|
13182
|
+
`classname="${xmlEscape(file)}"`,
|
|
13183
|
+
`file="${xmlEscape(file)}"`
|
|
13184
|
+
];
|
|
13185
|
+
if (finding6.line !== void 0) attrs.push(`line="${finding6.line}"`);
|
|
13186
|
+
const open = `<testcase ${attrs.join(" ")}>`;
|
|
13187
|
+
if (!meetsThreshold(finding6.severity, "medium")) {
|
|
13188
|
+
return `${open}<skipped message="${xmlEscape(oneLine(clip(finding6.message)))}"/></testcase>`;
|
|
13189
|
+
}
|
|
13190
|
+
const detail = clip(`${finding6.message}${finding6.remediation.length > 0 ? `
|
|
13191
|
+
Fix: ${finding6.remediation}` : ""}`);
|
|
13192
|
+
return `${open}<failure type="${finding6.severity}" message="${xmlEscape(oneLine(clip(finding6.message)))}">${xmlEscape(detail)}</failure></testcase>`;
|
|
13193
|
+
}
|
|
13194
|
+
function renderJunitReport(input) {
|
|
13195
|
+
const result = redactScan(input);
|
|
13196
|
+
const header = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
13197
|
+
if (result.findings.length === 0) {
|
|
13198
|
+
return `${header}
|
|
13199
|
+
<testsuites name="fullshield" tests="1" failures="0" skipped="0"><testsuite name="fullshield" tests="1" failures="0" skipped="0"><testcase name="scan" classname="fullshield"/></testsuite></testsuites>
|
|
13200
|
+
`;
|
|
13201
|
+
}
|
|
13202
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13203
|
+
for (const finding6 of result.findings) {
|
|
13204
|
+
const list = groups.get(finding6.module) ?? [];
|
|
13205
|
+
list.push(finding6);
|
|
13206
|
+
groups.set(finding6.module, list);
|
|
13207
|
+
}
|
|
13208
|
+
let failures = 0;
|
|
13209
|
+
let skipped = 0;
|
|
13210
|
+
const suites = [];
|
|
13211
|
+
for (const [name, findings] of groups) {
|
|
13212
|
+
let suiteFailures = 0;
|
|
13213
|
+
let suiteSkipped = 0;
|
|
13214
|
+
const cases = findings.map((finding6) => {
|
|
13215
|
+
if (meetsThreshold(finding6.severity, "medium")) suiteFailures += 1;
|
|
13216
|
+
else suiteSkipped += 1;
|
|
13217
|
+
return junitCase(finding6);
|
|
13218
|
+
});
|
|
13219
|
+
failures += suiteFailures;
|
|
13220
|
+
skipped += suiteSkipped;
|
|
13221
|
+
suites.push(
|
|
13222
|
+
`<testsuite name="${xmlEscape(name)}" tests="${findings.length}" failures="${suiteFailures}" skipped="${suiteSkipped}">${cases.join("")}</testsuite>`
|
|
13223
|
+
);
|
|
13224
|
+
}
|
|
13225
|
+
return `${header}
|
|
13226
|
+
<testsuites name="fullshield" tests="${result.findings.length}" failures="${failures}" skipped="${skipped}">${suites.join("")}</testsuites>
|
|
13227
|
+
`;
|
|
13228
|
+
}
|
|
13229
|
+
function renderGitlabCodeQuality(input) {
|
|
13230
|
+
const result = redactScan(input);
|
|
13231
|
+
const report = result.findings.map((finding6) => {
|
|
13232
|
+
const path = finding6.file !== void 0 ? posix2(finding6.file) : ".";
|
|
13233
|
+
const location4 = { path };
|
|
13234
|
+
if (finding6.line !== void 0) location4.lines = { begin: finding6.line };
|
|
13235
|
+
return {
|
|
13236
|
+
description: clip(finding6.message),
|
|
13237
|
+
check_name: finding6.ruleId,
|
|
13238
|
+
fingerprint: lineIndependentFingerprint(finding6),
|
|
13239
|
+
severity: gitlabSeverity(finding6.severity),
|
|
13240
|
+
location: location4
|
|
13241
|
+
};
|
|
13242
|
+
});
|
|
13243
|
+
return `${JSON.stringify(report, null, 2)}
|
|
13244
|
+
`;
|
|
13245
|
+
}
|
|
13246
|
+
|
|
11172
13247
|
// src/reporters/console.ts
|
|
11173
13248
|
init_finding();
|
|
11174
13249
|
var SEVERITY_COLOR = {
|
|
@@ -11210,7 +13285,160 @@ function renderFinding(finding6) {
|
|
|
11210
13285
|
}
|
|
11211
13286
|
return lines;
|
|
11212
13287
|
}
|
|
11213
|
-
|
|
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 = {}) {
|
|
13441
|
+
const result = redactScan(input);
|
|
11214
13442
|
const { findings, summary, metadata } = result;
|
|
11215
13443
|
const lines = [];
|
|
11216
13444
|
lines.push(
|
|
@@ -11218,34 +13446,64 @@ function renderConsoleReport(result) {
|
|
|
11218
13446
|
` \u2014 scanned ${metadata.filesScanned} files in ${metadata.durationMs}ms (modules: ${metadata.modulesRun.length > 0 ? metadata.modulesRun.join(", ") : "none"})`
|
|
11219
13447
|
)
|
|
11220
13448
|
);
|
|
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
|
+
}
|
|
11221
13462
|
if (metadata.modulesRun.length < metadata.modulesRequested.length) {
|
|
11222
13463
|
const pending = metadata.modulesRequested.filter((m) => !metadata.modulesRun.includes(m));
|
|
11223
13464
|
lines.push(
|
|
11224
13465
|
pc.dim(`note: requested modules not implemented yet: ${pending.join(", ")}`)
|
|
11225
13466
|
);
|
|
11226
13467
|
}
|
|
11227
|
-
const
|
|
11228
|
-
const
|
|
11229
|
-
const
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
|
|
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;
|
|
11239
13489
|
}
|
|
13490
|
+
lines.push(...renderRow(row));
|
|
13491
|
+
}
|
|
13492
|
+
if (hiddenActionable > 0) {
|
|
13493
|
+
lines.push("");
|
|
13494
|
+
lines.push(`${hiddenActionable} more actionable \u2014 use --json`);
|
|
11240
13495
|
}
|
|
11241
|
-
if (
|
|
13496
|
+
if (!showAll && folded.length > 0) {
|
|
11242
13497
|
lines.push("");
|
|
11243
|
-
lines.push(
|
|
13498
|
+
lines.push(
|
|
13499
|
+
`${folded.length} more (not directly reachable, or below high confidence) \u2014 --all or see JSON`
|
|
13500
|
+
);
|
|
11244
13501
|
}
|
|
11245
13502
|
lines.push("");
|
|
11246
13503
|
if (summary.total === 0) {
|
|
11247
13504
|
lines.push(pc.green("\u2714 No findings."));
|
|
11248
13505
|
} else {
|
|
13506
|
+
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
11249
13507
|
const counts = bySeverityDesc.filter((s) => summary.bySeverity[s] > 0).map((s) => `${summary.bySeverity[s]} ${s}`).join(", ");
|
|
11250
13508
|
lines.push(pc.red(`\u2716 ${summary.total} finding(s): ${counts}`));
|
|
11251
13509
|
}
|
|
@@ -11261,7 +13519,8 @@ function location2(finding6) {
|
|
|
11261
13519
|
if (finding6.file === void 0) return "(project)";
|
|
11262
13520
|
return finding6.line !== void 0 ? `${finding6.file}:${finding6.line}` : finding6.file;
|
|
11263
13521
|
}
|
|
11264
|
-
function renderHtmlReport(
|
|
13522
|
+
function renderHtmlReport(input) {
|
|
13523
|
+
const result = redactScan(input);
|
|
11265
13524
|
const sections = [];
|
|
11266
13525
|
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
11267
13526
|
for (const severity of bySeverityDesc) {
|
|
@@ -11308,7 +13567,7 @@ ${items}`);
|
|
|
11308
13567
|
|
|
11309
13568
|
// src/reporters/json.ts
|
|
11310
13569
|
function publicFinding(finding6) {
|
|
11311
|
-
const copy =
|
|
13570
|
+
const copy = redactFinding(finding6);
|
|
11312
13571
|
delete copy.aiVerdict;
|
|
11313
13572
|
delete copy.exploitScenario;
|
|
11314
13573
|
delete copy.aiReasoning;
|
|
@@ -11332,7 +13591,8 @@ function location3(finding6) {
|
|
|
11332
13591
|
if (finding6.file === void 0) return "(project)";
|
|
11333
13592
|
return finding6.line !== void 0 ? `${finding6.file}:${finding6.line}` : finding6.file;
|
|
11334
13593
|
}
|
|
11335
|
-
function renderMarkdownReport(
|
|
13594
|
+
function renderMarkdownReport(input) {
|
|
13595
|
+
const result = redactScan(input);
|
|
11336
13596
|
const lines = [
|
|
11337
13597
|
"# FullShield report",
|
|
11338
13598
|
"",
|
|
@@ -11373,8 +13633,6 @@ function renderMarkdownReport(result) {
|
|
|
11373
13633
|
}
|
|
11374
13634
|
return lines.join("\n");
|
|
11375
13635
|
}
|
|
11376
|
-
|
|
11377
|
-
// src/reporters/sarif.ts
|
|
11378
13636
|
var SCHEMA_URI = "https://json.schemastore.org/sarif-2.1.0.json";
|
|
11379
13637
|
function sarifLevel(severity) {
|
|
11380
13638
|
if (severity === "critical" || severity === "high") return "error";
|
|
@@ -11384,14 +13642,37 @@ function sarifLevel(severity) {
|
|
|
11384
13642
|
function uri(file) {
|
|
11385
13643
|
return (file ?? "").replace(/\\/g, "/");
|
|
11386
13644
|
}
|
|
11387
|
-
function
|
|
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
|
+
}
|
|
13663
|
+
function renderSarifReport(input) {
|
|
13664
|
+
const result = redactScan(input);
|
|
13665
|
+
const v2 = sarifV2();
|
|
11388
13666
|
const rules = /* @__PURE__ */ new Map();
|
|
11389
13667
|
for (const finding6 of result.findings) {
|
|
11390
13668
|
if (!rules.has(finding6.ruleId)) {
|
|
13669
|
+
const markdown = v2 ? ruleMarkdown(finding6.ruleId) : void 0;
|
|
11391
13670
|
rules.set(finding6.ruleId, {
|
|
11392
13671
|
id: finding6.ruleId,
|
|
11393
13672
|
name: finding6.ruleId,
|
|
11394
|
-
shortDescription: { text: finding6.title }
|
|
13673
|
+
shortDescription: { text: finding6.title },
|
|
13674
|
+
...markdown !== void 0 ? { help: { markdown } } : {},
|
|
13675
|
+
...v2 ? { properties: { precision: "unreviewed" } } : {}
|
|
11395
13676
|
});
|
|
11396
13677
|
}
|
|
11397
13678
|
}
|
|
@@ -11420,6 +13701,7 @@ function renderSarifReport(result) {
|
|
|
11420
13701
|
}
|
|
11421
13702
|
}
|
|
11422
13703
|
] : [];
|
|
13704
|
+
const related = v2 ? relatedLocations(finding6) : void 0;
|
|
11423
13705
|
const codeFlows = finding6.trace !== void 0 && finding6.trace.length > 0 ? [
|
|
11424
13706
|
{
|
|
11425
13707
|
threadFlows: [
|
|
@@ -11442,7 +13724,12 @@ function renderSarifReport(result) {
|
|
|
11442
13724
|
level: sarifLevel(finding6.severity),
|
|
11443
13725
|
message: { text: finding6.message },
|
|
11444
13726
|
locations: location4,
|
|
11445
|
-
...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
|
+
} : {}
|
|
11446
13733
|
};
|
|
11447
13734
|
})
|
|
11448
13735
|
}
|
|
@@ -11451,16 +13738,285 @@ function renderSarifReport(result) {
|
|
|
11451
13738
|
return JSON.stringify(doc, null, 2);
|
|
11452
13739
|
}
|
|
11453
13740
|
|
|
13741
|
+
// src/diff/index.ts
|
|
13742
|
+
init_finding();
|
|
13743
|
+
init_experimental();
|
|
13744
|
+
function posix3(file) {
|
|
13745
|
+
return (file ?? "").replace(/\\/g, "/");
|
|
13746
|
+
}
|
|
13747
|
+
function diffKey(finding6) {
|
|
13748
|
+
return lineIndependentFingerprint(finding6);
|
|
13749
|
+
}
|
|
13750
|
+
function groupById(findings) {
|
|
13751
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13752
|
+
for (const finding6 of findings) {
|
|
13753
|
+
if (finding6.id === void 0 || finding6.id.length === 0) continue;
|
|
13754
|
+
const list = groups.get(finding6.id) ?? [];
|
|
13755
|
+
list.push(finding6);
|
|
13756
|
+
groups.set(finding6.id, list);
|
|
13757
|
+
}
|
|
13758
|
+
return groups;
|
|
13759
|
+
}
|
|
13760
|
+
function pairMoves(removed, added) {
|
|
13761
|
+
const removedIds = groupById(removed);
|
|
13762
|
+
const addedIds = groupById(added);
|
|
13763
|
+
const moved = [];
|
|
13764
|
+
for (const [id, group] of removedIds) {
|
|
13765
|
+
const match = addedIds.get(id);
|
|
13766
|
+
if (group.length === 1 && match?.length === 1 && group[0] !== void 0 && match[0] !== void 0) {
|
|
13767
|
+
moved.push({ before: group[0], after: match[0] });
|
|
13768
|
+
}
|
|
13769
|
+
}
|
|
13770
|
+
return moved;
|
|
13771
|
+
}
|
|
13772
|
+
function byLocation(a, b) {
|
|
13773
|
+
return (a.line ?? 0) - (b.line ?? 0) || a.id.localeCompare(b.id) || a.ruleId.localeCompare(b.ruleId);
|
|
13774
|
+
}
|
|
13775
|
+
function groupFindings(findings) {
|
|
13776
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13777
|
+
for (const finding6 of findings) {
|
|
13778
|
+
const key = diffKey(finding6);
|
|
13779
|
+
const list = groups.get(key) ?? [];
|
|
13780
|
+
list.push(finding6);
|
|
13781
|
+
groups.set(key, list);
|
|
13782
|
+
}
|
|
13783
|
+
return groups;
|
|
13784
|
+
}
|
|
13785
|
+
function diffScans(before, after) {
|
|
13786
|
+
const beforeGroups = groupFindings(before);
|
|
13787
|
+
const afterGroups = groupFindings(after);
|
|
13788
|
+
const unchanged = [];
|
|
13789
|
+
const added = [];
|
|
13790
|
+
const removed = [];
|
|
13791
|
+
const moved = [];
|
|
13792
|
+
const keys = [];
|
|
13793
|
+
for (const key of beforeGroups.keys()) keys.push(key);
|
|
13794
|
+
for (const key of afterGroups.keys()) {
|
|
13795
|
+
if (!beforeGroups.has(key)) keys.push(key);
|
|
13796
|
+
}
|
|
13797
|
+
for (const key of keys) {
|
|
13798
|
+
const left = [...beforeGroups.get(key) ?? []].sort(byLocation);
|
|
13799
|
+
const right = [...afterGroups.get(key) ?? []].sort(byLocation);
|
|
13800
|
+
const paired = Math.min(left.length, right.length);
|
|
13801
|
+
for (let i = 0; i < paired; i += 1) {
|
|
13802
|
+
const earlier = left[i];
|
|
13803
|
+
const later = right[i];
|
|
13804
|
+
if (earlier === void 0 || later === void 0) continue;
|
|
13805
|
+
if ((earlier.line ?? 0) === (later.line ?? 0)) unchanged.push(later);
|
|
13806
|
+
else moved.push({ before: earlier, after: later });
|
|
13807
|
+
}
|
|
13808
|
+
for (const finding6 of left.slice(paired)) removed.push(finding6);
|
|
13809
|
+
for (const finding6 of right.slice(paired)) added.push(finding6);
|
|
13810
|
+
}
|
|
13811
|
+
const idMoves = pairMoves(removed, added);
|
|
13812
|
+
const movedBefore = new Set(idMoves.map((item) => item.before));
|
|
13813
|
+
const movedAfter = new Set(idMoves.map((item) => item.after));
|
|
13814
|
+
return {
|
|
13815
|
+
unchanged,
|
|
13816
|
+
added: added.filter((finding6) => !movedAfter.has(finding6)),
|
|
13817
|
+
removed: removed.filter((finding6) => !movedBefore.has(finding6)),
|
|
13818
|
+
moved: [...moved, ...idMoves]
|
|
13819
|
+
};
|
|
13820
|
+
}
|
|
13821
|
+
function parseScanDocument(text) {
|
|
13822
|
+
let parsed;
|
|
13823
|
+
try {
|
|
13824
|
+
parsed = JSON.parse(text);
|
|
13825
|
+
} catch {
|
|
13826
|
+
throw new Error("scan JSON must be an object with a findings array or a findings array");
|
|
13827
|
+
}
|
|
13828
|
+
const list = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.findings) ? parsed.findings : void 0;
|
|
13829
|
+
if (list === void 0) {
|
|
13830
|
+
throw new Error("scan JSON must be an object with a findings array or a findings array");
|
|
13831
|
+
}
|
|
13832
|
+
return list.map((item) => {
|
|
13833
|
+
if (typeof item !== "object" || item === null) {
|
|
13834
|
+
throw new Error("scan JSON finding is missing ruleId or severity");
|
|
13835
|
+
}
|
|
13836
|
+
const record = item;
|
|
13837
|
+
const ruleId = record.ruleId;
|
|
13838
|
+
const severity = record.severity;
|
|
13839
|
+
if (typeof ruleId !== "string" || ruleId.length === 0 || typeof severity !== "string") {
|
|
13840
|
+
throw new Error("scan JSON finding is missing ruleId or severity");
|
|
13841
|
+
}
|
|
13842
|
+
if (!isSeverity(severity)) {
|
|
13843
|
+
throw new Error(`scan JSON finding has an unknown severity "${severity}"`);
|
|
13844
|
+
}
|
|
13845
|
+
if (record.line !== void 0 && (typeof record.line !== "number" || !Number.isInteger(record.line) || record.line < 0)) {
|
|
13846
|
+
throw new Error("scan JSON finding line must be an integer >= 0");
|
|
13847
|
+
}
|
|
13848
|
+
return item;
|
|
13849
|
+
});
|
|
13850
|
+
}
|
|
13851
|
+
function addedFails(diff, failOn) {
|
|
13852
|
+
return diff.added.some((finding6) => finding6.source !== "ai" && meetsThreshold(finding6.severity, failOn));
|
|
13853
|
+
}
|
|
13854
|
+
var ADDED_CAP = 20;
|
|
13855
|
+
function addedLine(finding6) {
|
|
13856
|
+
const where = finding6.file !== void 0 ? ` ${posix3(finding6.file)}:${finding6.line ?? 0}` : "";
|
|
13857
|
+
return `${finding6.severity} ${finding6.ruleId}${where}`;
|
|
13858
|
+
}
|
|
13859
|
+
function renderDiffText(diff) {
|
|
13860
|
+
const lines = [
|
|
13861
|
+
`fullshield diff: unchanged ${diff.unchanged.length}, added ${diff.added.length}, removed ${diff.removed.length}, moved ${diff.moved.length}`
|
|
13862
|
+
];
|
|
13863
|
+
if (diff.added.length === 0) return lines.join("\n");
|
|
13864
|
+
lines.push("added:");
|
|
13865
|
+
for (const finding6 of diff.added.slice(0, ADDED_CAP)) lines.push(addedLine(finding6));
|
|
13866
|
+
if (diff.added.length > ADDED_CAP) lines.push(`${diff.added.length - ADDED_CAP} more \u2014 use --json`);
|
|
13867
|
+
return lines.join("\n");
|
|
13868
|
+
}
|
|
13869
|
+
|
|
11454
13870
|
// src/cli/program.ts
|
|
13871
|
+
init_experimental();
|
|
11455
13872
|
init_policy();
|
|
11456
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
|
+
|
|
11457
14013
|
// src/sbom/index.ts
|
|
11458
14014
|
init_engine2();
|
|
11459
14015
|
init_lockfile();
|
|
11460
14016
|
init_pins();
|
|
11461
14017
|
init_cyclonedx();
|
|
11462
14018
|
var LOCKFILE_NAMES2 = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
11463
|
-
async function buildSbom(cwd) {
|
|
14019
|
+
async function buildSbom(cwd, options = {}) {
|
|
11464
14020
|
const result = await scan({ cwd, modules: ["deps"] });
|
|
11465
14021
|
let name = "app";
|
|
11466
14022
|
let version = "0.0.0";
|
|
@@ -11486,7 +14042,13 @@ async function buildSbom(cwd) {
|
|
|
11486
14042
|
if (components.length === 0) {
|
|
11487
14043
|
components = exactPinsFromManifest(manifest).map((p) => ({ name: p.name, version: p.version }));
|
|
11488
14044
|
}
|
|
11489
|
-
return renderCycloneDx({
|
|
14045
|
+
return renderCycloneDx({
|
|
14046
|
+
name,
|
|
14047
|
+
version,
|
|
14048
|
+
components,
|
|
14049
|
+
findings: result.findings,
|
|
14050
|
+
includeReach: options.includeReach === true
|
|
14051
|
+
});
|
|
11490
14052
|
}
|
|
11491
14053
|
|
|
11492
14054
|
// src/cli/program.ts
|
|
@@ -12139,6 +14701,9 @@ var FLAGS = [
|
|
|
12139
14701
|
"FULLSHIELD_EXPERIMENTAL_POLICY",
|
|
12140
14702
|
"FULLSHIELD_EXPERIMENTAL_HOST",
|
|
12141
14703
|
"FULLSHIELD_EXPERIMENTAL_TAINT_V2",
|
|
14704
|
+
"FULLSHIELD_EXPERIMENTAL_CI_REPORTS",
|
|
14705
|
+
"FULLSHIELD_EXPERIMENTAL_DIFF",
|
|
14706
|
+
"FULLSHIELD_EXPERIMENTAL_LICENSE",
|
|
12142
14707
|
"FULLSHIELD_PARSE_POOL"
|
|
12143
14708
|
];
|
|
12144
14709
|
function nodeOk(version) {
|
|
@@ -12200,6 +14765,31 @@ var KNOWN = [
|
|
|
12200
14765
|
test: /cannot read config/,
|
|
12201
14766
|
code: "FS-E004",
|
|
12202
14767
|
hint: "Check the path passed to --config."
|
|
14768
|
+
},
|
|
14769
|
+
{
|
|
14770
|
+
test: /FULLSHIELD_EXPERIMENTAL_CI_REPORTS/,
|
|
14771
|
+
code: "FS-E005",
|
|
14772
|
+
hint: "Set FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1 or pass --experimental-ci-reports."
|
|
14773
|
+
},
|
|
14774
|
+
{
|
|
14775
|
+
test: /FULLSHIELD_EXPERIMENTAL_DIFF|scan JSON|cannot read diff/,
|
|
14776
|
+
code: "FS-E006",
|
|
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."
|
|
12203
14793
|
}
|
|
12204
14794
|
];
|
|
12205
14795
|
var DOCS = "https://github.com/kathanp/fullshield/blob/main/docs/errors.md";
|
|
@@ -12251,7 +14841,16 @@ function parseFailOn(value) {
|
|
|
12251
14841
|
function createProgram(io = defaultIO) {
|
|
12252
14842
|
const program = new Command();
|
|
12253
14843
|
program.name("fullshield").description("Full-cycle security scanner for Node.js/TypeScript backends").version(packageVersion()).option("--debug", "print stack traces on CLI errors");
|
|
12254
|
-
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(
|
|
14845
|
+
"--experimental-ci-reports",
|
|
14846
|
+
"experimental: GitHub annotations, JUnit, and GitLab Code Quality (FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1)"
|
|
14847
|
+
).option(
|
|
14848
|
+
"--github-annotations <file>",
|
|
14849
|
+
"experimental: GitHub Actions workflow commands (requires FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1)"
|
|
14850
|
+
).option("--junit <file>", "experimental: JUnit XML (requires FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1)").option(
|
|
14851
|
+
"--gitlab <file>",
|
|
14852
|
+
"experimental: GitLab Code Quality JSON (requires FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1)"
|
|
14853
|
+
).option("--update-baseline", "record current findings as accepted (CI fails only on new ones)").option("--ai [mode]", "experimental: explain findings (off = zero AI network calls)").option("--emit-virtual-patches [file]", "experimental: write .fullshield/vpatches.json").option("--changed", "experimental: scan git staged/changed files only").option("--since <ref>", "experimental: scan files changed since a git ref").option(
|
|
12255
14854
|
"--experimental-sast-gaps",
|
|
12256
14855
|
"experimental: ORM raw / sh -c / execa / zx sinks (FULLSHIELD_EXPERIMENTAL_SAST_GAPS=1)"
|
|
12257
14856
|
).option(
|
|
@@ -12276,6 +14875,27 @@ function createProgram(io = defaultIO) {
|
|
|
12276
14875
|
"--experimental-vex",
|
|
12277
14876
|
"experimental: worm/workflow indicators, overrides, provenance, VEX (FULLSHIELD_EXPERIMENTAL_VEX=1)"
|
|
12278
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(
|
|
12279
14899
|
"--experimental-mcpcfg",
|
|
12280
14900
|
"experimental: MCP client/server config rules (FULLSHIELD_EXPERIMENTAL_MCPCFG=1)"
|
|
12281
14901
|
).option(
|
|
@@ -12304,12 +14924,45 @@ function createProgram(io = defaultIO) {
|
|
|
12304
14924
|
if (opts.experimentalVex === true) {
|
|
12305
14925
|
process.env.FULLSHIELD_EXPERIMENTAL_VEX = "1";
|
|
12306
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
|
+
}
|
|
12307
14951
|
if (opts.experimentalMcpcfg === true) {
|
|
12308
14952
|
process.env.FULLSHIELD_EXPERIMENTAL_MCPCFG = "1";
|
|
12309
14953
|
}
|
|
12310
14954
|
if (opts.prSummary === true && process.env[POLICY_FLAG] !== "1") {
|
|
12311
14955
|
throw new Error(`--pr-summary requires ${POLICY_FLAG}=1`);
|
|
12312
14956
|
}
|
|
14957
|
+
if (opts.experimentalCiReports === true) {
|
|
14958
|
+
process.env[CI_REPORTS_FLAG] = "1";
|
|
14959
|
+
}
|
|
14960
|
+
const wantsCiReport = opts.githubAnnotations !== void 0 || opts.junit !== void 0 || opts.gitlab !== void 0;
|
|
14961
|
+
if (wantsCiReport && process.env[CI_REPORTS_FLAG] !== "1") {
|
|
14962
|
+
throw new Error(
|
|
14963
|
+
`--github-annotations, --junit, and --gitlab require ${CI_REPORTS_FLAG}=1`
|
|
14964
|
+
);
|
|
14965
|
+
}
|
|
12313
14966
|
const result = await scan({
|
|
12314
14967
|
cwd: resolve(path),
|
|
12315
14968
|
modules,
|
|
@@ -12337,6 +14990,15 @@ function createProgram(io = defaultIO) {
|
|
|
12337
14990
|
if (opts.html !== void 0) {
|
|
12338
14991
|
await writeFile(resolve(opts.html), renderHtmlReport(result), "utf8");
|
|
12339
14992
|
}
|
|
14993
|
+
if (opts.githubAnnotations !== void 0) {
|
|
14994
|
+
await writeFile(resolve(opts.githubAnnotations), renderGithubAnnotations(result), "utf8");
|
|
14995
|
+
}
|
|
14996
|
+
if (opts.junit !== void 0) {
|
|
14997
|
+
await writeFile(resolve(opts.junit), renderJunitReport(result), "utf8");
|
|
14998
|
+
}
|
|
14999
|
+
if (opts.gitlab !== void 0) {
|
|
15000
|
+
await writeFile(resolve(opts.gitlab), renderGitlabCodeQuality(result), "utf8");
|
|
15001
|
+
}
|
|
12340
15002
|
await writeLastScan(resolve(path), result);
|
|
12341
15003
|
if (opts.emitVirtualPatches !== void 0) {
|
|
12342
15004
|
if (process.env.FULLSHIELD_EXPERIMENTAL_VPATCH !== "1") {
|
|
@@ -12350,7 +15012,9 @@ function createProgram(io = defaultIO) {
|
|
|
12350
15012
|
if (opts.prSummary === true) {
|
|
12351
15013
|
io.out(renderPrSummary(result.findings));
|
|
12352
15014
|
} else {
|
|
12353
|
-
io.out(
|
|
15015
|
+
io.out(
|
|
15016
|
+
opts.json === true ? renderJsonReport(result) : renderConsoleReport(result, { all: opts.all === true })
|
|
15017
|
+
);
|
|
12354
15018
|
}
|
|
12355
15019
|
if (result.policy !== void 0 && result.policy.errors.length > 0) {
|
|
12356
15020
|
for (const message of result.policy.errors) {
|
|
@@ -12358,6 +15022,12 @@ function createProgram(io = defaultIO) {
|
|
|
12358
15022
|
}
|
|
12359
15023
|
process.exitCode = 1;
|
|
12360
15024
|
}
|
|
15025
|
+
if (result.suppressionErrors !== void 0 && result.suppressionErrors.length > 0) {
|
|
15026
|
+
for (const message of result.suppressionErrors) {
|
|
15027
|
+
io.err(`fullshield: ${message}`);
|
|
15028
|
+
}
|
|
15029
|
+
process.exitCode = 1;
|
|
15030
|
+
}
|
|
12361
15031
|
const failOn = failOnOverride ?? result.policy?.failOn ?? result.config.failOn;
|
|
12362
15032
|
if (result.findings.some((f) => failsCi(f, failOn))) {
|
|
12363
15033
|
process.exitCode = 1;
|
|
@@ -12459,7 +15129,9 @@ function createProgram(io = defaultIO) {
|
|
|
12459
15129
|
if (opts.format !== void 0 && opts.format !== "cyclonedx") {
|
|
12460
15130
|
throw new Error(`unsupported SBOM format "${opts.format}" (valid: cyclonedx)`);
|
|
12461
15131
|
}
|
|
12462
|
-
const doc = await buildSbom(resolve(path)
|
|
15132
|
+
const doc = await buildSbom(resolve(path), {
|
|
15133
|
+
includeReach: process.env.FULLSHIELD_EXPERIMENTAL_SBOM_REACH === "1"
|
|
15134
|
+
});
|
|
12463
15135
|
const errors = validateCycloneDx15(doc);
|
|
12464
15136
|
if (errors.length > 0) throw new Error(`invalid SBOM: ${errors.join("; ")}`);
|
|
12465
15137
|
const json = JSON.stringify(doc, null, 2);
|
|
@@ -12613,6 +15285,87 @@ function createProgram(io = defaultIO) {
|
|
|
12613
15285
|
process.exitCode = 2;
|
|
12614
15286
|
}
|
|
12615
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
|
+
});
|
|
12616
15369
|
program.command("confirm").description("Experimental exploit confirm (localhost/127.0.0.1/FullShield container only)").argument("<url>", "target base URL").action(async (url) => {
|
|
12617
15370
|
try {
|
|
12618
15371
|
if (process.env.FULLSHIELD_EXPERIMENTAL_CONFIRM !== "1") {
|
|
@@ -12748,8 +15501,8 @@ function createProgram(io = defaultIO) {
|
|
|
12748
15501
|
let name = "app";
|
|
12749
15502
|
let version = "0.0.0";
|
|
12750
15503
|
try {
|
|
12751
|
-
const { readFile:
|
|
12752
|
-
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"));
|
|
12753
15506
|
if (typeof pkg.name === "string") name = pkg.name;
|
|
12754
15507
|
if (typeof pkg.version === "string") version = pkg.version;
|
|
12755
15508
|
} catch {
|
|
@@ -12770,6 +15523,31 @@ function createProgram(io = defaultIO) {
|
|
|
12770
15523
|
process.exitCode = 2;
|
|
12771
15524
|
}
|
|
12772
15525
|
});
|
|
15526
|
+
program.command("diff").description("Experimental comparison of two scan --json files (FULLSHIELD_EXPERIMENTAL_DIFF=1)").argument("<before>", "earlier scan JSON").argument("<after>", "later scan JSON").option("--experimental-diff", "enable this command (FULLSHIELD_EXPERIMENTAL_DIFF=1)").option("--json", "print the machine diff").option("--out <file>", "write the machine diff to this file").option("--fail-on <severity>", "exit 1 when an added finding is at or above this severity", "high").action(
|
|
15527
|
+
async (beforePath, afterPath, opts) => {
|
|
15528
|
+
try {
|
|
15529
|
+
if (opts.experimentalDiff === true) process.env[DIFF_FLAG] = "1";
|
|
15530
|
+
if (!diffEnabled()) throw new Error(`diff requires ${DIFF_FLAG}=1`);
|
|
15531
|
+
const failOn = parseFailOn(opts.failOn ?? "high");
|
|
15532
|
+
let beforeText;
|
|
15533
|
+
let afterText;
|
|
15534
|
+
try {
|
|
15535
|
+
beforeText = await readFile(resolve(beforePath), "utf8");
|
|
15536
|
+
afterText = await readFile(resolve(afterPath), "utf8");
|
|
15537
|
+
} catch {
|
|
15538
|
+
throw new Error("cannot read diff input");
|
|
15539
|
+
}
|
|
15540
|
+
const diff = diffScans(parseScanDocument(beforeText), parseScanDocument(afterText));
|
|
15541
|
+
const json = JSON.stringify(diff, null, 2);
|
|
15542
|
+
if (opts.out !== void 0) await writeFile(resolve(opts.out), json, "utf8");
|
|
15543
|
+
io.out(opts.json === true ? json : renderDiffText(diff));
|
|
15544
|
+
if (addedFails(diff, failOn)) process.exitCode = 1;
|
|
15545
|
+
} catch (error) {
|
|
15546
|
+
io.err(formatCliError(error));
|
|
15547
|
+
process.exitCode = 2;
|
|
15548
|
+
}
|
|
15549
|
+
}
|
|
15550
|
+
);
|
|
12773
15551
|
program.command("doctor").description("Report Node, lockfile, cache, flags, config, and whether OSV is offline").argument("[path]", "project directory", ".").option("--offline", "do not describe OSV as allowed to use the network").option("--config <file>", "explicit config file").action(async (path, opts) => {
|
|
12774
15552
|
try {
|
|
12775
15553
|
io.out(await runDoctor(resolve(path), opts));
|