fullshield 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -1
- package/README.md +27 -3
- package/dist/agent.js +934 -96
- package/dist/cli/index.js +1550 -139
- package/dist/compliance.js +5 -0
- package/dist/dashboard.js +18 -1
- package/dist/iac.js +48 -0
- package/dist/index.d.ts +69 -6
- package/dist/index.js +1227 -103
- package/dist/mcp.js +934 -96
- package/dist/supply.js +13 -3
- package/package.json +2 -2
- package/rules/SAST-SKIP-001.md +9 -0
package/dist/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { readFile, mkdir, writeFile, stat, unlink, readdir, mkdtemp } from 'fs/promises';
|
|
3
|
-
import { resolve, join, basename, dirname,
|
|
3
|
+
import { resolve, join, basename, dirname, posix, relative } from 'path';
|
|
4
4
|
import { execFile, spawnSync, spawn, execFileSync } from 'child_process';
|
|
5
5
|
import { createInterface } from 'readline';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
import { performance } from 'perf_hooks';
|
|
8
|
-
import { homedir, tmpdir } from 'os';
|
|
8
|
+
import { homedir, availableParallelism, tmpdir } from 'os';
|
|
9
9
|
import { existsSync, statSync, readFileSync, readdirSync, cpSync, mkdirSync, rmSync } from 'fs';
|
|
10
|
-
import
|
|
10
|
+
import { Worker } from 'worker_threads';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import ignoreModule from 'ignore';
|
|
12
13
|
import pc from 'picocolors';
|
|
13
14
|
|
|
14
15
|
var __defProp = Object.defineProperty;
|
|
@@ -564,6 +565,7 @@ var init_incremental = __esm({
|
|
|
564
565
|
// src/modules/secrets/history.ts
|
|
565
566
|
var history_exports = {};
|
|
566
567
|
__export(history_exports, {
|
|
568
|
+
historyCommitLimit: () => historyCommitLimit,
|
|
567
569
|
scanGitHistory: () => scanGitHistory
|
|
568
570
|
});
|
|
569
571
|
function gitError(error) {
|
|
@@ -606,7 +608,13 @@ function remapHit(hit, file) {
|
|
|
606
608
|
secret: hit.secret
|
|
607
609
|
};
|
|
608
610
|
}
|
|
609
|
-
|
|
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) {
|
|
610
618
|
const changes = [];
|
|
611
619
|
await gitOk(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
612
620
|
const child = spawn(
|
|
@@ -623,6 +631,8 @@ async function streamLogChanges(cwd) {
|
|
|
623
631
|
"--format=FSCOMMIT:%H",
|
|
624
632
|
"--no-textconv",
|
|
625
633
|
"--no-ext-diff",
|
|
634
|
+
"-n",
|
|
635
|
+
String(maxCommits),
|
|
626
636
|
"HEAD"
|
|
627
637
|
],
|
|
628
638
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -691,8 +701,14 @@ async function headSecretKeys(cwd) {
|
|
|
691
701
|
}
|
|
692
702
|
return keys;
|
|
693
703
|
}
|
|
694
|
-
async function
|
|
695
|
-
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);
|
|
696
712
|
const blobHits = /* @__PURE__ */ new Map();
|
|
697
713
|
const first = /* @__PURE__ */ new Map();
|
|
698
714
|
for (const change of changes) {
|
|
@@ -729,7 +745,13 @@ async function scanGitHistory(cwd) {
|
|
|
729
745
|
secret: hit.secret
|
|
730
746
|
});
|
|
731
747
|
}
|
|
732
|
-
|
|
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
|
+
};
|
|
733
755
|
}
|
|
734
756
|
var execFileAsync2, ZERO_HASH, MAX_BLOB, RAW_LINE;
|
|
735
757
|
var init_history = __esm({
|
|
@@ -1077,7 +1099,8 @@ function walk(node, visit) {
|
|
|
1077
1099
|
}
|
|
1078
1100
|
}
|
|
1079
1101
|
function attachParents(node, parent) {
|
|
1080
|
-
|
|
1102
|
+
const current = node;
|
|
1103
|
+
if (parent !== void 0 || current.parent === void 0) current.parent = parent;
|
|
1081
1104
|
for (const key of Object.keys(node)) {
|
|
1082
1105
|
if (SKIP_KEYS.has(String(key))) continue;
|
|
1083
1106
|
const value = node[key];
|
|
@@ -1147,6 +1170,14 @@ function classesEnabled() {
|
|
|
1147
1170
|
function mcpCfgEnabled() {
|
|
1148
1171
|
return process.env.FULLSHIELD_EXPERIMENTAL_MCPCFG === "1";
|
|
1149
1172
|
}
|
|
1173
|
+
function taintV2Enabled() {
|
|
1174
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
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
|
+
}
|
|
1150
1181
|
|
|
1151
1182
|
// src/modules/config/finding.ts
|
|
1152
1183
|
init_finding();
|
|
@@ -1956,6 +1987,103 @@ function exactPinsFromManifest(pkg) {
|
|
|
1956
1987
|
|
|
1957
1988
|
// src/modules/deps/reachability.ts
|
|
1958
1989
|
init_finding();
|
|
1990
|
+
|
|
1991
|
+
// src/modules/deps/symbol-reachability.ts
|
|
1992
|
+
function affectedFunctionNames(vuln) {
|
|
1993
|
+
const names = /* @__PURE__ */ new Set();
|
|
1994
|
+
for (const affected of vuln.affected ?? []) {
|
|
1995
|
+
const list = affected.ecosystem_specific?.affected_functions;
|
|
1996
|
+
if (!Array.isArray(list)) continue;
|
|
1997
|
+
for (const item of list) {
|
|
1998
|
+
if (typeof item === "string" && item.length > 0) names.add(item);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return [...names];
|
|
2002
|
+
}
|
|
2003
|
+
function escapeRegExp(value) {
|
|
2004
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2005
|
+
}
|
|
2006
|
+
function called(source, local) {
|
|
2007
|
+
return new RegExp(`\\b${escapeRegExp(local)}\\s*\\(`).test(source);
|
|
2008
|
+
}
|
|
2009
|
+
function memberCalled(source, object, member) {
|
|
2010
|
+
return new RegExp(`\\b${escapeRegExp(object)}\\s*\\.\\s*${escapeRegExp(member)}\\s*\\(`).test(source);
|
|
2011
|
+
}
|
|
2012
|
+
function sourceCallsAffectedSymbol(source, symbol) {
|
|
2013
|
+
const esc2 = escapeRegExp(symbol);
|
|
2014
|
+
const named = new RegExp(
|
|
2015
|
+
`import\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`,
|
|
2016
|
+
"g"
|
|
2017
|
+
);
|
|
2018
|
+
let match;
|
|
2019
|
+
while ((match = named.exec(source)) !== null) {
|
|
2020
|
+
const parts = (match[1] ?? "").split(",");
|
|
2021
|
+
for (const part of parts) {
|
|
2022
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
2023
|
+
const imported = bits[0]?.trim();
|
|
2024
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
2025
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
const star = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"][^'"]+['"]/g;
|
|
2029
|
+
while ((match = star.exec(source)) !== null) {
|
|
2030
|
+
const local = match[1];
|
|
2031
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
2032
|
+
}
|
|
2033
|
+
const subDefault = new RegExp(
|
|
2034
|
+
`import\\s+([A-Za-z_$][\\w$]*)\\s+from\\s+['"][^'"]+/${esc2}['"]`
|
|
2035
|
+
);
|
|
2036
|
+
const sub = subDefault.exec(source);
|
|
2037
|
+
if (sub?.[1] !== void 0 && called(source, sub[1])) return true;
|
|
2038
|
+
const reqMember = new RegExp(
|
|
2039
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+['"]\\s*\\)\\s*\\.\\s*${esc2}`
|
|
2040
|
+
);
|
|
2041
|
+
const reqHit = reqMember.exec(source);
|
|
2042
|
+
if (reqHit?.[1] !== void 0 && called(source, reqHit[1])) return true;
|
|
2043
|
+
const reqNs = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
2044
|
+
while ((match = reqNs.exec(source)) !== null) {
|
|
2045
|
+
const local = match[1];
|
|
2046
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
2047
|
+
}
|
|
2048
|
+
const reqNamed = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
2049
|
+
while ((match = reqNamed.exec(source)) !== null) {
|
|
2050
|
+
const parts = (match[1] ?? "").split(",");
|
|
2051
|
+
for (const part of parts) {
|
|
2052
|
+
const bits = part.trim().split(/\s*:\s*/);
|
|
2053
|
+
const imported = bits[0]?.trim();
|
|
2054
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
2055
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
const reqSub = new RegExp(
|
|
2059
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+/${esc2}['"]\\s*\\)`
|
|
2060
|
+
);
|
|
2061
|
+
const subReq = reqSub.exec(source);
|
|
2062
|
+
if (subReq?.[1] !== void 0 && called(source, subReq[1])) return true;
|
|
2063
|
+
const barrel = new RegExp(`export\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`, "g");
|
|
2064
|
+
while ((match = barrel.exec(source)) !== null) {
|
|
2065
|
+
const parts = (match[1] ?? "").split(",");
|
|
2066
|
+
for (const part of parts) {
|
|
2067
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
2068
|
+
const exported = (bits[1] ?? bits[0])?.trim();
|
|
2069
|
+
if (exported === symbol) {
|
|
2070
|
+
const local = bits[0]?.trim();
|
|
2071
|
+
if (local !== void 0 && called(source, local)) return true;
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return false;
|
|
2076
|
+
}
|
|
2077
|
+
function classifySymbolUse(symbols, sources) {
|
|
2078
|
+
for (const symbol of symbols) {
|
|
2079
|
+
for (const source of sources.values()) {
|
|
2080
|
+
if (sourceCallsAffectedSymbol(source, symbol)) return "symbol-reachable";
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return "symbol-not-called";
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// src/modules/deps/reachability.ts
|
|
1959
2087
|
function packageNameFromSnippet(snippet) {
|
|
1960
2088
|
if (snippet.startsWith("@")) {
|
|
1961
2089
|
const second = snippet.indexOf("@", 1);
|
|
@@ -1984,9 +2112,9 @@ function isProductionSource(rel) {
|
|
|
1984
2112
|
}
|
|
1985
2113
|
function indexPackageUsage(sources) {
|
|
1986
2114
|
const usage = /* @__PURE__ */ new Map();
|
|
1987
|
-
const bump = (pkg,
|
|
2115
|
+
const bump = (pkg, called2) => {
|
|
1988
2116
|
const prev = usage.get(pkg) ?? { called: false };
|
|
1989
|
-
usage.set(pkg, { imported: true, called: prev.called ||
|
|
2117
|
+
usage.set(pkg, { imported: true, called: prev.called || called2 });
|
|
1990
2118
|
};
|
|
1991
2119
|
const nsRe = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1992
2120
|
const namedRe = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
@@ -2042,6 +2170,29 @@ function shortestDependencyPath(target, starts, edges) {
|
|
|
2042
2170
|
}
|
|
2043
2171
|
return void 0;
|
|
2044
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
|
+
}
|
|
2045
2196
|
function reachableFrom(target, roots, edges) {
|
|
2046
2197
|
if (roots.has(target)) return true;
|
|
2047
2198
|
return shortestDependencyPath(target, roots, edges) !== void 0;
|
|
@@ -2062,9 +2213,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
2062
2213
|
}
|
|
2063
2214
|
if (graph !== void 0 && graph.edgesKnown) {
|
|
2064
2215
|
const roots = importedDirectNames(productionUsage, graph);
|
|
2065
|
-
const
|
|
2216
|
+
const paths = collectDependencyPaths(pkgName, roots, graph.edges);
|
|
2217
|
+
const path = paths[0];
|
|
2066
2218
|
if (path !== void 0 && path.length >= 2) {
|
|
2067
|
-
return { reachability: "reachable-via-dependency", path };
|
|
2219
|
+
return { reachability: "reachable-via-dependency", path, paths };
|
|
2068
2220
|
}
|
|
2069
2221
|
return { reachability: "not-imported" };
|
|
2070
2222
|
}
|
|
@@ -2074,10 +2226,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
2074
2226
|
return { reachability: "not-imported" };
|
|
2075
2227
|
}
|
|
2076
2228
|
function reachabilitySteps(reachability) {
|
|
2077
|
-
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown") {
|
|
2229
|
+
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
|
|
2078
2230
|
return 0;
|
|
2079
2231
|
}
|
|
2080
|
-
if (reachability === "imported-not-called") return 1;
|
|
2232
|
+
if (reachability === "imported-not-called" || reachability === "symbol-not-called") return 1;
|
|
2081
2233
|
return 2;
|
|
2082
2234
|
}
|
|
2083
2235
|
function downgradeSeverity(severity, steps) {
|
|
@@ -2094,8 +2246,108 @@ function isDevOnly(pkgName, ctx) {
|
|
|
2094
2246
|
if (inProd) return false;
|
|
2095
2247
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
2096
2248
|
}
|
|
2097
|
-
|
|
2098
|
-
|
|
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
|
+
}
|
|
2349
|
+
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
2350
|
+
const mapped = findings.map((finding3) => {
|
|
2099
2351
|
if (finding3.ruleId === "DEPS-MALICIOUS-001") {
|
|
2100
2352
|
if (finding3.snippet === void 0) return finding3;
|
|
2101
2353
|
const pkg2 = packageNameFromSnippet(finding3.snippet);
|
|
@@ -2105,19 +2357,25 @@ function applyReachability(findings, ctx) {
|
|
|
2105
2357
|
...finding3,
|
|
2106
2358
|
severity: "critical",
|
|
2107
2359
|
reachability: classified2.reachability,
|
|
2108
|
-
...classified2
|
|
2360
|
+
...pathFields(classified2)
|
|
2109
2361
|
};
|
|
2110
2362
|
}
|
|
2111
2363
|
if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
|
|
2112
2364
|
const pkg = packageNameFromSnippet(finding3.snippet);
|
|
2113
2365
|
if (pkg === void 0 || pkg.length === 0) return finding3;
|
|
2114
|
-
|
|
2366
|
+
let classified = classifyReachability(pkg, ctx.productionUsage, ctx.graph);
|
|
2367
|
+
const named = symbols.get(finding3.id) ?? [];
|
|
2368
|
+
if (named.length > 0 && ctx.sources !== void 0) {
|
|
2369
|
+
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
2370
|
+
classified = { reachability: symbolClass, path: classified.path, paths: classified.paths };
|
|
2371
|
+
}
|
|
2115
2372
|
const steps = reachabilitySteps(classified.reachability);
|
|
2116
2373
|
let severity = steps === 0 ? finding3.severity : downgradeSeverity(finding3.severity, steps);
|
|
2117
2374
|
let message = finding3.message;
|
|
2118
2375
|
if (classified.path !== void 0) {
|
|
2119
2376
|
const shown = classified.path.join(" > ");
|
|
2120
2377
|
message = `${message} Reachable via ${shown}.`;
|
|
2378
|
+
message = appendOtherPaths(message, classified.paths);
|
|
2121
2379
|
}
|
|
2122
2380
|
const devOnly = isDevOnly(pkg, ctx);
|
|
2123
2381
|
if (devOnly) {
|
|
@@ -2127,11 +2385,12 @@ function applyReachability(findings, ctx) {
|
|
|
2127
2385
|
return {
|
|
2128
2386
|
...finding3,
|
|
2129
2387
|
reachability: classified.reachability,
|
|
2130
|
-
...classified
|
|
2388
|
+
...pathFields(classified),
|
|
2131
2389
|
severity,
|
|
2132
2390
|
message
|
|
2133
2391
|
};
|
|
2134
2392
|
});
|
|
2393
|
+
return groupPackageAdvisories(mapped);
|
|
2135
2394
|
}
|
|
2136
2395
|
|
|
2137
2396
|
// src/modules/deps/semver.ts
|
|
@@ -4033,6 +4292,59 @@ function experimentalVex() {
|
|
|
4033
4292
|
return process.env.FULLSHIELD_EXPERIMENTAL_VEX === "1";
|
|
4034
4293
|
}
|
|
4035
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
|
+
|
|
4036
4348
|
// src/fixer/eol.ts
|
|
4037
4349
|
function detectEol(text) {
|
|
4038
4350
|
return text.includes("\r\n") ? "\r\n" : "\n";
|
|
@@ -4147,7 +4459,7 @@ function walkJs(dir, out = []) {
|
|
|
4147
4459
|
}
|
|
4148
4460
|
return out;
|
|
4149
4461
|
}
|
|
4150
|
-
function
|
|
4462
|
+
function readJson2(path) {
|
|
4151
4463
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
4152
4464
|
}
|
|
4153
4465
|
var TOKEN_RE = /(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/;
|
|
@@ -4161,7 +4473,7 @@ function packageSources(dir) {
|
|
|
4161
4473
|
let scripts = "";
|
|
4162
4474
|
if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
|
|
4163
4475
|
try {
|
|
4164
|
-
const pkg =
|
|
4476
|
+
const pkg = readJson2(manifestPath);
|
|
4165
4477
|
const block = pkg.scripts ?? {};
|
|
4166
4478
|
scripts = Object.values(block).join("\n");
|
|
4167
4479
|
} catch {
|
|
@@ -4451,14 +4763,17 @@ var depsModule = {
|
|
|
4451
4763
|
findings2.push(typosquatFinding("package.json", hit));
|
|
4452
4764
|
}
|
|
4453
4765
|
const osvHits2 = await queryOsv(pins, osvOpts);
|
|
4766
|
+
const symbols2 = /* @__PURE__ */ new Map();
|
|
4454
4767
|
for (const hit of osvHits2) {
|
|
4455
4768
|
for (const vuln of hit.vulns) {
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
);
|
|
4769
|
+
const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit.pkg, vuln) : vulnFinding("package.json", hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
4770
|
+
const names = affectedFunctionNames(vuln);
|
|
4771
|
+
if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols2.set(finding3.id, names);
|
|
4772
|
+
findings2.push(finding3);
|
|
4459
4773
|
}
|
|
4460
4774
|
}
|
|
4461
|
-
|
|
4775
|
+
const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
|
|
4776
|
+
return applyReachability(findings2, noLockCtx, symbols2);
|
|
4462
4777
|
}
|
|
4463
4778
|
let contents;
|
|
4464
4779
|
try {
|
|
@@ -4479,18 +4794,21 @@ var depsModule = {
|
|
|
4479
4794
|
}
|
|
4480
4795
|
}
|
|
4481
4796
|
const osvHits = await queryOsv(packages, osvOpts);
|
|
4797
|
+
const symbols = /* @__PURE__ */ new Map();
|
|
4482
4798
|
for (const hit of osvHits) {
|
|
4483
4799
|
for (const vuln of hit.vulns) {
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
);
|
|
4800
|
+
const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit.pkg, vuln) : vulnFinding(lockFile, hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
4801
|
+
const names = affectedFunctionNames(vuln);
|
|
4802
|
+
if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols.set(finding3.id, names);
|
|
4803
|
+
findings.push(finding3);
|
|
4487
4804
|
}
|
|
4488
4805
|
}
|
|
4489
|
-
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph));
|
|
4806
|
+
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph), symbols);
|
|
4490
4807
|
if (experimentalVex()) {
|
|
4491
4808
|
reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
|
|
4492
4809
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
4493
4810
|
}
|
|
4811
|
+
if (licenseEnabled()) reached.push(...await scanCopyleftLicenses(ctx.cwd));
|
|
4494
4812
|
return reached;
|
|
4495
4813
|
}
|
|
4496
4814
|
};
|
|
@@ -4520,7 +4838,7 @@ async function reachabilityCtx(cwd, files, graph) {
|
|
|
4520
4838
|
} catch {
|
|
4521
4839
|
}
|
|
4522
4840
|
}
|
|
4523
|
-
return { productionUsage: indexPackageUsage(prodSources), graph };
|
|
4841
|
+
return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
|
|
4524
4842
|
}
|
|
4525
4843
|
|
|
4526
4844
|
// src/modules/mcp/index.ts
|
|
@@ -5081,6 +5399,26 @@ function isXssSanitizerCall(node) {
|
|
|
5081
5399
|
return false;
|
|
5082
5400
|
}
|
|
5083
5401
|
var NO_RETURNERS = /* @__PURE__ */ new Set();
|
|
5402
|
+
var CALLBACK_METHODS = /* @__PURE__ */ new Set([
|
|
5403
|
+
"then",
|
|
5404
|
+
"catch",
|
|
5405
|
+
"finally",
|
|
5406
|
+
"map",
|
|
5407
|
+
"forEach",
|
|
5408
|
+
"filter",
|
|
5409
|
+
"flatMap",
|
|
5410
|
+
"find",
|
|
5411
|
+
"reduce"
|
|
5412
|
+
]);
|
|
5413
|
+
var taintedThisFields = /* @__PURE__ */ new WeakMap();
|
|
5414
|
+
function enclosingClass(node) {
|
|
5415
|
+
let current = node.parent;
|
|
5416
|
+
while (current !== void 0) {
|
|
5417
|
+
if (current.type === "ClassDeclaration" || current.type === "ClassExpression") return current;
|
|
5418
|
+
current = current.parent;
|
|
5419
|
+
}
|
|
5420
|
+
return void 0;
|
|
5421
|
+
}
|
|
5084
5422
|
function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
5085
5423
|
if (node === void 0) return false;
|
|
5086
5424
|
if (isTaintSource(node) || isFrameworkMemberTaint(node) || isFrameworkTaintCall(node) || isMcpArgsTaintSource(node)) {
|
|
@@ -5092,8 +5430,34 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
|
5092
5430
|
case "Identifier":
|
|
5093
5431
|
return tainted.has(node.name);
|
|
5094
5432
|
case "MemberExpression":
|
|
5433
|
+
if (taintV2Enabled() && node.object.type === "ThisExpression") {
|
|
5434
|
+
const cls = enclosingClass(node);
|
|
5435
|
+
const name = propName(node);
|
|
5436
|
+
if (cls !== void 0 && name !== void 0 && taintedThisFields.get(cls)?.has(name) === true) {
|
|
5437
|
+
return true;
|
|
5438
|
+
}
|
|
5439
|
+
}
|
|
5095
5440
|
return exprIsTainted(node.object, tainted, returners);
|
|
5441
|
+
case "ArrayExpression":
|
|
5442
|
+
if (!taintV2Enabled()) return false;
|
|
5443
|
+
return node.elements.some((el) => {
|
|
5444
|
+
if (el === null) return false;
|
|
5445
|
+
if (el.type === "SpreadElement") return exprIsTainted(el.argument, tainted, returners);
|
|
5446
|
+
return exprIsTainted(el, tainted, returners);
|
|
5447
|
+
});
|
|
5448
|
+
case "ObjectExpression":
|
|
5449
|
+
if (!taintV2Enabled()) return false;
|
|
5450
|
+
return node.properties.some((prop) => {
|
|
5451
|
+
if (prop.type === "SpreadElement") return exprIsTainted(prop.argument, tainted, returners);
|
|
5452
|
+
if (prop.type !== "Property") return false;
|
|
5453
|
+
return exprIsTainted(prop.value, tainted, returners);
|
|
5454
|
+
});
|
|
5096
5455
|
case "CallExpression":
|
|
5456
|
+
if (taintV2Enabled() && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
|
|
5457
|
+
return node.arguments.some(
|
|
5458
|
+
(arg) => arg.type !== "SpreadElement" && exprIsTainted(arg, tainted, returners)
|
|
5459
|
+
);
|
|
5460
|
+
}
|
|
5097
5461
|
if (isSchemaValidatorCall(node)) return false;
|
|
5098
5462
|
if (node.callee.type === "Identifier" && returners.has(node.callee.name)) return true;
|
|
5099
5463
|
if (node.callee.type === "MemberExpression" && exprIsTainted(node.callee.object, tainted, returners)) {
|
|
@@ -5324,7 +5688,66 @@ function scopeReturnsTaint(scope, tainted, returners) {
|
|
|
5324
5688
|
return yes;
|
|
5325
5689
|
}
|
|
5326
5690
|
var MAX_TAINT_HOPS = 5;
|
|
5327
|
-
function
|
|
5691
|
+
function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
5692
|
+
let changed = false;
|
|
5693
|
+
const mark = (fn, name) => {
|
|
5694
|
+
if (name === void 0) return;
|
|
5695
|
+
const set = seeds.get(fn) ?? /* @__PURE__ */ new Set();
|
|
5696
|
+
if (set.has(name)) return;
|
|
5697
|
+
set.add(name);
|
|
5698
|
+
seeds.set(fn, set);
|
|
5699
|
+
changed = true;
|
|
5700
|
+
};
|
|
5701
|
+
for (const fn of functions) {
|
|
5702
|
+
const f = facts.get(fn);
|
|
5703
|
+
if (f === void 0) continue;
|
|
5704
|
+
let parent = fn.parent;
|
|
5705
|
+
while (parent !== void 0 && !isFunctionNode(parent)) {
|
|
5706
|
+
parent = parent.parent;
|
|
5707
|
+
}
|
|
5708
|
+
if (parent !== void 0 && isFunctionNode(parent)) {
|
|
5709
|
+
const outer = facts.get(parent);
|
|
5710
|
+
if (outer !== void 0) {
|
|
5711
|
+
for (const name of outer.tainted) mark(fn, name);
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
walkScope(fn, (node) => {
|
|
5715
|
+
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") {
|
|
5716
|
+
if (node.left.object.type === "ThisExpression" && exprIsTainted(node.right, f.tainted, returners)) {
|
|
5717
|
+
const cls = enclosingClass(node);
|
|
5718
|
+
const field = propName(node.left);
|
|
5719
|
+
if (cls !== void 0 && field !== void 0) {
|
|
5720
|
+
const fields = taintedThisFields.get(cls) ?? /* @__PURE__ */ new Set();
|
|
5721
|
+
if (!fields.has(field)) {
|
|
5722
|
+
fields.add(field);
|
|
5723
|
+
taintedThisFields.set(cls, fields);
|
|
5724
|
+
changed = true;
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
}
|
|
5728
|
+
}
|
|
5729
|
+
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
5730
|
+
const method = propName(node.callee);
|
|
5731
|
+
if (method === void 0) return;
|
|
5732
|
+
if (CALLBACK_METHODS.has(method) && exprIsTainted(node.callee.object, f.tainted, returners)) {
|
|
5733
|
+
const callback = node.arguments[0];
|
|
5734
|
+
if (callback !== void 0 && isFunctionNode(callback)) {
|
|
5735
|
+
mark(callback, boundNames(callback.params[0])[0]);
|
|
5736
|
+
}
|
|
5737
|
+
}
|
|
5738
|
+
if ((method === "call" || method === "apply") && node.callee.object.type === "Identifier") {
|
|
5739
|
+
const target = nameToFn.get(node.callee.object.name);
|
|
5740
|
+
if (target === void 0 || !isFunctionNode(target)) return;
|
|
5741
|
+
const arg = node.arguments[1];
|
|
5742
|
+
if (arg !== void 0 && arg.type !== "SpreadElement" && exprIsTainted(arg, f.tainted, returners)) {
|
|
5743
|
+
mark(target, boundNames(target.params[0])[0]);
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
});
|
|
5747
|
+
}
|
|
5748
|
+
return changed;
|
|
5749
|
+
}
|
|
5750
|
+
function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
5328
5751
|
attachParents(ast);
|
|
5329
5752
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
5330
5753
|
const functions = [];
|
|
@@ -5338,7 +5761,7 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5338
5761
|
}
|
|
5339
5762
|
const seeds = /* @__PURE__ */ new Map();
|
|
5340
5763
|
for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
|
|
5341
|
-
const returners = new Set(
|
|
5764
|
+
const returners = new Set(extraReturners2);
|
|
5342
5765
|
for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
|
|
5343
5766
|
let changed = false;
|
|
5344
5767
|
facts.set(ast, analyzeScope(ast, [], returners));
|
|
@@ -5378,6 +5801,9 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5378
5801
|
});
|
|
5379
5802
|
});
|
|
5380
5803
|
}
|
|
5804
|
+
if (taintV2Enabled() && seedTaintV2(functions, facts, seeds, returners, nameToFn)) {
|
|
5805
|
+
changed = true;
|
|
5806
|
+
}
|
|
5381
5807
|
if (!changed) break;
|
|
5382
5808
|
}
|
|
5383
5809
|
return facts;
|
|
@@ -5520,6 +5946,21 @@ function requireCall(node) {
|
|
|
5520
5946
|
}
|
|
5521
5947
|
return void 0;
|
|
5522
5948
|
}
|
|
5949
|
+
function reexportsOf(ast) {
|
|
5950
|
+
const out = /* @__PURE__ */ new Map();
|
|
5951
|
+
walk(ast, (node) => {
|
|
5952
|
+
if (node.type !== "ExportNamedDeclaration" || node.source === null || node.source === void 0) return;
|
|
5953
|
+
const spec = literalString(node.source);
|
|
5954
|
+
if (spec === void 0) return;
|
|
5955
|
+
for (const specNode of node.specifiers) {
|
|
5956
|
+
if (specNode.type !== "ExportSpecifier") continue;
|
|
5957
|
+
const exported = specNode.exported.type === "Identifier" ? specNode.exported.name : void 0;
|
|
5958
|
+
const local = specNode.local.type === "Identifier" ? specNode.local.name : exported;
|
|
5959
|
+
if (exported !== void 0 && local !== void 0) out.set(exported, { spec, name: local });
|
|
5960
|
+
}
|
|
5961
|
+
});
|
|
5962
|
+
return out;
|
|
5963
|
+
}
|
|
5523
5964
|
function resolveRelative(fromFile, spec, files) {
|
|
5524
5965
|
if (!spec.startsWith(".")) return void 0;
|
|
5525
5966
|
const base = posix.normalize(posix.join(posix.dirname(fromFile), spec));
|
|
@@ -5597,6 +6038,22 @@ function runInterproc(parsed) {
|
|
|
5597
6038
|
functions.set(`${fn.file}#${fn.name}`, fn);
|
|
5598
6039
|
}
|
|
5599
6040
|
}
|
|
6041
|
+
if (taintV2Enabled()) {
|
|
6042
|
+
const barrels = /* @__PURE__ */ new Map();
|
|
6043
|
+
for (const p of parsed) barrels.set(p.file, reexportsOf(p.ast));
|
|
6044
|
+
for (const ns of requires.values()) {
|
|
6045
|
+
for (const [local, target] of ns) {
|
|
6046
|
+
const hash = target.indexOf("#");
|
|
6047
|
+
if (hash < 0) continue;
|
|
6048
|
+
const file = target.slice(0, hash);
|
|
6049
|
+
const name = target.slice(hash + 1);
|
|
6050
|
+
const re = barrels.get(file)?.get(name);
|
|
6051
|
+
if (re === void 0) continue;
|
|
6052
|
+
const resolved = resolveRelative(file, re.spec, files);
|
|
6053
|
+
if (resolved !== void 0) ns.set(local, `${resolved}#${re.name}`);
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
5600
6057
|
const queue = [];
|
|
5601
6058
|
for (const p of parsed) {
|
|
5602
6059
|
const facts = analyzeFile(p.ast);
|
|
@@ -6112,7 +6569,10 @@ function isMongoReceiver(node, source) {
|
|
|
6112
6569
|
const obj = node.callee.object;
|
|
6113
6570
|
if (obj.type === "Identifier") {
|
|
6114
6571
|
if (isCollectionName(obj.name)) return true;
|
|
6115
|
-
if (isPascalModel(obj.name))
|
|
6572
|
+
if (isPascalModel(obj.name)) {
|
|
6573
|
+
if (/extends\s+BaseEntity\b/.test(source)) return false;
|
|
6574
|
+
return !fileHasSqlOrm;
|
|
6575
|
+
}
|
|
6116
6576
|
return fileHasMongo;
|
|
6117
6577
|
}
|
|
6118
6578
|
if (obj.type === "MemberExpression") {
|
|
@@ -7174,6 +7634,136 @@ function scanPromptInjection(file, source) {
|
|
|
7174
7634
|
return findings;
|
|
7175
7635
|
}
|
|
7176
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
|
+
};
|
|
7669
|
+
function packageVersion() {
|
|
7670
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7671
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
7672
|
+
try {
|
|
7673
|
+
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
7674
|
+
const pkg = JSON.parse(raw);
|
|
7675
|
+
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
7676
|
+
return pkg.version;
|
|
7677
|
+
}
|
|
7678
|
+
} catch {
|
|
7679
|
+
}
|
|
7680
|
+
const parent = dirname(dir);
|
|
7681
|
+
if (parent === dir) break;
|
|
7682
|
+
dir = parent;
|
|
7683
|
+
}
|
|
7684
|
+
throw new Error("fullshield package.json not found");
|
|
7685
|
+
}
|
|
7686
|
+
|
|
7687
|
+
// src/core/parse-pool.ts
|
|
7688
|
+
var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7689
|
+
var cache = new LruMap(64);
|
|
7690
|
+
function parsePoolEnabled() {
|
|
7691
|
+
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
7692
|
+
}
|
|
7693
|
+
function parsePoolSize() {
|
|
7694
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
7695
|
+
if (raw !== void 0 && raw !== "") {
|
|
7696
|
+
const parsed = Number(raw);
|
|
7697
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
|
|
7698
|
+
}
|
|
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;
|
|
7706
|
+
}
|
|
7707
|
+
function parseCacheKey(code) {
|
|
7708
|
+
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
7709
|
+
}
|
|
7710
|
+
function parseOne(code) {
|
|
7711
|
+
const key = parseCacheKey(code);
|
|
7712
|
+
const hit = cache.get(key);
|
|
7713
|
+
if (hit !== void 0) return Promise.resolve(hit);
|
|
7714
|
+
return new Promise((resolve5, reject) => {
|
|
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;
|
|
7726
|
+
cache.set(key, ast);
|
|
7727
|
+
finish();
|
|
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"}`));
|
|
7745
|
+
});
|
|
7746
|
+
});
|
|
7747
|
+
}
|
|
7748
|
+
async function parseWithPool(codes) {
|
|
7749
|
+
const size = parsePoolSize();
|
|
7750
|
+
const out = new Array(codes.length);
|
|
7751
|
+
let next = 0;
|
|
7752
|
+
async function run() {
|
|
7753
|
+
for (; ; ) {
|
|
7754
|
+
const index = next;
|
|
7755
|
+
next += 1;
|
|
7756
|
+
if (index >= codes.length) return;
|
|
7757
|
+
const code = codes[index];
|
|
7758
|
+
if (code === void 0) return;
|
|
7759
|
+
out[index] = await parseOne(code);
|
|
7760
|
+
}
|
|
7761
|
+
}
|
|
7762
|
+
const workers = Math.min(size, Math.max(1, codes.length));
|
|
7763
|
+
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
7764
|
+
return out;
|
|
7765
|
+
}
|
|
7766
|
+
|
|
7177
7767
|
// src/modules/sast/engine.ts
|
|
7178
7768
|
var sastRules = [
|
|
7179
7769
|
sqliRule,
|
|
@@ -7206,8 +7796,8 @@ function snippetOf(source, node) {
|
|
|
7206
7796
|
if (node.range === void 0) return "";
|
|
7207
7797
|
return source.slice(node.range[0], node.range[1]).replace(/\s+/g, " ").trim().slice(0, 120);
|
|
7208
7798
|
}
|
|
7209
|
-
function findingsFromAst(filename, source, ast,
|
|
7210
|
-
const facts = analyzeFile(ast,
|
|
7799
|
+
function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
|
|
7800
|
+
const facts = analyzeFile(ast, extraReturners2);
|
|
7211
7801
|
const findings = [];
|
|
7212
7802
|
for (const rule of sastRules) {
|
|
7213
7803
|
const ctx = {
|
|
@@ -7285,51 +7875,203 @@ function findingsFromTemplate(filename, source) {
|
|
|
7285
7875
|
xssRule.create(ctx);
|
|
7286
7876
|
return findings;
|
|
7287
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
|
+
}
|
|
7288
8030
|
var sastModule = {
|
|
7289
8031
|
name: "sast",
|
|
7290
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;
|
|
7291
8036
|
const findings = [];
|
|
7292
8037
|
const parsed = [];
|
|
7293
|
-
for (const rel of
|
|
7294
|
-
const
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
source = await readFile(join(ctx.cwd, rel), "utf8");
|
|
7300
|
-
} 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}.`));
|
|
7301
8044
|
continue;
|
|
7302
8045
|
}
|
|
7303
|
-
if (
|
|
7304
|
-
if (isTemplate) {
|
|
8046
|
+
if (TEMPLATE_EXT.test(rel)) {
|
|
7305
8047
|
findings.push(...findingsFromTemplate(rel, source));
|
|
7306
8048
|
continue;
|
|
7307
8049
|
}
|
|
7308
|
-
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
} catch {
|
|
7312
|
-
}
|
|
8050
|
+
const ast = await parseCode(source);
|
|
8051
|
+
if (ast === void 0) continue;
|
|
8052
|
+
parsed.push({ file: rel, source, ast });
|
|
7313
8053
|
}
|
|
7314
8054
|
const files = new Set(parsed.map((p) => p.file));
|
|
7315
8055
|
const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
|
|
7316
8056
|
const returning = /* @__PURE__ */ new Map();
|
|
7317
|
-
for (const
|
|
7318
|
-
returning.set(
|
|
8057
|
+
for (const entry of parsed) {
|
|
8058
|
+
returning.set(entry.file, functionsThatReturnTaint(entry.ast));
|
|
7319
8059
|
}
|
|
7320
|
-
for (const
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
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
|
+
);
|
|
7330
8070
|
}
|
|
7331
8071
|
findings.push(...runInterproc(parsed));
|
|
7332
|
-
|
|
8072
|
+
const out = dedupeSastSinks(findings);
|
|
8073
|
+
releaseParsed(parsed);
|
|
8074
|
+
return out;
|
|
7333
8075
|
}
|
|
7334
8076
|
};
|
|
7335
8077
|
|
|
@@ -7701,7 +8443,7 @@ function createAiClient(options) {
|
|
|
7701
8443
|
const useFixtures = options.useFixtures ?? !isLiveAi();
|
|
7702
8444
|
const model = options.model ?? process.env.FULLSHIELD_AI_MODEL ?? (useFixtures ? "recorded-fixture" : "qwen2.5-coder:7b");
|
|
7703
8445
|
const provider = options.provider ?? process.env.FULLSHIELD_AI_PROVIDER ?? (useFixtures ? "fixture" : "ollama");
|
|
7704
|
-
const
|
|
8446
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
7705
8447
|
let spentUsd = 0;
|
|
7706
8448
|
let networkCalls = 0;
|
|
7707
8449
|
const liveCost = provider === "ollama" ? 0 : FIXTURE_COST;
|
|
@@ -7721,7 +8463,7 @@ function createAiClient(options) {
|
|
|
7721
8463
|
const redactedPrompt = `${modeInstruction(req.mode)}
|
|
7722
8464
|
${wrapUntrustedCode(req.prompt)}`;
|
|
7723
8465
|
const key = cacheKey(req.model ?? model, redactedPrompt);
|
|
7724
|
-
const cached =
|
|
8466
|
+
const cached = cache2.get(key);
|
|
7725
8467
|
if (cached !== void 0) {
|
|
7726
8468
|
return { ...cached, cached: true, costUsd: 0 };
|
|
7727
8469
|
}
|
|
@@ -7735,7 +8477,7 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7735
8477
|
cached: false,
|
|
7736
8478
|
redactedPrompt
|
|
7737
8479
|
};
|
|
7738
|
-
|
|
8480
|
+
cache2.set(key, response2);
|
|
7739
8481
|
return response2;
|
|
7740
8482
|
}
|
|
7741
8483
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -7785,21 +8527,42 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7785
8527
|
cached: false,
|
|
7786
8528
|
redactedPrompt
|
|
7787
8529
|
};
|
|
7788
|
-
|
|
8530
|
+
cache2.set(key, response);
|
|
7789
8531
|
return response;
|
|
7790
8532
|
}
|
|
7791
8533
|
};
|
|
7792
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
|
+
}
|
|
7793
8554
|
async function readBaseline(path) {
|
|
7794
8555
|
try {
|
|
7795
8556
|
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
7796
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;
|
|
7797
8559
|
return raw;
|
|
7798
8560
|
} catch {
|
|
7799
8561
|
return void 0;
|
|
7800
8562
|
}
|
|
7801
8563
|
}
|
|
7802
8564
|
async function writeBaseline(path, findings) {
|
|
8565
|
+
const existing = await readBaseline(path);
|
|
7803
8566
|
const doc = {
|
|
7804
8567
|
version: 1,
|
|
7805
8568
|
findings: findings.map((f) => ({
|
|
@@ -7807,7 +8570,8 @@ async function writeBaseline(path, findings) {
|
|
|
7807
8570
|
ruleId: f.ruleId,
|
|
7808
8571
|
file: f.file,
|
|
7809
8572
|
line: f.line
|
|
7810
|
-
}))
|
|
8573
|
+
})),
|
|
8574
|
+
...existing?.suppressions !== void 0 ? { suppressions: existing.suppressions } : {}
|
|
7811
8575
|
};
|
|
7812
8576
|
await writeFile(path, `${JSON.stringify(doc, null, 2)}
|
|
7813
8577
|
`, "utf8");
|
|
@@ -7817,6 +8581,30 @@ function applyBaseline(findings, baseline) {
|
|
|
7817
8581
|
const known = new Set(baseline.findings.map((f) => f.id));
|
|
7818
8582
|
return findings.filter((f) => !known.has(f.id));
|
|
7819
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
|
+
}
|
|
7820
8608
|
|
|
7821
8609
|
// src/core/config.ts
|
|
7822
8610
|
init_finding();
|
|
@@ -7825,14 +8613,27 @@ var DEFAULT_CONFIG = {
|
|
|
7825
8613
|
modules: [...MODULE_NAMES],
|
|
7826
8614
|
failOn: "high",
|
|
7827
8615
|
ignore: ["dist/**", "**/*.test.ts"],
|
|
8616
|
+
defaultExcludes: true,
|
|
7828
8617
|
baseline: ".fullshield-baseline.json",
|
|
7829
8618
|
notify: { slackWebhookEnv: "FULLSHIELD_SLACK_WEBHOOK" },
|
|
7830
8619
|
deps: { offline: false },
|
|
7831
|
-
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 }
|
|
8620
|
+
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 },
|
|
8621
|
+
sast: { maxFileBytes: 1024 * 1024 }
|
|
7832
8622
|
};
|
|
7833
8623
|
var ConfigError = class extends Error {
|
|
7834
8624
|
};
|
|
7835
|
-
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
|
+
]);
|
|
7836
8637
|
function validate(raw, source) {
|
|
7837
8638
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
7838
8639
|
throw new ConfigError(`${source}: config must be a JSON object`);
|
|
@@ -7934,6 +8735,32 @@ function validate(raw, source) {
|
|
|
7934
8735
|
}
|
|
7935
8736
|
out.ai = ai;
|
|
7936
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
|
+
}
|
|
7937
8764
|
return out;
|
|
7938
8765
|
}
|
|
7939
8766
|
async function loadConfig(cwd, configPath) {
|
|
@@ -7960,10 +8787,26 @@ async function loadConfig(cwd, configPath) {
|
|
|
7960
8787
|
return { ...structuredClone(DEFAULT_CONFIG), ...validate(raw, file) };
|
|
7961
8788
|
}
|
|
7962
8789
|
var ignoreFactory = ignoreModule.default;
|
|
7963
|
-
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
|
+
];
|
|
7964
8804
|
async function walkFiles(options) {
|
|
7965
8805
|
const ig = ignoreFactory();
|
|
7966
|
-
ig.add(
|
|
8806
|
+
ig.add(HARD_IGNORED);
|
|
8807
|
+
if (options.defaultExcludes !== false) {
|
|
8808
|
+
ig.add(options.excludes ?? DEFAULT_EXCLUDES);
|
|
8809
|
+
}
|
|
7967
8810
|
try {
|
|
7968
8811
|
ig.add(await readFile(join(options.cwd, ".gitignore"), "utf8"));
|
|
7969
8812
|
} catch {
|
|
@@ -8012,23 +8855,6 @@ async function listStagedFiles(cwd) {
|
|
|
8012
8855
|
// src/core/engine.ts
|
|
8013
8856
|
init_suppression();
|
|
8014
8857
|
init_finding();
|
|
8015
|
-
function packageVersion() {
|
|
8016
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
8017
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
8018
|
-
try {
|
|
8019
|
-
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
8020
|
-
const pkg = JSON.parse(raw);
|
|
8021
|
-
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
8022
|
-
return pkg.version;
|
|
8023
|
-
}
|
|
8024
|
-
} catch {
|
|
8025
|
-
}
|
|
8026
|
-
const parent = dirname(dir);
|
|
8027
|
-
if (parent === dir) break;
|
|
8028
|
-
dir = parent;
|
|
8029
|
-
}
|
|
8030
|
-
throw new Error("fullshield package.json not found");
|
|
8031
|
-
}
|
|
8032
8858
|
|
|
8033
8859
|
// src/policy/index.ts
|
|
8034
8860
|
init_finding();
|
|
@@ -8198,7 +9024,7 @@ function resolvePolicyBranch(cwd, env = process.env) {
|
|
|
8198
9024
|
return "*";
|
|
8199
9025
|
}
|
|
8200
9026
|
}
|
|
8201
|
-
function
|
|
9027
|
+
function expiresAtMs2(expires) {
|
|
8202
9028
|
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
|
|
8203
9029
|
return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8204
9030
|
}
|
|
@@ -8220,7 +9046,7 @@ function applyPolicy(findings, policy, ctx = {}) {
|
|
|
8220
9046
|
});
|
|
8221
9047
|
const suppressed = /* @__PURE__ */ new Set();
|
|
8222
9048
|
for (const exception of policy.exceptions ?? []) {
|
|
8223
|
-
if (
|
|
9049
|
+
if (expiresAtMs2(exception.expires) < now.getTime()) {
|
|
8224
9050
|
errors.push(
|
|
8225
9051
|
`policy exception for ${exception.ruleId} expired on ${exception.expires} (owner: ${exception.owner}); renew or remove it`
|
|
8226
9052
|
);
|
|
@@ -8289,7 +9115,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8289
9115
|
options.modules ?? config.modules,
|
|
8290
9116
|
loadedPolicy?.requiredModules
|
|
8291
9117
|
);
|
|
8292
|
-
let files = await walkFiles({
|
|
9118
|
+
let files = await walkFiles({
|
|
9119
|
+
cwd,
|
|
9120
|
+
ignore: config.ignore,
|
|
9121
|
+
defaultExcludes: config.defaultExcludes,
|
|
9122
|
+
excludes: config.excludes
|
|
9123
|
+
});
|
|
8293
9124
|
if (options.staged === true) {
|
|
8294
9125
|
const staged = new Set(await listStagedFiles(cwd));
|
|
8295
9126
|
files = files.filter((file) => staged.has(file));
|
|
@@ -8323,6 +9154,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8323
9154
|
);
|
|
8324
9155
|
}
|
|
8325
9156
|
const extraHits = [];
|
|
9157
|
+
let historyNotice;
|
|
8326
9158
|
const secretsWanted = modulesRequested.includes("secrets");
|
|
8327
9159
|
if (secretsAdvEnabled() && secretsWanted) {
|
|
8328
9160
|
if (options.history === true) {
|
|
@@ -8330,6 +9162,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8330
9162
|
const hist = await scanGitHistory2(cwd);
|
|
8331
9163
|
extraHits.push(...hist.hits);
|
|
8332
9164
|
collected.push(...hist.findings);
|
|
9165
|
+
historyNotice = hist.history;
|
|
8333
9166
|
}
|
|
8334
9167
|
if (options.bundle !== void 0 && options.bundle.length > 0) {
|
|
8335
9168
|
const { scanBundle: scanBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
@@ -8350,7 +9183,11 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8350
9183
|
if (options.updateBaseline === true) {
|
|
8351
9184
|
await writeBaseline(baselinePath, unsuppressed);
|
|
8352
9185
|
}
|
|
8353
|
-
|
|
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;
|
|
8354
9191
|
if (secretsAdvEnabled() && secretsWanted && options.verifySecrets === true && options.iOwnTheseCredentials === true) {
|
|
8355
9192
|
const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
8356
9193
|
const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
@@ -8422,10 +9259,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
8422
9259
|
cwd,
|
|
8423
9260
|
filesScanned: files.length,
|
|
8424
9261
|
modulesRequested: [...modulesRequested],
|
|
8425
|
-
modulesRun
|
|
9262
|
+
modulesRun,
|
|
9263
|
+
...historyNotice !== void 0 ? { history: historyNotice } : {}
|
|
8426
9264
|
},
|
|
8427
9265
|
config,
|
|
8428
|
-
policy: policyResult
|
|
9266
|
+
policy: policyResult,
|
|
9267
|
+
...suppressionErrors.length > 0 ? { suppressionErrors } : {}
|
|
8429
9268
|
};
|
|
8430
9269
|
}
|
|
8431
9270
|
function scan(options = {}) {
|
|
@@ -8761,6 +9600,27 @@ init_suppression();
|
|
|
8761
9600
|
|
|
8762
9601
|
// src/reporters/console.ts
|
|
8763
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
|
|
8764
9624
|
var SEVERITY_COLOR = {
|
|
8765
9625
|
critical: (t) => pc.bold(pc.red(t)),
|
|
8766
9626
|
high: (t) => pc.red(t),
|
|
@@ -8800,7 +9660,8 @@ function renderFinding(finding3) {
|
|
|
8800
9660
|
}
|
|
8801
9661
|
return lines;
|
|
8802
9662
|
}
|
|
8803
|
-
function renderConsoleReport(
|
|
9663
|
+
function renderConsoleReport(input) {
|
|
9664
|
+
const result = redactScan(input);
|
|
8804
9665
|
const { findings, summary, metadata } = result;
|
|
8805
9666
|
const lines = [];
|
|
8806
9667
|
lines.push(
|
|
@@ -8808,6 +9669,7 @@ function renderConsoleReport(result) {
|
|
|
8808
9669
|
` \u2014 scanned ${metadata.filesScanned} files in ${metadata.durationMs}ms (modules: ${metadata.modulesRun.length > 0 ? metadata.modulesRun.join(", ") : "none"})`
|
|
8809
9670
|
)
|
|
8810
9671
|
);
|
|
9672
|
+
if (metadata.history !== void 0) lines.push(metadata.history);
|
|
8811
9673
|
if (metadata.modulesRun.length < metadata.modulesRequested.length) {
|
|
8812
9674
|
const pending = metadata.modulesRequested.filter((m) => !metadata.modulesRun.includes(m));
|
|
8813
9675
|
lines.push(
|
|
@@ -8815,15 +9677,23 @@ function renderConsoleReport(result) {
|
|
|
8815
9677
|
);
|
|
8816
9678
|
}
|
|
8817
9679
|
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
9680
|
+
const ordered = bySeverityDesc.flatMap((severity) => findings.filter((f) => f.severity === severity));
|
|
9681
|
+
const shown = ordered.slice(0, 10);
|
|
9682
|
+
const hidden = ordered.length - shown.length;
|
|
8818
9683
|
for (const severity of bySeverityDesc) {
|
|
8819
|
-
const group =
|
|
9684
|
+
const group = shown.filter((f) => f.severity === severity);
|
|
8820
9685
|
if (group.length === 0) continue;
|
|
8821
9686
|
lines.push("");
|
|
8822
|
-
lines.push(SEVERITY_COLOR[severity](`${severity.toUpperCase()} (${
|
|
9687
|
+
lines.push(SEVERITY_COLOR[severity](`${severity.toUpperCase()} (${findings.filter((f) => f.severity === severity).length})`));
|
|
8823
9688
|
for (const finding3 of group) {
|
|
8824
9689
|
lines.push(...renderFinding(finding3));
|
|
9690
|
+
lines.push(` docs: rules/${finding3.ruleId}.md`);
|
|
8825
9691
|
}
|
|
8826
9692
|
}
|
|
9693
|
+
if (hidden > 0) {
|
|
9694
|
+
lines.push("");
|
|
9695
|
+
lines.push(`${hidden} more \u2014 use --json`);
|
|
9696
|
+
}
|
|
8827
9697
|
lines.push("");
|
|
8828
9698
|
if (summary.total === 0) {
|
|
8829
9699
|
lines.push(pc.green("\u2714 No findings."));
|
|
@@ -8843,7 +9713,8 @@ function location2(finding3) {
|
|
|
8843
9713
|
if (finding3.file === void 0) return "(project)";
|
|
8844
9714
|
return finding3.line !== void 0 ? `${finding3.file}:${finding3.line}` : finding3.file;
|
|
8845
9715
|
}
|
|
8846
|
-
function renderHtmlReport(
|
|
9716
|
+
function renderHtmlReport(input) {
|
|
9717
|
+
const result = redactScan(input);
|
|
8847
9718
|
const sections = [];
|
|
8848
9719
|
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
8849
9720
|
for (const severity of bySeverityDesc) {
|
|
@@ -8890,7 +9761,7 @@ ${items}`);
|
|
|
8890
9761
|
|
|
8891
9762
|
// src/reporters/json.ts
|
|
8892
9763
|
function publicFinding(finding3) {
|
|
8893
|
-
const copy =
|
|
9764
|
+
const copy = redactFinding(finding3);
|
|
8894
9765
|
delete copy.aiVerdict;
|
|
8895
9766
|
delete copy.exploitScenario;
|
|
8896
9767
|
delete copy.aiReasoning;
|
|
@@ -8914,7 +9785,8 @@ function location3(finding3) {
|
|
|
8914
9785
|
if (finding3.file === void 0) return "(project)";
|
|
8915
9786
|
return finding3.line !== void 0 ? `${finding3.file}:${finding3.line}` : finding3.file;
|
|
8916
9787
|
}
|
|
8917
|
-
function renderMarkdownReport(
|
|
9788
|
+
function renderMarkdownReport(input) {
|
|
9789
|
+
const result = redactScan(input);
|
|
8918
9790
|
const lines = [
|
|
8919
9791
|
"# FullShield report",
|
|
8920
9792
|
"",
|
|
@@ -8966,7 +9838,8 @@ function sarifLevel(severity) {
|
|
|
8966
9838
|
function uri(file) {
|
|
8967
9839
|
return (file ?? "").replace(/\\/g, "/");
|
|
8968
9840
|
}
|
|
8969
|
-
function renderSarifReport(
|
|
9841
|
+
function renderSarifReport(input) {
|
|
9842
|
+
const result = redactScan(input);
|
|
8970
9843
|
const rules = /* @__PURE__ */ new Map();
|
|
8971
9844
|
for (const finding3 of result.findings) {
|
|
8972
9845
|
if (!rules.has(finding3.ruleId)) {
|
|
@@ -9073,6 +9946,257 @@ function validateSarif21(doc) {
|
|
|
9073
9946
|
return errors;
|
|
9074
9947
|
}
|
|
9075
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
|
+
|
|
9076
10200
|
// src/sbom/cyclonedx.ts
|
|
9077
10201
|
function validateCycloneDx15(doc) {
|
|
9078
10202
|
const errors = [];
|
|
@@ -9299,4 +10423,4 @@ function buildVex(findings, format, meta = { name: "app", version: "0.0.0" }) {
|
|
|
9299
10423
|
return buildOpenVex(findings);
|
|
9300
10424
|
}
|
|
9301
10425
|
|
|
9302
|
-
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 };
|