fullshield 0.1.3 → 0.2.6
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 +47 -1
- package/README.md +20 -1
- package/dist/agent.js +344 -41
- package/dist/cli/index.js +487 -66
- package/dist/index.d.ts +1 -1
- package/dist/index.js +354 -43
- package/dist/mcp.js +344 -41
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ interface Finding {
|
|
|
42
42
|
note: string;
|
|
43
43
|
}>;
|
|
44
44
|
/** Dependency reachability (Phase 13 / harden-3). */
|
|
45
|
-
reachability?: 'reachable' | 'imported-not-called' | 'reachable-via-dependency' | 'not-imported' | 'reachability-unknown';
|
|
45
|
+
reachability?: 'reachable' | 'imported-not-called' | 'reachable-via-dependency' | 'not-imported' | 'reachability-unknown' | 'symbol-reachable' | 'symbol-not-called';
|
|
46
46
|
/** Lockfile path from an imported direct dependency, e.g. express > body-parser > qs. */
|
|
47
47
|
dependencyPath?: string[];
|
|
48
48
|
/** Deterministic (default) vs AI-added. AI findings never fail CI. */
|
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;
|
|
@@ -1147,6 +1148,9 @@ function classesEnabled() {
|
|
|
1147
1148
|
function mcpCfgEnabled() {
|
|
1148
1149
|
return process.env.FULLSHIELD_EXPERIMENTAL_MCPCFG === "1";
|
|
1149
1150
|
}
|
|
1151
|
+
function taintV2Enabled() {
|
|
1152
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
1153
|
+
}
|
|
1150
1154
|
|
|
1151
1155
|
// src/modules/config/finding.ts
|
|
1152
1156
|
init_finding();
|
|
@@ -1956,6 +1960,103 @@ function exactPinsFromManifest(pkg) {
|
|
|
1956
1960
|
|
|
1957
1961
|
// src/modules/deps/reachability.ts
|
|
1958
1962
|
init_finding();
|
|
1963
|
+
|
|
1964
|
+
// src/modules/deps/symbol-reachability.ts
|
|
1965
|
+
function affectedFunctionNames(vuln) {
|
|
1966
|
+
const names = /* @__PURE__ */ new Set();
|
|
1967
|
+
for (const affected of vuln.affected ?? []) {
|
|
1968
|
+
const list = affected.ecosystem_specific?.affected_functions;
|
|
1969
|
+
if (!Array.isArray(list)) continue;
|
|
1970
|
+
for (const item of list) {
|
|
1971
|
+
if (typeof item === "string" && item.length > 0) names.add(item);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
return [...names];
|
|
1975
|
+
}
|
|
1976
|
+
function escapeRegExp(value) {
|
|
1977
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1978
|
+
}
|
|
1979
|
+
function called(source, local) {
|
|
1980
|
+
return new RegExp(`\\b${escapeRegExp(local)}\\s*\\(`).test(source);
|
|
1981
|
+
}
|
|
1982
|
+
function memberCalled(source, object, member) {
|
|
1983
|
+
return new RegExp(`\\b${escapeRegExp(object)}\\s*\\.\\s*${escapeRegExp(member)}\\s*\\(`).test(source);
|
|
1984
|
+
}
|
|
1985
|
+
function sourceCallsAffectedSymbol(source, symbol) {
|
|
1986
|
+
const esc2 = escapeRegExp(symbol);
|
|
1987
|
+
const named = new RegExp(
|
|
1988
|
+
`import\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`,
|
|
1989
|
+
"g"
|
|
1990
|
+
);
|
|
1991
|
+
let match;
|
|
1992
|
+
while ((match = named.exec(source)) !== null) {
|
|
1993
|
+
const parts = (match[1] ?? "").split(",");
|
|
1994
|
+
for (const part of parts) {
|
|
1995
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
1996
|
+
const imported = bits[0]?.trim();
|
|
1997
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
1998
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
const star = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"][^'"]+['"]/g;
|
|
2002
|
+
while ((match = star.exec(source)) !== null) {
|
|
2003
|
+
const local = match[1];
|
|
2004
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
2005
|
+
}
|
|
2006
|
+
const subDefault = new RegExp(
|
|
2007
|
+
`import\\s+([A-Za-z_$][\\w$]*)\\s+from\\s+['"][^'"]+/${esc2}['"]`
|
|
2008
|
+
);
|
|
2009
|
+
const sub = subDefault.exec(source);
|
|
2010
|
+
if (sub?.[1] !== void 0 && called(source, sub[1])) return true;
|
|
2011
|
+
const reqMember = new RegExp(
|
|
2012
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+['"]\\s*\\)\\s*\\.\\s*${esc2}`
|
|
2013
|
+
);
|
|
2014
|
+
const reqHit = reqMember.exec(source);
|
|
2015
|
+
if (reqHit?.[1] !== void 0 && called(source, reqHit[1])) return true;
|
|
2016
|
+
const reqNs = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
2017
|
+
while ((match = reqNs.exec(source)) !== null) {
|
|
2018
|
+
const local = match[1];
|
|
2019
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
2020
|
+
}
|
|
2021
|
+
const reqNamed = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
2022
|
+
while ((match = reqNamed.exec(source)) !== null) {
|
|
2023
|
+
const parts = (match[1] ?? "").split(",");
|
|
2024
|
+
for (const part of parts) {
|
|
2025
|
+
const bits = part.trim().split(/\s*:\s*/);
|
|
2026
|
+
const imported = bits[0]?.trim();
|
|
2027
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
2028
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
const reqSub = new RegExp(
|
|
2032
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+/${esc2}['"]\\s*\\)`
|
|
2033
|
+
);
|
|
2034
|
+
const subReq = reqSub.exec(source);
|
|
2035
|
+
if (subReq?.[1] !== void 0 && called(source, subReq[1])) return true;
|
|
2036
|
+
const barrel = new RegExp(`export\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`, "g");
|
|
2037
|
+
while ((match = barrel.exec(source)) !== null) {
|
|
2038
|
+
const parts = (match[1] ?? "").split(",");
|
|
2039
|
+
for (const part of parts) {
|
|
2040
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
2041
|
+
const exported = (bits[1] ?? bits[0])?.trim();
|
|
2042
|
+
if (exported === symbol) {
|
|
2043
|
+
const local = bits[0]?.trim();
|
|
2044
|
+
if (local !== void 0 && called(source, local)) return true;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
return false;
|
|
2049
|
+
}
|
|
2050
|
+
function classifySymbolUse(symbols, sources) {
|
|
2051
|
+
for (const symbol of symbols) {
|
|
2052
|
+
for (const source of sources.values()) {
|
|
2053
|
+
if (sourceCallsAffectedSymbol(source, symbol)) return "symbol-reachable";
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return "symbol-not-called";
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/modules/deps/reachability.ts
|
|
1959
2060
|
function packageNameFromSnippet(snippet) {
|
|
1960
2061
|
if (snippet.startsWith("@")) {
|
|
1961
2062
|
const second = snippet.indexOf("@", 1);
|
|
@@ -1984,9 +2085,9 @@ function isProductionSource(rel) {
|
|
|
1984
2085
|
}
|
|
1985
2086
|
function indexPackageUsage(sources) {
|
|
1986
2087
|
const usage = /* @__PURE__ */ new Map();
|
|
1987
|
-
const bump = (pkg,
|
|
2088
|
+
const bump = (pkg, called2) => {
|
|
1988
2089
|
const prev = usage.get(pkg) ?? { called: false };
|
|
1989
|
-
usage.set(pkg, { imported: true, called: prev.called ||
|
|
2090
|
+
usage.set(pkg, { imported: true, called: prev.called || called2 });
|
|
1990
2091
|
};
|
|
1991
2092
|
const nsRe = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1992
2093
|
const namedRe = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
@@ -2074,10 +2175,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
2074
2175
|
return { reachability: "not-imported" };
|
|
2075
2176
|
}
|
|
2076
2177
|
function reachabilitySteps(reachability) {
|
|
2077
|
-
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown") {
|
|
2178
|
+
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
|
|
2078
2179
|
return 0;
|
|
2079
2180
|
}
|
|
2080
|
-
if (reachability === "imported-not-called") return 1;
|
|
2181
|
+
if (reachability === "imported-not-called" || reachability === "symbol-not-called") return 1;
|
|
2081
2182
|
return 2;
|
|
2082
2183
|
}
|
|
2083
2184
|
function downgradeSeverity(severity, steps) {
|
|
@@ -2094,7 +2195,7 @@ function isDevOnly(pkgName, ctx) {
|
|
|
2094
2195
|
if (inProd) return false;
|
|
2095
2196
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
2096
2197
|
}
|
|
2097
|
-
function applyReachability(findings, ctx) {
|
|
2198
|
+
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
2098
2199
|
return findings.map((finding3) => {
|
|
2099
2200
|
if (finding3.ruleId === "DEPS-MALICIOUS-001") {
|
|
2100
2201
|
if (finding3.snippet === void 0) return finding3;
|
|
@@ -2111,7 +2212,12 @@ function applyReachability(findings, ctx) {
|
|
|
2111
2212
|
if (finding3.ruleId !== "DEPS-VULN-001" || finding3.snippet === void 0) return finding3;
|
|
2112
2213
|
const pkg = packageNameFromSnippet(finding3.snippet);
|
|
2113
2214
|
if (pkg === void 0 || pkg.length === 0) return finding3;
|
|
2114
|
-
|
|
2215
|
+
let classified = classifyReachability(pkg, ctx.productionUsage, ctx.graph);
|
|
2216
|
+
const named = symbols.get(finding3.id) ?? [];
|
|
2217
|
+
if (named.length > 0 && ctx.sources !== void 0) {
|
|
2218
|
+
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
2219
|
+
classified = { reachability: symbolClass, path: classified.path };
|
|
2220
|
+
}
|
|
2115
2221
|
const steps = reachabilitySteps(classified.reachability);
|
|
2116
2222
|
let severity = steps === 0 ? finding3.severity : downgradeSeverity(finding3.severity, steps);
|
|
2117
2223
|
let message = finding3.message;
|
|
@@ -4451,14 +4557,17 @@ var depsModule = {
|
|
|
4451
4557
|
findings2.push(typosquatFinding("package.json", hit));
|
|
4452
4558
|
}
|
|
4453
4559
|
const osvHits2 = await queryOsv(pins, osvOpts);
|
|
4560
|
+
const symbols2 = /* @__PURE__ */ new Map();
|
|
4454
4561
|
for (const hit of osvHits2) {
|
|
4455
4562
|
for (const vuln of hit.vulns) {
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
);
|
|
4563
|
+
const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit.pkg, vuln) : vulnFinding("package.json", hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
4564
|
+
const names = affectedFunctionNames(vuln);
|
|
4565
|
+
if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols2.set(finding3.id, names);
|
|
4566
|
+
findings2.push(finding3);
|
|
4459
4567
|
}
|
|
4460
4568
|
}
|
|
4461
|
-
|
|
4569
|
+
const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
|
|
4570
|
+
return applyReachability(findings2, noLockCtx, symbols2);
|
|
4462
4571
|
}
|
|
4463
4572
|
let contents;
|
|
4464
4573
|
try {
|
|
@@ -4479,14 +4588,16 @@ var depsModule = {
|
|
|
4479
4588
|
}
|
|
4480
4589
|
}
|
|
4481
4590
|
const osvHits = await queryOsv(packages, osvOpts);
|
|
4591
|
+
const symbols = /* @__PURE__ */ new Map();
|
|
4482
4592
|
for (const hit of osvHits) {
|
|
4483
4593
|
for (const vuln of hit.vulns) {
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
);
|
|
4594
|
+
const finding3 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit.pkg, vuln) : vulnFinding(lockFile, hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
4595
|
+
const names = affectedFunctionNames(vuln);
|
|
4596
|
+
if (names.length > 0 && finding3.ruleId === "DEPS-VULN-001") symbols.set(finding3.id, names);
|
|
4597
|
+
findings.push(finding3);
|
|
4487
4598
|
}
|
|
4488
4599
|
}
|
|
4489
|
-
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph));
|
|
4600
|
+
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph), symbols);
|
|
4490
4601
|
if (experimentalVex()) {
|
|
4491
4602
|
reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
|
|
4492
4603
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
@@ -4520,7 +4631,7 @@ async function reachabilityCtx(cwd, files, graph) {
|
|
|
4520
4631
|
} catch {
|
|
4521
4632
|
}
|
|
4522
4633
|
}
|
|
4523
|
-
return { productionUsage: indexPackageUsage(prodSources), graph };
|
|
4634
|
+
return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
|
|
4524
4635
|
}
|
|
4525
4636
|
|
|
4526
4637
|
// src/modules/mcp/index.ts
|
|
@@ -5081,6 +5192,26 @@ function isXssSanitizerCall(node) {
|
|
|
5081
5192
|
return false;
|
|
5082
5193
|
}
|
|
5083
5194
|
var NO_RETURNERS = /* @__PURE__ */ new Set();
|
|
5195
|
+
var CALLBACK_METHODS = /* @__PURE__ */ new Set([
|
|
5196
|
+
"then",
|
|
5197
|
+
"catch",
|
|
5198
|
+
"finally",
|
|
5199
|
+
"map",
|
|
5200
|
+
"forEach",
|
|
5201
|
+
"filter",
|
|
5202
|
+
"flatMap",
|
|
5203
|
+
"find",
|
|
5204
|
+
"reduce"
|
|
5205
|
+
]);
|
|
5206
|
+
var taintedThisFields = /* @__PURE__ */ new WeakMap();
|
|
5207
|
+
function enclosingClass(node) {
|
|
5208
|
+
let current = node.parent;
|
|
5209
|
+
while (current !== void 0) {
|
|
5210
|
+
if (current.type === "ClassDeclaration" || current.type === "ClassExpression") return current;
|
|
5211
|
+
current = current.parent;
|
|
5212
|
+
}
|
|
5213
|
+
return void 0;
|
|
5214
|
+
}
|
|
5084
5215
|
function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
5085
5216
|
if (node === void 0) return false;
|
|
5086
5217
|
if (isTaintSource(node) || isFrameworkMemberTaint(node) || isFrameworkTaintCall(node) || isMcpArgsTaintSource(node)) {
|
|
@@ -5092,8 +5223,34 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
|
5092
5223
|
case "Identifier":
|
|
5093
5224
|
return tainted.has(node.name);
|
|
5094
5225
|
case "MemberExpression":
|
|
5226
|
+
if (taintV2Enabled() && node.object.type === "ThisExpression") {
|
|
5227
|
+
const cls = enclosingClass(node);
|
|
5228
|
+
const name = propName(node);
|
|
5229
|
+
if (cls !== void 0 && name !== void 0 && taintedThisFields.get(cls)?.has(name) === true) {
|
|
5230
|
+
return true;
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5095
5233
|
return exprIsTainted(node.object, tainted, returners);
|
|
5234
|
+
case "ArrayExpression":
|
|
5235
|
+
if (!taintV2Enabled()) return false;
|
|
5236
|
+
return node.elements.some((el) => {
|
|
5237
|
+
if (el === null) return false;
|
|
5238
|
+
if (el.type === "SpreadElement") return exprIsTainted(el.argument, tainted, returners);
|
|
5239
|
+
return exprIsTainted(el, tainted, returners);
|
|
5240
|
+
});
|
|
5241
|
+
case "ObjectExpression":
|
|
5242
|
+
if (!taintV2Enabled()) return false;
|
|
5243
|
+
return node.properties.some((prop) => {
|
|
5244
|
+
if (prop.type === "SpreadElement") return exprIsTainted(prop.argument, tainted, returners);
|
|
5245
|
+
if (prop.type !== "Property") return false;
|
|
5246
|
+
return exprIsTainted(prop.value, tainted, returners);
|
|
5247
|
+
});
|
|
5096
5248
|
case "CallExpression":
|
|
5249
|
+
if (taintV2Enabled() && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
|
|
5250
|
+
return node.arguments.some(
|
|
5251
|
+
(arg) => arg.type !== "SpreadElement" && exprIsTainted(arg, tainted, returners)
|
|
5252
|
+
);
|
|
5253
|
+
}
|
|
5097
5254
|
if (isSchemaValidatorCall(node)) return false;
|
|
5098
5255
|
if (node.callee.type === "Identifier" && returners.has(node.callee.name)) return true;
|
|
5099
5256
|
if (node.callee.type === "MemberExpression" && exprIsTainted(node.callee.object, tainted, returners)) {
|
|
@@ -5324,6 +5481,65 @@ function scopeReturnsTaint(scope, tainted, returners) {
|
|
|
5324
5481
|
return yes;
|
|
5325
5482
|
}
|
|
5326
5483
|
var MAX_TAINT_HOPS = 5;
|
|
5484
|
+
function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
5485
|
+
let changed = false;
|
|
5486
|
+
const mark = (fn, name) => {
|
|
5487
|
+
if (name === void 0) return;
|
|
5488
|
+
const set = seeds.get(fn) ?? /* @__PURE__ */ new Set();
|
|
5489
|
+
if (set.has(name)) return;
|
|
5490
|
+
set.add(name);
|
|
5491
|
+
seeds.set(fn, set);
|
|
5492
|
+
changed = true;
|
|
5493
|
+
};
|
|
5494
|
+
for (const fn of functions) {
|
|
5495
|
+
const f = facts.get(fn);
|
|
5496
|
+
if (f === void 0) continue;
|
|
5497
|
+
let parent = fn.parent;
|
|
5498
|
+
while (parent !== void 0 && !isFunctionNode(parent)) {
|
|
5499
|
+
parent = parent.parent;
|
|
5500
|
+
}
|
|
5501
|
+
if (parent !== void 0 && isFunctionNode(parent)) {
|
|
5502
|
+
const outer = facts.get(parent);
|
|
5503
|
+
if (outer !== void 0) {
|
|
5504
|
+
for (const name of outer.tainted) mark(fn, name);
|
|
5505
|
+
}
|
|
5506
|
+
}
|
|
5507
|
+
walkScope(fn, (node) => {
|
|
5508
|
+
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") {
|
|
5509
|
+
if (node.left.object.type === "ThisExpression" && exprIsTainted(node.right, f.tainted, returners)) {
|
|
5510
|
+
const cls = enclosingClass(node);
|
|
5511
|
+
const field = propName(node.left);
|
|
5512
|
+
if (cls !== void 0 && field !== void 0) {
|
|
5513
|
+
const fields = taintedThisFields.get(cls) ?? /* @__PURE__ */ new Set();
|
|
5514
|
+
if (!fields.has(field)) {
|
|
5515
|
+
fields.add(field);
|
|
5516
|
+
taintedThisFields.set(cls, fields);
|
|
5517
|
+
changed = true;
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
5523
|
+
const method = propName(node.callee);
|
|
5524
|
+
if (method === void 0) return;
|
|
5525
|
+
if (CALLBACK_METHODS.has(method) && exprIsTainted(node.callee.object, f.tainted, returners)) {
|
|
5526
|
+
const callback = node.arguments[0];
|
|
5527
|
+
if (callback !== void 0 && isFunctionNode(callback)) {
|
|
5528
|
+
mark(callback, boundNames(callback.params[0])[0]);
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
if ((method === "call" || method === "apply") && node.callee.object.type === "Identifier") {
|
|
5532
|
+
const target = nameToFn.get(node.callee.object.name);
|
|
5533
|
+
if (target === void 0 || !isFunctionNode(target)) return;
|
|
5534
|
+
const arg = node.arguments[1];
|
|
5535
|
+
if (arg !== void 0 && arg.type !== "SpreadElement" && exprIsTainted(arg, f.tainted, returners)) {
|
|
5536
|
+
mark(target, boundNames(target.params[0])[0]);
|
|
5537
|
+
}
|
|
5538
|
+
}
|
|
5539
|
+
});
|
|
5540
|
+
}
|
|
5541
|
+
return changed;
|
|
5542
|
+
}
|
|
5327
5543
|
function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
5328
5544
|
attachParents(ast);
|
|
5329
5545
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
@@ -5378,6 +5594,9 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5378
5594
|
});
|
|
5379
5595
|
});
|
|
5380
5596
|
}
|
|
5597
|
+
if (taintV2Enabled() && seedTaintV2(functions, facts, seeds, returners, nameToFn)) {
|
|
5598
|
+
changed = true;
|
|
5599
|
+
}
|
|
5381
5600
|
if (!changed) break;
|
|
5382
5601
|
}
|
|
5383
5602
|
return facts;
|
|
@@ -5520,6 +5739,21 @@ function requireCall(node) {
|
|
|
5520
5739
|
}
|
|
5521
5740
|
return void 0;
|
|
5522
5741
|
}
|
|
5742
|
+
function reexportsOf(ast) {
|
|
5743
|
+
const out = /* @__PURE__ */ new Map();
|
|
5744
|
+
walk(ast, (node) => {
|
|
5745
|
+
if (node.type !== "ExportNamedDeclaration" || node.source === null || node.source === void 0) return;
|
|
5746
|
+
const spec = literalString(node.source);
|
|
5747
|
+
if (spec === void 0) return;
|
|
5748
|
+
for (const specNode of node.specifiers) {
|
|
5749
|
+
if (specNode.type !== "ExportSpecifier") continue;
|
|
5750
|
+
const exported = specNode.exported.type === "Identifier" ? specNode.exported.name : void 0;
|
|
5751
|
+
const local = specNode.local.type === "Identifier" ? specNode.local.name : exported;
|
|
5752
|
+
if (exported !== void 0 && local !== void 0) out.set(exported, { spec, name: local });
|
|
5753
|
+
}
|
|
5754
|
+
});
|
|
5755
|
+
return out;
|
|
5756
|
+
}
|
|
5523
5757
|
function resolveRelative(fromFile, spec, files) {
|
|
5524
5758
|
if (!spec.startsWith(".")) return void 0;
|
|
5525
5759
|
const base = posix.normalize(posix.join(posix.dirname(fromFile), spec));
|
|
@@ -5597,6 +5831,22 @@ function runInterproc(parsed) {
|
|
|
5597
5831
|
functions.set(`${fn.file}#${fn.name}`, fn);
|
|
5598
5832
|
}
|
|
5599
5833
|
}
|
|
5834
|
+
if (taintV2Enabled()) {
|
|
5835
|
+
const barrels = /* @__PURE__ */ new Map();
|
|
5836
|
+
for (const p of parsed) barrels.set(p.file, reexportsOf(p.ast));
|
|
5837
|
+
for (const ns of requires.values()) {
|
|
5838
|
+
for (const [local, target] of ns) {
|
|
5839
|
+
const hash = target.indexOf("#");
|
|
5840
|
+
if (hash < 0) continue;
|
|
5841
|
+
const file = target.slice(0, hash);
|
|
5842
|
+
const name = target.slice(hash + 1);
|
|
5843
|
+
const re = barrels.get(file)?.get(name);
|
|
5844
|
+
if (re === void 0) continue;
|
|
5845
|
+
const resolved = resolveRelative(file, re.spec, files);
|
|
5846
|
+
if (resolved !== void 0) ns.set(local, `${resolved}#${re.name}`);
|
|
5847
|
+
}
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5600
5850
|
const queue = [];
|
|
5601
5851
|
for (const p of parsed) {
|
|
5602
5852
|
const facts = analyzeFile(p.ast);
|
|
@@ -6112,7 +6362,10 @@ function isMongoReceiver(node, source) {
|
|
|
6112
6362
|
const obj = node.callee.object;
|
|
6113
6363
|
if (obj.type === "Identifier") {
|
|
6114
6364
|
if (isCollectionName(obj.name)) return true;
|
|
6115
|
-
if (isPascalModel(obj.name))
|
|
6365
|
+
if (isPascalModel(obj.name)) {
|
|
6366
|
+
if (/extends\s+BaseEntity\b/.test(source)) return false;
|
|
6367
|
+
return !fileHasSqlOrm;
|
|
6368
|
+
}
|
|
6116
6369
|
return fileHasMongo;
|
|
6117
6370
|
}
|
|
6118
6371
|
if (obj.type === "MemberExpression") {
|
|
@@ -7173,6 +7426,72 @@ function scanPromptInjection(file, source) {
|
|
|
7173
7426
|
});
|
|
7174
7427
|
return findings;
|
|
7175
7428
|
}
|
|
7429
|
+
function packageVersion() {
|
|
7430
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7431
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
7432
|
+
try {
|
|
7433
|
+
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
7434
|
+
const pkg = JSON.parse(raw);
|
|
7435
|
+
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
7436
|
+
return pkg.version;
|
|
7437
|
+
}
|
|
7438
|
+
} catch {
|
|
7439
|
+
}
|
|
7440
|
+
const parent = dirname(dir);
|
|
7441
|
+
if (parent === dir) break;
|
|
7442
|
+
dir = parent;
|
|
7443
|
+
}
|
|
7444
|
+
throw new Error("fullshield package.json not found");
|
|
7445
|
+
}
|
|
7446
|
+
|
|
7447
|
+
// src/core/parse-pool.ts
|
|
7448
|
+
var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7449
|
+
var cache = /* @__PURE__ */ new Map();
|
|
7450
|
+
function parsePoolEnabled() {
|
|
7451
|
+
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
7452
|
+
}
|
|
7453
|
+
function parsePoolSize() {
|
|
7454
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
7455
|
+
if (raw !== void 0 && raw !== "") {
|
|
7456
|
+
const parsed = Number(raw);
|
|
7457
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7458
|
+
}
|
|
7459
|
+
return Math.max(1, availableParallelism() - 1);
|
|
7460
|
+
}
|
|
7461
|
+
function parseCacheKey(code) {
|
|
7462
|
+
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
7463
|
+
}
|
|
7464
|
+
function parseOne(code) {
|
|
7465
|
+
const key = parseCacheKey(code);
|
|
7466
|
+
const hit = cache.get(key);
|
|
7467
|
+
if (hit !== void 0) return Promise.resolve(hit);
|
|
7468
|
+
return new Promise((resolve5, reject) => {
|
|
7469
|
+
const worker = new Worker(fileURLToPath(workerUrl), { workerData: { code } });
|
|
7470
|
+
worker.once("message", (ast) => {
|
|
7471
|
+
cache.set(key, ast);
|
|
7472
|
+
resolve5(ast);
|
|
7473
|
+
});
|
|
7474
|
+
worker.once("error", reject);
|
|
7475
|
+
});
|
|
7476
|
+
}
|
|
7477
|
+
async function parseWithPool(codes) {
|
|
7478
|
+
const size = parsePoolSize();
|
|
7479
|
+
const out = new Array(codes.length);
|
|
7480
|
+
let next = 0;
|
|
7481
|
+
async function run() {
|
|
7482
|
+
for (; ; ) {
|
|
7483
|
+
const index = next;
|
|
7484
|
+
next += 1;
|
|
7485
|
+
if (index >= codes.length) return;
|
|
7486
|
+
const code = codes[index];
|
|
7487
|
+
if (code === void 0) return;
|
|
7488
|
+
out[index] = await parseOne(code);
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
const workers = Math.min(size, Math.max(1, codes.length));
|
|
7492
|
+
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
7493
|
+
return out;
|
|
7494
|
+
}
|
|
7176
7495
|
|
|
7177
7496
|
// src/modules/sast/engine.ts
|
|
7178
7497
|
var sastRules = [
|
|
@@ -7306,7 +7625,8 @@ var sastModule = {
|
|
|
7306
7625
|
continue;
|
|
7307
7626
|
}
|
|
7308
7627
|
try {
|
|
7309
|
-
const ast = await parseSource(source);
|
|
7628
|
+
const ast = parsePoolEnabled() ? (await parseWithPool([source]))[0] : await parseSource(source);
|
|
7629
|
+
if (ast === void 0) continue;
|
|
7310
7630
|
parsed.push({ file: rel, source, ast });
|
|
7311
7631
|
} catch {
|
|
7312
7632
|
}
|
|
@@ -7701,7 +8021,7 @@ function createAiClient(options) {
|
|
|
7701
8021
|
const useFixtures = options.useFixtures ?? !isLiveAi();
|
|
7702
8022
|
const model = options.model ?? process.env.FULLSHIELD_AI_MODEL ?? (useFixtures ? "recorded-fixture" : "qwen2.5-coder:7b");
|
|
7703
8023
|
const provider = options.provider ?? process.env.FULLSHIELD_AI_PROVIDER ?? (useFixtures ? "fixture" : "ollama");
|
|
7704
|
-
const
|
|
8024
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
7705
8025
|
let spentUsd = 0;
|
|
7706
8026
|
let networkCalls = 0;
|
|
7707
8027
|
const liveCost = provider === "ollama" ? 0 : FIXTURE_COST;
|
|
@@ -7721,7 +8041,7 @@ function createAiClient(options) {
|
|
|
7721
8041
|
const redactedPrompt = `${modeInstruction(req.mode)}
|
|
7722
8042
|
${wrapUntrustedCode(req.prompt)}`;
|
|
7723
8043
|
const key = cacheKey(req.model ?? model, redactedPrompt);
|
|
7724
|
-
const cached =
|
|
8044
|
+
const cached = cache2.get(key);
|
|
7725
8045
|
if (cached !== void 0) {
|
|
7726
8046
|
return { ...cached, cached: true, costUsd: 0 };
|
|
7727
8047
|
}
|
|
@@ -7735,7 +8055,7 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7735
8055
|
cached: false,
|
|
7736
8056
|
redactedPrompt
|
|
7737
8057
|
};
|
|
7738
|
-
|
|
8058
|
+
cache2.set(key, response2);
|
|
7739
8059
|
return response2;
|
|
7740
8060
|
}
|
|
7741
8061
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -7785,7 +8105,7 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7785
8105
|
cached: false,
|
|
7786
8106
|
redactedPrompt
|
|
7787
8107
|
};
|
|
7788
|
-
|
|
8108
|
+
cache2.set(key, response);
|
|
7789
8109
|
return response;
|
|
7790
8110
|
}
|
|
7791
8111
|
};
|
|
@@ -8012,23 +8332,6 @@ async function listStagedFiles(cwd) {
|
|
|
8012
8332
|
// src/core/engine.ts
|
|
8013
8333
|
init_suppression();
|
|
8014
8334
|
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
8335
|
|
|
8033
8336
|
// src/policy/index.ts
|
|
8034
8337
|
init_finding();
|
|
@@ -8815,15 +9118,23 @@ function renderConsoleReport(result) {
|
|
|
8815
9118
|
);
|
|
8816
9119
|
}
|
|
8817
9120
|
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
9121
|
+
const ordered = bySeverityDesc.flatMap((severity) => findings.filter((f) => f.severity === severity));
|
|
9122
|
+
const shown = ordered.slice(0, 10);
|
|
9123
|
+
const hidden = ordered.length - shown.length;
|
|
8818
9124
|
for (const severity of bySeverityDesc) {
|
|
8819
|
-
const group =
|
|
9125
|
+
const group = shown.filter((f) => f.severity === severity);
|
|
8820
9126
|
if (group.length === 0) continue;
|
|
8821
9127
|
lines.push("");
|
|
8822
|
-
lines.push(SEVERITY_COLOR[severity](`${severity.toUpperCase()} (${
|
|
9128
|
+
lines.push(SEVERITY_COLOR[severity](`${severity.toUpperCase()} (${findings.filter((f) => f.severity === severity).length})`));
|
|
8823
9129
|
for (const finding3 of group) {
|
|
8824
9130
|
lines.push(...renderFinding(finding3));
|
|
9131
|
+
lines.push(` docs: rules/${finding3.ruleId}.md`);
|
|
8825
9132
|
}
|
|
8826
9133
|
}
|
|
9134
|
+
if (hidden > 0) {
|
|
9135
|
+
lines.push("");
|
|
9136
|
+
lines.push(`${hidden} more \u2014 use --json`);
|
|
9137
|
+
}
|
|
8827
9138
|
lines.push("");
|
|
8828
9139
|
if (summary.total === 0) {
|
|
8829
9140
|
lines.push(pc.green("\u2714 No findings."));
|