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/mcp.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { readFile, mkdir, writeFile, stat, readdir } from 'fs/promises';
|
|
3
|
-
import { resolve, join, basename, dirname,
|
|
3
|
+
import { resolve, join, basename, dirname, posix, relative } from 'path';
|
|
4
4
|
import { execFile, spawnSync, spawn, execFileSync } from 'child_process';
|
|
5
5
|
import { createInterface } from 'readline';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
7
|
import { existsSync, statSync, readFileSync, readdirSync, mkdtempSync, cpSync, writeFileSync, rmSync } from 'fs';
|
|
8
|
-
import { homedir, tmpdir } from 'os';
|
|
8
|
+
import { homedir, availableParallelism, tmpdir } from 'os';
|
|
9
9
|
import { performance } from 'perf_hooks';
|
|
10
|
-
import
|
|
10
|
+
import { Worker } from 'worker_threads';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import ignoreModule from 'ignore';
|
|
12
13
|
|
|
13
14
|
var __defProp = Object.defineProperty;
|
|
14
15
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -1139,6 +1140,9 @@ function classesEnabled() {
|
|
|
1139
1140
|
function mcpCfgEnabled() {
|
|
1140
1141
|
return process.env.FULLSHIELD_EXPERIMENTAL_MCPCFG === "1";
|
|
1141
1142
|
}
|
|
1143
|
+
function taintV2Enabled() {
|
|
1144
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
1145
|
+
}
|
|
1142
1146
|
|
|
1143
1147
|
// src/modules/config/finding.ts
|
|
1144
1148
|
init_finding();
|
|
@@ -1945,6 +1949,103 @@ function exactPinsFromManifest(pkg) {
|
|
|
1945
1949
|
|
|
1946
1950
|
// src/modules/deps/reachability.ts
|
|
1947
1951
|
init_finding();
|
|
1952
|
+
|
|
1953
|
+
// src/modules/deps/symbol-reachability.ts
|
|
1954
|
+
function affectedFunctionNames(vuln) {
|
|
1955
|
+
const names = /* @__PURE__ */ new Set();
|
|
1956
|
+
for (const affected of vuln.affected ?? []) {
|
|
1957
|
+
const list = affected.ecosystem_specific?.affected_functions;
|
|
1958
|
+
if (!Array.isArray(list)) continue;
|
|
1959
|
+
for (const item of list) {
|
|
1960
|
+
if (typeof item === "string" && item.length > 0) names.add(item);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
return [...names];
|
|
1964
|
+
}
|
|
1965
|
+
function escapeRegExp(value) {
|
|
1966
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1967
|
+
}
|
|
1968
|
+
function called(source, local) {
|
|
1969
|
+
return new RegExp(`\\b${escapeRegExp(local)}\\s*\\(`).test(source);
|
|
1970
|
+
}
|
|
1971
|
+
function memberCalled(source, object, member) {
|
|
1972
|
+
return new RegExp(`\\b${escapeRegExp(object)}\\s*\\.\\s*${escapeRegExp(member)}\\s*\\(`).test(source);
|
|
1973
|
+
}
|
|
1974
|
+
function sourceCallsAffectedSymbol(source, symbol) {
|
|
1975
|
+
const esc = escapeRegExp(symbol);
|
|
1976
|
+
const named = new RegExp(
|
|
1977
|
+
`import\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`,
|
|
1978
|
+
"g"
|
|
1979
|
+
);
|
|
1980
|
+
let match;
|
|
1981
|
+
while ((match = named.exec(source)) !== null) {
|
|
1982
|
+
const parts = (match[1] ?? "").split(",");
|
|
1983
|
+
for (const part of parts) {
|
|
1984
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
1985
|
+
const imported = bits[0]?.trim();
|
|
1986
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
1987
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const star = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"][^'"]+['"]/g;
|
|
1991
|
+
while ((match = star.exec(source)) !== null) {
|
|
1992
|
+
const local = match[1];
|
|
1993
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
1994
|
+
}
|
|
1995
|
+
const subDefault = new RegExp(
|
|
1996
|
+
`import\\s+([A-Za-z_$][\\w$]*)\\s+from\\s+['"][^'"]+/${esc}['"]`
|
|
1997
|
+
);
|
|
1998
|
+
const sub = subDefault.exec(source);
|
|
1999
|
+
if (sub?.[1] !== void 0 && called(source, sub[1])) return true;
|
|
2000
|
+
const reqMember = new RegExp(
|
|
2001
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+['"]\\s*\\)\\s*\\.\\s*${esc}`
|
|
2002
|
+
);
|
|
2003
|
+
const reqHit = reqMember.exec(source);
|
|
2004
|
+
if (reqHit?.[1] !== void 0 && called(source, reqHit[1])) return true;
|
|
2005
|
+
const reqNs = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
2006
|
+
while ((match = reqNs.exec(source)) !== null) {
|
|
2007
|
+
const local = match[1];
|
|
2008
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
2009
|
+
}
|
|
2010
|
+
const reqNamed = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
2011
|
+
while ((match = reqNamed.exec(source)) !== null) {
|
|
2012
|
+
const parts = (match[1] ?? "").split(",");
|
|
2013
|
+
for (const part of parts) {
|
|
2014
|
+
const bits = part.trim().split(/\s*:\s*/);
|
|
2015
|
+
const imported = bits[0]?.trim();
|
|
2016
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
2017
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
const reqSub = new RegExp(
|
|
2021
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+/${esc}['"]\\s*\\)`
|
|
2022
|
+
);
|
|
2023
|
+
const subReq = reqSub.exec(source);
|
|
2024
|
+
if (subReq?.[1] !== void 0 && called(source, subReq[1])) return true;
|
|
2025
|
+
const barrel = new RegExp(`export\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`, "g");
|
|
2026
|
+
while ((match = barrel.exec(source)) !== null) {
|
|
2027
|
+
const parts = (match[1] ?? "").split(",");
|
|
2028
|
+
for (const part of parts) {
|
|
2029
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
2030
|
+
const exported = (bits[1] ?? bits[0])?.trim();
|
|
2031
|
+
if (exported === symbol) {
|
|
2032
|
+
const local = bits[0]?.trim();
|
|
2033
|
+
if (local !== void 0 && called(source, local)) return true;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
return false;
|
|
2038
|
+
}
|
|
2039
|
+
function classifySymbolUse(symbols, sources) {
|
|
2040
|
+
for (const symbol of symbols) {
|
|
2041
|
+
for (const source of sources.values()) {
|
|
2042
|
+
if (sourceCallsAffectedSymbol(source, symbol)) return "symbol-reachable";
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
return "symbol-not-called";
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// src/modules/deps/reachability.ts
|
|
1948
2049
|
function packageNameFromSnippet(snippet) {
|
|
1949
2050
|
if (snippet.startsWith("@")) {
|
|
1950
2051
|
const second = snippet.indexOf("@", 1);
|
|
@@ -1973,9 +2074,9 @@ function isProductionSource(rel) {
|
|
|
1973
2074
|
}
|
|
1974
2075
|
function indexPackageUsage(sources) {
|
|
1975
2076
|
const usage = /* @__PURE__ */ new Map();
|
|
1976
|
-
const bump = (pkg,
|
|
2077
|
+
const bump = (pkg, called2) => {
|
|
1977
2078
|
const prev = usage.get(pkg) ?? { called: false };
|
|
1978
|
-
usage.set(pkg, { imported: true, called: prev.called ||
|
|
2079
|
+
usage.set(pkg, { imported: true, called: prev.called || called2 });
|
|
1979
2080
|
};
|
|
1980
2081
|
const nsRe = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1981
2082
|
const namedRe = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
@@ -2063,10 +2164,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
2063
2164
|
return { reachability: "not-imported" };
|
|
2064
2165
|
}
|
|
2065
2166
|
function reachabilitySteps(reachability) {
|
|
2066
|
-
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown") {
|
|
2167
|
+
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
|
|
2067
2168
|
return 0;
|
|
2068
2169
|
}
|
|
2069
|
-
if (reachability === "imported-not-called") return 1;
|
|
2170
|
+
if (reachability === "imported-not-called" || reachability === "symbol-not-called") return 1;
|
|
2070
2171
|
return 2;
|
|
2071
2172
|
}
|
|
2072
2173
|
function downgradeSeverity(severity, steps) {
|
|
@@ -2083,7 +2184,7 @@ function isDevOnly(pkgName, ctx) {
|
|
|
2083
2184
|
if (inProd) return false;
|
|
2084
2185
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
2085
2186
|
}
|
|
2086
|
-
function applyReachability(findings, ctx) {
|
|
2187
|
+
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
2087
2188
|
return findings.map((finding4) => {
|
|
2088
2189
|
if (finding4.ruleId === "DEPS-MALICIOUS-001") {
|
|
2089
2190
|
if (finding4.snippet === void 0) return finding4;
|
|
@@ -2100,7 +2201,12 @@ function applyReachability(findings, ctx) {
|
|
|
2100
2201
|
if (finding4.ruleId !== "DEPS-VULN-001" || finding4.snippet === void 0) return finding4;
|
|
2101
2202
|
const pkg = packageNameFromSnippet(finding4.snippet);
|
|
2102
2203
|
if (pkg === void 0 || pkg.length === 0) return finding4;
|
|
2103
|
-
|
|
2204
|
+
let classified = classifyReachability(pkg, ctx.productionUsage, ctx.graph);
|
|
2205
|
+
const named = symbols.get(finding4.id) ?? [];
|
|
2206
|
+
if (named.length > 0 && ctx.sources !== void 0) {
|
|
2207
|
+
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
2208
|
+
classified = { reachability: symbolClass, path: classified.path };
|
|
2209
|
+
}
|
|
2104
2210
|
const steps = reachabilitySteps(classified.reachability);
|
|
2105
2211
|
let severity = steps === 0 ? finding4.severity : downgradeSeverity(finding4.severity, steps);
|
|
2106
2212
|
let message = finding4.message;
|
|
@@ -4408,14 +4514,17 @@ var depsModule = {
|
|
|
4408
4514
|
findings2.push(typosquatFinding("package.json", hit));
|
|
4409
4515
|
}
|
|
4410
4516
|
const osvHits2 = await queryOsv(pins, osvOpts);
|
|
4517
|
+
const symbols2 = /* @__PURE__ */ new Map();
|
|
4411
4518
|
for (const hit of osvHits2) {
|
|
4412
4519
|
for (const vuln of hit.vulns) {
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
);
|
|
4520
|
+
const finding4 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit.pkg, vuln) : vulnFinding("package.json", hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
4521
|
+
const names = affectedFunctionNames(vuln);
|
|
4522
|
+
if (names.length > 0 && finding4.ruleId === "DEPS-VULN-001") symbols2.set(finding4.id, names);
|
|
4523
|
+
findings2.push(finding4);
|
|
4416
4524
|
}
|
|
4417
4525
|
}
|
|
4418
|
-
|
|
4526
|
+
const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
|
|
4527
|
+
return applyReachability(findings2, noLockCtx, symbols2);
|
|
4419
4528
|
}
|
|
4420
4529
|
let contents;
|
|
4421
4530
|
try {
|
|
@@ -4436,14 +4545,16 @@ var depsModule = {
|
|
|
4436
4545
|
}
|
|
4437
4546
|
}
|
|
4438
4547
|
const osvHits = await queryOsv(packages, osvOpts);
|
|
4548
|
+
const symbols = /* @__PURE__ */ new Map();
|
|
4439
4549
|
for (const hit of osvHits) {
|
|
4440
4550
|
for (const vuln of hit.vulns) {
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
);
|
|
4551
|
+
const finding4 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit.pkg, vuln) : vulnFinding(lockFile, hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
4552
|
+
const names = affectedFunctionNames(vuln);
|
|
4553
|
+
if (names.length > 0 && finding4.ruleId === "DEPS-VULN-001") symbols.set(finding4.id, names);
|
|
4554
|
+
findings.push(finding4);
|
|
4444
4555
|
}
|
|
4445
4556
|
}
|
|
4446
|
-
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph));
|
|
4557
|
+
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph), symbols);
|
|
4447
4558
|
if (experimentalVex()) {
|
|
4448
4559
|
reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
|
|
4449
4560
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
@@ -4477,7 +4588,7 @@ async function reachabilityCtx(cwd, files, graph) {
|
|
|
4477
4588
|
} catch {
|
|
4478
4589
|
}
|
|
4479
4590
|
}
|
|
4480
|
-
return { productionUsage: indexPackageUsage(prodSources), graph };
|
|
4591
|
+
return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
|
|
4481
4592
|
}
|
|
4482
4593
|
|
|
4483
4594
|
// src/modules/mcp/index.ts
|
|
@@ -5015,6 +5126,26 @@ function isXssSanitizerCall(node) {
|
|
|
5015
5126
|
return false;
|
|
5016
5127
|
}
|
|
5017
5128
|
var NO_RETURNERS = /* @__PURE__ */ new Set();
|
|
5129
|
+
var CALLBACK_METHODS = /* @__PURE__ */ new Set([
|
|
5130
|
+
"then",
|
|
5131
|
+
"catch",
|
|
5132
|
+
"finally",
|
|
5133
|
+
"map",
|
|
5134
|
+
"forEach",
|
|
5135
|
+
"filter",
|
|
5136
|
+
"flatMap",
|
|
5137
|
+
"find",
|
|
5138
|
+
"reduce"
|
|
5139
|
+
]);
|
|
5140
|
+
var taintedThisFields = /* @__PURE__ */ new WeakMap();
|
|
5141
|
+
function enclosingClass(node) {
|
|
5142
|
+
let current = node.parent;
|
|
5143
|
+
while (current !== void 0) {
|
|
5144
|
+
if (current.type === "ClassDeclaration" || current.type === "ClassExpression") return current;
|
|
5145
|
+
current = current.parent;
|
|
5146
|
+
}
|
|
5147
|
+
return void 0;
|
|
5148
|
+
}
|
|
5018
5149
|
function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
5019
5150
|
if (node === void 0) return false;
|
|
5020
5151
|
if (isTaintSource(node) || isFrameworkMemberTaint(node) || isFrameworkTaintCall(node) || isMcpArgsTaintSource(node)) {
|
|
@@ -5026,8 +5157,34 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
|
5026
5157
|
case "Identifier":
|
|
5027
5158
|
return tainted.has(node.name);
|
|
5028
5159
|
case "MemberExpression":
|
|
5160
|
+
if (taintV2Enabled() && node.object.type === "ThisExpression") {
|
|
5161
|
+
const cls = enclosingClass(node);
|
|
5162
|
+
const name = propName(node);
|
|
5163
|
+
if (cls !== void 0 && name !== void 0 && taintedThisFields.get(cls)?.has(name) === true) {
|
|
5164
|
+
return true;
|
|
5165
|
+
}
|
|
5166
|
+
}
|
|
5029
5167
|
return exprIsTainted(node.object, tainted, returners);
|
|
5168
|
+
case "ArrayExpression":
|
|
5169
|
+
if (!taintV2Enabled()) return false;
|
|
5170
|
+
return node.elements.some((el) => {
|
|
5171
|
+
if (el === null) return false;
|
|
5172
|
+
if (el.type === "SpreadElement") return exprIsTainted(el.argument, tainted, returners);
|
|
5173
|
+
return exprIsTainted(el, tainted, returners);
|
|
5174
|
+
});
|
|
5175
|
+
case "ObjectExpression":
|
|
5176
|
+
if (!taintV2Enabled()) return false;
|
|
5177
|
+
return node.properties.some((prop) => {
|
|
5178
|
+
if (prop.type === "SpreadElement") return exprIsTainted(prop.argument, tainted, returners);
|
|
5179
|
+
if (prop.type !== "Property") return false;
|
|
5180
|
+
return exprIsTainted(prop.value, tainted, returners);
|
|
5181
|
+
});
|
|
5030
5182
|
case "CallExpression":
|
|
5183
|
+
if (taintV2Enabled() && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
|
|
5184
|
+
return node.arguments.some(
|
|
5185
|
+
(arg) => arg.type !== "SpreadElement" && exprIsTainted(arg, tainted, returners)
|
|
5186
|
+
);
|
|
5187
|
+
}
|
|
5031
5188
|
if (isSchemaValidatorCall(node)) return false;
|
|
5032
5189
|
if (node.callee.type === "Identifier" && returners.has(node.callee.name)) return true;
|
|
5033
5190
|
if (node.callee.type === "MemberExpression" && exprIsTainted(node.callee.object, tainted, returners)) {
|
|
@@ -5258,6 +5415,65 @@ function scopeReturnsTaint(scope, tainted, returners) {
|
|
|
5258
5415
|
return yes;
|
|
5259
5416
|
}
|
|
5260
5417
|
var MAX_TAINT_HOPS = 5;
|
|
5418
|
+
function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
5419
|
+
let changed = false;
|
|
5420
|
+
const mark = (fn, name) => {
|
|
5421
|
+
if (name === void 0) return;
|
|
5422
|
+
const set = seeds.get(fn) ?? /* @__PURE__ */ new Set();
|
|
5423
|
+
if (set.has(name)) return;
|
|
5424
|
+
set.add(name);
|
|
5425
|
+
seeds.set(fn, set);
|
|
5426
|
+
changed = true;
|
|
5427
|
+
};
|
|
5428
|
+
for (const fn of functions) {
|
|
5429
|
+
const f = facts.get(fn);
|
|
5430
|
+
if (f === void 0) continue;
|
|
5431
|
+
let parent = fn.parent;
|
|
5432
|
+
while (parent !== void 0 && !isFunctionNode(parent)) {
|
|
5433
|
+
parent = parent.parent;
|
|
5434
|
+
}
|
|
5435
|
+
if (parent !== void 0 && isFunctionNode(parent)) {
|
|
5436
|
+
const outer = facts.get(parent);
|
|
5437
|
+
if (outer !== void 0) {
|
|
5438
|
+
for (const name of outer.tainted) mark(fn, name);
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
walkScope(fn, (node) => {
|
|
5442
|
+
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") {
|
|
5443
|
+
if (node.left.object.type === "ThisExpression" && exprIsTainted(node.right, f.tainted, returners)) {
|
|
5444
|
+
const cls = enclosingClass(node);
|
|
5445
|
+
const field = propName(node.left);
|
|
5446
|
+
if (cls !== void 0 && field !== void 0) {
|
|
5447
|
+
const fields = taintedThisFields.get(cls) ?? /* @__PURE__ */ new Set();
|
|
5448
|
+
if (!fields.has(field)) {
|
|
5449
|
+
fields.add(field);
|
|
5450
|
+
taintedThisFields.set(cls, fields);
|
|
5451
|
+
changed = true;
|
|
5452
|
+
}
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
5457
|
+
const method = propName(node.callee);
|
|
5458
|
+
if (method === void 0) return;
|
|
5459
|
+
if (CALLBACK_METHODS.has(method) && exprIsTainted(node.callee.object, f.tainted, returners)) {
|
|
5460
|
+
const callback = node.arguments[0];
|
|
5461
|
+
if (callback !== void 0 && isFunctionNode(callback)) {
|
|
5462
|
+
mark(callback, boundNames(callback.params[0])[0]);
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5465
|
+
if ((method === "call" || method === "apply") && node.callee.object.type === "Identifier") {
|
|
5466
|
+
const target = nameToFn.get(node.callee.object.name);
|
|
5467
|
+
if (target === void 0 || !isFunctionNode(target)) return;
|
|
5468
|
+
const arg = node.arguments[1];
|
|
5469
|
+
if (arg !== void 0 && arg.type !== "SpreadElement" && exprIsTainted(arg, f.tainted, returners)) {
|
|
5470
|
+
mark(target, boundNames(target.params[0])[0]);
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
});
|
|
5474
|
+
}
|
|
5475
|
+
return changed;
|
|
5476
|
+
}
|
|
5261
5477
|
function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
5262
5478
|
attachParents(ast);
|
|
5263
5479
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
@@ -5312,6 +5528,9 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
5312
5528
|
});
|
|
5313
5529
|
});
|
|
5314
5530
|
}
|
|
5531
|
+
if (taintV2Enabled() && seedTaintV2(functions, facts, seeds, returners, nameToFn)) {
|
|
5532
|
+
changed = true;
|
|
5533
|
+
}
|
|
5315
5534
|
if (!changed) break;
|
|
5316
5535
|
}
|
|
5317
5536
|
return facts;
|
|
@@ -5454,6 +5673,21 @@ function requireCall(node) {
|
|
|
5454
5673
|
}
|
|
5455
5674
|
return void 0;
|
|
5456
5675
|
}
|
|
5676
|
+
function reexportsOf(ast) {
|
|
5677
|
+
const out = /* @__PURE__ */ new Map();
|
|
5678
|
+
walk(ast, (node) => {
|
|
5679
|
+
if (node.type !== "ExportNamedDeclaration" || node.source === null || node.source === void 0) return;
|
|
5680
|
+
const spec = literalString(node.source);
|
|
5681
|
+
if (spec === void 0) return;
|
|
5682
|
+
for (const specNode of node.specifiers) {
|
|
5683
|
+
if (specNode.type !== "ExportSpecifier") continue;
|
|
5684
|
+
const exported = specNode.exported.type === "Identifier" ? specNode.exported.name : void 0;
|
|
5685
|
+
const local = specNode.local.type === "Identifier" ? specNode.local.name : exported;
|
|
5686
|
+
if (exported !== void 0 && local !== void 0) out.set(exported, { spec, name: local });
|
|
5687
|
+
}
|
|
5688
|
+
});
|
|
5689
|
+
return out;
|
|
5690
|
+
}
|
|
5457
5691
|
function resolveRelative(fromFile, spec, files) {
|
|
5458
5692
|
if (!spec.startsWith(".")) return void 0;
|
|
5459
5693
|
const base = posix.normalize(posix.join(posix.dirname(fromFile), spec));
|
|
@@ -5531,6 +5765,22 @@ function runInterproc(parsed) {
|
|
|
5531
5765
|
functions.set(`${fn.file}#${fn.name}`, fn);
|
|
5532
5766
|
}
|
|
5533
5767
|
}
|
|
5768
|
+
if (taintV2Enabled()) {
|
|
5769
|
+
const barrels = /* @__PURE__ */ new Map();
|
|
5770
|
+
for (const p of parsed) barrels.set(p.file, reexportsOf(p.ast));
|
|
5771
|
+
for (const ns of requires.values()) {
|
|
5772
|
+
for (const [local, target] of ns) {
|
|
5773
|
+
const hash = target.indexOf("#");
|
|
5774
|
+
if (hash < 0) continue;
|
|
5775
|
+
const file = target.slice(0, hash);
|
|
5776
|
+
const name = target.slice(hash + 1);
|
|
5777
|
+
const re = barrels.get(file)?.get(name);
|
|
5778
|
+
if (re === void 0) continue;
|
|
5779
|
+
const resolved = resolveRelative(file, re.spec, files);
|
|
5780
|
+
if (resolved !== void 0) ns.set(local, `${resolved}#${re.name}`);
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5534
5784
|
const queue = [];
|
|
5535
5785
|
for (const p of parsed) {
|
|
5536
5786
|
const facts = analyzeFile(p.ast);
|
|
@@ -6046,7 +6296,10 @@ function isMongoReceiver(node, source) {
|
|
|
6046
6296
|
const obj = node.callee.object;
|
|
6047
6297
|
if (obj.type === "Identifier") {
|
|
6048
6298
|
if (isCollectionName(obj.name)) return true;
|
|
6049
|
-
if (isPascalModel(obj.name))
|
|
6299
|
+
if (isPascalModel(obj.name)) {
|
|
6300
|
+
if (/extends\s+BaseEntity\b/.test(source)) return false;
|
|
6301
|
+
return !fileHasSqlOrm;
|
|
6302
|
+
}
|
|
6050
6303
|
return fileHasMongo;
|
|
6051
6304
|
}
|
|
6052
6305
|
if (obj.type === "MemberExpression") {
|
|
@@ -7107,6 +7360,72 @@ function scanPromptInjection(file, source) {
|
|
|
7107
7360
|
});
|
|
7108
7361
|
return findings;
|
|
7109
7362
|
}
|
|
7363
|
+
function packageVersion() {
|
|
7364
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7365
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
7366
|
+
try {
|
|
7367
|
+
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
7368
|
+
const pkg = JSON.parse(raw);
|
|
7369
|
+
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
7370
|
+
return pkg.version;
|
|
7371
|
+
}
|
|
7372
|
+
} catch {
|
|
7373
|
+
}
|
|
7374
|
+
const parent = dirname(dir);
|
|
7375
|
+
if (parent === dir) break;
|
|
7376
|
+
dir = parent;
|
|
7377
|
+
}
|
|
7378
|
+
throw new Error("fullshield package.json not found");
|
|
7379
|
+
}
|
|
7380
|
+
|
|
7381
|
+
// src/core/parse-pool.ts
|
|
7382
|
+
var workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7383
|
+
var cache = /* @__PURE__ */ new Map();
|
|
7384
|
+
function parsePoolEnabled() {
|
|
7385
|
+
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
7386
|
+
}
|
|
7387
|
+
function parsePoolSize() {
|
|
7388
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
7389
|
+
if (raw !== void 0 && raw !== "") {
|
|
7390
|
+
const parsed = Number(raw);
|
|
7391
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7392
|
+
}
|
|
7393
|
+
return Math.max(1, availableParallelism() - 1);
|
|
7394
|
+
}
|
|
7395
|
+
function parseCacheKey(code) {
|
|
7396
|
+
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
7397
|
+
}
|
|
7398
|
+
function parseOne(code) {
|
|
7399
|
+
const key = parseCacheKey(code);
|
|
7400
|
+
const hit = cache.get(key);
|
|
7401
|
+
if (hit !== void 0) return Promise.resolve(hit);
|
|
7402
|
+
return new Promise((resolve5, reject) => {
|
|
7403
|
+
const worker = new Worker(fileURLToPath(workerUrl), { workerData: { code } });
|
|
7404
|
+
worker.once("message", (ast) => {
|
|
7405
|
+
cache.set(key, ast);
|
|
7406
|
+
resolve5(ast);
|
|
7407
|
+
});
|
|
7408
|
+
worker.once("error", reject);
|
|
7409
|
+
});
|
|
7410
|
+
}
|
|
7411
|
+
async function parseWithPool(codes) {
|
|
7412
|
+
const size = parsePoolSize();
|
|
7413
|
+
const out = new Array(codes.length);
|
|
7414
|
+
let next = 0;
|
|
7415
|
+
async function run() {
|
|
7416
|
+
for (; ; ) {
|
|
7417
|
+
const index = next;
|
|
7418
|
+
next += 1;
|
|
7419
|
+
if (index >= codes.length) return;
|
|
7420
|
+
const code = codes[index];
|
|
7421
|
+
if (code === void 0) return;
|
|
7422
|
+
out[index] = await parseOne(code);
|
|
7423
|
+
}
|
|
7424
|
+
}
|
|
7425
|
+
const workers = Math.min(size, Math.max(1, codes.length));
|
|
7426
|
+
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
7427
|
+
return out;
|
|
7428
|
+
}
|
|
7110
7429
|
|
|
7111
7430
|
// src/modules/sast/engine.ts
|
|
7112
7431
|
var sastRules = [
|
|
@@ -7240,7 +7559,8 @@ var sastModule = {
|
|
|
7240
7559
|
continue;
|
|
7241
7560
|
}
|
|
7242
7561
|
try {
|
|
7243
|
-
const ast = await parseSource(source);
|
|
7562
|
+
const ast = parsePoolEnabled() ? (await parseWithPool([source]))[0] : await parseSource(source);
|
|
7563
|
+
if (ast === void 0) continue;
|
|
7244
7564
|
parsed.push({ file: rel, source, ast });
|
|
7245
7565
|
} catch {
|
|
7246
7566
|
}
|
|
@@ -7618,7 +7938,7 @@ function createAiClient(options) {
|
|
|
7618
7938
|
const useFixtures = options.useFixtures ?? !isLiveAi();
|
|
7619
7939
|
const model = options.model ?? process.env.FULLSHIELD_AI_MODEL ?? (useFixtures ? "recorded-fixture" : "qwen2.5-coder:7b");
|
|
7620
7940
|
const provider = options.provider ?? process.env.FULLSHIELD_AI_PROVIDER ?? (useFixtures ? "fixture" : "ollama");
|
|
7621
|
-
const
|
|
7941
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
7622
7942
|
let spentUsd = 0;
|
|
7623
7943
|
let networkCalls = 0;
|
|
7624
7944
|
const liveCost = provider === "ollama" ? 0 : FIXTURE_COST;
|
|
@@ -7638,7 +7958,7 @@ function createAiClient(options) {
|
|
|
7638
7958
|
const redactedPrompt = `${modeInstruction(req.mode)}
|
|
7639
7959
|
${wrapUntrustedCode(req.prompt)}`;
|
|
7640
7960
|
const key = cacheKey(req.model ?? model, redactedPrompt);
|
|
7641
|
-
const cached =
|
|
7961
|
+
const cached = cache2.get(key);
|
|
7642
7962
|
if (cached !== void 0) {
|
|
7643
7963
|
return { ...cached, cached: true, costUsd: 0 };
|
|
7644
7964
|
}
|
|
@@ -7652,7 +7972,7 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7652
7972
|
cached: false,
|
|
7653
7973
|
redactedPrompt
|
|
7654
7974
|
};
|
|
7655
|
-
|
|
7975
|
+
cache2.set(key, response2);
|
|
7656
7976
|
return response2;
|
|
7657
7977
|
}
|
|
7658
7978
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -7702,7 +8022,7 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7702
8022
|
cached: false,
|
|
7703
8023
|
redactedPrompt
|
|
7704
8024
|
};
|
|
7705
|
-
|
|
8025
|
+
cache2.set(key, response);
|
|
7706
8026
|
return response;
|
|
7707
8027
|
}
|
|
7708
8028
|
};
|
|
@@ -7929,23 +8249,6 @@ async function listStagedFiles(cwd) {
|
|
|
7929
8249
|
// src/core/engine.ts
|
|
7930
8250
|
init_suppression();
|
|
7931
8251
|
init_finding();
|
|
7932
|
-
function packageVersion() {
|
|
7933
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
7934
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
7935
|
-
try {
|
|
7936
|
-
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
7937
|
-
const pkg = JSON.parse(raw);
|
|
7938
|
-
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
7939
|
-
return pkg.version;
|
|
7940
|
-
}
|
|
7941
|
-
} catch {
|
|
7942
|
-
}
|
|
7943
|
-
const parent = dirname(dir);
|
|
7944
|
-
if (parent === dir) break;
|
|
7945
|
-
dir = parent;
|
|
7946
|
-
}
|
|
7947
|
-
throw new Error("fullshield package.json not found");
|
|
7948
|
-
}
|
|
7949
8252
|
|
|
7950
8253
|
// src/policy/index.ts
|
|
7951
8254
|
init_finding();
|