fullshield 0.2.6 → 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 +65 -0
- package/README.md +8 -3
- package/dist/agent.js +599 -64
- package/dist/cli/index.js +1074 -84
- package/dist/compliance.js +5 -0
- package/dist/dashboard.js +18 -1
- package/dist/iac.js +48 -0
- package/dist/index.d.ts +68 -5
- package/dist/index.js +882 -69
- package/dist/mcp.js +599 -64
- package/dist/supply.js +13 -3
- package/package.json +2 -2
- package/rules/SAST-SKIP-001.md +9 -0
package/dist/agent.js
CHANGED
|
@@ -557,6 +557,7 @@ var init_incremental = __esm({
|
|
|
557
557
|
// src/modules/secrets/history.ts
|
|
558
558
|
var history_exports = {};
|
|
559
559
|
__export(history_exports, {
|
|
560
|
+
historyCommitLimit: () => historyCommitLimit,
|
|
560
561
|
scanGitHistory: () => scanGitHistory
|
|
561
562
|
});
|
|
562
563
|
function gitError(error) {
|
|
@@ -599,7 +600,13 @@ function remapHit(hit, file) {
|
|
|
599
600
|
secret: hit.secret
|
|
600
601
|
};
|
|
601
602
|
}
|
|
602
|
-
|
|
603
|
+
function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
|
|
604
|
+
if (raw === void 0 || raw.trim() === "") return 500;
|
|
605
|
+
const parsed = Number(raw);
|
|
606
|
+
if (!Number.isInteger(parsed) || parsed < 1) return 500;
|
|
607
|
+
return Math.min(parsed, 5e3);
|
|
608
|
+
}
|
|
609
|
+
async function streamLogChanges(cwd, maxCommits) {
|
|
603
610
|
const changes = [];
|
|
604
611
|
await gitOk(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
605
612
|
const child = spawn(
|
|
@@ -616,6 +623,8 @@ async function streamLogChanges(cwd) {
|
|
|
616
623
|
"--format=FSCOMMIT:%H",
|
|
617
624
|
"--no-textconv",
|
|
618
625
|
"--no-ext-diff",
|
|
626
|
+
"-n",
|
|
627
|
+
String(maxCommits),
|
|
619
628
|
"HEAD"
|
|
620
629
|
],
|
|
621
630
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -684,8 +693,14 @@ async function headSecretKeys(cwd) {
|
|
|
684
693
|
}
|
|
685
694
|
return keys;
|
|
686
695
|
}
|
|
687
|
-
async function
|
|
688
|
-
const
|
|
696
|
+
async function commitCount(cwd) {
|
|
697
|
+
const raw = await gitOk(cwd, ["rev-list", "--count", "HEAD"]);
|
|
698
|
+
const total = Number(raw.trim());
|
|
699
|
+
return Number.isInteger(total) && total >= 0 ? total : 0;
|
|
700
|
+
}
|
|
701
|
+
async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
702
|
+
const total = await commitCount(cwd);
|
|
703
|
+
const changes = await streamLogChanges(cwd, maxCommits);
|
|
689
704
|
const blobHits = /* @__PURE__ */ new Map();
|
|
690
705
|
const first = /* @__PURE__ */ new Map();
|
|
691
706
|
for (const change of changes) {
|
|
@@ -722,7 +737,13 @@ async function scanGitHistory(cwd) {
|
|
|
722
737
|
secret: hit.secret
|
|
723
738
|
});
|
|
724
739
|
}
|
|
725
|
-
|
|
740
|
+
const scanned = Math.min(maxCommits, total);
|
|
741
|
+
const history = total > maxCommits ? `scanned ${scanned} of ${total} commits` : void 0;
|
|
742
|
+
return {
|
|
743
|
+
findings: hits.map((h) => h.finding),
|
|
744
|
+
hits,
|
|
745
|
+
...history !== void 0 ? { history } : {}
|
|
746
|
+
};
|
|
726
747
|
}
|
|
727
748
|
var execFileAsync2, ZERO_HASH, MAX_BLOB, RAW_LINE;
|
|
728
749
|
var init_history = __esm({
|
|
@@ -1070,7 +1091,8 @@ function walk(node, visit) {
|
|
|
1070
1091
|
}
|
|
1071
1092
|
}
|
|
1072
1093
|
function attachParents(node, parent) {
|
|
1073
|
-
|
|
1094
|
+
const current = node;
|
|
1095
|
+
if (parent !== void 0 || current.parent === void 0) current.parent = parent;
|
|
1074
1096
|
for (const key of Object.keys(node)) {
|
|
1075
1097
|
if (SKIP_KEYS.has(String(key))) continue;
|
|
1076
1098
|
const value = node[key];
|
|
@@ -1143,6 +1165,10 @@ function mcpCfgEnabled() {
|
|
|
1143
1165
|
function taintV2Enabled() {
|
|
1144
1166
|
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
1145
1167
|
}
|
|
1168
|
+
var LICENSE_FLAG = "FULLSHIELD_EXPERIMENTAL_LICENSE";
|
|
1169
|
+
function licenseEnabled() {
|
|
1170
|
+
return process.env[LICENSE_FLAG] === "1";
|
|
1171
|
+
}
|
|
1146
1172
|
|
|
1147
1173
|
// src/modules/config/finding.ts
|
|
1148
1174
|
init_finding();
|
|
@@ -2132,6 +2158,29 @@ function shortestDependencyPath(target, starts, edges) {
|
|
|
2132
2158
|
}
|
|
2133
2159
|
return void 0;
|
|
2134
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
|
+
}
|
|
2135
2184
|
function reachableFrom(target, roots, edges) {
|
|
2136
2185
|
if (roots.has(target)) return true;
|
|
2137
2186
|
return shortestDependencyPath(target, roots, edges) !== void 0;
|
|
@@ -2152,9 +2201,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
2152
2201
|
}
|
|
2153
2202
|
if (graph !== void 0 && graph.edgesKnown) {
|
|
2154
2203
|
const roots = importedDirectNames(productionUsage, graph);
|
|
2155
|
-
const
|
|
2204
|
+
const paths = collectDependencyPaths(pkgName, roots, graph.edges);
|
|
2205
|
+
const path = paths[0];
|
|
2156
2206
|
if (path !== void 0 && path.length >= 2) {
|
|
2157
|
-
return { reachability: "reachable-via-dependency", path };
|
|
2207
|
+
return { reachability: "reachable-via-dependency", path, paths };
|
|
2158
2208
|
}
|
|
2159
2209
|
return { reachability: "not-imported" };
|
|
2160
2210
|
}
|
|
@@ -2184,8 +2234,108 @@ function isDevOnly(pkgName, ctx) {
|
|
|
2184
2234
|
if (inProd) return false;
|
|
2185
2235
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
2186
2236
|
}
|
|
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 finding3 of findings) {
|
|
2268
|
+
if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") {
|
|
2269
|
+
passthrough.push(finding3);
|
|
2270
|
+
continue;
|
|
2271
|
+
}
|
|
2272
|
+
const name = packageNameFromSnippet(finding3.snippet ?? "") ?? finding3.snippet ?? finding3.id;
|
|
2273
|
+
const advisory = finding3.references?.[0] ?? finding3.id;
|
|
2274
|
+
const key = `${finding3.ruleId}\0${name}\0${advisory}`;
|
|
2275
|
+
const list = groups.get(key);
|
|
2276
|
+
if (list === void 0) groups.set(key, [finding3]);
|
|
2277
|
+
else list.push(finding3);
|
|
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 finding3 of list) {
|
|
2303
|
+
const candidates = finding3.dependencyPaths ?? (finding3.dependencyPath !== void 0 ? [finding3.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
|
+
}
|
|
2187
2337
|
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
2188
|
-
|
|
2338
|
+
const mapped = findings.map((finding3) => {
|
|
2189
2339
|
if (finding3.ruleId === "DEPS-MALICIOUS-001") {
|
|
2190
2340
|
if (finding3.snippet === void 0) return finding3;
|
|
2191
2341
|
const pkg2 = packageNameFromSnippet(finding3.snippet);
|
|
@@ -2195,7 +2345,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2195
2345
|
...finding3,
|
|
2196
2346
|
severity: "critical",
|
|
2197
2347
|
reachability: classified2.reachability,
|
|
2198
|
-
...classified2
|
|
2348
|
+
...pathFields(classified2)
|
|
2199
2349
|
};
|
|
2200
2350
|
}
|
|
2201
2351
|
if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
|
|
@@ -2205,7 +2355,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2205
2355
|
const named = symbols.get(finding3.id) ?? [];
|
|
2206
2356
|
if (named.length > 0 && ctx.sources !== void 0) {
|
|
2207
2357
|
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
2208
|
-
classified = { reachability: symbolClass, path: classified.path };
|
|
2358
|
+
classified = { reachability: symbolClass, path: classified.path, paths: classified.paths };
|
|
2209
2359
|
}
|
|
2210
2360
|
const steps = reachabilitySteps(classified.reachability);
|
|
2211
2361
|
let severity = steps === 0 ? finding3.severity : downgradeSeverity(finding3.severity, steps);
|
|
@@ -2213,6 +2363,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2213
2363
|
if (classified.path !== void 0) {
|
|
2214
2364
|
const shown = classified.path.join(" > ");
|
|
2215
2365
|
message = `${message} Reachable via ${shown}.`;
|
|
2366
|
+
message = appendOtherPaths(message, classified.paths);
|
|
2216
2367
|
}
|
|
2217
2368
|
const devOnly = isDevOnly(pkg, ctx);
|
|
2218
2369
|
if (devOnly) {
|
|
@@ -2222,11 +2373,12 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2222
2373
|
return {
|
|
2223
2374
|
...finding3,
|
|
2224
2375
|
reachability: classified.reachability,
|
|
2225
|
-
...classified
|
|
2376
|
+
...pathFields(classified),
|
|
2226
2377
|
severity,
|
|
2227
2378
|
message
|
|
2228
2379
|
};
|
|
2229
2380
|
});
|
|
2381
|
+
return groupPackageAdvisories(mapped);
|
|
2230
2382
|
}
|
|
2231
2383
|
|
|
2232
2384
|
// src/modules/deps/semver.ts
|
|
@@ -4128,6 +4280,59 @@ function experimentalVex() {
|
|
|
4128
4280
|
return process.env.FULLSHIELD_EXPERIMENTAL_VEX === "1";
|
|
4129
4281
|
}
|
|
4130
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
|
+
|
|
4131
4336
|
// src/fixer/eol.ts
|
|
4132
4337
|
function detectEol(text) {
|
|
4133
4338
|
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -4226,7 +4431,7 @@ function walkJs(dir, out = []) {
|
|
|
4226
4431
|
}
|
|
4227
4432
|
return out;
|
|
4228
4433
|
}
|
|
4229
|
-
function
|
|
4434
|
+
function readJson2(path) {
|
|
4230
4435
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
4231
4436
|
}
|
|
4232
4437
|
var TOKEN_RE = /(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/;
|
|
@@ -4240,7 +4445,7 @@ function packageSources(dir) {
|
|
|
4240
4445
|
let scripts = "";
|
|
4241
4446
|
if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
|
|
4242
4447
|
try {
|
|
4243
|
-
const pkg =
|
|
4448
|
+
const pkg = readJson2(manifestPath);
|
|
4244
4449
|
const block = pkg.scripts ?? {};
|
|
4245
4450
|
scripts = Object.values(block).join("\n");
|
|
4246
4451
|
} catch {
|
|
@@ -4559,6 +4764,7 @@ var depsModule = {
|
|
|
4559
4764
|
reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
|
|
4560
4765
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
4561
4766
|
}
|
|
4767
|
+
if (licenseEnabled()) reached.push(...await scanCopyleftLicenses(ctx.cwd));
|
|
4562
4768
|
return reached;
|
|
4563
4769
|
}
|
|
4564
4770
|
};
|
|
@@ -5474,7 +5680,7 @@ function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
|
5474
5680
|
}
|
|
5475
5681
|
return changed;
|
|
5476
5682
|
}
|
|
5477
|
-
function analyzeFile(ast,
|
|
5683
|
+
function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
5478
5684
|
attachParents(ast);
|
|
5479
5685
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
5480
5686
|
const functions = [];
|
|
@@ -5488,7 +5694,7 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5488
5694
|
}
|
|
5489
5695
|
const seeds = /* @__PURE__ */ new Map();
|
|
5490
5696
|
for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
|
|
5491
|
-
const returners = new Set(
|
|
5697
|
+
const returners = new Set(extraReturners2);
|
|
5492
5698
|
for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
|
|
5493
5699
|
let changed = false;
|
|
5494
5700
|
facts.set(ast, analyzeScope(ast, [], returners));
|
|
@@ -7360,6 +7566,39 @@ function scanPromptInjection(file, source) {
|
|
|
7360
7566
|
});
|
|
7361
7567
|
return findings;
|
|
7362
7568
|
}
|
|
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
|
+
};
|
|
7363
7602
|
function packageVersion() {
|
|
7364
7603
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7365
7604
|
for (let depth = 0; depth < 6; depth += 1) {
|
|
@@ -7380,7 +7619,7 @@ function packageVersion() {
|
|
|
7380
7619
|
|
|
7381
7620
|
// src/core/parse-pool.ts
|
|
7382
7621
|
var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7383
|
-
var cache =
|
|
7622
|
+
var cache = new LruMap(64);
|
|
7384
7623
|
function parsePoolEnabled() {
|
|
7385
7624
|
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
7386
7625
|
}
|
|
@@ -7388,9 +7627,15 @@ function parsePoolSize() {
|
|
|
7388
7627
|
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
7389
7628
|
if (raw !== void 0 && raw !== "") {
|
|
7390
7629
|
const parsed = Number(raw);
|
|
7391
|
-
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7630
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
|
|
7392
7631
|
}
|
|
7393
|
-
return Math.max(1, availableParallelism() - 1);
|
|
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;
|
|
7394
7639
|
}
|
|
7395
7640
|
function parseCacheKey(code) {
|
|
7396
7641
|
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
@@ -7400,12 +7645,37 @@ function parseOne(code) {
|
|
|
7400
7645
|
const hit = cache.get(key);
|
|
7401
7646
|
if (hit !== void 0) return Promise.resolve(hit);
|
|
7402
7647
|
return new Promise((resolve5, reject) => {
|
|
7403
|
-
const worker = new Worker(fileURLToPath(workerUrl), {
|
|
7404
|
-
|
|
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;
|
|
7405
7659
|
cache.set(key, ast);
|
|
7660
|
+
finish();
|
|
7406
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"}`));
|
|
7407
7678
|
});
|
|
7408
|
-
worker.once("error", reject);
|
|
7409
7679
|
});
|
|
7410
7680
|
}
|
|
7411
7681
|
async function parseWithPool(codes) {
|
|
@@ -7459,8 +7729,8 @@ function snippetOf(source, node) {
|
|
|
7459
7729
|
if (node.range === void 0) return "";
|
|
7460
7730
|
return source.slice(node.range[0], node.range[1]).replace(/\s+/g, " ").trim().slice(0, 120);
|
|
7461
7731
|
}
|
|
7462
|
-
function findingsFromAst(filename, source, ast,
|
|
7463
|
-
const facts = analyzeFile(ast,
|
|
7732
|
+
function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
|
|
7733
|
+
const facts = analyzeFile(ast, extraReturners2);
|
|
7464
7734
|
const findings = [];
|
|
7465
7735
|
for (const rule of sastRules) {
|
|
7466
7736
|
const ctx = {
|
|
@@ -7538,52 +7808,203 @@ function findingsFromTemplate(filename, source) {
|
|
|
7538
7808
|
xssRule.create(ctx);
|
|
7539
7809
|
return findings;
|
|
7540
7810
|
}
|
|
7811
|
+
function dedupeSastSinks(findings) {
|
|
7812
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7813
|
+
const out = [];
|
|
7814
|
+
for (const finding3 of findings) {
|
|
7815
|
+
if (finding3.module !== "sast" || finding3.line === void 0) {
|
|
7816
|
+
out.push(finding3);
|
|
7817
|
+
continue;
|
|
7818
|
+
}
|
|
7819
|
+
const key = `${finding3.ruleId}\0${finding3.file ?? ""}\0${finding3.line}`;
|
|
7820
|
+
if (seen.has(key)) continue;
|
|
7821
|
+
seen.add(key);
|
|
7822
|
+
out.push(finding3);
|
|
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
|
+
}
|
|
7541
7963
|
var sastModule = {
|
|
7542
7964
|
name: "sast",
|
|
7543
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;
|
|
7544
7969
|
const findings = [];
|
|
7545
7970
|
const parsed = [];
|
|
7546
|
-
for (const rel of
|
|
7547
|
-
const
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
source = await readFile(join(ctx.cwd, rel), "utf8");
|
|
7553
|
-
} 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}.`));
|
|
7554
7977
|
continue;
|
|
7555
7978
|
}
|
|
7556
|
-
if (
|
|
7557
|
-
if (isTemplate) {
|
|
7979
|
+
if (TEMPLATE_EXT.test(rel)) {
|
|
7558
7980
|
findings.push(...findingsFromTemplate(rel, source));
|
|
7559
7981
|
continue;
|
|
7560
7982
|
}
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
parsed.push({ file: rel, source, ast });
|
|
7565
|
-
} catch {
|
|
7566
|
-
}
|
|
7983
|
+
const ast = await parseCode(source);
|
|
7984
|
+
if (ast === void 0) continue;
|
|
7985
|
+
parsed.push({ file: rel, source, ast });
|
|
7567
7986
|
}
|
|
7568
7987
|
const files = new Set(parsed.map((p) => p.file));
|
|
7569
7988
|
const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
|
|
7570
7989
|
const returning = /* @__PURE__ */ new Map();
|
|
7571
|
-
for (const
|
|
7572
|
-
returning.set(
|
|
7573
|
-
}
|
|
7574
|
-
for (const
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
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
|
+
);
|
|
7584
8003
|
}
|
|
7585
8004
|
findings.push(...runInterproc(parsed));
|
|
7586
|
-
|
|
8005
|
+
const out = dedupeSastSinks(findings);
|
|
8006
|
+
releaseParsed(parsed);
|
|
8007
|
+
return out;
|
|
7587
8008
|
}
|
|
7588
8009
|
};
|
|
7589
8010
|
|
|
@@ -8027,16 +8448,37 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
8027
8448
|
}
|
|
8028
8449
|
};
|
|
8029
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
|
+
}
|
|
8030
8470
|
async function readBaseline(path) {
|
|
8031
8471
|
try {
|
|
8032
8472
|
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
8033
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;
|
|
8034
8475
|
return raw;
|
|
8035
8476
|
} catch {
|
|
8036
8477
|
return void 0;
|
|
8037
8478
|
}
|
|
8038
8479
|
}
|
|
8039
8480
|
async function writeBaseline(path, findings) {
|
|
8481
|
+
const existing = await readBaseline(path);
|
|
8040
8482
|
const doc = {
|
|
8041
8483
|
version: 1,
|
|
8042
8484
|
findings: findings.map((f) => ({
|
|
@@ -8044,7 +8486,8 @@ async function writeBaseline(path, findings) {
|
|
|
8044
8486
|
ruleId: f.ruleId,
|
|
8045
8487
|
file: f.file,
|
|
8046
8488
|
line: f.line
|
|
8047
|
-
}))
|
|
8489
|
+
})),
|
|
8490
|
+
...existing?.suppressions !== void 0 ? { suppressions: existing.suppressions } : {}
|
|
8048
8491
|
};
|
|
8049
8492
|
await writeFile(path, `${JSON.stringify(doc, null, 2)}
|
|
8050
8493
|
`, "utf8");
|
|
@@ -8054,6 +8497,30 @@ function applyBaseline(findings, baseline) {
|
|
|
8054
8497
|
const known = new Set(baseline.findings.map((f) => f.id));
|
|
8055
8498
|
return findings.filter((f) => !known.has(f.id));
|
|
8056
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((finding3) => {
|
|
8519
|
+
const file = (finding3.file ?? "").replace(/\\/g, "/");
|
|
8520
|
+
return !active.some((row) => row.ruleId === finding3.ruleId && (row.file === void 0 || row.file === file));
|
|
8521
|
+
});
|
|
8522
|
+
return { findings: kept, errors };
|
|
8523
|
+
}
|
|
8057
8524
|
|
|
8058
8525
|
// src/core/config.ts
|
|
8059
8526
|
init_finding();
|
|
@@ -8062,14 +8529,27 @@ var DEFAULT_CONFIG = {
|
|
|
8062
8529
|
modules: [...MODULE_NAMES],
|
|
8063
8530
|
failOn: "high",
|
|
8064
8531
|
ignore: ["dist/**", "**/*.test.ts"],
|
|
8532
|
+
defaultExcludes: true,
|
|
8065
8533
|
baseline: ".fullshield-baseline.json",
|
|
8066
8534
|
notify: { slackWebhookEnv: "FULLSHIELD_SLACK_WEBHOOK" },
|
|
8067
8535
|
deps: { offline: false },
|
|
8068
|
-
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 }
|
|
8536
|
+
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 },
|
|
8537
|
+
sast: { maxFileBytes: 1024 * 1024 }
|
|
8069
8538
|
};
|
|
8070
8539
|
var ConfigError = class extends Error {
|
|
8071
8540
|
};
|
|
8072
|
-
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
|
+
]);
|
|
8073
8553
|
function validate(raw, source) {
|
|
8074
8554
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
8075
8555
|
throw new ConfigError(`${source}: config must be a JSON object`);
|
|
@@ -8171,6 +8651,32 @@ function validate(raw, source) {
|
|
|
8171
8651
|
}
|
|
8172
8652
|
out.ai = ai;
|
|
8173
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
|
+
}
|
|
8174
8680
|
return out;
|
|
8175
8681
|
}
|
|
8176
8682
|
async function loadConfig(cwd, configPath) {
|
|
@@ -8197,10 +8703,26 @@ async function loadConfig(cwd, configPath) {
|
|
|
8197
8703
|
return { ...structuredClone(DEFAULT_CONFIG), ...validate(raw, file) };
|
|
8198
8704
|
}
|
|
8199
8705
|
var ignoreFactory = ignoreModule.default;
|
|
8200
|
-
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
|
+
];
|
|
8201
8720
|
async function walkFiles(options) {
|
|
8202
8721
|
const ig = ignoreFactory();
|
|
8203
|
-
ig.add(
|
|
8722
|
+
ig.add(HARD_IGNORED);
|
|
8723
|
+
if (options.defaultExcludes !== false) {
|
|
8724
|
+
ig.add(options.excludes ?? DEFAULT_EXCLUDES);
|
|
8725
|
+
}
|
|
8204
8726
|
try {
|
|
8205
8727
|
ig.add(await readFile(join(options.cwd, ".gitignore"), "utf8"));
|
|
8206
8728
|
} catch {
|
|
@@ -8382,7 +8904,7 @@ function resolvePolicyBranch(cwd, env = process.env) {
|
|
|
8382
8904
|
return "*";
|
|
8383
8905
|
}
|
|
8384
8906
|
}
|
|
8385
|
-
function
|
|
8907
|
+
function expiresAtMs2(expires) {
|
|
8386
8908
|
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
|
|
8387
8909
|
return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8388
8910
|
}
|
|
@@ -8404,7 +8926,7 @@ function applyPolicy(findings, policy, ctx = {}) {
|
|
|
8404
8926
|
});
|
|
8405
8927
|
const suppressed = /* @__PURE__ */ new Set();
|
|
8406
8928
|
for (const exception of policy.exceptions ?? []) {
|
|
8407
|
-
if (
|
|
8929
|
+
if (expiresAtMs2(exception.expires) < now.getTime()) {
|
|
8408
8930
|
errors.push(
|
|
8409
8931
|
`policy exception for ${exception.ruleId} expired on ${exception.expires} (owner: ${exception.owner}); renew or remove it`
|
|
8410
8932
|
);
|
|
@@ -8473,7 +8995,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8473
8995
|
options.modules ?? config.modules,
|
|
8474
8996
|
loadedPolicy?.requiredModules
|
|
8475
8997
|
);
|
|
8476
|
-
let files = await walkFiles({
|
|
8998
|
+
let files = await walkFiles({
|
|
8999
|
+
cwd,
|
|
9000
|
+
ignore: config.ignore,
|
|
9001
|
+
defaultExcludes: config.defaultExcludes,
|
|
9002
|
+
excludes: config.excludes
|
|
9003
|
+
});
|
|
8477
9004
|
if (options.staged === true) {
|
|
8478
9005
|
const staged = new Set(await listStagedFiles(cwd));
|
|
8479
9006
|
files = files.filter((file) => staged.has(file));
|
|
@@ -8507,6 +9034,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8507
9034
|
);
|
|
8508
9035
|
}
|
|
8509
9036
|
const extraHits = [];
|
|
9037
|
+
let historyNotice;
|
|
8510
9038
|
const secretsWanted = modulesRequested.includes("secrets");
|
|
8511
9039
|
if (secretsAdvEnabled() && secretsWanted) {
|
|
8512
9040
|
if (options.history === true) {
|
|
@@ -8514,6 +9042,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8514
9042
|
const hist = await scanGitHistory2(cwd);
|
|
8515
9043
|
extraHits.push(...hist.hits);
|
|
8516
9044
|
collected.push(...hist.findings);
|
|
9045
|
+
historyNotice = hist.history;
|
|
8517
9046
|
}
|
|
8518
9047
|
if (options.bundle !== void 0 && options.bundle.length > 0) {
|
|
8519
9048
|
const { scanBundle: scanBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
@@ -8534,7 +9063,11 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8534
9063
|
if (options.updateBaseline === true) {
|
|
8535
9064
|
await writeBaseline(baselinePath, unsuppressed);
|
|
8536
9065
|
}
|
|
8537
|
-
|
|
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;
|
|
8538
9071
|
if (secretsAdvEnabled() && secretsWanted && options.verifySecrets === true && options.iOwnTheseCredentials === true) {
|
|
8539
9072
|
const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
8540
9073
|
const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
@@ -8606,10 +9139,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8606
9139
|
cwd,
|
|
8607
9140
|
filesScanned: files.length,
|
|
8608
9141
|
modulesRequested: [...modulesRequested],
|
|
8609
|
-
modulesRun
|
|
9142
|
+
modulesRun,
|
|
9143
|
+
...historyNotice !== void 0 ? { history: historyNotice } : {}
|
|
8610
9144
|
},
|
|
8611
9145
|
config,
|
|
8612
|
-
policy: policyResult
|
|
9146
|
+
policy: policyResult,
|
|
9147
|
+
...suppressionErrors.length > 0 ? { suppressionErrors } : {}
|
|
8613
9148
|
};
|
|
8614
9149
|
}
|
|
8615
9150
|
function scan(options = {}) {
|