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/index.js
CHANGED
|
@@ -565,6 +565,7 @@ var init_incremental = __esm({
|
|
|
565
565
|
// src/modules/secrets/history.ts
|
|
566
566
|
var history_exports = {};
|
|
567
567
|
__export(history_exports, {
|
|
568
|
+
historyCommitLimit: () => historyCommitLimit,
|
|
568
569
|
scanGitHistory: () => scanGitHistory
|
|
569
570
|
});
|
|
570
571
|
function gitError(error) {
|
|
@@ -607,7 +608,13 @@ function remapHit(hit, file) {
|
|
|
607
608
|
secret: hit.secret
|
|
608
609
|
};
|
|
609
610
|
}
|
|
610
|
-
|
|
611
|
+
function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
|
|
612
|
+
if (raw === void 0 || raw.trim() === "") return 500;
|
|
613
|
+
const parsed = Number(raw);
|
|
614
|
+
if (!Number.isInteger(parsed) || parsed < 1) return 500;
|
|
615
|
+
return Math.min(parsed, 5e3);
|
|
616
|
+
}
|
|
617
|
+
async function streamLogChanges(cwd, maxCommits) {
|
|
611
618
|
const changes = [];
|
|
612
619
|
await gitOk(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
613
620
|
const child = spawn(
|
|
@@ -624,6 +631,8 @@ async function streamLogChanges(cwd) {
|
|
|
624
631
|
"--format=FSCOMMIT:%H",
|
|
625
632
|
"--no-textconv",
|
|
626
633
|
"--no-ext-diff",
|
|
634
|
+
"-n",
|
|
635
|
+
String(maxCommits),
|
|
627
636
|
"HEAD"
|
|
628
637
|
],
|
|
629
638
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -692,8 +701,14 @@ async function headSecretKeys(cwd) {
|
|
|
692
701
|
}
|
|
693
702
|
return keys;
|
|
694
703
|
}
|
|
695
|
-
async function
|
|
696
|
-
const
|
|
704
|
+
async function commitCount(cwd) {
|
|
705
|
+
const raw = await gitOk(cwd, ["rev-list", "--count", "HEAD"]);
|
|
706
|
+
const total = Number(raw.trim());
|
|
707
|
+
return Number.isInteger(total) && total >= 0 ? total : 0;
|
|
708
|
+
}
|
|
709
|
+
async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
710
|
+
const total = await commitCount(cwd);
|
|
711
|
+
const changes = await streamLogChanges(cwd, maxCommits);
|
|
697
712
|
const blobHits = /* @__PURE__ */ new Map();
|
|
698
713
|
const first = /* @__PURE__ */ new Map();
|
|
699
714
|
for (const change of changes) {
|
|
@@ -730,7 +745,13 @@ async function scanGitHistory(cwd) {
|
|
|
730
745
|
secret: hit.secret
|
|
731
746
|
});
|
|
732
747
|
}
|
|
733
|
-
|
|
748
|
+
const scanned = Math.min(maxCommits, total);
|
|
749
|
+
const history = total > maxCommits ? `scanned ${scanned} of ${total} commits` : void 0;
|
|
750
|
+
return {
|
|
751
|
+
findings: hits.map((h) => h.finding),
|
|
752
|
+
hits,
|
|
753
|
+
...history !== void 0 ? { history } : {}
|
|
754
|
+
};
|
|
734
755
|
}
|
|
735
756
|
var execFileAsync2, ZERO_HASH, MAX_BLOB, RAW_LINE;
|
|
736
757
|
var init_history = __esm({
|
|
@@ -1078,7 +1099,8 @@ function walk(node, visit) {
|
|
|
1078
1099
|
}
|
|
1079
1100
|
}
|
|
1080
1101
|
function attachParents(node, parent) {
|
|
1081
|
-
|
|
1102
|
+
const current = node;
|
|
1103
|
+
if (parent !== void 0 || current.parent === void 0) current.parent = parent;
|
|
1082
1104
|
for (const key of Object.keys(node)) {
|
|
1083
1105
|
if (SKIP_KEYS.has(String(key))) continue;
|
|
1084
1106
|
const value = node[key];
|
|
@@ -1151,6 +1173,11 @@ function mcpCfgEnabled() {
|
|
|
1151
1173
|
function taintV2Enabled() {
|
|
1152
1174
|
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
1153
1175
|
}
|
|
1176
|
+
var DIFF_FLAG = "FULLSHIELD_EXPERIMENTAL_DIFF";
|
|
1177
|
+
var LICENSE_FLAG = "FULLSHIELD_EXPERIMENTAL_LICENSE";
|
|
1178
|
+
function licenseEnabled() {
|
|
1179
|
+
return process.env[LICENSE_FLAG] === "1";
|
|
1180
|
+
}
|
|
1154
1181
|
|
|
1155
1182
|
// src/modules/config/finding.ts
|
|
1156
1183
|
init_finding();
|
|
@@ -2143,6 +2170,29 @@ function shortestDependencyPath(target, starts, edges) {
|
|
|
2143
2170
|
}
|
|
2144
2171
|
return void 0;
|
|
2145
2172
|
}
|
|
2173
|
+
function collectDependencyPaths(target, starts, edges, limit = 8) {
|
|
2174
|
+
const out = [];
|
|
2175
|
+
const queue = [];
|
|
2176
|
+
let steps = 0;
|
|
2177
|
+
for (const start of starts) queue.push([start]);
|
|
2178
|
+
while (queue.length > 0 && out.length < limit && steps < 4e3) {
|
|
2179
|
+
steps += 1;
|
|
2180
|
+
const path = queue.shift();
|
|
2181
|
+
if (path === void 0) continue;
|
|
2182
|
+
const last = path[path.length - 1];
|
|
2183
|
+
if (last === void 0) continue;
|
|
2184
|
+
if (last === target) {
|
|
2185
|
+
if (path.length >= 2) out.push(path);
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
if (path.length >= 12) continue;
|
|
2189
|
+
for (const next of edges.get(last) ?? []) {
|
|
2190
|
+
if (path.includes(next)) continue;
|
|
2191
|
+
queue.push([...path, next]);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
return out;
|
|
2195
|
+
}
|
|
2146
2196
|
function reachableFrom(target, roots, edges) {
|
|
2147
2197
|
if (roots.has(target)) return true;
|
|
2148
2198
|
return shortestDependencyPath(target, roots, edges) !== void 0;
|
|
@@ -2163,9 +2213,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
2163
2213
|
}
|
|
2164
2214
|
if (graph !== void 0 && graph.edgesKnown) {
|
|
2165
2215
|
const roots = importedDirectNames(productionUsage, graph);
|
|
2166
|
-
const
|
|
2216
|
+
const paths = collectDependencyPaths(pkgName, roots, graph.edges);
|
|
2217
|
+
const path = paths[0];
|
|
2167
2218
|
if (path !== void 0 && path.length >= 2) {
|
|
2168
|
-
return { reachability: "reachable-via-dependency", path };
|
|
2219
|
+
return { reachability: "reachable-via-dependency", path, paths };
|
|
2169
2220
|
}
|
|
2170
2221
|
return { reachability: "not-imported" };
|
|
2171
2222
|
}
|
|
@@ -2195,8 +2246,108 @@ function isDevOnly(pkgName, ctx) {
|
|
|
2195
2246
|
if (inProd) return false;
|
|
2196
2247
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
2197
2248
|
}
|
|
2249
|
+
var REACH_RANK = {
|
|
2250
|
+
reachable: 0,
|
|
2251
|
+
"symbol-reachable": 1,
|
|
2252
|
+
"reachable-via-dependency": 2,
|
|
2253
|
+
"imported-not-called": 3,
|
|
2254
|
+
"symbol-not-called": 4,
|
|
2255
|
+
"reachability-unknown": 5,
|
|
2256
|
+
"not-imported": 6
|
|
2257
|
+
};
|
|
2258
|
+
function versionFromSnippet(snippet) {
|
|
2259
|
+
const name = packageNameFromSnippet(snippet);
|
|
2260
|
+
if (name === void 0) return snippet;
|
|
2261
|
+
return snippet.slice(name.length + 1);
|
|
2262
|
+
}
|
|
2263
|
+
function pathFields(classified) {
|
|
2264
|
+
const paths = classified.paths ?? (classified.path !== void 0 ? [classified.path] : []);
|
|
2265
|
+
if (paths.length === 0) return {};
|
|
2266
|
+
return {
|
|
2267
|
+
dependencyPath: paths[0],
|
|
2268
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
function appendOtherPaths(message, paths) {
|
|
2272
|
+
if (paths === void 0 || paths.length < 2) return message;
|
|
2273
|
+
const rest = paths.slice(1).map((p) => p.join(" > ")).join("; ");
|
|
2274
|
+
return `${message} Other paths: ${rest}.`;
|
|
2275
|
+
}
|
|
2276
|
+
function groupPackageAdvisories(findings) {
|
|
2277
|
+
const passthrough = [];
|
|
2278
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2279
|
+
for (const finding3 of findings) {
|
|
2280
|
+
if (finding3.ruleId !== "DEPS-VULN-001" && finding3.ruleId !== "DEPS-MALICIOUS-001") {
|
|
2281
|
+
passthrough.push(finding3);
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
const name = packageNameFromSnippet(finding3.snippet ?? "") ?? finding3.snippet ?? finding3.id;
|
|
2285
|
+
const advisory = finding3.references?.[0] ?? finding3.id;
|
|
2286
|
+
const key = `${finding3.ruleId}\0${name}\0${advisory}`;
|
|
2287
|
+
const list = groups.get(key);
|
|
2288
|
+
if (list === void 0) groups.set(key, [finding3]);
|
|
2289
|
+
else list.push(finding3);
|
|
2290
|
+
}
|
|
2291
|
+
const merged = [];
|
|
2292
|
+
for (const list of groups.values()) {
|
|
2293
|
+
const only = list[0];
|
|
2294
|
+
if (list.length === 1 && only !== void 0) {
|
|
2295
|
+
merged.push(only);
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
merged.push(mergeAdvisory(list));
|
|
2299
|
+
}
|
|
2300
|
+
return [...passthrough, ...merged];
|
|
2301
|
+
}
|
|
2302
|
+
function mergeAdvisory(list) {
|
|
2303
|
+
const ranked = [...list].sort(
|
|
2304
|
+
(a, b) => severityRank(b.severity) - severityRank(a.severity) || (REACH_RANK[a.reachability ?? "not-imported"] ?? 9) - (REACH_RANK[b.reachability ?? "not-imported"] ?? 9)
|
|
2305
|
+
);
|
|
2306
|
+
const primary = ranked[0];
|
|
2307
|
+
if (primary === void 0) {
|
|
2308
|
+
throw new Error("empty advisory group");
|
|
2309
|
+
}
|
|
2310
|
+
const name = packageNameFromSnippet(primary.snippet ?? "") ?? "package";
|
|
2311
|
+
const versions = [...new Set(list.map((f) => versionFromSnippet(f.snippet ?? "")))].filter((v) => v.length > 0).sort();
|
|
2312
|
+
const paths = [];
|
|
2313
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
2314
|
+
for (const finding3 of list) {
|
|
2315
|
+
const candidates = finding3.dependencyPaths ?? (finding3.dependencyPath !== void 0 ? [finding3.dependencyPath] : []);
|
|
2316
|
+
for (const path of candidates) {
|
|
2317
|
+
const key = path.join("\0");
|
|
2318
|
+
if (seenPath.has(key)) continue;
|
|
2319
|
+
seenPath.add(key);
|
|
2320
|
+
paths.push(path);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
paths.sort((a, b) => a.length - b.length || a.join(">").localeCompare(b.join(">")));
|
|
2324
|
+
const advisory = primary.references?.[0] ?? "";
|
|
2325
|
+
const onManifest = list.find((f) => f.file === "package.json");
|
|
2326
|
+
const file = onManifest?.file ?? primary.file;
|
|
2327
|
+
const line = onManifest?.line ?? primary.line;
|
|
2328
|
+
const snippet = versions.length > 0 ? `${name}@${versions.join(",")}` : primary.snippet ?? name;
|
|
2329
|
+
let message = primary.message;
|
|
2330
|
+
if (versions.length > 1) {
|
|
2331
|
+
message = `${message} Versions: ${versions.join(", ")}.`;
|
|
2332
|
+
}
|
|
2333
|
+
if (paths.length > 1) {
|
|
2334
|
+
message = `${message} Dependency paths: ${paths.map((p) => p.join(" > ")).join("; ")}.`;
|
|
2335
|
+
}
|
|
2336
|
+
return {
|
|
2337
|
+
...primary,
|
|
2338
|
+
id: makeFindingId(primary.ruleId, file, `${name}|${advisory}|${versions.join(",")}`),
|
|
2339
|
+
file,
|
|
2340
|
+
line,
|
|
2341
|
+
snippet,
|
|
2342
|
+
message,
|
|
2343
|
+
severity: primary.severity,
|
|
2344
|
+
reachability: primary.reachability,
|
|
2345
|
+
...paths[0] !== void 0 ? { dependencyPath: paths[0] } : {},
|
|
2346
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2198
2349
|
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
2199
|
-
|
|
2350
|
+
const mapped = findings.map((finding3) => {
|
|
2200
2351
|
if (finding3.ruleId === "DEPS-MALICIOUS-001") {
|
|
2201
2352
|
if (finding3.snippet === void 0) return finding3;
|
|
2202
2353
|
const pkg2 = packageNameFromSnippet(finding3.snippet);
|
|
@@ -2206,7 +2357,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2206
2357
|
...finding3,
|
|
2207
2358
|
severity: "critical",
|
|
2208
2359
|
reachability: classified2.reachability,
|
|
2209
|
-
...classified2
|
|
2360
|
+
...pathFields(classified2)
|
|
2210
2361
|
};
|
|
2211
2362
|
}
|
|
2212
2363
|
if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
|
|
@@ -2216,7 +2367,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2216
2367
|
const named = symbols.get(finding3.id) ?? [];
|
|
2217
2368
|
if (named.length > 0 && ctx.sources !== void 0) {
|
|
2218
2369
|
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
2219
|
-
classified = { reachability: symbolClass, path: classified.path };
|
|
2370
|
+
classified = { reachability: symbolClass, path: classified.path, paths: classified.paths };
|
|
2220
2371
|
}
|
|
2221
2372
|
const steps = reachabilitySteps(classified.reachability);
|
|
2222
2373
|
let severity = steps === 0 ? finding3.severity : downgradeSeverity(finding3.severity, steps);
|
|
@@ -2224,6 +2375,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2224
2375
|
if (classified.path !== void 0) {
|
|
2225
2376
|
const shown = classified.path.join(" > ");
|
|
2226
2377
|
message = `${message} Reachable via ${shown}.`;
|
|
2378
|
+
message = appendOtherPaths(message, classified.paths);
|
|
2227
2379
|
}
|
|
2228
2380
|
const devOnly = isDevOnly(pkg, ctx);
|
|
2229
2381
|
if (devOnly) {
|
|
@@ -2233,11 +2385,12 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
2233
2385
|
return {
|
|
2234
2386
|
...finding3,
|
|
2235
2387
|
reachability: classified.reachability,
|
|
2236
|
-
...classified
|
|
2388
|
+
...pathFields(classified),
|
|
2237
2389
|
severity,
|
|
2238
2390
|
message
|
|
2239
2391
|
};
|
|
2240
2392
|
});
|
|
2393
|
+
return groupPackageAdvisories(mapped);
|
|
2241
2394
|
}
|
|
2242
2395
|
|
|
2243
2396
|
// src/modules/deps/semver.ts
|
|
@@ -4139,6 +4292,59 @@ function experimentalVex() {
|
|
|
4139
4292
|
return process.env.FULLSHIELD_EXPERIMENTAL_VEX === "1";
|
|
4140
4293
|
}
|
|
4141
4294
|
|
|
4295
|
+
// src/modules/deps/license.ts
|
|
4296
|
+
init_finding();
|
|
4297
|
+
var COPYLEFT = /(?:^|[^\w])(?:AGPL|GPL)(?:-|[^\w]|$)/i;
|
|
4298
|
+
function isCopyleftLicense(license) {
|
|
4299
|
+
return COPYLEFT.test(license);
|
|
4300
|
+
}
|
|
4301
|
+
function licenseText(value) {
|
|
4302
|
+
if (typeof value === "string") return value;
|
|
4303
|
+
if (typeof value === "object" && value !== null && "type" in value) {
|
|
4304
|
+
const type = value.type;
|
|
4305
|
+
if (typeof type === "string") return type;
|
|
4306
|
+
}
|
|
4307
|
+
return void 0;
|
|
4308
|
+
}
|
|
4309
|
+
async function readJson(path) {
|
|
4310
|
+
try {
|
|
4311
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
4312
|
+
} catch {
|
|
4313
|
+
return void 0;
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
async function scanCopyleftLicenses(cwd) {
|
|
4317
|
+
const manifest = await readJson(join(cwd, "package.json"));
|
|
4318
|
+
if (typeof manifest !== "object" || manifest === null) return [];
|
|
4319
|
+
const names = /* @__PURE__ */ new Set();
|
|
4320
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
4321
|
+
const block = manifest[field];
|
|
4322
|
+
if (typeof block === "object" && block !== null && !Array.isArray(block)) {
|
|
4323
|
+
for (const name of Object.keys(block)) names.add(name);
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
const findings = [];
|
|
4327
|
+
for (const name of names) {
|
|
4328
|
+
const pkg = await readJson(join(cwd, "node_modules", name, "package.json"));
|
|
4329
|
+
if (typeof pkg !== "object" || pkg === null) continue;
|
|
4330
|
+
const license = licenseText(pkg.license);
|
|
4331
|
+
if (license === void 0 || !isCopyleftLicense(license)) continue;
|
|
4332
|
+
findings.push({
|
|
4333
|
+
id: makeFindingId("DEPS-LICENSE-001", "package.json", `${name}|${license}`),
|
|
4334
|
+
ruleId: "DEPS-LICENSE-001",
|
|
4335
|
+
module: "deps",
|
|
4336
|
+
title: `Copyleft dependency ${name}`,
|
|
4337
|
+
severity: "medium",
|
|
4338
|
+
confidence: "high",
|
|
4339
|
+
file: "package.json",
|
|
4340
|
+
snippet: `${name} ${license}`,
|
|
4341
|
+
message: `${name} is licensed ${license}.`,
|
|
4342
|
+
remediation: "Replace the dependency or get a legal review before shipping it in a commercial product."
|
|
4343
|
+
});
|
|
4344
|
+
}
|
|
4345
|
+
return findings;
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4142
4348
|
// src/fixer/eol.ts
|
|
4143
4349
|
function detectEol(text) {
|
|
4144
4350
|
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -4253,7 +4459,7 @@ function walkJs(dir, out = []) {
|
|
|
4253
4459
|
}
|
|
4254
4460
|
return out;
|
|
4255
4461
|
}
|
|
4256
|
-
function
|
|
4462
|
+
function readJson2(path) {
|
|
4257
4463
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
4258
4464
|
}
|
|
4259
4465
|
var TOKEN_RE = /(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/;
|
|
@@ -4267,7 +4473,7 @@ function packageSources(dir) {
|
|
|
4267
4473
|
let scripts = "";
|
|
4268
4474
|
if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
|
|
4269
4475
|
try {
|
|
4270
|
-
const pkg =
|
|
4476
|
+
const pkg = readJson2(manifestPath);
|
|
4271
4477
|
const block = pkg.scripts ?? {};
|
|
4272
4478
|
scripts = Object.values(block).join("\n");
|
|
4273
4479
|
} catch {
|
|
@@ -4602,6 +4808,7 @@ var depsModule = {
|
|
|
4602
4808
|
reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
|
|
4603
4809
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
4604
4810
|
}
|
|
4811
|
+
if (licenseEnabled()) reached.push(...await scanCopyleftLicenses(ctx.cwd));
|
|
4605
4812
|
return reached;
|
|
4606
4813
|
}
|
|
4607
4814
|
};
|
|
@@ -5540,7 +5747,7 @@ function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
|
5540
5747
|
}
|
|
5541
5748
|
return changed;
|
|
5542
5749
|
}
|
|
5543
|
-
function analyzeFile(ast,
|
|
5750
|
+
function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
5544
5751
|
attachParents(ast);
|
|
5545
5752
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
5546
5753
|
const functions = [];
|
|
@@ -5554,7 +5761,7 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5554
5761
|
}
|
|
5555
5762
|
const seeds = /* @__PURE__ */ new Map();
|
|
5556
5763
|
for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
|
|
5557
|
-
const returners = new Set(
|
|
5764
|
+
const returners = new Set(extraReturners2);
|
|
5558
5765
|
for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
|
|
5559
5766
|
let changed = false;
|
|
5560
5767
|
facts.set(ast, analyzeScope(ast, [], returners));
|
|
@@ -7426,6 +7633,39 @@ function scanPromptInjection(file, source) {
|
|
|
7426
7633
|
});
|
|
7427
7634
|
return findings;
|
|
7428
7635
|
}
|
|
7636
|
+
|
|
7637
|
+
// src/runtime/lru.ts
|
|
7638
|
+
var LruMap = class {
|
|
7639
|
+
constructor(max) {
|
|
7640
|
+
this.max = max;
|
|
7641
|
+
if (max < 1) throw new Error("LruMap max must be >= 1");
|
|
7642
|
+
}
|
|
7643
|
+
max;
|
|
7644
|
+
map = /* @__PURE__ */ new Map();
|
|
7645
|
+
get size() {
|
|
7646
|
+
return this.map.size;
|
|
7647
|
+
}
|
|
7648
|
+
has(key) {
|
|
7649
|
+
return this.map.has(key);
|
|
7650
|
+
}
|
|
7651
|
+
get(key) {
|
|
7652
|
+
if (!this.map.has(key)) return void 0;
|
|
7653
|
+
const value = this.map.get(key);
|
|
7654
|
+
this.map.delete(key);
|
|
7655
|
+
this.map.set(key, value);
|
|
7656
|
+
return value;
|
|
7657
|
+
}
|
|
7658
|
+
set(key, value) {
|
|
7659
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
7660
|
+
this.map.set(key, value);
|
|
7661
|
+
while (this.map.size > this.max) {
|
|
7662
|
+
const oldest = this.map.keys().next().value;
|
|
7663
|
+
if (oldest === void 0) break;
|
|
7664
|
+
this.map.delete(oldest);
|
|
7665
|
+
}
|
|
7666
|
+
return this;
|
|
7667
|
+
}
|
|
7668
|
+
};
|
|
7429
7669
|
function packageVersion() {
|
|
7430
7670
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7431
7671
|
for (let depth = 0; depth < 6; depth += 1) {
|
|
@@ -7446,7 +7686,7 @@ function packageVersion() {
|
|
|
7446
7686
|
|
|
7447
7687
|
// src/core/parse-pool.ts
|
|
7448
7688
|
var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7449
|
-
var cache =
|
|
7689
|
+
var cache = new LruMap(64);
|
|
7450
7690
|
function parsePoolEnabled() {
|
|
7451
7691
|
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
7452
7692
|
}
|
|
@@ -7454,9 +7694,15 @@ function parsePoolSize() {
|
|
|
7454
7694
|
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
7455
7695
|
if (raw !== void 0 && raw !== "") {
|
|
7456
7696
|
const parsed = Number(raw);
|
|
7457
|
-
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7697
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
|
|
7458
7698
|
}
|
|
7459
|
-
return Math.max(1, availableParallelism() - 1);
|
|
7699
|
+
return Math.min(2, Math.max(1, availableParallelism() - 1));
|
|
7700
|
+
}
|
|
7701
|
+
function workerMemoryMb() {
|
|
7702
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
|
|
7703
|
+
const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
|
|
7704
|
+
if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
|
|
7705
|
+
return 512;
|
|
7460
7706
|
}
|
|
7461
7707
|
function parseCacheKey(code) {
|
|
7462
7708
|
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
@@ -7466,12 +7712,37 @@ function parseOne(code) {
|
|
|
7466
7712
|
const hit = cache.get(key);
|
|
7467
7713
|
if (hit !== void 0) return Promise.resolve(hit);
|
|
7468
7714
|
return new Promise((resolve5, reject) => {
|
|
7469
|
-
const worker = new Worker(fileURLToPath(workerUrl), {
|
|
7470
|
-
|
|
7715
|
+
const worker = new Worker(fileURLToPath(workerUrl), {
|
|
7716
|
+
workerData: { code },
|
|
7717
|
+
resourceLimits: { maxOldGenerationSizeMb: workerMemoryMb() }
|
|
7718
|
+
});
|
|
7719
|
+
let settled = false;
|
|
7720
|
+
const finish = () => {
|
|
7721
|
+
void worker.terminate();
|
|
7722
|
+
};
|
|
7723
|
+
const succeed = (ast) => {
|
|
7724
|
+
if (settled) return;
|
|
7725
|
+
settled = true;
|
|
7471
7726
|
cache.set(key, ast);
|
|
7727
|
+
finish();
|
|
7472
7728
|
resolve5(ast);
|
|
7729
|
+
};
|
|
7730
|
+
const fail = (error) => {
|
|
7731
|
+
if (settled) return;
|
|
7732
|
+
settled = true;
|
|
7733
|
+
finish();
|
|
7734
|
+
reject(error);
|
|
7735
|
+
};
|
|
7736
|
+
worker.once("message", (ast) => {
|
|
7737
|
+
succeed(ast);
|
|
7738
|
+
});
|
|
7739
|
+
worker.once("error", (error) => {
|
|
7740
|
+
fail(error);
|
|
7741
|
+
});
|
|
7742
|
+
worker.once("exit", (code2) => {
|
|
7743
|
+
if (settled) return;
|
|
7744
|
+
fail(new Error(`parse worker exited ${code2 ?? "null"}`));
|
|
7473
7745
|
});
|
|
7474
|
-
worker.once("error", reject);
|
|
7475
7746
|
});
|
|
7476
7747
|
}
|
|
7477
7748
|
async function parseWithPool(codes) {
|
|
@@ -7525,8 +7796,8 @@ function snippetOf(source, node) {
|
|
|
7525
7796
|
if (node.range === void 0) return "";
|
|
7526
7797
|
return source.slice(node.range[0], node.range[1]).replace(/\s+/g, " ").trim().slice(0, 120);
|
|
7527
7798
|
}
|
|
7528
|
-
function findingsFromAst(filename, source, ast,
|
|
7529
|
-
const facts = analyzeFile(ast,
|
|
7799
|
+
function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
|
|
7800
|
+
const facts = analyzeFile(ast, extraReturners2);
|
|
7530
7801
|
const findings = [];
|
|
7531
7802
|
for (const rule of sastRules) {
|
|
7532
7803
|
const ctx = {
|
|
@@ -7604,52 +7875,203 @@ function findingsFromTemplate(filename, source) {
|
|
|
7604
7875
|
xssRule.create(ctx);
|
|
7605
7876
|
return findings;
|
|
7606
7877
|
}
|
|
7878
|
+
function dedupeSastSinks(findings) {
|
|
7879
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7880
|
+
const out = [];
|
|
7881
|
+
for (const finding3 of findings) {
|
|
7882
|
+
if (finding3.module !== "sast" || finding3.line === void 0) {
|
|
7883
|
+
out.push(finding3);
|
|
7884
|
+
continue;
|
|
7885
|
+
}
|
|
7886
|
+
const key = `${finding3.ruleId}\0${finding3.file ?? ""}\0${finding3.line}`;
|
|
7887
|
+
if (seen.has(key)) continue;
|
|
7888
|
+
seen.add(key);
|
|
7889
|
+
out.push(finding3);
|
|
7890
|
+
}
|
|
7891
|
+
return out;
|
|
7892
|
+
}
|
|
7893
|
+
function sastStreamThreshold() {
|
|
7894
|
+
const raw = process.env.FULLSHIELD_SAST_STREAM_AT;
|
|
7895
|
+
if (raw !== void 0 && raw !== "") {
|
|
7896
|
+
const parsed = Number(raw);
|
|
7897
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7898
|
+
}
|
|
7899
|
+
return 4e3;
|
|
7900
|
+
}
|
|
7901
|
+
function sastBatchSize() {
|
|
7902
|
+
const raw = process.env.FULLSHIELD_SAST_BATCH;
|
|
7903
|
+
if (raw !== void 0 && raw !== "") {
|
|
7904
|
+
const parsed = Number(raw);
|
|
7905
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7906
|
+
}
|
|
7907
|
+
return 150;
|
|
7908
|
+
}
|
|
7909
|
+
function skipFinding(rel, message) {
|
|
7910
|
+
return {
|
|
7911
|
+
id: makeFindingId("SAST-SKIP-001", rel, message),
|
|
7912
|
+
ruleId: "SAST-SKIP-001",
|
|
7913
|
+
module: "sast",
|
|
7914
|
+
title: "File skipped by SAST",
|
|
7915
|
+
severity: "info",
|
|
7916
|
+
confidence: "high",
|
|
7917
|
+
file: rel,
|
|
7918
|
+
message,
|
|
7919
|
+
remediation: "Raise sast.maxFileBytes in fullshield.config.json to parse this file, or leave it skipped."
|
|
7920
|
+
};
|
|
7921
|
+
}
|
|
7922
|
+
function skipReason(source, maxBytes) {
|
|
7923
|
+
if (source.includes("\0")) return "it contains NUL bytes";
|
|
7924
|
+
const bytes = Buffer.byteLength(source);
|
|
7925
|
+
if (bytes > maxBytes) return `${bytes} bytes exceeds the SAST parse limit of ${maxBytes} bytes`;
|
|
7926
|
+
return void 0;
|
|
7927
|
+
}
|
|
7928
|
+
async function readScanSource(cwd, rel) {
|
|
7929
|
+
try {
|
|
7930
|
+
return await readFile(join(cwd, rel), "utf8");
|
|
7931
|
+
} catch {
|
|
7932
|
+
return void 0;
|
|
7933
|
+
}
|
|
7934
|
+
}
|
|
7935
|
+
function cloneAst(value) {
|
|
7936
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
7937
|
+
try {
|
|
7938
|
+
return structuredClone(value);
|
|
7939
|
+
} catch {
|
|
7940
|
+
return void 0;
|
|
7941
|
+
}
|
|
7942
|
+
}
|
|
7943
|
+
async function parseInProcess(source) {
|
|
7944
|
+
try {
|
|
7945
|
+
return await parseSource(source);
|
|
7946
|
+
} catch {
|
|
7947
|
+
return void 0;
|
|
7948
|
+
}
|
|
7949
|
+
}
|
|
7950
|
+
async function parseCode(source) {
|
|
7951
|
+
if (!parsePoolEnabled()) return parseInProcess(source);
|
|
7952
|
+
try {
|
|
7953
|
+
const cloned = cloneAst((await parseWithPool([source]))[0]);
|
|
7954
|
+
if (cloned !== void 0) return cloned;
|
|
7955
|
+
} catch {
|
|
7956
|
+
}
|
|
7957
|
+
return parseInProcess(source);
|
|
7958
|
+
}
|
|
7959
|
+
function releaseParsed(parsed) {
|
|
7960
|
+
for (const entry of parsed) {
|
|
7961
|
+
entry.source = "";
|
|
7962
|
+
entry.ast = void 0;
|
|
7963
|
+
}
|
|
7964
|
+
parsed.length = 0;
|
|
7965
|
+
}
|
|
7966
|
+
function extraReturners(rel, ast, files, returning) {
|
|
7967
|
+
const ns = collectRequires(rel, ast, files);
|
|
7968
|
+
const extra = new Set(returning.get(rel));
|
|
7969
|
+
for (const [local, spec] of ns) {
|
|
7970
|
+
const hash = spec.indexOf("#");
|
|
7971
|
+
const file = hash >= 0 ? spec.slice(0, hash) : spec;
|
|
7972
|
+
const exp = hash >= 0 ? spec.slice(hash + 1) : local;
|
|
7973
|
+
if (returning.get(file)?.has(exp)) extra.add(local);
|
|
7974
|
+
}
|
|
7975
|
+
return extra;
|
|
7976
|
+
}
|
|
7977
|
+
async function runSastStreaming(ctx) {
|
|
7978
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
7979
|
+
const findings = [];
|
|
7980
|
+
const returning = /* @__PURE__ */ new Map();
|
|
7981
|
+
const parseable = [];
|
|
7982
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
7983
|
+
let projectCsrf = false;
|
|
7984
|
+
for (const rel of ctx.files) {
|
|
7985
|
+
const isSource = SOURCE_EXT.test(rel);
|
|
7986
|
+
const isTemplate = TEMPLATE_EXT.test(rel);
|
|
7987
|
+
if (!isSource && !isTemplate) continue;
|
|
7988
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
7989
|
+
if (source === void 0) continue;
|
|
7990
|
+
const why = skipReason(source, maxBytes);
|
|
7991
|
+
if (why !== void 0) {
|
|
7992
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
7993
|
+
continue;
|
|
7994
|
+
}
|
|
7995
|
+
if (sourceHasCsrfGuard(source)) projectCsrf = true;
|
|
7996
|
+
if (isTemplate) {
|
|
7997
|
+
findings.push(...findingsFromTemplate(rel, source));
|
|
7998
|
+
continue;
|
|
7999
|
+
}
|
|
8000
|
+
const ast = await parseCode(source);
|
|
8001
|
+
if (ast === void 0) continue;
|
|
8002
|
+
returning.set(rel, functionsThatReturnTaint(ast));
|
|
8003
|
+
parseable.push(rel);
|
|
8004
|
+
fileSet.add(rel);
|
|
8005
|
+
}
|
|
8006
|
+
for (const rel of parseable) {
|
|
8007
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
8008
|
+
if (source === void 0) continue;
|
|
8009
|
+
const ast = await parseCode(source);
|
|
8010
|
+
if (ast === void 0) continue;
|
|
8011
|
+
findings.push(
|
|
8012
|
+
...findingsFromAst(rel, source, ast, extraReturners(rel, ast, fileSet, returning), projectCsrf)
|
|
8013
|
+
);
|
|
8014
|
+
}
|
|
8015
|
+
const batch = sastBatchSize();
|
|
8016
|
+
for (let i = 0; i < parseable.length; i += batch) {
|
|
8017
|
+
const parsed = [];
|
|
8018
|
+
for (const rel of parseable.slice(i, i + batch)) {
|
|
8019
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
8020
|
+
if (source === void 0) continue;
|
|
8021
|
+
const ast = await parseCode(source);
|
|
8022
|
+
if (ast === void 0) continue;
|
|
8023
|
+
parsed.push({ file: rel, source, ast });
|
|
8024
|
+
}
|
|
8025
|
+
findings.push(...runInterproc(parsed));
|
|
8026
|
+
releaseParsed(parsed);
|
|
8027
|
+
}
|
|
8028
|
+
return dedupeSastSinks(findings);
|
|
8029
|
+
}
|
|
7607
8030
|
var sastModule = {
|
|
7608
8031
|
name: "sast",
|
|
7609
8032
|
async run(ctx) {
|
|
8033
|
+
const candidates = ctx.files.filter((rel) => SOURCE_EXT.test(rel) || TEMPLATE_EXT.test(rel));
|
|
8034
|
+
if (candidates.length > sastStreamThreshold()) return runSastStreaming(ctx);
|
|
8035
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
7610
8036
|
const findings = [];
|
|
7611
8037
|
const parsed = [];
|
|
7612
|
-
for (const rel of
|
|
7613
|
-
const
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
source = await readFile(join(ctx.cwd, rel), "utf8");
|
|
7619
|
-
} catch {
|
|
8038
|
+
for (const rel of candidates) {
|
|
8039
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
8040
|
+
if (source === void 0) continue;
|
|
8041
|
+
const why = skipReason(source, maxBytes);
|
|
8042
|
+
if (why !== void 0) {
|
|
8043
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
7620
8044
|
continue;
|
|
7621
8045
|
}
|
|
7622
|
-
if (
|
|
7623
|
-
if (isTemplate) {
|
|
8046
|
+
if (TEMPLATE_EXT.test(rel)) {
|
|
7624
8047
|
findings.push(...findingsFromTemplate(rel, source));
|
|
7625
8048
|
continue;
|
|
7626
8049
|
}
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
parsed.push({ file: rel, source, ast });
|
|
7631
|
-
} catch {
|
|
7632
|
-
}
|
|
8050
|
+
const ast = await parseCode(source);
|
|
8051
|
+
if (ast === void 0) continue;
|
|
8052
|
+
parsed.push({ file: rel, source, ast });
|
|
7633
8053
|
}
|
|
7634
8054
|
const files = new Set(parsed.map((p) => p.file));
|
|
7635
8055
|
const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
|
|
7636
8056
|
const returning = /* @__PURE__ */ new Map();
|
|
7637
|
-
for (const
|
|
7638
|
-
returning.set(
|
|
8057
|
+
for (const entry of parsed) {
|
|
8058
|
+
returning.set(entry.file, functionsThatReturnTaint(entry.ast));
|
|
7639
8059
|
}
|
|
7640
|
-
for (const
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
8060
|
+
for (const entry of parsed) {
|
|
8061
|
+
findings.push(
|
|
8062
|
+
...findingsFromAst(
|
|
8063
|
+
entry.file,
|
|
8064
|
+
entry.source,
|
|
8065
|
+
entry.ast,
|
|
8066
|
+
extraReturners(entry.file, entry.ast, files, returning),
|
|
8067
|
+
projectCsrf
|
|
8068
|
+
)
|
|
8069
|
+
);
|
|
7650
8070
|
}
|
|
7651
8071
|
findings.push(...runInterproc(parsed));
|
|
7652
|
-
|
|
8072
|
+
const out = dedupeSastSinks(findings);
|
|
8073
|
+
releaseParsed(parsed);
|
|
8074
|
+
return out;
|
|
7653
8075
|
}
|
|
7654
8076
|
};
|
|
7655
8077
|
|
|
@@ -8110,16 +8532,37 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
8110
8532
|
}
|
|
8111
8533
|
};
|
|
8112
8534
|
}
|
|
8535
|
+
function isIsoDate(value) {
|
|
8536
|
+
if (!/^\d{4}-\d{2}-\d{2}(T[\d:.+\-Z]+)?$/.test(value)) return false;
|
|
8537
|
+
return Number.isFinite(Date.parse(value));
|
|
8538
|
+
}
|
|
8539
|
+
function expiresAtMs(expires) {
|
|
8540
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8541
|
+
return Date.parse(expires);
|
|
8542
|
+
}
|
|
8543
|
+
function validSuppression(value) {
|
|
8544
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
8545
|
+
const row = value;
|
|
8546
|
+
if (typeof row.ruleId !== "string" || row.ruleId.length === 0) return void 0;
|
|
8547
|
+
if (typeof row.owner !== "string" || row.owner.length === 0) return void 0;
|
|
8548
|
+
if (typeof row.reason !== "string" || row.reason.length === 0) return void 0;
|
|
8549
|
+
if (typeof row.expires !== "string" || !isIsoDate(row.expires)) return void 0;
|
|
8550
|
+
const file = row.file === void 0 ? void 0 : row.file.replace(/\\/g, "/");
|
|
8551
|
+
if (row.file !== void 0 && (typeof row.file !== "string" || row.file.length === 0)) return void 0;
|
|
8552
|
+
return { ruleId: row.ruleId, file, owner: row.owner, reason: row.reason, expires: row.expires };
|
|
8553
|
+
}
|
|
8113
8554
|
async function readBaseline(path) {
|
|
8114
8555
|
try {
|
|
8115
8556
|
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
8116
8557
|
if (raw.version !== 1 || !Array.isArray(raw.findings)) return void 0;
|
|
8558
|
+
if (raw.suppressions !== void 0 && !Array.isArray(raw.suppressions)) return void 0;
|
|
8117
8559
|
return raw;
|
|
8118
8560
|
} catch {
|
|
8119
8561
|
return void 0;
|
|
8120
8562
|
}
|
|
8121
8563
|
}
|
|
8122
8564
|
async function writeBaseline(path, findings) {
|
|
8565
|
+
const existing = await readBaseline(path);
|
|
8123
8566
|
const doc = {
|
|
8124
8567
|
version: 1,
|
|
8125
8568
|
findings: findings.map((f) => ({
|
|
@@ -8127,7 +8570,8 @@ async function writeBaseline(path, findings) {
|
|
|
8127
8570
|
ruleId: f.ruleId,
|
|
8128
8571
|
file: f.file,
|
|
8129
8572
|
line: f.line
|
|
8130
|
-
}))
|
|
8573
|
+
})),
|
|
8574
|
+
...existing?.suppressions !== void 0 ? { suppressions: existing.suppressions } : {}
|
|
8131
8575
|
};
|
|
8132
8576
|
await writeFile(path, `${JSON.stringify(doc, null, 2)}
|
|
8133
8577
|
`, "utf8");
|
|
@@ -8137,6 +8581,30 @@ function applyBaseline(findings, baseline) {
|
|
|
8137
8581
|
const known = new Set(baseline.findings.map((f) => f.id));
|
|
8138
8582
|
return findings.filter((f) => !known.has(f.id));
|
|
8139
8583
|
}
|
|
8584
|
+
function applySuppressions(findings, rows, now = /* @__PURE__ */ new Date()) {
|
|
8585
|
+
if (rows === void 0 || rows.length === 0) return { findings: [...findings], errors: [] };
|
|
8586
|
+
const errors = [];
|
|
8587
|
+
const active = [];
|
|
8588
|
+
rows.forEach((row, index) => {
|
|
8589
|
+
const parsed = validSuppression(row);
|
|
8590
|
+
if (parsed === void 0) {
|
|
8591
|
+
errors.push(`suppression ${index + 1} needs ruleId, owner, reason, and an ISO expires date`);
|
|
8592
|
+
return;
|
|
8593
|
+
}
|
|
8594
|
+
if (expiresAtMs(parsed.expires) < now.getTime()) {
|
|
8595
|
+
errors.push(
|
|
8596
|
+
`suppression for ${parsed.ruleId} expired on ${parsed.expires} (owner: ${parsed.owner}); renew or remove it`
|
|
8597
|
+
);
|
|
8598
|
+
return;
|
|
8599
|
+
}
|
|
8600
|
+
active.push(parsed);
|
|
8601
|
+
});
|
|
8602
|
+
const kept = findings.filter((finding3) => {
|
|
8603
|
+
const file = (finding3.file ?? "").replace(/\\/g, "/");
|
|
8604
|
+
return !active.some((row) => row.ruleId === finding3.ruleId && (row.file === void 0 || row.file === file));
|
|
8605
|
+
});
|
|
8606
|
+
return { findings: kept, errors };
|
|
8607
|
+
}
|
|
8140
8608
|
|
|
8141
8609
|
// src/core/config.ts
|
|
8142
8610
|
init_finding();
|
|
@@ -8145,14 +8613,27 @@ var DEFAULT_CONFIG = {
|
|
|
8145
8613
|
modules: [...MODULE_NAMES],
|
|
8146
8614
|
failOn: "high",
|
|
8147
8615
|
ignore: ["dist/**", "**/*.test.ts"],
|
|
8616
|
+
defaultExcludes: true,
|
|
8148
8617
|
baseline: ".fullshield-baseline.json",
|
|
8149
8618
|
notify: { slackWebhookEnv: "FULLSHIELD_SLACK_WEBHOOK" },
|
|
8150
8619
|
deps: { offline: false },
|
|
8151
|
-
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 }
|
|
8620
|
+
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 },
|
|
8621
|
+
sast: { maxFileBytes: 1024 * 1024 }
|
|
8152
8622
|
};
|
|
8153
8623
|
var ConfigError = class extends Error {
|
|
8154
8624
|
};
|
|
8155
|
-
var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
8625
|
+
var KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
8626
|
+
"modules",
|
|
8627
|
+
"failOn",
|
|
8628
|
+
"ignore",
|
|
8629
|
+
"defaultExcludes",
|
|
8630
|
+
"excludes",
|
|
8631
|
+
"baseline",
|
|
8632
|
+
"notify",
|
|
8633
|
+
"deps",
|
|
8634
|
+
"ai",
|
|
8635
|
+
"sast"
|
|
8636
|
+
]);
|
|
8156
8637
|
function validate(raw, source) {
|
|
8157
8638
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
8158
8639
|
throw new ConfigError(`${source}: config must be a JSON object`);
|
|
@@ -8254,6 +8735,32 @@ function validate(raw, source) {
|
|
|
8254
8735
|
}
|
|
8255
8736
|
out.ai = ai;
|
|
8256
8737
|
}
|
|
8738
|
+
if (record.defaultExcludes !== void 0) {
|
|
8739
|
+
if (typeof record.defaultExcludes !== "boolean") {
|
|
8740
|
+
throw new ConfigError(`${source}: "defaultExcludes" must be a boolean`);
|
|
8741
|
+
}
|
|
8742
|
+
out.defaultExcludes = record.defaultExcludes;
|
|
8743
|
+
}
|
|
8744
|
+
if (record.excludes !== void 0) {
|
|
8745
|
+
if (!Array.isArray(record.excludes) || record.excludes.some((p) => typeof p !== "string")) {
|
|
8746
|
+
throw new ConfigError(`${source}: "excludes" must be an array of glob strings`);
|
|
8747
|
+
}
|
|
8748
|
+
out.excludes = record.excludes;
|
|
8749
|
+
}
|
|
8750
|
+
if (record.sast !== void 0) {
|
|
8751
|
+
if (typeof record.sast !== "object" || record.sast === null || Array.isArray(record.sast)) {
|
|
8752
|
+
throw new ConfigError(`${source}: "sast" must be an object`);
|
|
8753
|
+
}
|
|
8754
|
+
const rawSast = record.sast;
|
|
8755
|
+
const sast = { maxFileBytes: 1024 * 1024 };
|
|
8756
|
+
if (rawSast.maxFileBytes !== void 0) {
|
|
8757
|
+
if (typeof rawSast.maxFileBytes !== "number" || rawSast.maxFileBytes < 1) {
|
|
8758
|
+
throw new ConfigError(`${source}: "sast.maxFileBytes" must be a positive number`);
|
|
8759
|
+
}
|
|
8760
|
+
sast.maxFileBytes = rawSast.maxFileBytes;
|
|
8761
|
+
}
|
|
8762
|
+
out.sast = sast;
|
|
8763
|
+
}
|
|
8257
8764
|
return out;
|
|
8258
8765
|
}
|
|
8259
8766
|
async function loadConfig(cwd, configPath) {
|
|
@@ -8280,10 +8787,26 @@ async function loadConfig(cwd, configPath) {
|
|
|
8280
8787
|
return { ...structuredClone(DEFAULT_CONFIG), ...validate(raw, file) };
|
|
8281
8788
|
}
|
|
8282
8789
|
var ignoreFactory = ignoreModule.default;
|
|
8283
|
-
var
|
|
8790
|
+
var HARD_IGNORED = [".git/", ".fullshield/"];
|
|
8791
|
+
var DEFAULT_EXCLUDES = [
|
|
8792
|
+
"node_modules/",
|
|
8793
|
+
"dist/",
|
|
8794
|
+
"build/",
|
|
8795
|
+
".next/",
|
|
8796
|
+
"coverage/",
|
|
8797
|
+
"vendor/",
|
|
8798
|
+
"vendored/",
|
|
8799
|
+
"third_party/",
|
|
8800
|
+
"third-party/",
|
|
8801
|
+
"compiled/",
|
|
8802
|
+
"*.min.js"
|
|
8803
|
+
];
|
|
8284
8804
|
async function walkFiles(options) {
|
|
8285
8805
|
const ig = ignoreFactory();
|
|
8286
|
-
ig.add(
|
|
8806
|
+
ig.add(HARD_IGNORED);
|
|
8807
|
+
if (options.defaultExcludes !== false) {
|
|
8808
|
+
ig.add(options.excludes ?? DEFAULT_EXCLUDES);
|
|
8809
|
+
}
|
|
8287
8810
|
try {
|
|
8288
8811
|
ig.add(await readFile(join(options.cwd, ".gitignore"), "utf8"));
|
|
8289
8812
|
} catch {
|
|
@@ -8501,7 +9024,7 @@ function resolvePolicyBranch(cwd, env = process.env) {
|
|
|
8501
9024
|
return "*";
|
|
8502
9025
|
}
|
|
8503
9026
|
}
|
|
8504
|
-
function
|
|
9027
|
+
function expiresAtMs2(expires) {
|
|
8505
9028
|
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
|
|
8506
9029
|
return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8507
9030
|
}
|
|
@@ -8523,7 +9046,7 @@ function applyPolicy(findings, policy, ctx = {}) {
|
|
|
8523
9046
|
});
|
|
8524
9047
|
const suppressed = /* @__PURE__ */ new Set();
|
|
8525
9048
|
for (const exception of policy.exceptions ?? []) {
|
|
8526
|
-
if (
|
|
9049
|
+
if (expiresAtMs2(exception.expires) < now.getTime()) {
|
|
8527
9050
|
errors.push(
|
|
8528
9051
|
`policy exception for ${exception.ruleId} expired on ${exception.expires} (owner: ${exception.owner}); renew or remove it`
|
|
8529
9052
|
);
|
|
@@ -8592,7 +9115,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8592
9115
|
options.modules ?? config.modules,
|
|
8593
9116
|
loadedPolicy?.requiredModules
|
|
8594
9117
|
);
|
|
8595
|
-
let files = await walkFiles({
|
|
9118
|
+
let files = await walkFiles({
|
|
9119
|
+
cwd,
|
|
9120
|
+
ignore: config.ignore,
|
|
9121
|
+
defaultExcludes: config.defaultExcludes,
|
|
9122
|
+
excludes: config.excludes
|
|
9123
|
+
});
|
|
8596
9124
|
if (options.staged === true) {
|
|
8597
9125
|
const staged = new Set(await listStagedFiles(cwd));
|
|
8598
9126
|
files = files.filter((file) => staged.has(file));
|
|
@@ -8626,6 +9154,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8626
9154
|
);
|
|
8627
9155
|
}
|
|
8628
9156
|
const extraHits = [];
|
|
9157
|
+
let historyNotice;
|
|
8629
9158
|
const secretsWanted = modulesRequested.includes("secrets");
|
|
8630
9159
|
if (secretsAdvEnabled() && secretsWanted) {
|
|
8631
9160
|
if (options.history === true) {
|
|
@@ -8633,6 +9162,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8633
9162
|
const hist = await scanGitHistory2(cwd);
|
|
8634
9163
|
extraHits.push(...hist.hits);
|
|
8635
9164
|
collected.push(...hist.findings);
|
|
9165
|
+
historyNotice = hist.history;
|
|
8636
9166
|
}
|
|
8637
9167
|
if (options.bundle !== void 0 && options.bundle.length > 0) {
|
|
8638
9168
|
const { scanBundle: scanBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
@@ -8653,7 +9183,11 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8653
9183
|
if (options.updateBaseline === true) {
|
|
8654
9184
|
await writeBaseline(baselinePath, unsuppressed);
|
|
8655
9185
|
}
|
|
8656
|
-
|
|
9186
|
+
const baselineDoc = await readBaseline(baselinePath);
|
|
9187
|
+
let findings = sortFindings(applyBaseline(unsuppressed, baselineDoc));
|
|
9188
|
+
const suppressed = applySuppressions(findings, baselineDoc?.suppressions);
|
|
9189
|
+
findings = sortFindings(suppressed.findings);
|
|
9190
|
+
const suppressionErrors = suppressed.errors;
|
|
8657
9191
|
if (secretsAdvEnabled() && secretsWanted && options.verifySecrets === true && options.iOwnTheseCredentials === true) {
|
|
8658
9192
|
const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
8659
9193
|
const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
@@ -8725,10 +9259,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8725
9259
|
cwd,
|
|
8726
9260
|
filesScanned: files.length,
|
|
8727
9261
|
modulesRequested: [...modulesRequested],
|
|
8728
|
-
modulesRun
|
|
9262
|
+
modulesRun,
|
|
9263
|
+
...historyNotice !== void 0 ? { history: historyNotice } : {}
|
|
8729
9264
|
},
|
|
8730
9265
|
config,
|
|
8731
|
-
policy: policyResult
|
|
9266
|
+
policy: policyResult,
|
|
9267
|
+
...suppressionErrors.length > 0 ? { suppressionErrors } : {}
|
|
8732
9268
|
};
|
|
8733
9269
|
}
|
|
8734
9270
|
function scan(options = {}) {
|
|
@@ -9064,6 +9600,27 @@ init_suppression();
|
|
|
9064
9600
|
|
|
9065
9601
|
// src/reporters/console.ts
|
|
9066
9602
|
init_finding();
|
|
9603
|
+
|
|
9604
|
+
// src/reporters/redact.ts
|
|
9605
|
+
init_mask();
|
|
9606
|
+
var SECRET = /\b(?:AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk_live_[A-Za-z0-9]{10,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g;
|
|
9607
|
+
function redactSecrets2(text) {
|
|
9608
|
+
return text.replace(SECRET, (match) => maskSecret(match));
|
|
9609
|
+
}
|
|
9610
|
+
function redactFinding(finding3) {
|
|
9611
|
+
return {
|
|
9612
|
+
...finding3,
|
|
9613
|
+
title: redactSecrets2(finding3.title),
|
|
9614
|
+
message: redactSecrets2(finding3.message),
|
|
9615
|
+
remediation: redactSecrets2(finding3.remediation),
|
|
9616
|
+
snippet: finding3.snippet === void 0 ? void 0 : redactSecrets2(finding3.snippet)
|
|
9617
|
+
};
|
|
9618
|
+
}
|
|
9619
|
+
function redactScan(result) {
|
|
9620
|
+
return { ...result, findings: result.findings.map(redactFinding) };
|
|
9621
|
+
}
|
|
9622
|
+
|
|
9623
|
+
// src/reporters/console.ts
|
|
9067
9624
|
var SEVERITY_COLOR = {
|
|
9068
9625
|
critical: (t) => pc.bold(pc.red(t)),
|
|
9069
9626
|
high: (t) => pc.red(t),
|
|
@@ -9103,7 +9660,8 @@ function renderFinding(finding3) {
|
|
|
9103
9660
|
}
|
|
9104
9661
|
return lines;
|
|
9105
9662
|
}
|
|
9106
|
-
function renderConsoleReport(
|
|
9663
|
+
function renderConsoleReport(input) {
|
|
9664
|
+
const result = redactScan(input);
|
|
9107
9665
|
const { findings, summary, metadata } = result;
|
|
9108
9666
|
const lines = [];
|
|
9109
9667
|
lines.push(
|
|
@@ -9111,6 +9669,7 @@ function renderConsoleReport(result) {
|
|
|
9111
9669
|
` \u2014 scanned ${metadata.filesScanned} files in ${metadata.durationMs}ms (modules: ${metadata.modulesRun.length > 0 ? metadata.modulesRun.join(", ") : "none"})`
|
|
9112
9670
|
)
|
|
9113
9671
|
);
|
|
9672
|
+
if (metadata.history !== void 0) lines.push(metadata.history);
|
|
9114
9673
|
if (metadata.modulesRun.length < metadata.modulesRequested.length) {
|
|
9115
9674
|
const pending = metadata.modulesRequested.filter((m) => !metadata.modulesRun.includes(m));
|
|
9116
9675
|
lines.push(
|
|
@@ -9154,7 +9713,8 @@ function location2(finding3) {
|
|
|
9154
9713
|
if (finding3.file === void 0) return "(project)";
|
|
9155
9714
|
return finding3.line !== void 0 ? `${finding3.file}:${finding3.line}` : finding3.file;
|
|
9156
9715
|
}
|
|
9157
|
-
function renderHtmlReport(
|
|
9716
|
+
function renderHtmlReport(input) {
|
|
9717
|
+
const result = redactScan(input);
|
|
9158
9718
|
const sections = [];
|
|
9159
9719
|
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
9160
9720
|
for (const severity of bySeverityDesc) {
|
|
@@ -9201,7 +9761,7 @@ ${items}`);
|
|
|
9201
9761
|
|
|
9202
9762
|
// src/reporters/json.ts
|
|
9203
9763
|
function publicFinding(finding3) {
|
|
9204
|
-
const copy =
|
|
9764
|
+
const copy = redactFinding(finding3);
|
|
9205
9765
|
delete copy.aiVerdict;
|
|
9206
9766
|
delete copy.exploitScenario;
|
|
9207
9767
|
delete copy.aiReasoning;
|
|
@@ -9225,7 +9785,8 @@ function location3(finding3) {
|
|
|
9225
9785
|
if (finding3.file === void 0) return "(project)";
|
|
9226
9786
|
return finding3.line !== void 0 ? `${finding3.file}:${finding3.line}` : finding3.file;
|
|
9227
9787
|
}
|
|
9228
|
-
function renderMarkdownReport(
|
|
9788
|
+
function renderMarkdownReport(input) {
|
|
9789
|
+
const result = redactScan(input);
|
|
9229
9790
|
const lines = [
|
|
9230
9791
|
"# FullShield report",
|
|
9231
9792
|
"",
|
|
@@ -9277,7 +9838,8 @@ function sarifLevel(severity) {
|
|
|
9277
9838
|
function uri(file) {
|
|
9278
9839
|
return (file ?? "").replace(/\\/g, "/");
|
|
9279
9840
|
}
|
|
9280
|
-
function renderSarifReport(
|
|
9841
|
+
function renderSarifReport(input) {
|
|
9842
|
+
const result = redactScan(input);
|
|
9281
9843
|
const rules = /* @__PURE__ */ new Map();
|
|
9282
9844
|
for (const finding3 of result.findings) {
|
|
9283
9845
|
if (!rules.has(finding3.ruleId)) {
|
|
@@ -9384,6 +9946,257 @@ function validateSarif21(doc) {
|
|
|
9384
9946
|
return errors;
|
|
9385
9947
|
}
|
|
9386
9948
|
|
|
9949
|
+
// src/core/fingerprint.ts
|
|
9950
|
+
init_finding();
|
|
9951
|
+
function lineIndependentFingerprint(finding3) {
|
|
9952
|
+
const file = (finding3.file ?? "").replace(/\\/g, "/");
|
|
9953
|
+
const text = (finding3.snippet ?? finding3.message ?? "").replace(/\s+/g, " ").trim();
|
|
9954
|
+
return makeFindingId(finding3.ruleId, file, `${finding3.severity}\0${text}`);
|
|
9955
|
+
}
|
|
9956
|
+
|
|
9957
|
+
// src/reporters/ci.ts
|
|
9958
|
+
init_finding();
|
|
9959
|
+
var MESSAGE_CAP = 4e3;
|
|
9960
|
+
function posix2(file) {
|
|
9961
|
+
return file.replace(/\\/g, "/");
|
|
9962
|
+
}
|
|
9963
|
+
function clip(text) {
|
|
9964
|
+
return text.length <= MESSAGE_CAP ? text : `${text.slice(0, MESSAGE_CAP)}\u2026`;
|
|
9965
|
+
}
|
|
9966
|
+
function githubEscape(value, property) {
|
|
9967
|
+
let out = value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A");
|
|
9968
|
+
if (property) out = out.replace(/,/g, "%2C");
|
|
9969
|
+
return out;
|
|
9970
|
+
}
|
|
9971
|
+
function githubLevel(severity) {
|
|
9972
|
+
if (severity === "critical" || severity === "high") return "error";
|
|
9973
|
+
if (severity === "medium") return "warning";
|
|
9974
|
+
return "notice";
|
|
9975
|
+
}
|
|
9976
|
+
function gitlabSeverity(severity) {
|
|
9977
|
+
if (severity === "info") return "info";
|
|
9978
|
+
if (severity === "low") return "minor";
|
|
9979
|
+
if (severity === "medium") return "major";
|
|
9980
|
+
if (severity === "high") return "critical";
|
|
9981
|
+
return "blocker";
|
|
9982
|
+
}
|
|
9983
|
+
function xmlEscape(value) {
|
|
9984
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9985
|
+
}
|
|
9986
|
+
function oneLine(value) {
|
|
9987
|
+
return value.replace(/\s+/g, " ").trim();
|
|
9988
|
+
}
|
|
9989
|
+
function renderGithubAnnotations(input) {
|
|
9990
|
+
const result = redactScan(input);
|
|
9991
|
+
if (result.findings.length === 0) {
|
|
9992
|
+
return "::notice title=FullShield::No findings\n";
|
|
9993
|
+
}
|
|
9994
|
+
const lines = result.findings.map((finding3) => {
|
|
9995
|
+
const props = [];
|
|
9996
|
+
if (finding3.file !== void 0) props.push(`file=${githubEscape(posix2(finding3.file), true)}`);
|
|
9997
|
+
if (finding3.line !== void 0) props.push(`line=${finding3.line}`);
|
|
9998
|
+
if (finding3.column !== void 0) props.push(`col=${finding3.column + 1}`);
|
|
9999
|
+
props.push(`title=${githubEscape(finding3.ruleId, true)}`);
|
|
10000
|
+
const message = githubEscape(clip(finding3.message), false);
|
|
10001
|
+
return `::${githubLevel(finding3.severity)} ${props.join(",")}::${message}`;
|
|
10002
|
+
});
|
|
10003
|
+
return `${lines.join("\n")}
|
|
10004
|
+
`;
|
|
10005
|
+
}
|
|
10006
|
+
function junitCase(finding3) {
|
|
10007
|
+
const file = finding3.file !== void 0 ? posix2(finding3.file) : "fullshield";
|
|
10008
|
+
const attrs = [
|
|
10009
|
+
`name="${xmlEscape(finding3.ruleId)}"`,
|
|
10010
|
+
`classname="${xmlEscape(file)}"`,
|
|
10011
|
+
`file="${xmlEscape(file)}"`
|
|
10012
|
+
];
|
|
10013
|
+
if (finding3.line !== void 0) attrs.push(`line="${finding3.line}"`);
|
|
10014
|
+
const open = `<testcase ${attrs.join(" ")}>`;
|
|
10015
|
+
if (!meetsThreshold(finding3.severity, "medium")) {
|
|
10016
|
+
return `${open}<skipped message="${xmlEscape(oneLine(clip(finding3.message)))}"/></testcase>`;
|
|
10017
|
+
}
|
|
10018
|
+
const detail = clip(`${finding3.message}${finding3.remediation.length > 0 ? `
|
|
10019
|
+
Fix: ${finding3.remediation}` : ""}`);
|
|
10020
|
+
return `${open}<failure type="${finding3.severity}" message="${xmlEscape(oneLine(clip(finding3.message)))}">${xmlEscape(detail)}</failure></testcase>`;
|
|
10021
|
+
}
|
|
10022
|
+
function renderJunitReport(input) {
|
|
10023
|
+
const result = redactScan(input);
|
|
10024
|
+
const header = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
10025
|
+
if (result.findings.length === 0) {
|
|
10026
|
+
return `${header}
|
|
10027
|
+
<testsuites name="fullshield" tests="1" failures="0" skipped="0"><testsuite name="fullshield" tests="1" failures="0" skipped="0"><testcase name="scan" classname="fullshield"/></testsuite></testsuites>
|
|
10028
|
+
`;
|
|
10029
|
+
}
|
|
10030
|
+
const groups = /* @__PURE__ */ new Map();
|
|
10031
|
+
for (const finding3 of result.findings) {
|
|
10032
|
+
const list = groups.get(finding3.module) ?? [];
|
|
10033
|
+
list.push(finding3);
|
|
10034
|
+
groups.set(finding3.module, list);
|
|
10035
|
+
}
|
|
10036
|
+
let failures = 0;
|
|
10037
|
+
let skipped = 0;
|
|
10038
|
+
const suites = [];
|
|
10039
|
+
for (const [name, findings] of groups) {
|
|
10040
|
+
let suiteFailures = 0;
|
|
10041
|
+
let suiteSkipped = 0;
|
|
10042
|
+
const cases = findings.map((finding3) => {
|
|
10043
|
+
if (meetsThreshold(finding3.severity, "medium")) suiteFailures += 1;
|
|
10044
|
+
else suiteSkipped += 1;
|
|
10045
|
+
return junitCase(finding3);
|
|
10046
|
+
});
|
|
10047
|
+
failures += suiteFailures;
|
|
10048
|
+
skipped += suiteSkipped;
|
|
10049
|
+
suites.push(
|
|
10050
|
+
`<testsuite name="${xmlEscape(name)}" tests="${findings.length}" failures="${suiteFailures}" skipped="${suiteSkipped}">${cases.join("")}</testsuite>`
|
|
10051
|
+
);
|
|
10052
|
+
}
|
|
10053
|
+
return `${header}
|
|
10054
|
+
<testsuites name="fullshield" tests="${result.findings.length}" failures="${failures}" skipped="${skipped}">${suites.join("")}</testsuites>
|
|
10055
|
+
`;
|
|
10056
|
+
}
|
|
10057
|
+
function renderGitlabCodeQuality(input) {
|
|
10058
|
+
const result = redactScan(input);
|
|
10059
|
+
const report = result.findings.map((finding3) => {
|
|
10060
|
+
const path = finding3.file !== void 0 ? posix2(finding3.file) : ".";
|
|
10061
|
+
const location4 = { path };
|
|
10062
|
+
if (finding3.line !== void 0) location4.lines = { begin: finding3.line };
|
|
10063
|
+
return {
|
|
10064
|
+
description: clip(finding3.message),
|
|
10065
|
+
check_name: finding3.ruleId,
|
|
10066
|
+
fingerprint: lineIndependentFingerprint(finding3),
|
|
10067
|
+
severity: gitlabSeverity(finding3.severity),
|
|
10068
|
+
location: location4
|
|
10069
|
+
};
|
|
10070
|
+
});
|
|
10071
|
+
return `${JSON.stringify(report, null, 2)}
|
|
10072
|
+
`;
|
|
10073
|
+
}
|
|
10074
|
+
|
|
10075
|
+
// src/diff/index.ts
|
|
10076
|
+
init_finding();
|
|
10077
|
+
function posix3(file) {
|
|
10078
|
+
return (file ?? "").replace(/\\/g, "/");
|
|
10079
|
+
}
|
|
10080
|
+
function diffKey(finding3) {
|
|
10081
|
+
return lineIndependentFingerprint(finding3);
|
|
10082
|
+
}
|
|
10083
|
+
function groupById(findings) {
|
|
10084
|
+
const groups = /* @__PURE__ */ new Map();
|
|
10085
|
+
for (const finding3 of findings) {
|
|
10086
|
+
if (finding3.id === void 0 || finding3.id.length === 0) continue;
|
|
10087
|
+
const list = groups.get(finding3.id) ?? [];
|
|
10088
|
+
list.push(finding3);
|
|
10089
|
+
groups.set(finding3.id, list);
|
|
10090
|
+
}
|
|
10091
|
+
return groups;
|
|
10092
|
+
}
|
|
10093
|
+
function pairMoves(removed, added) {
|
|
10094
|
+
const removedIds = groupById(removed);
|
|
10095
|
+
const addedIds = groupById(added);
|
|
10096
|
+
const moved = [];
|
|
10097
|
+
for (const [id, group] of removedIds) {
|
|
10098
|
+
const match = addedIds.get(id);
|
|
10099
|
+
if (group.length === 1 && match?.length === 1 && group[0] !== void 0 && match[0] !== void 0) {
|
|
10100
|
+
moved.push({ before: group[0], after: match[0] });
|
|
10101
|
+
}
|
|
10102
|
+
}
|
|
10103
|
+
return moved;
|
|
10104
|
+
}
|
|
10105
|
+
function byLocation(a, b) {
|
|
10106
|
+
return (a.line ?? 0) - (b.line ?? 0) || a.id.localeCompare(b.id) || a.ruleId.localeCompare(b.ruleId);
|
|
10107
|
+
}
|
|
10108
|
+
function groupFindings(findings) {
|
|
10109
|
+
const groups = /* @__PURE__ */ new Map();
|
|
10110
|
+
for (const finding3 of findings) {
|
|
10111
|
+
const key = diffKey(finding3);
|
|
10112
|
+
const list = groups.get(key) ?? [];
|
|
10113
|
+
list.push(finding3);
|
|
10114
|
+
groups.set(key, list);
|
|
10115
|
+
}
|
|
10116
|
+
return groups;
|
|
10117
|
+
}
|
|
10118
|
+
function diffScans(before, after) {
|
|
10119
|
+
const beforeGroups = groupFindings(before);
|
|
10120
|
+
const afterGroups = groupFindings(after);
|
|
10121
|
+
const unchanged = [];
|
|
10122
|
+
const added = [];
|
|
10123
|
+
const removed = [];
|
|
10124
|
+
const moved = [];
|
|
10125
|
+
const keys = [];
|
|
10126
|
+
for (const key of beforeGroups.keys()) keys.push(key);
|
|
10127
|
+
for (const key of afterGroups.keys()) {
|
|
10128
|
+
if (!beforeGroups.has(key)) keys.push(key);
|
|
10129
|
+
}
|
|
10130
|
+
for (const key of keys) {
|
|
10131
|
+
const left = [...beforeGroups.get(key) ?? []].sort(byLocation);
|
|
10132
|
+
const right = [...afterGroups.get(key) ?? []].sort(byLocation);
|
|
10133
|
+
const paired = Math.min(left.length, right.length);
|
|
10134
|
+
for (let i = 0; i < paired; i += 1) {
|
|
10135
|
+
const earlier = left[i];
|
|
10136
|
+
const later = right[i];
|
|
10137
|
+
if (earlier === void 0 || later === void 0) continue;
|
|
10138
|
+
if ((earlier.line ?? 0) === (later.line ?? 0)) unchanged.push(later);
|
|
10139
|
+
else moved.push({ before: earlier, after: later });
|
|
10140
|
+
}
|
|
10141
|
+
for (const finding3 of left.slice(paired)) removed.push(finding3);
|
|
10142
|
+
for (const finding3 of right.slice(paired)) added.push(finding3);
|
|
10143
|
+
}
|
|
10144
|
+
const idMoves = pairMoves(removed, added);
|
|
10145
|
+
const movedBefore = new Set(idMoves.map((item) => item.before));
|
|
10146
|
+
const movedAfter = new Set(idMoves.map((item) => item.after));
|
|
10147
|
+
return {
|
|
10148
|
+
unchanged,
|
|
10149
|
+
added: added.filter((finding3) => !movedAfter.has(finding3)),
|
|
10150
|
+
removed: removed.filter((finding3) => !movedBefore.has(finding3)),
|
|
10151
|
+
moved: [...moved, ...idMoves]
|
|
10152
|
+
};
|
|
10153
|
+
}
|
|
10154
|
+
function parseScanDocument(text) {
|
|
10155
|
+
let parsed;
|
|
10156
|
+
try {
|
|
10157
|
+
parsed = JSON.parse(text);
|
|
10158
|
+
} catch {
|
|
10159
|
+
throw new Error("scan JSON must be an object with a findings array or a findings array");
|
|
10160
|
+
}
|
|
10161
|
+
const list = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.findings) ? parsed.findings : void 0;
|
|
10162
|
+
if (list === void 0) {
|
|
10163
|
+
throw new Error("scan JSON must be an object with a findings array or a findings array");
|
|
10164
|
+
}
|
|
10165
|
+
return list.map((item) => {
|
|
10166
|
+
if (typeof item !== "object" || item === null) {
|
|
10167
|
+
throw new Error("scan JSON finding is missing ruleId or severity");
|
|
10168
|
+
}
|
|
10169
|
+
const record = item;
|
|
10170
|
+
const ruleId = record.ruleId;
|
|
10171
|
+
const severity = record.severity;
|
|
10172
|
+
if (typeof ruleId !== "string" || ruleId.length === 0 || typeof severity !== "string") {
|
|
10173
|
+
throw new Error("scan JSON finding is missing ruleId or severity");
|
|
10174
|
+
}
|
|
10175
|
+
if (!isSeverity(severity)) {
|
|
10176
|
+
throw new Error(`scan JSON finding has an unknown severity "${severity}"`);
|
|
10177
|
+
}
|
|
10178
|
+
if (record.line !== void 0 && (typeof record.line !== "number" || !Number.isInteger(record.line) || record.line < 0)) {
|
|
10179
|
+
throw new Error("scan JSON finding line must be an integer >= 0");
|
|
10180
|
+
}
|
|
10181
|
+
return item;
|
|
10182
|
+
});
|
|
10183
|
+
}
|
|
10184
|
+
var ADDED_CAP = 20;
|
|
10185
|
+
function addedLine(finding3) {
|
|
10186
|
+
const where = finding3.file !== void 0 ? ` ${posix3(finding3.file)}:${finding3.line ?? 0}` : "";
|
|
10187
|
+
return `${finding3.severity} ${finding3.ruleId}${where}`;
|
|
10188
|
+
}
|
|
10189
|
+
function renderDiffText(diff) {
|
|
10190
|
+
const lines = [
|
|
10191
|
+
`fullshield diff: unchanged ${diff.unchanged.length}, added ${diff.added.length}, removed ${diff.removed.length}, moved ${diff.moved.length}`
|
|
10192
|
+
];
|
|
10193
|
+
if (diff.added.length === 0) return lines.join("\n");
|
|
10194
|
+
lines.push("added:");
|
|
10195
|
+
for (const finding3 of diff.added.slice(0, ADDED_CAP)) lines.push(addedLine(finding3));
|
|
10196
|
+
if (diff.added.length > ADDED_CAP) lines.push(`${diff.added.length - ADDED_CAP} more \u2014 use --json`);
|
|
10197
|
+
return lines.join("\n");
|
|
10198
|
+
}
|
|
10199
|
+
|
|
9387
10200
|
// src/sbom/cyclonedx.ts
|
|
9388
10201
|
function validateCycloneDx15(doc) {
|
|
9389
10202
|
const errors = [];
|
|
@@ -9610,4 +10423,4 @@ function buildVex(findings, format, meta = { name: "app", version: "0.0.0" }) {
|
|
|
9610
10423
|
return buildOpenVex(findings);
|
|
9611
10424
|
}
|
|
9612
10425
|
|
|
9613
|
-
export { CONFIG_FILE_NAME, ConfigError, DEFAULT_CONFIG, IMPLEMENTED_MODULES, MODULE_NAMES, POLICY_FILE_NAME, POLICY_FLAG, POLICY_PEER_ERROR, PolicyError, SEVERITIES, analyzeWorkflowTampering, analyzeWormIndicators, applyBaseline, applyInlineSuppressions, applyPolicy, buildCycloneDxVex, buildOpenVex, buildSbom, buildVex, builtinModules, createAiClient, experimentalVex, failsCi, guardTools, isInternalAi, isLiveAi, isModuleName, isPolicyEnabled, isSeverity, loadConfig, loadPolicy, makeFindingId, mcpCfgEnabled2 as mcpCfgEnabled, mcpHomeConfigPaths, meetsThreshold, packageVersion, parseIgnore, parsePolicy, provenanceCoverage, readBaseline, renderConsoleReport, renderCycloneDx, renderHtmlReport, renderJsonReport, renderMarkdownReport, renderPrSummary, renderSarifReport, ruleDocsUrl, runAiFix, runFix, runScan, scan, scanMcpConfig, scanTextForSecrets, severityRank, sortFindings, summarize, validateCycloneDx15, validateCycloneDxVex, validateOpenVex, validateSarif21, walkFiles, writeBaseline };
|
|
10426
|
+
export { CONFIG_FILE_NAME, ConfigError, DEFAULT_CONFIG, DIFF_FLAG, IMPLEMENTED_MODULES, MODULE_NAMES, POLICY_FILE_NAME, POLICY_FLAG, POLICY_PEER_ERROR, PolicyError, SEVERITIES, analyzeWorkflowTampering, analyzeWormIndicators, applyBaseline, applyInlineSuppressions, applyPolicy, buildCycloneDxVex, buildOpenVex, buildSbom, buildVex, builtinModules, createAiClient, diffScans, experimentalVex, failsCi, guardTools, isInternalAi, isLiveAi, isModuleName, isPolicyEnabled, isSeverity, loadConfig, loadPolicy, makeFindingId, mcpCfgEnabled2 as mcpCfgEnabled, mcpHomeConfigPaths, meetsThreshold, packageVersion, parseIgnore, parsePolicy, parseScanDocument, provenanceCoverage, readBaseline, renderConsoleReport, renderCycloneDx, renderDiffText, renderGithubAnnotations, renderGitlabCodeQuality, renderHtmlReport, renderJsonReport, renderJunitReport, renderMarkdownReport, renderPrSummary, renderSarifReport, ruleDocsUrl, runAiFix, runFix, runScan, scan, scanMcpConfig, scanTextForSecrets, severityRank, sortFindings, summarize, validateCycloneDx15, validateCycloneDxVex, validateOpenVex, validateSarif21, walkFiles, writeBaseline };
|