fullshield 0.1.3 → 0.3.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 +112 -1
- package/README.md +27 -3
- package/dist/agent.js +934 -96
- package/dist/cli/index.js +1550 -139
- package/dist/compliance.js +5 -0
- package/dist/dashboard.js +18 -1
- package/dist/iac.js +48 -0
- package/dist/index.d.ts +69 -6
- package/dist/index.js +1227 -103
- package/dist/mcp.js +934 -96
- package/dist/supply.js +13 -3
- package/package.json +2 -2
- package/rules/SAST-SKIP-001.md +9 -0
package/dist/mcp.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { readFile, mkdir, writeFile, stat, readdir } from 'fs/promises';
|
|
3
|
-
import { resolve, join, basename, dirname,
|
|
3
|
+
import { resolve, join, basename, dirname, posix, relative } from 'path';
|
|
4
4
|
import { execFile, spawnSync, spawn, execFileSync } from 'child_process';
|
|
5
5
|
import { createInterface } from 'readline';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
import { existsSync, statSync, readFileSync, readdirSync, mkdtempSync, cpSync, writeFileSync, rmSync } from 'fs';
|
|
8
|
-
import { homedir, tmpdir } from 'os';
|
|
8
|
+
import { homedir, availableParallelism, tmpdir } from 'os';
|
|
9
9
|
import { performance } from 'perf_hooks';
|
|
10
|
-
import
|
|
10
|
+
import { Worker } from 'worker_threads';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import ignoreModule from 'ignore';
|
|
12
13
|
|
|
13
14
|
var __defProp = Object.defineProperty;
|
|
14
15
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -556,6 +557,7 @@ var init_incremental = __esm({
|
|
|
556
557
|
// src/modules/secrets/history.ts
|
|
557
558
|
var history_exports = {};
|
|
558
559
|
__export(history_exports, {
|
|
560
|
+
historyCommitLimit: () => historyCommitLimit,
|
|
559
561
|
scanGitHistory: () => scanGitHistory
|
|
560
562
|
});
|
|
561
563
|
function gitError(error) {
|
|
@@ -598,7 +600,13 @@ function remapHit(hit, file) {
|
|
|
598
600
|
secret: hit.secret
|
|
599
601
|
};
|
|
600
602
|
}
|
|
601
|
-
|
|
603
|
+
function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
|
|
604
|
+
if (raw === void 0 || raw.trim() === "") return 500;
|
|
605
|
+
const parsed = Number(raw);
|
|
606
|
+
if (!Number.isInteger(parsed) || parsed < 1) return 500;
|
|
607
|
+
return Math.min(parsed, 5e3);
|
|
608
|
+
}
|
|
609
|
+
async function streamLogChanges(cwd, maxCommits) {
|
|
602
610
|
const changes = [];
|
|
603
611
|
await gitOk(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
604
612
|
const child = spawn(
|
|
@@ -615,6 +623,8 @@ async function streamLogChanges(cwd) {
|
|
|
615
623
|
"--format=FSCOMMIT:%H",
|
|
616
624
|
"--no-textconv",
|
|
617
625
|
"--no-ext-diff",
|
|
626
|
+
"-n",
|
|
627
|
+
String(maxCommits),
|
|
618
628
|
"HEAD"
|
|
619
629
|
],
|
|
620
630
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -683,8 +693,14 @@ async function headSecretKeys(cwd) {
|
|
|
683
693
|
}
|
|
684
694
|
return keys;
|
|
685
695
|
}
|
|
686
|
-
async function
|
|
687
|
-
const
|
|
696
|
+
async function commitCount(cwd) {
|
|
697
|
+
const raw = await gitOk(cwd, ["rev-list", "--count", "HEAD"]);
|
|
698
|
+
const total = Number(raw.trim());
|
|
699
|
+
return Number.isInteger(total) && total >= 0 ? total : 0;
|
|
700
|
+
}
|
|
701
|
+
async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
702
|
+
const total = await commitCount(cwd);
|
|
703
|
+
const changes = await streamLogChanges(cwd, maxCommits);
|
|
688
704
|
const blobHits = /* @__PURE__ */ new Map();
|
|
689
705
|
const first = /* @__PURE__ */ new Map();
|
|
690
706
|
for (const change of changes) {
|
|
@@ -721,7 +737,13 @@ async function scanGitHistory(cwd) {
|
|
|
721
737
|
secret: hit.secret
|
|
722
738
|
});
|
|
723
739
|
}
|
|
724
|
-
|
|
740
|
+
const scanned = Math.min(maxCommits, total);
|
|
741
|
+
const history = total > maxCommits ? `scanned ${scanned} of ${total} commits` : void 0;
|
|
742
|
+
return {
|
|
743
|
+
findings: hits.map((h) => h.finding),
|
|
744
|
+
hits,
|
|
745
|
+
...history !== void 0 ? { history } : {}
|
|
746
|
+
};
|
|
725
747
|
}
|
|
726
748
|
var execFileAsync2, ZERO_HASH, MAX_BLOB, RAW_LINE;
|
|
727
749
|
var init_history = __esm({
|
|
@@ -1069,7 +1091,8 @@ function walk(node, visit) {
|
|
|
1069
1091
|
}
|
|
1070
1092
|
}
|
|
1071
1093
|
function attachParents(node, parent) {
|
|
1072
|
-
|
|
1094
|
+
const current = node;
|
|
1095
|
+
if (parent !== void 0 || current.parent === void 0) current.parent = parent;
|
|
1073
1096
|
for (const key of Object.keys(node)) {
|
|
1074
1097
|
if (SKIP_KEYS.has(String(key))) continue;
|
|
1075
1098
|
const value = node[key];
|
|
@@ -1139,6 +1162,13 @@ function classesEnabled() {
|
|
|
1139
1162
|
function mcpCfgEnabled() {
|
|
1140
1163
|
return process.env.FULLSHIELD_EXPERIMENTAL_MCPCFG === "1";
|
|
1141
1164
|
}
|
|
1165
|
+
function taintV2Enabled() {
|
|
1166
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
1167
|
+
}
|
|
1168
|
+
var LICENSE_FLAG = "FULLSHIELD_EXPERIMENTAL_LICENSE";
|
|
1169
|
+
function licenseEnabled() {
|
|
1170
|
+
return process.env[LICENSE_FLAG] === "1";
|
|
1171
|
+
}
|
|
1142
1172
|
|
|
1143
1173
|
// src/modules/config/finding.ts
|
|
1144
1174
|
init_finding();
|
|
@@ -1945,6 +1975,103 @@ function exactPinsFromManifest(pkg) {
|
|
|
1945
1975
|
|
|
1946
1976
|
// src/modules/deps/reachability.ts
|
|
1947
1977
|
init_finding();
|
|
1978
|
+
|
|
1979
|
+
// src/modules/deps/symbol-reachability.ts
|
|
1980
|
+
function affectedFunctionNames(vuln) {
|
|
1981
|
+
const names = /* @__PURE__ */ new Set();
|
|
1982
|
+
for (const affected of vuln.affected ?? []) {
|
|
1983
|
+
const list = affected.ecosystem_specific?.affected_functions;
|
|
1984
|
+
if (!Array.isArray(list)) continue;
|
|
1985
|
+
for (const item of list) {
|
|
1986
|
+
if (typeof item === "string" && item.length > 0) names.add(item);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return [...names];
|
|
1990
|
+
}
|
|
1991
|
+
function escapeRegExp(value) {
|
|
1992
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1993
|
+
}
|
|
1994
|
+
function called(source, local) {
|
|
1995
|
+
return new RegExp(`\\b${escapeRegExp(local)}\\s*\\(`).test(source);
|
|
1996
|
+
}
|
|
1997
|
+
function memberCalled(source, object, member) {
|
|
1998
|
+
return new RegExp(`\\b${escapeRegExp(object)}\\s*\\.\\s*${escapeRegExp(member)}\\s*\\(`).test(source);
|
|
1999
|
+
}
|
|
2000
|
+
function sourceCallsAffectedSymbol(source, symbol) {
|
|
2001
|
+
const esc = escapeRegExp(symbol);
|
|
2002
|
+
const named = new RegExp(
|
|
2003
|
+
`import\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`,
|
|
2004
|
+
"g"
|
|
2005
|
+
);
|
|
2006
|
+
let match;
|
|
2007
|
+
while ((match = named.exec(source)) !== null) {
|
|
2008
|
+
const parts = (match[1] ?? "").split(",");
|
|
2009
|
+
for (const part of parts) {
|
|
2010
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
2011
|
+
const imported = bits[0]?.trim();
|
|
2012
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
2013
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
const star = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"][^'"]+['"]/g;
|
|
2017
|
+
while ((match = star.exec(source)) !== null) {
|
|
2018
|
+
const local = match[1];
|
|
2019
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
2020
|
+
}
|
|
2021
|
+
const subDefault = new RegExp(
|
|
2022
|
+
`import\\s+([A-Za-z_$][\\w$]*)\\s+from\\s+['"][^'"]+/${esc}['"]`
|
|
2023
|
+
);
|
|
2024
|
+
const sub = subDefault.exec(source);
|
|
2025
|
+
if (sub?.[1] !== void 0 && called(source, sub[1])) return true;
|
|
2026
|
+
const reqMember = new RegExp(
|
|
2027
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+['"]\\s*\\)\\s*\\.\\s*${esc}`
|
|
2028
|
+
);
|
|
2029
|
+
const reqHit = reqMember.exec(source);
|
|
2030
|
+
if (reqHit?.[1] !== void 0 && called(source, reqHit[1])) return true;
|
|
2031
|
+
const reqNs = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
2032
|
+
while ((match = reqNs.exec(source)) !== null) {
|
|
2033
|
+
const local = match[1];
|
|
2034
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
2035
|
+
}
|
|
2036
|
+
const reqNamed = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
2037
|
+
while ((match = reqNamed.exec(source)) !== null) {
|
|
2038
|
+
const parts = (match[1] ?? "").split(",");
|
|
2039
|
+
for (const part of parts) {
|
|
2040
|
+
const bits = part.trim().split(/\s*:\s*/);
|
|
2041
|
+
const imported = bits[0]?.trim();
|
|
2042
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
2043
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
const reqSub = new RegExp(
|
|
2047
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+/${esc}['"]\\s*\\)`
|
|
2048
|
+
);
|
|
2049
|
+
const subReq = reqSub.exec(source);
|
|
2050
|
+
if (subReq?.[1] !== void 0 && called(source, subReq[1])) return true;
|
|
2051
|
+
const barrel = new RegExp(`export\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`, "g");
|
|
2052
|
+
while ((match = barrel.exec(source)) !== null) {
|
|
2053
|
+
const parts = (match[1] ?? "").split(",");
|
|
2054
|
+
for (const part of parts) {
|
|
2055
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
2056
|
+
const exported = (bits[1] ?? bits[0])?.trim();
|
|
2057
|
+
if (exported === symbol) {
|
|
2058
|
+
const local = bits[0]?.trim();
|
|
2059
|
+
if (local !== void 0 && called(source, local)) return true;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
return false;
|
|
2064
|
+
}
|
|
2065
|
+
function classifySymbolUse(symbols, sources) {
|
|
2066
|
+
for (const symbol of symbols) {
|
|
2067
|
+
for (const source of sources.values()) {
|
|
2068
|
+
if (sourceCallsAffectedSymbol(source, symbol)) return "symbol-reachable";
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
return "symbol-not-called";
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// src/modules/deps/reachability.ts
|
|
1948
2075
|
function packageNameFromSnippet(snippet) {
|
|
1949
2076
|
if (snippet.startsWith("@")) {
|
|
1950
2077
|
const second = snippet.indexOf("@", 1);
|
|
@@ -1973,9 +2100,9 @@ function isProductionSource(rel) {
|
|
|
1973
2100
|
}
|
|
1974
2101
|
function indexPackageUsage(sources) {
|
|
1975
2102
|
const usage = /* @__PURE__ */ new Map();
|
|
1976
|
-
const bump = (pkg,
|
|
2103
|
+
const bump = (pkg, called2) => {
|
|
1977
2104
|
const prev = usage.get(pkg) ?? { called: false };
|
|
1978
|
-
usage.set(pkg, { imported: true, called: prev.called ||
|
|
2105
|
+
usage.set(pkg, { imported: true, called: prev.called || called2 });
|
|
1979
2106
|
};
|
|
1980
2107
|
const nsRe = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1981
2108
|
const namedRe = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
@@ -2031,6 +2158,29 @@ function shortestDependencyPath(target, starts, edges) {
|
|
|
2031
2158
|
}
|
|
2032
2159
|
return void 0;
|
|
2033
2160
|
}
|
|
2161
|
+
function collectDependencyPaths(target, starts, edges, limit = 8) {
|
|
2162
|
+
const out = [];
|
|
2163
|
+
const queue = [];
|
|
2164
|
+
let steps = 0;
|
|
2165
|
+
for (const start of starts) queue.push([start]);
|
|
2166
|
+
while (queue.length > 0 && out.length < limit && steps < 4e3) {
|
|
2167
|
+
steps += 1;
|
|
2168
|
+
const path = queue.shift();
|
|
2169
|
+
if (path === void 0) continue;
|
|
2170
|
+
const last = path[path.length - 1];
|
|
2171
|
+
if (last === void 0) continue;
|
|
2172
|
+
if (last === target) {
|
|
2173
|
+
if (path.length >= 2) out.push(path);
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
if (path.length >= 12) continue;
|
|
2177
|
+
for (const next of edges.get(last) ?? []) {
|
|
2178
|
+
if (path.includes(next)) continue;
|
|
2179
|
+
queue.push([...path, next]);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return out;
|
|
2183
|
+
}
|
|
2034
2184
|
function reachableFrom(target, roots, edges) {
|
|
2035
2185
|
if (roots.has(target)) return true;
|
|
2036
2186
|
return shortestDependencyPath(target, roots, edges) !== void 0;
|
|
@@ -2051,9 +2201,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
2051
2201
|
}
|
|
2052
2202
|
if (graph !== void 0 && graph.edgesKnown) {
|
|
2053
2203
|
const roots = importedDirectNames(productionUsage, graph);
|
|
2054
|
-
const
|
|
2204
|
+
const paths = collectDependencyPaths(pkgName, roots, graph.edges);
|
|
2205
|
+
const path = paths[0];
|
|
2055
2206
|
if (path !== void 0 && path.length >= 2) {
|
|
2056
|
-
return { reachability: "reachable-via-dependency", path };
|
|
2207
|
+
return { reachability: "reachable-via-dependency", path, paths };
|
|
2057
2208
|
}
|
|
2058
2209
|
return { reachability: "not-imported" };
|
|
2059
2210
|
}
|
|
@@ -2063,10 +2214,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
2063
2214
|
return { reachability: "not-imported" };
|
|
2064
2215
|
}
|
|
2065
2216
|
function reachabilitySteps(reachability) {
|
|
2066
|
-
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown") {
|
|
2217
|
+
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
|
|
2067
2218
|
return 0;
|
|
2068
2219
|
}
|
|
2069
|
-
if (reachability === "imported-not-called") return 1;
|
|
2220
|
+
if (reachability === "imported-not-called" || reachability === "symbol-not-called") return 1;
|
|
2070
2221
|
return 2;
|
|
2071
2222
|
}
|
|
2072
2223
|
function downgradeSeverity(severity, steps) {
|
|
@@ -2083,8 +2234,108 @@ function isDevOnly(pkgName, ctx) {
|
|
|
2083
2234
|
if (inProd) return false;
|
|
2084
2235
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
2085
2236
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2237
|
+
var REACH_RANK = {
|
|
2238
|
+
reachable: 0,
|
|
2239
|
+
"symbol-reachable": 1,
|
|
2240
|
+
"reachable-via-dependency": 2,
|
|
2241
|
+
"imported-not-called": 3,
|
|
2242
|
+
"symbol-not-called": 4,
|
|
2243
|
+
"reachability-unknown": 5,
|
|
2244
|
+
"not-imported": 6
|
|
2245
|
+
};
|
|
2246
|
+
function versionFromSnippet(snippet) {
|
|
2247
|
+
const name = packageNameFromSnippet(snippet);
|
|
2248
|
+
if (name === void 0) return snippet;
|
|
2249
|
+
return snippet.slice(name.length + 1);
|
|
2250
|
+
}
|
|
2251
|
+
function pathFields(classified) {
|
|
2252
|
+
const paths = classified.paths ?? (classified.path !== void 0 ? [classified.path] : []);
|
|
2253
|
+
if (paths.length === 0) return {};
|
|
2254
|
+
return {
|
|
2255
|
+
dependencyPath: paths[0],
|
|
2256
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
function appendOtherPaths(message, paths) {
|
|
2260
|
+
if (paths === void 0 || paths.length < 2) return message;
|
|
2261
|
+
const rest = paths.slice(1).map((p) => p.join(" > ")).join("; ");
|
|
2262
|
+
return `${message} Other paths: ${rest}.`;
|
|
2263
|
+
}
|
|
2264
|
+
function groupPackageAdvisories(findings) {
|
|
2265
|
+
const passthrough = [];
|
|
2266
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2267
|
+
for (const finding4 of findings) {
|
|
2268
|
+
if (finding4.ruleId !== "DEPS-VULN-001" && finding4.ruleId !== "DEPS-MALICIOUS-001") {
|
|
2269
|
+
passthrough.push(finding4);
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
const name = packageNameFromSnippet(finding4.snippet ?? "") ?? finding4.snippet ?? finding4.id;
|
|
2273
|
+
const advisory = finding4.references?.[0] ?? finding4.id;
|
|
2274
|
+
const key = `${finding4.ruleId}\0${name}\0${advisory}`;
|
|
2275
|
+
const list = groups.get(key);
|
|
2276
|
+
if (list === void 0) groups.set(key, [finding4]);
|
|
2277
|
+
else list.push(finding4);
|
|
2278
|
+
}
|
|
2279
|
+
const merged = [];
|
|
2280
|
+
for (const list of groups.values()) {
|
|
2281
|
+
const only = list[0];
|
|
2282
|
+
if (list.length === 1 && only !== void 0) {
|
|
2283
|
+
merged.push(only);
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
merged.push(mergeAdvisory(list));
|
|
2287
|
+
}
|
|
2288
|
+
return [...passthrough, ...merged];
|
|
2289
|
+
}
|
|
2290
|
+
function mergeAdvisory(list) {
|
|
2291
|
+
const ranked = [...list].sort(
|
|
2292
|
+
(a, b) => severityRank(b.severity) - severityRank(a.severity) || (REACH_RANK[a.reachability ?? "not-imported"] ?? 9) - (REACH_RANK[b.reachability ?? "not-imported"] ?? 9)
|
|
2293
|
+
);
|
|
2294
|
+
const primary = ranked[0];
|
|
2295
|
+
if (primary === void 0) {
|
|
2296
|
+
throw new Error("empty advisory group");
|
|
2297
|
+
}
|
|
2298
|
+
const name = packageNameFromSnippet(primary.snippet ?? "") ?? "package";
|
|
2299
|
+
const versions = [...new Set(list.map((f) => versionFromSnippet(f.snippet ?? "")))].filter((v) => v.length > 0).sort();
|
|
2300
|
+
const paths = [];
|
|
2301
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
2302
|
+
for (const finding4 of list) {
|
|
2303
|
+
const candidates = finding4.dependencyPaths ?? (finding4.dependencyPath !== void 0 ? [finding4.dependencyPath] : []);
|
|
2304
|
+
for (const path of candidates) {
|
|
2305
|
+
const key = path.join("\0");
|
|
2306
|
+
if (seenPath.has(key)) continue;
|
|
2307
|
+
seenPath.add(key);
|
|
2308
|
+
paths.push(path);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
paths.sort((a, b) => a.length - b.length || a.join(">").localeCompare(b.join(">")));
|
|
2312
|
+
const advisory = primary.references?.[0] ?? "";
|
|
2313
|
+
const onManifest = list.find((f) => f.file === "package.json");
|
|
2314
|
+
const file = onManifest?.file ?? primary.file;
|
|
2315
|
+
const line = onManifest?.line ?? primary.line;
|
|
2316
|
+
const snippet = versions.length > 0 ? `${name}@${versions.join(",")}` : primary.snippet ?? name;
|
|
2317
|
+
let message = primary.message;
|
|
2318
|
+
if (versions.length > 1) {
|
|
2319
|
+
message = `${message} Versions: ${versions.join(", ")}.`;
|
|
2320
|
+
}
|
|
2321
|
+
if (paths.length > 1) {
|
|
2322
|
+
message = `${message} Dependency paths: ${paths.map((p) => p.join(" > ")).join("; ")}.`;
|
|
2323
|
+
}
|
|
2324
|
+
return {
|
|
2325
|
+
...primary,
|
|
2326
|
+
id: makeFindingId(primary.ruleId, file, `${name}|${advisory}|${versions.join(",")}`),
|
|
2327
|
+
file,
|
|
2328
|
+
line,
|
|
2329
|
+
snippet,
|
|
2330
|
+
message,
|
|
2331
|
+
severity: primary.severity,
|
|
2332
|
+
reachability: primary.reachability,
|
|
2333
|
+
...paths[0] !== void 0 ? { dependencyPath: paths[0] } : {},
|
|
2334
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
2338
|
+
const mapped = findings.map((finding4) => {
|
|
2088
2339
|
if (finding4.ruleId === "DEPS-MALICIOUS-001") {
|
|
2089
2340
|
if (finding4.snippet === void 0) return finding4;
|
|
2090
2341
|
const pkg2 = packageNameFromSnippet(finding4.snippet);
|
|
@@ -2094,19 +2345,25 @@ function applyReachability(findings, ctx) {
|
|
|
2094
2345
|
...finding4,
|
|
2095
2346
|
severity: "critical",
|
|
2096
2347
|
reachability: classified2.reachability,
|
|
2097
|
-
...classified2
|
|
2348
|
+
...pathFields(classified2)
|
|
2098
2349
|
};
|
|
2099
2350
|
}
|
|
2100
2351
|
if (finding4.ruleId !== "DEPS-VULN-001" || finding4.snippet === void 0) return finding4;
|
|
2101
2352
|
const pkg = packageNameFromSnippet(finding4.snippet);
|
|
2102
2353
|
if (pkg === void 0 || pkg.length === 0) return finding4;
|
|
2103
|
-
|
|
2354
|
+
let classified = classifyReachability(pkg, ctx.productionUsage, ctx.graph);
|
|
2355
|
+
const named = symbols.get(finding4.id) ?? [];
|
|
2356
|
+
if (named.length > 0 && ctx.sources !== void 0) {
|
|
2357
|
+
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
2358
|
+
classified = { reachability: symbolClass, path: classified.path, paths: classified.paths };
|
|
2359
|
+
}
|
|
2104
2360
|
const steps = reachabilitySteps(classified.reachability);
|
|
2105
2361
|
let severity = steps === 0 ? finding4.severity : downgradeSeverity(finding4.severity, steps);
|
|
2106
2362
|
let message = finding4.message;
|
|
2107
2363
|
if (classified.path !== void 0) {
|
|
2108
2364
|
const shown = classified.path.join(" > ");
|
|
2109
2365
|
message = `${message} Reachable via ${shown}.`;
|
|
2366
|
+
message = appendOtherPaths(message, classified.paths);
|
|
2110
2367
|
}
|
|
2111
2368
|
const devOnly = isDevOnly(pkg, ctx);
|
|
2112
2369
|
if (devOnly) {
|
|
@@ -2116,11 +2373,12 @@ function applyReachability(findings, ctx) {
|
|
|
2116
2373
|
return {
|
|
2117
2374
|
...finding4,
|
|
2118
2375
|
reachability: classified.reachability,
|
|
2119
|
-
...classified
|
|
2376
|
+
...pathFields(classified),
|
|
2120
2377
|
severity,
|
|
2121
2378
|
message
|
|
2122
2379
|
};
|
|
2123
2380
|
});
|
|
2381
|
+
return groupPackageAdvisories(mapped);
|
|
2124
2382
|
}
|
|
2125
2383
|
|
|
2126
2384
|
// src/modules/deps/semver.ts
|
|
@@ -4022,6 +4280,59 @@ function experimentalVex() {
|
|
|
4022
4280
|
return process.env.FULLSHIELD_EXPERIMENTAL_VEX === "1";
|
|
4023
4281
|
}
|
|
4024
4282
|
|
|
4283
|
+
// src/modules/deps/license.ts
|
|
4284
|
+
init_finding();
|
|
4285
|
+
var COPYLEFT = /(?:^|[^\w])(?:AGPL|GPL)(?:-|[^\w]|$)/i;
|
|
4286
|
+
function isCopyleftLicense(license) {
|
|
4287
|
+
return COPYLEFT.test(license);
|
|
4288
|
+
}
|
|
4289
|
+
function licenseText(value) {
|
|
4290
|
+
if (typeof value === "string") return value;
|
|
4291
|
+
if (typeof value === "object" && value !== null && "type" in value) {
|
|
4292
|
+
const type = value.type;
|
|
4293
|
+
if (typeof type === "string") return type;
|
|
4294
|
+
}
|
|
4295
|
+
return void 0;
|
|
4296
|
+
}
|
|
4297
|
+
async function readJson(path) {
|
|
4298
|
+
try {
|
|
4299
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
4300
|
+
} catch {
|
|
4301
|
+
return void 0;
|
|
4302
|
+
}
|
|
4303
|
+
}
|
|
4304
|
+
async function scanCopyleftLicenses(cwd) {
|
|
4305
|
+
const manifest = await readJson(join(cwd, "package.json"));
|
|
4306
|
+
if (typeof manifest !== "object" || manifest === null) return [];
|
|
4307
|
+
const names = /* @__PURE__ */ new Set();
|
|
4308
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
4309
|
+
const block = manifest[field];
|
|
4310
|
+
if (typeof block === "object" && block !== null && !Array.isArray(block)) {
|
|
4311
|
+
for (const name of Object.keys(block)) names.add(name);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
const findings = [];
|
|
4315
|
+
for (const name of names) {
|
|
4316
|
+
const pkg = await readJson(join(cwd, "node_modules", name, "package.json"));
|
|
4317
|
+
if (typeof pkg !== "object" || pkg === null) continue;
|
|
4318
|
+
const license = licenseText(pkg.license);
|
|
4319
|
+
if (license === void 0 || !isCopyleftLicense(license)) continue;
|
|
4320
|
+
findings.push({
|
|
4321
|
+
id: makeFindingId("DEPS-LICENSE-001", "package.json", `${name}|${license}`),
|
|
4322
|
+
ruleId: "DEPS-LICENSE-001",
|
|
4323
|
+
module: "deps",
|
|
4324
|
+
title: `Copyleft dependency ${name}`,
|
|
4325
|
+
severity: "medium",
|
|
4326
|
+
confidence: "high",
|
|
4327
|
+
file: "package.json",
|
|
4328
|
+
snippet: `${name} ${license}`,
|
|
4329
|
+
message: `${name} is licensed ${license}.`,
|
|
4330
|
+
remediation: "Replace the dependency or get a legal review before shipping it in a commercial product."
|
|
4331
|
+
});
|
|
4332
|
+
}
|
|
4333
|
+
return findings;
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4025
4336
|
// src/fixer/eol.ts
|
|
4026
4337
|
function detectEol(text) {
|
|
4027
4338
|
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -4120,7 +4431,7 @@ function walkJs(dir, out = []) {
|
|
|
4120
4431
|
}
|
|
4121
4432
|
return out;
|
|
4122
4433
|
}
|
|
4123
|
-
function
|
|
4434
|
+
function readJson2(path) {
|
|
4124
4435
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
4125
4436
|
}
|
|
4126
4437
|
var TOKEN_RE = /(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/;
|
|
@@ -4134,7 +4445,7 @@ function packageSources(dir) {
|
|
|
4134
4445
|
let scripts = "";
|
|
4135
4446
|
if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
|
|
4136
4447
|
try {
|
|
4137
|
-
const pkg =
|
|
4448
|
+
const pkg = readJson2(manifestPath);
|
|
4138
4449
|
const block = pkg.scripts ?? {};
|
|
4139
4450
|
scripts = Object.values(block).join("\n");
|
|
4140
4451
|
} catch {
|
|
@@ -4408,14 +4719,17 @@ var depsModule = {
|
|
|
4408
4719
|
findings2.push(typosquatFinding("package.json", hit));
|
|
4409
4720
|
}
|
|
4410
4721
|
const osvHits2 = await queryOsv(pins, osvOpts);
|
|
4722
|
+
const symbols2 = /* @__PURE__ */ new Map();
|
|
4411
4723
|
for (const hit of osvHits2) {
|
|
4412
4724
|
for (const vuln of hit.vulns) {
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
);
|
|
4725
|
+
const finding4 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit.pkg, vuln) : vulnFinding("package.json", hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
4726
|
+
const names = affectedFunctionNames(vuln);
|
|
4727
|
+
if (names.length > 0 && finding4.ruleId === "DEPS-VULN-001") symbols2.set(finding4.id, names);
|
|
4728
|
+
findings2.push(finding4);
|
|
4416
4729
|
}
|
|
4417
4730
|
}
|
|
4418
|
-
|
|
4731
|
+
const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
|
|
4732
|
+
return applyReachability(findings2, noLockCtx, symbols2);
|
|
4419
4733
|
}
|
|
4420
4734
|
let contents;
|
|
4421
4735
|
try {
|
|
@@ -4436,18 +4750,21 @@ var depsModule = {
|
|
|
4436
4750
|
}
|
|
4437
4751
|
}
|
|
4438
4752
|
const osvHits = await queryOsv(packages, osvOpts);
|
|
4753
|
+
const symbols = /* @__PURE__ */ new Map();
|
|
4439
4754
|
for (const hit of osvHits) {
|
|
4440
4755
|
for (const vuln of hit.vulns) {
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
);
|
|
4756
|
+
const finding4 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit.pkg, vuln) : vulnFinding(lockFile, hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
4757
|
+
const names = affectedFunctionNames(vuln);
|
|
4758
|
+
if (names.length > 0 && finding4.ruleId === "DEPS-VULN-001") symbols.set(finding4.id, names);
|
|
4759
|
+
findings.push(finding4);
|
|
4444
4760
|
}
|
|
4445
4761
|
}
|
|
4446
|
-
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph));
|
|
4762
|
+
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph), symbols);
|
|
4447
4763
|
if (experimentalVex()) {
|
|
4448
4764
|
reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
|
|
4449
4765
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
4450
4766
|
}
|
|
4767
|
+
if (licenseEnabled()) reached.push(...await scanCopyleftLicenses(ctx.cwd));
|
|
4451
4768
|
return reached;
|
|
4452
4769
|
}
|
|
4453
4770
|
};
|
|
@@ -4477,7 +4794,7 @@ async function reachabilityCtx(cwd, files, graph) {
|
|
|
4477
4794
|
} catch {
|
|
4478
4795
|
}
|
|
4479
4796
|
}
|
|
4480
|
-
return { productionUsage: indexPackageUsage(prodSources), graph };
|
|
4797
|
+
return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
|
|
4481
4798
|
}
|
|
4482
4799
|
|
|
4483
4800
|
// src/modules/mcp/index.ts
|
|
@@ -5015,6 +5332,26 @@ function isXssSanitizerCall(node) {
|
|
|
5015
5332
|
return false;
|
|
5016
5333
|
}
|
|
5017
5334
|
var NO_RETURNERS = /* @__PURE__ */ new Set();
|
|
5335
|
+
var CALLBACK_METHODS = /* @__PURE__ */ new Set([
|
|
5336
|
+
"then",
|
|
5337
|
+
"catch",
|
|
5338
|
+
"finally",
|
|
5339
|
+
"map",
|
|
5340
|
+
"forEach",
|
|
5341
|
+
"filter",
|
|
5342
|
+
"flatMap",
|
|
5343
|
+
"find",
|
|
5344
|
+
"reduce"
|
|
5345
|
+
]);
|
|
5346
|
+
var taintedThisFields = /* @__PURE__ */ new WeakMap();
|
|
5347
|
+
function enclosingClass(node) {
|
|
5348
|
+
let current = node.parent;
|
|
5349
|
+
while (current !== void 0) {
|
|
5350
|
+
if (current.type === "ClassDeclaration" || current.type === "ClassExpression") return current;
|
|
5351
|
+
current = current.parent;
|
|
5352
|
+
}
|
|
5353
|
+
return void 0;
|
|
5354
|
+
}
|
|
5018
5355
|
function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
5019
5356
|
if (node === void 0) return false;
|
|
5020
5357
|
if (isTaintSource(node) || isFrameworkMemberTaint(node) || isFrameworkTaintCall(node) || isMcpArgsTaintSource(node)) {
|
|
@@ -5026,8 +5363,34 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
|
5026
5363
|
case "Identifier":
|
|
5027
5364
|
return tainted.has(node.name);
|
|
5028
5365
|
case "MemberExpression":
|
|
5366
|
+
if (taintV2Enabled() && node.object.type === "ThisExpression") {
|
|
5367
|
+
const cls = enclosingClass(node);
|
|
5368
|
+
const name = propName(node);
|
|
5369
|
+
if (cls !== void 0 && name !== void 0 && taintedThisFields.get(cls)?.has(name) === true) {
|
|
5370
|
+
return true;
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5029
5373
|
return exprIsTainted(node.object, tainted, returners);
|
|
5374
|
+
case "ArrayExpression":
|
|
5375
|
+
if (!taintV2Enabled()) return false;
|
|
5376
|
+
return node.elements.some((el) => {
|
|
5377
|
+
if (el === null) return false;
|
|
5378
|
+
if (el.type === "SpreadElement") return exprIsTainted(el.argument, tainted, returners);
|
|
5379
|
+
return exprIsTainted(el, tainted, returners);
|
|
5380
|
+
});
|
|
5381
|
+
case "ObjectExpression":
|
|
5382
|
+
if (!taintV2Enabled()) return false;
|
|
5383
|
+
return node.properties.some((prop) => {
|
|
5384
|
+
if (prop.type === "SpreadElement") return exprIsTainted(prop.argument, tainted, returners);
|
|
5385
|
+
if (prop.type !== "Property") return false;
|
|
5386
|
+
return exprIsTainted(prop.value, tainted, returners);
|
|
5387
|
+
});
|
|
5030
5388
|
case "CallExpression":
|
|
5389
|
+
if (taintV2Enabled() && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
|
|
5390
|
+
return node.arguments.some(
|
|
5391
|
+
(arg) => arg.type !== "SpreadElement" && exprIsTainted(arg, tainted, returners)
|
|
5392
|
+
);
|
|
5393
|
+
}
|
|
5031
5394
|
if (isSchemaValidatorCall(node)) return false;
|
|
5032
5395
|
if (node.callee.type === "Identifier" && returners.has(node.callee.name)) return true;
|
|
5033
5396
|
if (node.callee.type === "MemberExpression" && exprIsTainted(node.callee.object, tainted, returners)) {
|
|
@@ -5258,7 +5621,66 @@ function scopeReturnsTaint(scope, tainted, returners) {
|
|
|
5258
5621
|
return yes;
|
|
5259
5622
|
}
|
|
5260
5623
|
var MAX_TAINT_HOPS = 5;
|
|
5261
|
-
function
|
|
5624
|
+
function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
5625
|
+
let changed = false;
|
|
5626
|
+
const mark = (fn, name) => {
|
|
5627
|
+
if (name === void 0) return;
|
|
5628
|
+
const set = seeds.get(fn) ?? /* @__PURE__ */ new Set();
|
|
5629
|
+
if (set.has(name)) return;
|
|
5630
|
+
set.add(name);
|
|
5631
|
+
seeds.set(fn, set);
|
|
5632
|
+
changed = true;
|
|
5633
|
+
};
|
|
5634
|
+
for (const fn of functions) {
|
|
5635
|
+
const f = facts.get(fn);
|
|
5636
|
+
if (f === void 0) continue;
|
|
5637
|
+
let parent = fn.parent;
|
|
5638
|
+
while (parent !== void 0 && !isFunctionNode(parent)) {
|
|
5639
|
+
parent = parent.parent;
|
|
5640
|
+
}
|
|
5641
|
+
if (parent !== void 0 && isFunctionNode(parent)) {
|
|
5642
|
+
const outer = facts.get(parent);
|
|
5643
|
+
if (outer !== void 0) {
|
|
5644
|
+
for (const name of outer.tainted) mark(fn, name);
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
walkScope(fn, (node) => {
|
|
5648
|
+
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") {
|
|
5649
|
+
if (node.left.object.type === "ThisExpression" && exprIsTainted(node.right, f.tainted, returners)) {
|
|
5650
|
+
const cls = enclosingClass(node);
|
|
5651
|
+
const field = propName(node.left);
|
|
5652
|
+
if (cls !== void 0 && field !== void 0) {
|
|
5653
|
+
const fields = taintedThisFields.get(cls) ?? /* @__PURE__ */ new Set();
|
|
5654
|
+
if (!fields.has(field)) {
|
|
5655
|
+
fields.add(field);
|
|
5656
|
+
taintedThisFields.set(cls, fields);
|
|
5657
|
+
changed = true;
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
5663
|
+
const method = propName(node.callee);
|
|
5664
|
+
if (method === void 0) return;
|
|
5665
|
+
if (CALLBACK_METHODS.has(method) && exprIsTainted(node.callee.object, f.tainted, returners)) {
|
|
5666
|
+
const callback = node.arguments[0];
|
|
5667
|
+
if (callback !== void 0 && isFunctionNode(callback)) {
|
|
5668
|
+
mark(callback, boundNames(callback.params[0])[0]);
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5671
|
+
if ((method === "call" || method === "apply") && node.callee.object.type === "Identifier") {
|
|
5672
|
+
const target = nameToFn.get(node.callee.object.name);
|
|
5673
|
+
if (target === void 0 || !isFunctionNode(target)) return;
|
|
5674
|
+
const arg = node.arguments[1];
|
|
5675
|
+
if (arg !== void 0 && arg.type !== "SpreadElement" && exprIsTainted(arg, f.tainted, returners)) {
|
|
5676
|
+
mark(target, boundNames(target.params[0])[0]);
|
|
5677
|
+
}
|
|
5678
|
+
}
|
|
5679
|
+
});
|
|
5680
|
+
}
|
|
5681
|
+
return changed;
|
|
5682
|
+
}
|
|
5683
|
+
function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
5262
5684
|
attachParents(ast);
|
|
5263
5685
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
5264
5686
|
const functions = [];
|
|
@@ -5272,7 +5694,7 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5272
5694
|
}
|
|
5273
5695
|
const seeds = /* @__PURE__ */ new Map();
|
|
5274
5696
|
for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
|
|
5275
|
-
const returners = new Set(
|
|
5697
|
+
const returners = new Set(extraReturners2);
|
|
5276
5698
|
for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
|
|
5277
5699
|
let changed = false;
|
|
5278
5700
|
facts.set(ast, analyzeScope(ast, [], returners));
|
|
@@ -5312,6 +5734,9 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5312
5734
|
});
|
|
5313
5735
|
});
|
|
5314
5736
|
}
|
|
5737
|
+
if (taintV2Enabled() && seedTaintV2(functions, facts, seeds, returners, nameToFn)) {
|
|
5738
|
+
changed = true;
|
|
5739
|
+
}
|
|
5315
5740
|
if (!changed) break;
|
|
5316
5741
|
}
|
|
5317
5742
|
return facts;
|
|
@@ -5454,6 +5879,21 @@ function requireCall(node) {
|
|
|
5454
5879
|
}
|
|
5455
5880
|
return void 0;
|
|
5456
5881
|
}
|
|
5882
|
+
function reexportsOf(ast) {
|
|
5883
|
+
const out = /* @__PURE__ */ new Map();
|
|
5884
|
+
walk(ast, (node) => {
|
|
5885
|
+
if (node.type !== "ExportNamedDeclaration" || node.source === null || node.source === void 0) return;
|
|
5886
|
+
const spec = literalString(node.source);
|
|
5887
|
+
if (spec === void 0) return;
|
|
5888
|
+
for (const specNode of node.specifiers) {
|
|
5889
|
+
if (specNode.type !== "ExportSpecifier") continue;
|
|
5890
|
+
const exported = specNode.exported.type === "Identifier" ? specNode.exported.name : void 0;
|
|
5891
|
+
const local = specNode.local.type === "Identifier" ? specNode.local.name : exported;
|
|
5892
|
+
if (exported !== void 0 && local !== void 0) out.set(exported, { spec, name: local });
|
|
5893
|
+
}
|
|
5894
|
+
});
|
|
5895
|
+
return out;
|
|
5896
|
+
}
|
|
5457
5897
|
function resolveRelative(fromFile, spec, files) {
|
|
5458
5898
|
if (!spec.startsWith(".")) return void 0;
|
|
5459
5899
|
const base = posix.normalize(posix.join(posix.dirname(fromFile), spec));
|
|
@@ -5531,6 +5971,22 @@ function runInterproc(parsed) {
|
|
|
5531
5971
|
functions.set(`${fn.file}#${fn.name}`, fn);
|
|
5532
5972
|
}
|
|
5533
5973
|
}
|
|
5974
|
+
if (taintV2Enabled()) {
|
|
5975
|
+
const barrels = /* @__PURE__ */ new Map();
|
|
5976
|
+
for (const p of parsed) barrels.set(p.file, reexportsOf(p.ast));
|
|
5977
|
+
for (const ns of requires.values()) {
|
|
5978
|
+
for (const [local, target] of ns) {
|
|
5979
|
+
const hash = target.indexOf("#");
|
|
5980
|
+
if (hash < 0) continue;
|
|
5981
|
+
const file = target.slice(0, hash);
|
|
5982
|
+
const name = target.slice(hash + 1);
|
|
5983
|
+
const re = barrels.get(file)?.get(name);
|
|
5984
|
+
if (re === void 0) continue;
|
|
5985
|
+
const resolved = resolveRelative(file, re.spec, files);
|
|
5986
|
+
if (resolved !== void 0) ns.set(local, `${resolved}#${re.name}`);
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5534
5990
|
const queue = [];
|
|
5535
5991
|
for (const p of parsed) {
|
|
5536
5992
|
const facts = analyzeFile(p.ast);
|
|
@@ -6046,7 +6502,10 @@ function isMongoReceiver(node, source) {
|
|
|
6046
6502
|
const obj = node.callee.object;
|
|
6047
6503
|
if (obj.type === "Identifier") {
|
|
6048
6504
|
if (isCollectionName(obj.name)) return true;
|
|
6049
|
-
if (isPascalModel(obj.name))
|
|
6505
|
+
if (isPascalModel(obj.name)) {
|
|
6506
|
+
if (/extends\s+BaseEntity\b/.test(source)) return false;
|
|
6507
|
+
return !fileHasSqlOrm;
|
|
6508
|
+
}
|
|
6050
6509
|
return fileHasMongo;
|
|
6051
6510
|
}
|
|
6052
6511
|
if (obj.type === "MemberExpression") {
|
|
@@ -7108,6 +7567,136 @@ function scanPromptInjection(file, source) {
|
|
|
7108
7567
|
return findings;
|
|
7109
7568
|
}
|
|
7110
7569
|
|
|
7570
|
+
// src/runtime/lru.ts
|
|
7571
|
+
var LruMap = class {
|
|
7572
|
+
constructor(max) {
|
|
7573
|
+
this.max = max;
|
|
7574
|
+
if (max < 1) throw new Error("LruMap max must be >= 1");
|
|
7575
|
+
}
|
|
7576
|
+
max;
|
|
7577
|
+
map = /* @__PURE__ */ new Map();
|
|
7578
|
+
get size() {
|
|
7579
|
+
return this.map.size;
|
|
7580
|
+
}
|
|
7581
|
+
has(key) {
|
|
7582
|
+
return this.map.has(key);
|
|
7583
|
+
}
|
|
7584
|
+
get(key) {
|
|
7585
|
+
if (!this.map.has(key)) return void 0;
|
|
7586
|
+
const value = this.map.get(key);
|
|
7587
|
+
this.map.delete(key);
|
|
7588
|
+
this.map.set(key, value);
|
|
7589
|
+
return value;
|
|
7590
|
+
}
|
|
7591
|
+
set(key, value) {
|
|
7592
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
7593
|
+
this.map.set(key, value);
|
|
7594
|
+
while (this.map.size > this.max) {
|
|
7595
|
+
const oldest = this.map.keys().next().value;
|
|
7596
|
+
if (oldest === void 0) break;
|
|
7597
|
+
this.map.delete(oldest);
|
|
7598
|
+
}
|
|
7599
|
+
return this;
|
|
7600
|
+
}
|
|
7601
|
+
};
|
|
7602
|
+
function packageVersion() {
|
|
7603
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7604
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
7605
|
+
try {
|
|
7606
|
+
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
7607
|
+
const pkg = JSON.parse(raw);
|
|
7608
|
+
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
7609
|
+
return pkg.version;
|
|
7610
|
+
}
|
|
7611
|
+
} catch {
|
|
7612
|
+
}
|
|
7613
|
+
const parent = dirname(dir);
|
|
7614
|
+
if (parent === dir) break;
|
|
7615
|
+
dir = parent;
|
|
7616
|
+
}
|
|
7617
|
+
throw new Error("fullshield package.json not found");
|
|
7618
|
+
}
|
|
7619
|
+
|
|
7620
|
+
// src/core/parse-pool.ts
|
|
7621
|
+
var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7622
|
+
var cache = new LruMap(64);
|
|
7623
|
+
function parsePoolEnabled() {
|
|
7624
|
+
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
7625
|
+
}
|
|
7626
|
+
function parsePoolSize() {
|
|
7627
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
7628
|
+
if (raw !== void 0 && raw !== "") {
|
|
7629
|
+
const parsed = Number(raw);
|
|
7630
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
|
|
7631
|
+
}
|
|
7632
|
+
return Math.min(2, Math.max(1, availableParallelism() - 1));
|
|
7633
|
+
}
|
|
7634
|
+
function workerMemoryMb() {
|
|
7635
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
|
|
7636
|
+
const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
|
|
7637
|
+
if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
|
|
7638
|
+
return 512;
|
|
7639
|
+
}
|
|
7640
|
+
function parseCacheKey(code) {
|
|
7641
|
+
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
7642
|
+
}
|
|
7643
|
+
function parseOne(code) {
|
|
7644
|
+
const key = parseCacheKey(code);
|
|
7645
|
+
const hit = cache.get(key);
|
|
7646
|
+
if (hit !== void 0) return Promise.resolve(hit);
|
|
7647
|
+
return new Promise((resolve5, reject) => {
|
|
7648
|
+
const worker = new Worker(fileURLToPath(workerUrl), {
|
|
7649
|
+
workerData: { code },
|
|
7650
|
+
resourceLimits: { maxOldGenerationSizeMb: workerMemoryMb() }
|
|
7651
|
+
});
|
|
7652
|
+
let settled = false;
|
|
7653
|
+
const finish = () => {
|
|
7654
|
+
void worker.terminate();
|
|
7655
|
+
};
|
|
7656
|
+
const succeed = (ast) => {
|
|
7657
|
+
if (settled) return;
|
|
7658
|
+
settled = true;
|
|
7659
|
+
cache.set(key, ast);
|
|
7660
|
+
finish();
|
|
7661
|
+
resolve5(ast);
|
|
7662
|
+
};
|
|
7663
|
+
const fail = (error) => {
|
|
7664
|
+
if (settled) return;
|
|
7665
|
+
settled = true;
|
|
7666
|
+
finish();
|
|
7667
|
+
reject(error);
|
|
7668
|
+
};
|
|
7669
|
+
worker.once("message", (ast) => {
|
|
7670
|
+
succeed(ast);
|
|
7671
|
+
});
|
|
7672
|
+
worker.once("error", (error) => {
|
|
7673
|
+
fail(error);
|
|
7674
|
+
});
|
|
7675
|
+
worker.once("exit", (code2) => {
|
|
7676
|
+
if (settled) return;
|
|
7677
|
+
fail(new Error(`parse worker exited ${code2 ?? "null"}`));
|
|
7678
|
+
});
|
|
7679
|
+
});
|
|
7680
|
+
}
|
|
7681
|
+
async function parseWithPool(codes) {
|
|
7682
|
+
const size = parsePoolSize();
|
|
7683
|
+
const out = new Array(codes.length);
|
|
7684
|
+
let next = 0;
|
|
7685
|
+
async function run() {
|
|
7686
|
+
for (; ; ) {
|
|
7687
|
+
const index = next;
|
|
7688
|
+
next += 1;
|
|
7689
|
+
if (index >= codes.length) return;
|
|
7690
|
+
const code = codes[index];
|
|
7691
|
+
if (code === void 0) return;
|
|
7692
|
+
out[index] = await parseOne(code);
|
|
7693
|
+
}
|
|
7694
|
+
}
|
|
7695
|
+
const workers = Math.min(size, Math.max(1, codes.length));
|
|
7696
|
+
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
7697
|
+
return out;
|
|
7698
|
+
}
|
|
7699
|
+
|
|
7111
7700
|
// src/modules/sast/engine.ts
|
|
7112
7701
|
var sastRules = [
|
|
7113
7702
|
sqliRule,
|
|
@@ -7140,8 +7729,8 @@ function snippetOf(source, node) {
|
|
|
7140
7729
|
if (node.range === void 0) return "";
|
|
7141
7730
|
return source.slice(node.range[0], node.range[1]).replace(/\s+/g, " ").trim().slice(0, 120);
|
|
7142
7731
|
}
|
|
7143
|
-
function findingsFromAst(filename, source, ast,
|
|
7144
|
-
const facts = analyzeFile(ast,
|
|
7732
|
+
function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
|
|
7733
|
+
const facts = analyzeFile(ast, extraReturners2);
|
|
7145
7734
|
const findings = [];
|
|
7146
7735
|
for (const rule of sastRules) {
|
|
7147
7736
|
const ctx = {
|
|
@@ -7219,51 +7808,203 @@ function findingsFromTemplate(filename, source) {
|
|
|
7219
7808
|
xssRule.create(ctx);
|
|
7220
7809
|
return findings;
|
|
7221
7810
|
}
|
|
7811
|
+
function dedupeSastSinks(findings) {
|
|
7812
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7813
|
+
const out = [];
|
|
7814
|
+
for (const finding4 of findings) {
|
|
7815
|
+
if (finding4.module !== "sast" || finding4.line === void 0) {
|
|
7816
|
+
out.push(finding4);
|
|
7817
|
+
continue;
|
|
7818
|
+
}
|
|
7819
|
+
const key = `${finding4.ruleId}\0${finding4.file ?? ""}\0${finding4.line}`;
|
|
7820
|
+
if (seen.has(key)) continue;
|
|
7821
|
+
seen.add(key);
|
|
7822
|
+
out.push(finding4);
|
|
7823
|
+
}
|
|
7824
|
+
return out;
|
|
7825
|
+
}
|
|
7826
|
+
function sastStreamThreshold() {
|
|
7827
|
+
const raw = process.env.FULLSHIELD_SAST_STREAM_AT;
|
|
7828
|
+
if (raw !== void 0 && raw !== "") {
|
|
7829
|
+
const parsed = Number(raw);
|
|
7830
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7831
|
+
}
|
|
7832
|
+
return 4e3;
|
|
7833
|
+
}
|
|
7834
|
+
function sastBatchSize() {
|
|
7835
|
+
const raw = process.env.FULLSHIELD_SAST_BATCH;
|
|
7836
|
+
if (raw !== void 0 && raw !== "") {
|
|
7837
|
+
const parsed = Number(raw);
|
|
7838
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7839
|
+
}
|
|
7840
|
+
return 150;
|
|
7841
|
+
}
|
|
7842
|
+
function skipFinding(rel, message) {
|
|
7843
|
+
return {
|
|
7844
|
+
id: makeFindingId("SAST-SKIP-001", rel, message),
|
|
7845
|
+
ruleId: "SAST-SKIP-001",
|
|
7846
|
+
module: "sast",
|
|
7847
|
+
title: "File skipped by SAST",
|
|
7848
|
+
severity: "info",
|
|
7849
|
+
confidence: "high",
|
|
7850
|
+
file: rel,
|
|
7851
|
+
message,
|
|
7852
|
+
remediation: "Raise sast.maxFileBytes in fullshield.config.json to parse this file, or leave it skipped."
|
|
7853
|
+
};
|
|
7854
|
+
}
|
|
7855
|
+
function skipReason(source, maxBytes) {
|
|
7856
|
+
if (source.includes("\0")) return "it contains NUL bytes";
|
|
7857
|
+
const bytes = Buffer.byteLength(source);
|
|
7858
|
+
if (bytes > maxBytes) return `${bytes} bytes exceeds the SAST parse limit of ${maxBytes} bytes`;
|
|
7859
|
+
return void 0;
|
|
7860
|
+
}
|
|
7861
|
+
async function readScanSource(cwd, rel) {
|
|
7862
|
+
try {
|
|
7863
|
+
return await readFile(join(cwd, rel), "utf8");
|
|
7864
|
+
} catch {
|
|
7865
|
+
return void 0;
|
|
7866
|
+
}
|
|
7867
|
+
}
|
|
7868
|
+
function cloneAst(value) {
|
|
7869
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
7870
|
+
try {
|
|
7871
|
+
return structuredClone(value);
|
|
7872
|
+
} catch {
|
|
7873
|
+
return void 0;
|
|
7874
|
+
}
|
|
7875
|
+
}
|
|
7876
|
+
async function parseInProcess(source) {
|
|
7877
|
+
try {
|
|
7878
|
+
return await parseSource(source);
|
|
7879
|
+
} catch {
|
|
7880
|
+
return void 0;
|
|
7881
|
+
}
|
|
7882
|
+
}
|
|
7883
|
+
async function parseCode(source) {
|
|
7884
|
+
if (!parsePoolEnabled()) return parseInProcess(source);
|
|
7885
|
+
try {
|
|
7886
|
+
const cloned = cloneAst((await parseWithPool([source]))[0]);
|
|
7887
|
+
if (cloned !== void 0) return cloned;
|
|
7888
|
+
} catch {
|
|
7889
|
+
}
|
|
7890
|
+
return parseInProcess(source);
|
|
7891
|
+
}
|
|
7892
|
+
function releaseParsed(parsed) {
|
|
7893
|
+
for (const entry of parsed) {
|
|
7894
|
+
entry.source = "";
|
|
7895
|
+
entry.ast = void 0;
|
|
7896
|
+
}
|
|
7897
|
+
parsed.length = 0;
|
|
7898
|
+
}
|
|
7899
|
+
function extraReturners(rel, ast, files, returning) {
|
|
7900
|
+
const ns = collectRequires(rel, ast, files);
|
|
7901
|
+
const extra = new Set(returning.get(rel));
|
|
7902
|
+
for (const [local, spec] of ns) {
|
|
7903
|
+
const hash = spec.indexOf("#");
|
|
7904
|
+
const file = hash >= 0 ? spec.slice(0, hash) : spec;
|
|
7905
|
+
const exp = hash >= 0 ? spec.slice(hash + 1) : local;
|
|
7906
|
+
if (returning.get(file)?.has(exp)) extra.add(local);
|
|
7907
|
+
}
|
|
7908
|
+
return extra;
|
|
7909
|
+
}
|
|
7910
|
+
async function runSastStreaming(ctx) {
|
|
7911
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
7912
|
+
const findings = [];
|
|
7913
|
+
const returning = /* @__PURE__ */ new Map();
|
|
7914
|
+
const parseable = [];
|
|
7915
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
7916
|
+
let projectCsrf = false;
|
|
7917
|
+
for (const rel of ctx.files) {
|
|
7918
|
+
const isSource = SOURCE_EXT.test(rel);
|
|
7919
|
+
const isTemplate = TEMPLATE_EXT.test(rel);
|
|
7920
|
+
if (!isSource && !isTemplate) continue;
|
|
7921
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
7922
|
+
if (source === void 0) continue;
|
|
7923
|
+
const why = skipReason(source, maxBytes);
|
|
7924
|
+
if (why !== void 0) {
|
|
7925
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
7926
|
+
continue;
|
|
7927
|
+
}
|
|
7928
|
+
if (sourceHasCsrfGuard(source)) projectCsrf = true;
|
|
7929
|
+
if (isTemplate) {
|
|
7930
|
+
findings.push(...findingsFromTemplate(rel, source));
|
|
7931
|
+
continue;
|
|
7932
|
+
}
|
|
7933
|
+
const ast = await parseCode(source);
|
|
7934
|
+
if (ast === void 0) continue;
|
|
7935
|
+
returning.set(rel, functionsThatReturnTaint(ast));
|
|
7936
|
+
parseable.push(rel);
|
|
7937
|
+
fileSet.add(rel);
|
|
7938
|
+
}
|
|
7939
|
+
for (const rel of parseable) {
|
|
7940
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
7941
|
+
if (source === void 0) continue;
|
|
7942
|
+
const ast = await parseCode(source);
|
|
7943
|
+
if (ast === void 0) continue;
|
|
7944
|
+
findings.push(
|
|
7945
|
+
...findingsFromAst(rel, source, ast, extraReturners(rel, ast, fileSet, returning), projectCsrf)
|
|
7946
|
+
);
|
|
7947
|
+
}
|
|
7948
|
+
const batch = sastBatchSize();
|
|
7949
|
+
for (let i = 0; i < parseable.length; i += batch) {
|
|
7950
|
+
const parsed = [];
|
|
7951
|
+
for (const rel of parseable.slice(i, i + batch)) {
|
|
7952
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
7953
|
+
if (source === void 0) continue;
|
|
7954
|
+
const ast = await parseCode(source);
|
|
7955
|
+
if (ast === void 0) continue;
|
|
7956
|
+
parsed.push({ file: rel, source, ast });
|
|
7957
|
+
}
|
|
7958
|
+
findings.push(...runInterproc(parsed));
|
|
7959
|
+
releaseParsed(parsed);
|
|
7960
|
+
}
|
|
7961
|
+
return dedupeSastSinks(findings);
|
|
7962
|
+
}
|
|
7222
7963
|
var sastModule = {
|
|
7223
7964
|
name: "sast",
|
|
7224
7965
|
async run(ctx) {
|
|
7966
|
+
const candidates = ctx.files.filter((rel) => SOURCE_EXT.test(rel) || TEMPLATE_EXT.test(rel));
|
|
7967
|
+
if (candidates.length > sastStreamThreshold()) return runSastStreaming(ctx);
|
|
7968
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
7225
7969
|
const findings = [];
|
|
7226
7970
|
const parsed = [];
|
|
7227
|
-
for (const rel of
|
|
7228
|
-
const
|
|
7229
|
-
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
source = await readFile(join(ctx.cwd, rel), "utf8");
|
|
7234
|
-
} catch {
|
|
7971
|
+
for (const rel of candidates) {
|
|
7972
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
7973
|
+
if (source === void 0) continue;
|
|
7974
|
+
const why = skipReason(source, maxBytes);
|
|
7975
|
+
if (why !== void 0) {
|
|
7976
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
7235
7977
|
continue;
|
|
7236
7978
|
}
|
|
7237
|
-
if (
|
|
7238
|
-
if (isTemplate) {
|
|
7979
|
+
if (TEMPLATE_EXT.test(rel)) {
|
|
7239
7980
|
findings.push(...findingsFromTemplate(rel, source));
|
|
7240
7981
|
continue;
|
|
7241
7982
|
}
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
} catch {
|
|
7246
|
-
}
|
|
7983
|
+
const ast = await parseCode(source);
|
|
7984
|
+
if (ast === void 0) continue;
|
|
7985
|
+
parsed.push({ file: rel, source, ast });
|
|
7247
7986
|
}
|
|
7248
7987
|
const files = new Set(parsed.map((p) => p.file));
|
|
7249
7988
|
const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
|
|
7250
7989
|
const returning = /* @__PURE__ */ new Map();
|
|
7251
|
-
for (const
|
|
7252
|
-
returning.set(
|
|
7253
|
-
}
|
|
7254
|
-
for (const
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7990
|
+
for (const entry of parsed) {
|
|
7991
|
+
returning.set(entry.file, functionsThatReturnTaint(entry.ast));
|
|
7992
|
+
}
|
|
7993
|
+
for (const entry of parsed) {
|
|
7994
|
+
findings.push(
|
|
7995
|
+
...findingsFromAst(
|
|
7996
|
+
entry.file,
|
|
7997
|
+
entry.source,
|
|
7998
|
+
entry.ast,
|
|
7999
|
+
extraReturners(entry.file, entry.ast, files, returning),
|
|
8000
|
+
projectCsrf
|
|
8001
|
+
)
|
|
8002
|
+
);
|
|
7264
8003
|
}
|
|
7265
8004
|
findings.push(...runInterproc(parsed));
|
|
7266
|
-
|
|
8005
|
+
const out = dedupeSastSinks(findings);
|
|
8006
|
+
releaseParsed(parsed);
|
|
8007
|
+
return out;
|
|
7267
8008
|
}
|
|
7268
8009
|
};
|
|
7269
8010
|
|
|
@@ -7618,7 +8359,7 @@ function createAiClient(options) {
|
|
|
7618
8359
|
const useFixtures = options.useFixtures ?? !isLiveAi();
|
|
7619
8360
|
const model = options.model ?? process.env.FULLSHIELD_AI_MODEL ?? (useFixtures ? "recorded-fixture" : "qwen2.5-coder:7b");
|
|
7620
8361
|
const provider = options.provider ?? process.env.FULLSHIELD_AI_PROVIDER ?? (useFixtures ? "fixture" : "ollama");
|
|
7621
|
-
const
|
|
8362
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
7622
8363
|
let spentUsd = 0;
|
|
7623
8364
|
let networkCalls = 0;
|
|
7624
8365
|
const liveCost = provider === "ollama" ? 0 : FIXTURE_COST;
|
|
@@ -7638,7 +8379,7 @@ function createAiClient(options) {
|
|
|
7638
8379
|
const redactedPrompt = `${modeInstruction(req.mode)}
|
|
7639
8380
|
${wrapUntrustedCode(req.prompt)}`;
|
|
7640
8381
|
const key = cacheKey(req.model ?? model, redactedPrompt);
|
|
7641
|
-
const cached =
|
|
8382
|
+
const cached = cache2.get(key);
|
|
7642
8383
|
if (cached !== void 0) {
|
|
7643
8384
|
return { ...cached, cached: true, costUsd: 0 };
|
|
7644
8385
|
}
|
|
@@ -7652,7 +8393,7 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7652
8393
|
cached: false,
|
|
7653
8394
|
redactedPrompt
|
|
7654
8395
|
};
|
|
7655
|
-
|
|
8396
|
+
cache2.set(key, response2);
|
|
7656
8397
|
return response2;
|
|
7657
8398
|
}
|
|
7658
8399
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -7702,21 +8443,42 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7702
8443
|
cached: false,
|
|
7703
8444
|
redactedPrompt
|
|
7704
8445
|
};
|
|
7705
|
-
|
|
8446
|
+
cache2.set(key, response);
|
|
7706
8447
|
return response;
|
|
7707
8448
|
}
|
|
7708
8449
|
};
|
|
7709
8450
|
}
|
|
8451
|
+
function isIsoDate(value) {
|
|
8452
|
+
if (!/^\d{4}-\d{2}-\d{2}(T[\d:.+\-Z]+)?$/.test(value)) return false;
|
|
8453
|
+
return Number.isFinite(Date.parse(value));
|
|
8454
|
+
}
|
|
8455
|
+
function expiresAtMs(expires) {
|
|
8456
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8457
|
+
return Date.parse(expires);
|
|
8458
|
+
}
|
|
8459
|
+
function validSuppression(value) {
|
|
8460
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
8461
|
+
const row = value;
|
|
8462
|
+
if (typeof row.ruleId !== "string" || row.ruleId.length === 0) return void 0;
|
|
8463
|
+
if (typeof row.owner !== "string" || row.owner.length === 0) return void 0;
|
|
8464
|
+
if (typeof row.reason !== "string" || row.reason.length === 0) return void 0;
|
|
8465
|
+
if (typeof row.expires !== "string" || !isIsoDate(row.expires)) return void 0;
|
|
8466
|
+
const file = row.file === void 0 ? void 0 : row.file.replace(/\\/g, "/");
|
|
8467
|
+
if (row.file !== void 0 && (typeof row.file !== "string" || row.file.length === 0)) return void 0;
|
|
8468
|
+
return { ruleId: row.ruleId, file, owner: row.owner, reason: row.reason, expires: row.expires };
|
|
8469
|
+
}
|
|
7710
8470
|
async function readBaseline(path) {
|
|
7711
8471
|
try {
|
|
7712
8472
|
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
7713
8473
|
if (raw.version !== 1 || !Array.isArray(raw.findings)) return void 0;
|
|
8474
|
+
if (raw.suppressions !== void 0 && !Array.isArray(raw.suppressions)) return void 0;
|
|
7714
8475
|
return raw;
|
|
7715
8476
|
} catch {
|
|
7716
8477
|
return void 0;
|
|
7717
8478
|
}
|
|
7718
8479
|
}
|
|
7719
8480
|
async function writeBaseline(path, findings) {
|
|
8481
|
+
const existing = await readBaseline(path);
|
|
7720
8482
|
const doc = {
|
|
7721
8483
|
version: 1,
|
|
7722
8484
|
findings: findings.map((f) => ({
|
|
@@ -7724,7 +8486,8 @@ async function writeBaseline(path, findings) {
|
|
|
7724
8486
|
ruleId: f.ruleId,
|
|
7725
8487
|
file: f.file,
|
|
7726
8488
|
line: f.line
|
|
7727
|
-
}))
|
|
8489
|
+
})),
|
|
8490
|
+
...existing?.suppressions !== void 0 ? { suppressions: existing.suppressions } : {}
|
|
7728
8491
|
};
|
|
7729
8492
|
await writeFile(path, `${JSON.stringify(doc, null, 2)}
|
|
7730
8493
|
`, "utf8");
|
|
@@ -7734,6 +8497,30 @@ function applyBaseline(findings, baseline) {
|
|
|
7734
8497
|
const known = new Set(baseline.findings.map((f) => f.id));
|
|
7735
8498
|
return findings.filter((f) => !known.has(f.id));
|
|
7736
8499
|
}
|
|
8500
|
+
function applySuppressions(findings, rows, now = /* @__PURE__ */ new Date()) {
|
|
8501
|
+
if (rows === void 0 || rows.length === 0) return { findings: [...findings], errors: [] };
|
|
8502
|
+
const errors = [];
|
|
8503
|
+
const active = [];
|
|
8504
|
+
rows.forEach((row, index) => {
|
|
8505
|
+
const parsed = validSuppression(row);
|
|
8506
|
+
if (parsed === void 0) {
|
|
8507
|
+
errors.push(`suppression ${index + 1} needs ruleId, owner, reason, and an ISO expires date`);
|
|
8508
|
+
return;
|
|
8509
|
+
}
|
|
8510
|
+
if (expiresAtMs(parsed.expires) < now.getTime()) {
|
|
8511
|
+
errors.push(
|
|
8512
|
+
`suppression for ${parsed.ruleId} expired on ${parsed.expires} (owner: ${parsed.owner}); renew or remove it`
|
|
8513
|
+
);
|
|
8514
|
+
return;
|
|
8515
|
+
}
|
|
8516
|
+
active.push(parsed);
|
|
8517
|
+
});
|
|
8518
|
+
const kept = findings.filter((finding4) => {
|
|
8519
|
+
const file = (finding4.file ?? "").replace(/\\/g, "/");
|
|
8520
|
+
return !active.some((row) => row.ruleId === finding4.ruleId && (row.file === void 0 || row.file === file));
|
|
8521
|
+
});
|
|
8522
|
+
return { findings: kept, errors };
|
|
8523
|
+
}
|
|
7737
8524
|
|
|
7738
8525
|
// src/core/config.ts
|
|
7739
8526
|
init_finding();
|
|
@@ -7742,14 +8529,27 @@ var DEFAULT_CONFIG = {
|
|
|
7742
8529
|
modules: [...MODULE_NAMES],
|
|
7743
8530
|
failOn: "high",
|
|
7744
8531
|
ignore: ["dist/**", "**/*.test.ts"],
|
|
8532
|
+
defaultExcludes: true,
|
|
7745
8533
|
baseline: ".fullshield-baseline.json",
|
|
7746
8534
|
notify: { slackWebhookEnv: "FULLSHIELD_SLACK_WEBHOOK" },
|
|
7747
8535
|
deps: { offline: false },
|
|
7748
|
-
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 }
|
|
8536
|
+
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 },
|
|
8537
|
+
sast: { maxFileBytes: 1024 * 1024 }
|
|
7749
8538
|
};
|
|
7750
8539
|
var ConfigError = class extends Error {
|
|
7751
8540
|
};
|
|
7752
|
-
var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
8541
|
+
var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
8542
|
+
"modules",
|
|
8543
|
+
"failOn",
|
|
8544
|
+
"ignore",
|
|
8545
|
+
"defaultExcludes",
|
|
8546
|
+
"excludes",
|
|
8547
|
+
"baseline",
|
|
8548
|
+
"notify",
|
|
8549
|
+
"deps",
|
|
8550
|
+
"ai",
|
|
8551
|
+
"sast"
|
|
8552
|
+
]);
|
|
7753
8553
|
function validate(raw, source) {
|
|
7754
8554
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
7755
8555
|
throw new ConfigError(`${source}: config must be a JSON object`);
|
|
@@ -7851,6 +8651,32 @@ function validate(raw, source) {
|
|
|
7851
8651
|
}
|
|
7852
8652
|
out.ai = ai;
|
|
7853
8653
|
}
|
|
8654
|
+
if (record.defaultExcludes !== void 0) {
|
|
8655
|
+
if (typeof record.defaultExcludes !== "boolean") {
|
|
8656
|
+
throw new ConfigError(`${source}: "defaultExcludes" must be a boolean`);
|
|
8657
|
+
}
|
|
8658
|
+
out.defaultExcludes = record.defaultExcludes;
|
|
8659
|
+
}
|
|
8660
|
+
if (record.excludes !== void 0) {
|
|
8661
|
+
if (!Array.isArray(record.excludes) || record.excludes.some((p) => typeof p !== "string")) {
|
|
8662
|
+
throw new ConfigError(`${source}: "excludes" must be an array of glob strings`);
|
|
8663
|
+
}
|
|
8664
|
+
out.excludes = record.excludes;
|
|
8665
|
+
}
|
|
8666
|
+
if (record.sast !== void 0) {
|
|
8667
|
+
if (typeof record.sast !== "object" || record.sast === null || Array.isArray(record.sast)) {
|
|
8668
|
+
throw new ConfigError(`${source}: "sast" must be an object`);
|
|
8669
|
+
}
|
|
8670
|
+
const rawSast = record.sast;
|
|
8671
|
+
const sast = { maxFileBytes: 1024 * 1024 };
|
|
8672
|
+
if (rawSast.maxFileBytes !== void 0) {
|
|
8673
|
+
if (typeof rawSast.maxFileBytes !== "number" || rawSast.maxFileBytes < 1) {
|
|
8674
|
+
throw new ConfigError(`${source}: "sast.maxFileBytes" must be a positive number`);
|
|
8675
|
+
}
|
|
8676
|
+
sast.maxFileBytes = rawSast.maxFileBytes;
|
|
8677
|
+
}
|
|
8678
|
+
out.sast = sast;
|
|
8679
|
+
}
|
|
7854
8680
|
return out;
|
|
7855
8681
|
}
|
|
7856
8682
|
async function loadConfig(cwd, configPath) {
|
|
@@ -7877,10 +8703,26 @@ async function loadConfig(cwd, configPath) {
|
|
|
7877
8703
|
return { ...structuredClone(DEFAULT_CONFIG), ...validate(raw, file) };
|
|
7878
8704
|
}
|
|
7879
8705
|
var ignoreFactory = ignoreModule.default;
|
|
7880
|
-
var
|
|
8706
|
+
var HARD_IGNORED = [".git/", ".fullshield/"];
|
|
8707
|
+
var DEFAULT_EXCLUDES = [
|
|
8708
|
+
"node_modules/",
|
|
8709
|
+
"dist/",
|
|
8710
|
+
"build/",
|
|
8711
|
+
".next/",
|
|
8712
|
+
"coverage/",
|
|
8713
|
+
"vendor/",
|
|
8714
|
+
"vendored/",
|
|
8715
|
+
"third_party/",
|
|
8716
|
+
"third-party/",
|
|
8717
|
+
"compiled/",
|
|
8718
|
+
"*.min.js"
|
|
8719
|
+
];
|
|
7881
8720
|
async function walkFiles(options) {
|
|
7882
8721
|
const ig = ignoreFactory();
|
|
7883
|
-
ig.add(
|
|
8722
|
+
ig.add(HARD_IGNORED);
|
|
8723
|
+
if (options.defaultExcludes !== false) {
|
|
8724
|
+
ig.add(options.excludes ?? DEFAULT_EXCLUDES);
|
|
8725
|
+
}
|
|
7884
8726
|
try {
|
|
7885
8727
|
ig.add(await readFile(join(options.cwd, ".gitignore"), "utf8"));
|
|
7886
8728
|
} catch {
|
|
@@ -7929,23 +8771,6 @@ async function listStagedFiles(cwd) {
|
|
|
7929
8771
|
// src/core/engine.ts
|
|
7930
8772
|
init_suppression();
|
|
7931
8773
|
init_finding();
|
|
7932
|
-
function packageVersion() {
|
|
7933
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7934
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
7935
|
-
try {
|
|
7936
|
-
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
7937
|
-
const pkg = JSON.parse(raw);
|
|
7938
|
-
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
7939
|
-
return pkg.version;
|
|
7940
|
-
}
|
|
7941
|
-
} catch {
|
|
7942
|
-
}
|
|
7943
|
-
const parent = dirname(dir);
|
|
7944
|
-
if (parent === dir) break;
|
|
7945
|
-
dir = parent;
|
|
7946
|
-
}
|
|
7947
|
-
throw new Error("fullshield package.json not found");
|
|
7948
|
-
}
|
|
7949
8774
|
|
|
7950
8775
|
// src/policy/index.ts
|
|
7951
8776
|
init_finding();
|
|
@@ -8079,7 +8904,7 @@ function resolvePolicyBranch(cwd, env = process.env) {
|
|
|
8079
8904
|
return "*";
|
|
8080
8905
|
}
|
|
8081
8906
|
}
|
|
8082
|
-
function
|
|
8907
|
+
function expiresAtMs2(expires) {
|
|
8083
8908
|
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
|
|
8084
8909
|
return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8085
8910
|
}
|
|
@@ -8101,7 +8926,7 @@ function applyPolicy(findings, policy, ctx = {}) {
|
|
|
8101
8926
|
});
|
|
8102
8927
|
const suppressed = /* @__PURE__ */ new Set();
|
|
8103
8928
|
for (const exception of policy.exceptions ?? []) {
|
|
8104
|
-
if (
|
|
8929
|
+
if (expiresAtMs2(exception.expires) < now.getTime()) {
|
|
8105
8930
|
errors.push(
|
|
8106
8931
|
`policy exception for ${exception.ruleId} expired on ${exception.expires} (owner: ${exception.owner}); renew or remove it`
|
|
8107
8932
|
);
|
|
@@ -8170,7 +8995,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8170
8995
|
options.modules ?? config.modules,
|
|
8171
8996
|
loadedPolicy?.requiredModules
|
|
8172
8997
|
);
|
|
8173
|
-
let files = await walkFiles({
|
|
8998
|
+
let files = await walkFiles({
|
|
8999
|
+
cwd,
|
|
9000
|
+
ignore: config.ignore,
|
|
9001
|
+
defaultExcludes: config.defaultExcludes,
|
|
9002
|
+
excludes: config.excludes
|
|
9003
|
+
});
|
|
8174
9004
|
if (options.staged === true) {
|
|
8175
9005
|
const staged = new Set(await listStagedFiles(cwd));
|
|
8176
9006
|
files = files.filter((file) => staged.has(file));
|
|
@@ -8204,6 +9034,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8204
9034
|
);
|
|
8205
9035
|
}
|
|
8206
9036
|
const extraHits = [];
|
|
9037
|
+
let historyNotice;
|
|
8207
9038
|
const secretsWanted = modulesRequested.includes("secrets");
|
|
8208
9039
|
if (secretsAdvEnabled() && secretsWanted) {
|
|
8209
9040
|
if (options.history === true) {
|
|
@@ -8211,6 +9042,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8211
9042
|
const hist = await scanGitHistory2(cwd);
|
|
8212
9043
|
extraHits.push(...hist.hits);
|
|
8213
9044
|
collected.push(...hist.findings);
|
|
9045
|
+
historyNotice = hist.history;
|
|
8214
9046
|
}
|
|
8215
9047
|
if (options.bundle !== void 0 && options.bundle.length > 0) {
|
|
8216
9048
|
const { scanBundle: scanBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
@@ -8231,7 +9063,11 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8231
9063
|
if (options.updateBaseline === true) {
|
|
8232
9064
|
await writeBaseline(baselinePath, unsuppressed);
|
|
8233
9065
|
}
|
|
8234
|
-
|
|
9066
|
+
const baselineDoc = await readBaseline(baselinePath);
|
|
9067
|
+
let findings = sortFindings(applyBaseline(unsuppressed, baselineDoc));
|
|
9068
|
+
const suppressed = applySuppressions(findings, baselineDoc?.suppressions);
|
|
9069
|
+
findings = sortFindings(suppressed.findings);
|
|
9070
|
+
const suppressionErrors = suppressed.errors;
|
|
8235
9071
|
if (secretsAdvEnabled() && secretsWanted && options.verifySecrets === true && options.iOwnTheseCredentials === true) {
|
|
8236
9072
|
const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
8237
9073
|
const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
@@ -8303,10 +9139,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8303
9139
|
cwd,
|
|
8304
9140
|
filesScanned: files.length,
|
|
8305
9141
|
modulesRequested: [...modulesRequested],
|
|
8306
|
-
modulesRun
|
|
9142
|
+
modulesRun,
|
|
9143
|
+
...historyNotice !== void 0 ? { history: historyNotice } : {}
|
|
8307
9144
|
},
|
|
8308
9145
|
config,
|
|
8309
|
-
policy: policyResult
|
|
9146
|
+
policy: policyResult,
|
|
9147
|
+
...suppressionErrors.length > 0 ? { suppressionErrors } : {}
|
|
8310
9148
|
};
|
|
8311
9149
|
}
|
|
8312
9150
|
function scan(options = {}) {
|