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/cli/index.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
import { writeFile, mkdir, readFile, stat, mkdtemp, unlink, readdir } from 'fs/promises';
|
|
4
4
|
import { resolve, join, dirname, relative, basename, posix } from 'path';
|
|
5
|
-
import { readFileSync,
|
|
6
|
-
import { tmpdir,
|
|
5
|
+
import { readFileSync, existsSync, readdirSync, cpSync, mkdirSync, rmSync, statSync, writeFileSync, mkdtempSync, unlinkSync } from 'fs';
|
|
6
|
+
import { homedir, tmpdir, availableParallelism } from 'os';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { Worker } from 'worker_threads';
|
|
7
9
|
import ignoreModule from 'ignore';
|
|
8
10
|
import { execFile, spawnSync, execFileSync, spawn } from 'child_process';
|
|
9
11
|
import { promisify } from 'util';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
12
|
import { createInterface } from 'readline';
|
|
12
13
|
import { performance } from 'perf_hooks';
|
|
13
14
|
import { createRequire } from 'module';
|
|
@@ -188,6 +189,9 @@ function classesEnabled() {
|
|
|
188
189
|
function mcpCfgEnabled() {
|
|
189
190
|
return process.env.FULLSHIELD_EXPERIMENTAL_MCPCFG === "1";
|
|
190
191
|
}
|
|
192
|
+
function taintV2Enabled() {
|
|
193
|
+
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
194
|
+
}
|
|
191
195
|
var init_experimental = __esm({
|
|
192
196
|
"src/modules/sast/experimental.ts"() {
|
|
193
197
|
}
|
|
@@ -1077,6 +1081,105 @@ var init_pins = __esm({
|
|
|
1077
1081
|
}
|
|
1078
1082
|
});
|
|
1079
1083
|
|
|
1084
|
+
// src/modules/deps/symbol-reachability.ts
|
|
1085
|
+
function affectedFunctionNames(vuln) {
|
|
1086
|
+
const names = /* @__PURE__ */ new Set();
|
|
1087
|
+
for (const affected of vuln.affected ?? []) {
|
|
1088
|
+
const list = affected.ecosystem_specific?.affected_functions;
|
|
1089
|
+
if (!Array.isArray(list)) continue;
|
|
1090
|
+
for (const item of list) {
|
|
1091
|
+
if (typeof item === "string" && item.length > 0) names.add(item);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return [...names];
|
|
1095
|
+
}
|
|
1096
|
+
function escapeRegExp(value) {
|
|
1097
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1098
|
+
}
|
|
1099
|
+
function called(source, local) {
|
|
1100
|
+
return new RegExp(`\\b${escapeRegExp(local)}\\s*\\(`).test(source);
|
|
1101
|
+
}
|
|
1102
|
+
function memberCalled(source, object, member) {
|
|
1103
|
+
return new RegExp(`\\b${escapeRegExp(object)}\\s*\\.\\s*${escapeRegExp(member)}\\s*\\(`).test(source);
|
|
1104
|
+
}
|
|
1105
|
+
function sourceCallsAffectedSymbol(source, symbol) {
|
|
1106
|
+
const esc2 = escapeRegExp(symbol);
|
|
1107
|
+
const named = new RegExp(
|
|
1108
|
+
`import\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`,
|
|
1109
|
+
"g"
|
|
1110
|
+
);
|
|
1111
|
+
let match;
|
|
1112
|
+
while ((match = named.exec(source)) !== null) {
|
|
1113
|
+
const parts = (match[1] ?? "").split(",");
|
|
1114
|
+
for (const part of parts) {
|
|
1115
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
1116
|
+
const imported = bits[0]?.trim();
|
|
1117
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
1118
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const star = /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['"][^'"]+['"]/g;
|
|
1122
|
+
while ((match = star.exec(source)) !== null) {
|
|
1123
|
+
const local = match[1];
|
|
1124
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
1125
|
+
}
|
|
1126
|
+
const subDefault = new RegExp(
|
|
1127
|
+
`import\\s+([A-Za-z_$][\\w$]*)\\s+from\\s+['"][^'"]+/${esc2}['"]`
|
|
1128
|
+
);
|
|
1129
|
+
const sub = subDefault.exec(source);
|
|
1130
|
+
if (sub?.[1] !== void 0 && called(source, sub[1])) return true;
|
|
1131
|
+
const reqMember = new RegExp(
|
|
1132
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+['"]\\s*\\)\\s*\\.\\s*${esc2}`
|
|
1133
|
+
);
|
|
1134
|
+
const reqHit = reqMember.exec(source);
|
|
1135
|
+
if (reqHit?.[1] !== void 0 && called(source, reqHit[1])) return true;
|
|
1136
|
+
const reqNs = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
1137
|
+
while ((match = reqNs.exec(source)) !== null) {
|
|
1138
|
+
const local = match[1];
|
|
1139
|
+
if (local !== void 0 && memberCalled(source, local, symbol)) return true;
|
|
1140
|
+
}
|
|
1141
|
+
const reqNamed = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"][^'"]+['"]\s*\)/g;
|
|
1142
|
+
while ((match = reqNamed.exec(source)) !== null) {
|
|
1143
|
+
const parts = (match[1] ?? "").split(",");
|
|
1144
|
+
for (const part of parts) {
|
|
1145
|
+
const bits = part.trim().split(/\s*:\s*/);
|
|
1146
|
+
const imported = bits[0]?.trim();
|
|
1147
|
+
const local = (bits[1] ?? bits[0])?.trim();
|
|
1148
|
+
if (imported === symbol && local !== void 0 && called(source, local)) return true;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const reqSub = new RegExp(
|
|
1152
|
+
`(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*require\\(\\s*['"][^'"]+/${esc2}['"]\\s*\\)`
|
|
1153
|
+
);
|
|
1154
|
+
const subReq = reqSub.exec(source);
|
|
1155
|
+
if (subReq?.[1] !== void 0 && called(source, subReq[1])) return true;
|
|
1156
|
+
const barrel = new RegExp(`export\\s*\\{([^}]+)\\}\\s*from\\s*['"][^'"]+['"]`, "g");
|
|
1157
|
+
while ((match = barrel.exec(source)) !== null) {
|
|
1158
|
+
const parts = (match[1] ?? "").split(",");
|
|
1159
|
+
for (const part of parts) {
|
|
1160
|
+
const bits = part.trim().split(/\s+as\s+/);
|
|
1161
|
+
const exported = (bits[1] ?? bits[0])?.trim();
|
|
1162
|
+
if (exported === symbol) {
|
|
1163
|
+
const local = bits[0]?.trim();
|
|
1164
|
+
if (local !== void 0 && called(source, local)) return true;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
function classifySymbolUse(symbols, sources) {
|
|
1171
|
+
for (const symbol of symbols) {
|
|
1172
|
+
for (const source of sources.values()) {
|
|
1173
|
+
if (sourceCallsAffectedSymbol(source, symbol)) return "symbol-reachable";
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return "symbol-not-called";
|
|
1177
|
+
}
|
|
1178
|
+
var init_symbol_reachability = __esm({
|
|
1179
|
+
"src/modules/deps/symbol-reachability.ts"() {
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1080
1183
|
// src/modules/deps/reachability.ts
|
|
1081
1184
|
function packageNameFromSnippet(snippet) {
|
|
1082
1185
|
if (snippet.startsWith("@")) {
|
|
@@ -1106,9 +1209,9 @@ function isProductionSource(rel) {
|
|
|
1106
1209
|
}
|
|
1107
1210
|
function indexPackageUsage(sources) {
|
|
1108
1211
|
const usage = /* @__PURE__ */ new Map();
|
|
1109
|
-
const bump = (pkg,
|
|
1212
|
+
const bump = (pkg, called2) => {
|
|
1110
1213
|
const prev = usage.get(pkg) ?? { called: false };
|
|
1111
|
-
usage.set(pkg, { imported: true, called: prev.called ||
|
|
1214
|
+
usage.set(pkg, { imported: true, called: prev.called || called2 });
|
|
1112
1215
|
};
|
|
1113
1216
|
const nsRe = /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1114
1217
|
const namedRe = /(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
@@ -1196,10 +1299,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
1196
1299
|
return { reachability: "not-imported" };
|
|
1197
1300
|
}
|
|
1198
1301
|
function reachabilitySteps(reachability) {
|
|
1199
|
-
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown") {
|
|
1302
|
+
if (reachability === "reachable" || reachability === "reachable-via-dependency" || reachability === "reachability-unknown" || reachability === "symbol-reachable") {
|
|
1200
1303
|
return 0;
|
|
1201
1304
|
}
|
|
1202
|
-
if (reachability === "imported-not-called") return 1;
|
|
1305
|
+
if (reachability === "imported-not-called" || reachability === "symbol-not-called") return 1;
|
|
1203
1306
|
return 2;
|
|
1204
1307
|
}
|
|
1205
1308
|
function downgradeSeverity(severity, steps) {
|
|
@@ -1216,7 +1319,7 @@ function isDevOnly(pkgName, ctx) {
|
|
|
1216
1319
|
if (inProd) return false;
|
|
1217
1320
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
1218
1321
|
}
|
|
1219
|
-
function applyReachability(findings, ctx) {
|
|
1322
|
+
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
1220
1323
|
return findings.map((finding6) => {
|
|
1221
1324
|
if (finding6.ruleId === "DEPS-MALICIOUS-001") {
|
|
1222
1325
|
if (finding6.snippet === void 0) return finding6;
|
|
@@ -1233,7 +1336,12 @@ function applyReachability(findings, ctx) {
|
|
|
1233
1336
|
if (finding6.ruleId !== "DEPS-VULN-001" || finding6.snippet === void 0) return finding6;
|
|
1234
1337
|
const pkg = packageNameFromSnippet(finding6.snippet);
|
|
1235
1338
|
if (pkg === void 0 || pkg.length === 0) return finding6;
|
|
1236
|
-
|
|
1339
|
+
let classified = classifyReachability(pkg, ctx.productionUsage, ctx.graph);
|
|
1340
|
+
const named = symbols.get(finding6.id) ?? [];
|
|
1341
|
+
if (named.length > 0 && ctx.sources !== void 0) {
|
|
1342
|
+
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
1343
|
+
classified = { reachability: symbolClass, path: classified.path };
|
|
1344
|
+
}
|
|
1237
1345
|
const steps = reachabilitySteps(classified.reachability);
|
|
1238
1346
|
let severity = steps === 0 ? finding6.severity : downgradeSeverity(finding6.severity, steps);
|
|
1239
1347
|
let message = finding6.message;
|
|
@@ -1258,6 +1366,7 @@ function applyReachability(findings, ctx) {
|
|
|
1258
1366
|
var init_reachability = __esm({
|
|
1259
1367
|
"src/modules/deps/reachability.ts"() {
|
|
1260
1368
|
init_finding();
|
|
1369
|
+
init_symbol_reachability();
|
|
1261
1370
|
}
|
|
1262
1371
|
});
|
|
1263
1372
|
|
|
@@ -3822,7 +3931,7 @@ async function reachabilityCtx(cwd, files, graph) {
|
|
|
3822
3931
|
} catch {
|
|
3823
3932
|
}
|
|
3824
3933
|
}
|
|
3825
|
-
return { productionUsage: indexPackageUsage(prodSources), graph };
|
|
3934
|
+
return { productionUsage: indexPackageUsage(prodSources), graph, sources: prodSources };
|
|
3826
3935
|
}
|
|
3827
3936
|
var LOCKFILE_NAMES, MANIFEST_FIELDS2, MALICIOUS_REMEDIATION, depsModule;
|
|
3828
3937
|
var init_deps = __esm({
|
|
@@ -3832,6 +3941,7 @@ var init_deps = __esm({
|
|
|
3832
3941
|
init_osv();
|
|
3833
3942
|
init_pins();
|
|
3834
3943
|
init_reachability();
|
|
3944
|
+
init_symbol_reachability();
|
|
3835
3945
|
init_semver();
|
|
3836
3946
|
init_typosquat();
|
|
3837
3947
|
init_experimental2();
|
|
@@ -3871,14 +3981,17 @@ var init_deps = __esm({
|
|
|
3871
3981
|
findings2.push(typosquatFinding("package.json", hit));
|
|
3872
3982
|
}
|
|
3873
3983
|
const osvHits2 = await queryOsv(pins, osvOpts);
|
|
3984
|
+
const symbols2 = /* @__PURE__ */ new Map();
|
|
3874
3985
|
for (const hit of osvHits2) {
|
|
3875
3986
|
for (const vuln of hit.vulns) {
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
);
|
|
3987
|
+
const finding6 = isMaliciousAdvisory(vuln) ? maliciousFinding("package.json", hit.pkg, vuln) : vulnFinding("package.json", hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
3988
|
+
const names = affectedFunctionNames(vuln);
|
|
3989
|
+
if (names.length > 0 && finding6.ruleId === "DEPS-VULN-001") symbols2.set(finding6.id, names);
|
|
3990
|
+
findings2.push(finding6);
|
|
3879
3991
|
}
|
|
3880
3992
|
}
|
|
3881
|
-
|
|
3993
|
+
const noLockCtx = await reachabilityCtx(ctx.cwd, ctx.files, graphFromManifest(manifest));
|
|
3994
|
+
return applyReachability(findings2, noLockCtx, symbols2);
|
|
3882
3995
|
}
|
|
3883
3996
|
let contents;
|
|
3884
3997
|
try {
|
|
@@ -3899,14 +4012,16 @@ var init_deps = __esm({
|
|
|
3899
4012
|
}
|
|
3900
4013
|
}
|
|
3901
4014
|
const osvHits = await queryOsv(packages, osvOpts);
|
|
4015
|
+
const symbols = /* @__PURE__ */ new Map();
|
|
3902
4016
|
for (const hit of osvHits) {
|
|
3903
4017
|
for (const vuln of hit.vulns) {
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
);
|
|
4018
|
+
const finding6 = isMaliciousAdvisory(vuln) ? maliciousFinding(lockFile, hit.pkg, vuln) : vulnFinding(lockFile, hit.pkg, vuln, depLoc(hit.pkg.name));
|
|
4019
|
+
const names = affectedFunctionNames(vuln);
|
|
4020
|
+
if (names.length > 0 && finding6.ruleId === "DEPS-VULN-001") symbols.set(finding6.id, names);
|
|
4021
|
+
findings.push(finding6);
|
|
3907
4022
|
}
|
|
3908
4023
|
}
|
|
3909
|
-
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph));
|
|
4024
|
+
let reached = applyReachability(findings, await reachabilityCtx(ctx.cwd, ctx.files, graph), symbols);
|
|
3910
4025
|
if (experimentalVex()) {
|
|
3911
4026
|
reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
|
|
3912
4027
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
@@ -4489,6 +4604,14 @@ function isXssSanitizerCall(node) {
|
|
|
4489
4604
|
}
|
|
4490
4605
|
return false;
|
|
4491
4606
|
}
|
|
4607
|
+
function enclosingClass(node) {
|
|
4608
|
+
let current = node.parent;
|
|
4609
|
+
while (current !== void 0) {
|
|
4610
|
+
if (current.type === "ClassDeclaration" || current.type === "ClassExpression") return current;
|
|
4611
|
+
current = current.parent;
|
|
4612
|
+
}
|
|
4613
|
+
return void 0;
|
|
4614
|
+
}
|
|
4492
4615
|
function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
4493
4616
|
if (node === void 0) return false;
|
|
4494
4617
|
if (isTaintSource(node) || isFrameworkMemberTaint(node) || isFrameworkTaintCall(node) || isMcpArgsTaintSource(node)) {
|
|
@@ -4500,8 +4623,34 @@ function exprIsTainted(node, tainted, returners = NO_RETURNERS) {
|
|
|
4500
4623
|
case "Identifier":
|
|
4501
4624
|
return tainted.has(node.name);
|
|
4502
4625
|
case "MemberExpression":
|
|
4626
|
+
if (taintV2Enabled() && node.object.type === "ThisExpression") {
|
|
4627
|
+
const cls = enclosingClass(node);
|
|
4628
|
+
const name = propName(node);
|
|
4629
|
+
if (cls !== void 0 && name !== void 0 && taintedThisFields.get(cls)?.has(name) === true) {
|
|
4630
|
+
return true;
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4503
4633
|
return exprIsTainted(node.object, tainted, returners);
|
|
4634
|
+
case "ArrayExpression":
|
|
4635
|
+
if (!taintV2Enabled()) return false;
|
|
4636
|
+
return node.elements.some((el) => {
|
|
4637
|
+
if (el === null) return false;
|
|
4638
|
+
if (el.type === "SpreadElement") return exprIsTainted(el.argument, tainted, returners);
|
|
4639
|
+
return exprIsTainted(el, tainted, returners);
|
|
4640
|
+
});
|
|
4641
|
+
case "ObjectExpression":
|
|
4642
|
+
if (!taintV2Enabled()) return false;
|
|
4643
|
+
return node.properties.some((prop) => {
|
|
4644
|
+
if (prop.type === "SpreadElement") return exprIsTainted(prop.argument, tainted, returners);
|
|
4645
|
+
if (prop.type !== "Property") return false;
|
|
4646
|
+
return exprIsTainted(prop.value, tainted, returners);
|
|
4647
|
+
});
|
|
4504
4648
|
case "CallExpression":
|
|
4649
|
+
if (taintV2Enabled() && node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "Object" && propName(node.callee) === "assign") {
|
|
4650
|
+
return node.arguments.some(
|
|
4651
|
+
(arg) => arg.type !== "SpreadElement" && exprIsTainted(arg, tainted, returners)
|
|
4652
|
+
);
|
|
4653
|
+
}
|
|
4505
4654
|
if (isSchemaValidatorCall(node)) return false;
|
|
4506
4655
|
if (node.callee.type === "Identifier" && returners.has(node.callee.name)) return true;
|
|
4507
4656
|
if (node.callee.type === "MemberExpression" && exprIsTainted(node.callee.object, tainted, returners)) {
|
|
@@ -4731,6 +4880,65 @@ function scopeReturnsTaint(scope, tainted, returners) {
|
|
|
4731
4880
|
});
|
|
4732
4881
|
return yes;
|
|
4733
4882
|
}
|
|
4883
|
+
function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
4884
|
+
let changed = false;
|
|
4885
|
+
const mark = (fn, name) => {
|
|
4886
|
+
if (name === void 0) return;
|
|
4887
|
+
const set = seeds.get(fn) ?? /* @__PURE__ */ new Set();
|
|
4888
|
+
if (set.has(name)) return;
|
|
4889
|
+
set.add(name);
|
|
4890
|
+
seeds.set(fn, set);
|
|
4891
|
+
changed = true;
|
|
4892
|
+
};
|
|
4893
|
+
for (const fn of functions) {
|
|
4894
|
+
const f = facts.get(fn);
|
|
4895
|
+
if (f === void 0) continue;
|
|
4896
|
+
let parent = fn.parent;
|
|
4897
|
+
while (parent !== void 0 && !isFunctionNode(parent)) {
|
|
4898
|
+
parent = parent.parent;
|
|
4899
|
+
}
|
|
4900
|
+
if (parent !== void 0 && isFunctionNode(parent)) {
|
|
4901
|
+
const outer = facts.get(parent);
|
|
4902
|
+
if (outer !== void 0) {
|
|
4903
|
+
for (const name of outer.tainted) mark(fn, name);
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
walkScope(fn, (node) => {
|
|
4907
|
+
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression") {
|
|
4908
|
+
if (node.left.object.type === "ThisExpression" && exprIsTainted(node.right, f.tainted, returners)) {
|
|
4909
|
+
const cls = enclosingClass(node);
|
|
4910
|
+
const field = propName(node.left);
|
|
4911
|
+
if (cls !== void 0 && field !== void 0) {
|
|
4912
|
+
const fields = taintedThisFields.get(cls) ?? /* @__PURE__ */ new Set();
|
|
4913
|
+
if (!fields.has(field)) {
|
|
4914
|
+
fields.add(field);
|
|
4915
|
+
taintedThisFields.set(cls, fields);
|
|
4916
|
+
changed = true;
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return;
|
|
4922
|
+
const method = propName(node.callee);
|
|
4923
|
+
if (method === void 0) return;
|
|
4924
|
+
if (CALLBACK_METHODS.has(method) && exprIsTainted(node.callee.object, f.tainted, returners)) {
|
|
4925
|
+
const callback = node.arguments[0];
|
|
4926
|
+
if (callback !== void 0 && isFunctionNode(callback)) {
|
|
4927
|
+
mark(callback, boundNames(callback.params[0])[0]);
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
if ((method === "call" || method === "apply") && node.callee.object.type === "Identifier") {
|
|
4931
|
+
const target = nameToFn.get(node.callee.object.name);
|
|
4932
|
+
if (target === void 0 || !isFunctionNode(target)) return;
|
|
4933
|
+
const arg = node.arguments[1];
|
|
4934
|
+
if (arg !== void 0 && arg.type !== "SpreadElement" && exprIsTainted(arg, f.tainted, returners)) {
|
|
4935
|
+
mark(target, boundNames(target.params[0])[0]);
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
return changed;
|
|
4941
|
+
}
|
|
4734
4942
|
function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
4735
4943
|
attachParents(ast);
|
|
4736
4944
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
@@ -4785,6 +4993,9 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
4785
4993
|
});
|
|
4786
4994
|
});
|
|
4787
4995
|
}
|
|
4996
|
+
if (taintV2Enabled() && seedTaintV2(functions, facts, seeds, returners, nameToFn)) {
|
|
4997
|
+
changed = true;
|
|
4998
|
+
}
|
|
4788
4999
|
if (!changed) break;
|
|
4789
5000
|
}
|
|
4790
5001
|
return facts;
|
|
@@ -4843,7 +5054,7 @@ function nodeIsNosqlSanitized(node, map) {
|
|
|
4843
5054
|
if (node.type === "Identifier") return factsFor(node, map).nosqlSanitized.has(node.name);
|
|
4844
5055
|
return false;
|
|
4845
5056
|
}
|
|
4846
|
-
var SCHEMA_ROOT, NO_RETURNERS, MAX_TAINT_HOPS;
|
|
5057
|
+
var SCHEMA_ROOT, NO_RETURNERS, CALLBACK_METHODS, taintedThisFields, MAX_TAINT_HOPS;
|
|
4847
5058
|
var init_taint = __esm({
|
|
4848
5059
|
"src/modules/sast/taint.ts"() {
|
|
4849
5060
|
init_ast();
|
|
@@ -4852,6 +5063,18 @@ var init_taint = __esm({
|
|
|
4852
5063
|
init_mcp2();
|
|
4853
5064
|
SCHEMA_ROOT = /^(z|zod|joi|yup|schema)$/i;
|
|
4854
5065
|
NO_RETURNERS = /* @__PURE__ */ new Set();
|
|
5066
|
+
CALLBACK_METHODS = /* @__PURE__ */ new Set([
|
|
5067
|
+
"then",
|
|
5068
|
+
"catch",
|
|
5069
|
+
"finally",
|
|
5070
|
+
"map",
|
|
5071
|
+
"forEach",
|
|
5072
|
+
"filter",
|
|
5073
|
+
"flatMap",
|
|
5074
|
+
"find",
|
|
5075
|
+
"reduce"
|
|
5076
|
+
]);
|
|
5077
|
+
taintedThisFields = /* @__PURE__ */ new WeakMap();
|
|
4855
5078
|
MAX_TAINT_HOPS = 5;
|
|
4856
5079
|
}
|
|
4857
5080
|
});
|
|
@@ -4936,6 +5159,21 @@ function requireCall(node) {
|
|
|
4936
5159
|
}
|
|
4937
5160
|
return void 0;
|
|
4938
5161
|
}
|
|
5162
|
+
function reexportsOf(ast) {
|
|
5163
|
+
const out = /* @__PURE__ */ new Map();
|
|
5164
|
+
walk(ast, (node) => {
|
|
5165
|
+
if (node.type !== "ExportNamedDeclaration" || node.source === null || node.source === void 0) return;
|
|
5166
|
+
const spec = literalString(node.source);
|
|
5167
|
+
if (spec === void 0) return;
|
|
5168
|
+
for (const specNode of node.specifiers) {
|
|
5169
|
+
if (specNode.type !== "ExportSpecifier") continue;
|
|
5170
|
+
const exported = specNode.exported.type === "Identifier" ? specNode.exported.name : void 0;
|
|
5171
|
+
const local = specNode.local.type === "Identifier" ? specNode.local.name : exported;
|
|
5172
|
+
if (exported !== void 0 && local !== void 0) out.set(exported, { spec, name: local });
|
|
5173
|
+
}
|
|
5174
|
+
});
|
|
5175
|
+
return out;
|
|
5176
|
+
}
|
|
4939
5177
|
function resolveRelative(fromFile, spec, files) {
|
|
4940
5178
|
if (!spec.startsWith(".")) return void 0;
|
|
4941
5179
|
const base = posix.normalize(posix.join(posix.dirname(fromFile), spec));
|
|
@@ -5013,6 +5251,22 @@ function runInterproc(parsed) {
|
|
|
5013
5251
|
functions.set(`${fn.file}#${fn.name}`, fn);
|
|
5014
5252
|
}
|
|
5015
5253
|
}
|
|
5254
|
+
if (taintV2Enabled()) {
|
|
5255
|
+
const barrels = /* @__PURE__ */ new Map();
|
|
5256
|
+
for (const p of parsed) barrels.set(p.file, reexportsOf(p.ast));
|
|
5257
|
+
for (const ns of requires.values()) {
|
|
5258
|
+
for (const [local, target] of ns) {
|
|
5259
|
+
const hash = target.indexOf("#");
|
|
5260
|
+
if (hash < 0) continue;
|
|
5261
|
+
const file = target.slice(0, hash);
|
|
5262
|
+
const name = target.slice(hash + 1);
|
|
5263
|
+
const re = barrels.get(file)?.get(name);
|
|
5264
|
+
if (re === void 0) continue;
|
|
5265
|
+
const resolved = resolveRelative(file, re.spec, files);
|
|
5266
|
+
if (resolved !== void 0) ns.set(local, `${resolved}#${re.name}`);
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
}
|
|
5016
5270
|
const queue = [];
|
|
5017
5271
|
for (const p of parsed) {
|
|
5018
5272
|
const facts = analyzeFile(p.ast);
|
|
@@ -5149,6 +5403,7 @@ var init_interproc = __esm({
|
|
|
5149
5403
|
"src/modules/sast/interproc.ts"() {
|
|
5150
5404
|
init_finding();
|
|
5151
5405
|
init_ast();
|
|
5406
|
+
init_experimental();
|
|
5152
5407
|
init_taint();
|
|
5153
5408
|
MAX_HOPS = 5;
|
|
5154
5409
|
}
|
|
@@ -5546,7 +5801,10 @@ function isMongoReceiver(node, source) {
|
|
|
5546
5801
|
const obj = node.callee.object;
|
|
5547
5802
|
if (obj.type === "Identifier") {
|
|
5548
5803
|
if (isCollectionName(obj.name)) return true;
|
|
5549
|
-
if (isPascalModel(obj.name))
|
|
5804
|
+
if (isPascalModel(obj.name)) {
|
|
5805
|
+
if (/extends\s+BaseEntity\b/.test(source)) return false;
|
|
5806
|
+
return !fileHasSqlOrm;
|
|
5807
|
+
}
|
|
5550
5808
|
return fileHasMongo;
|
|
5551
5809
|
}
|
|
5552
5810
|
if (obj.type === "MemberExpression") {
|
|
@@ -6686,6 +6944,80 @@ var init_injection = __esm({
|
|
|
6686
6944
|
PHRASES = [/ignore previous/i, /mark as safe/i, /ai reviewer/i];
|
|
6687
6945
|
}
|
|
6688
6946
|
});
|
|
6947
|
+
function packageVersion() {
|
|
6948
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
6949
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
6950
|
+
try {
|
|
6951
|
+
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
6952
|
+
const pkg = JSON.parse(raw);
|
|
6953
|
+
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
6954
|
+
return pkg.version;
|
|
6955
|
+
}
|
|
6956
|
+
} catch {
|
|
6957
|
+
}
|
|
6958
|
+
const parent = dirname(dir);
|
|
6959
|
+
if (parent === dir) break;
|
|
6960
|
+
dir = parent;
|
|
6961
|
+
}
|
|
6962
|
+
throw new Error("fullshield package.json not found");
|
|
6963
|
+
}
|
|
6964
|
+
var init_version = __esm({
|
|
6965
|
+
"src/core/version.ts"() {
|
|
6966
|
+
}
|
|
6967
|
+
});
|
|
6968
|
+
function parsePoolEnabled() {
|
|
6969
|
+
return process.env.FULLSHIELD_PARSE_POOL === "1";
|
|
6970
|
+
}
|
|
6971
|
+
function parsePoolSize() {
|
|
6972
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
6973
|
+
if (raw !== void 0 && raw !== "") {
|
|
6974
|
+
const parsed = Number(raw);
|
|
6975
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
6976
|
+
}
|
|
6977
|
+
return Math.max(1, availableParallelism() - 1);
|
|
6978
|
+
}
|
|
6979
|
+
function parseCacheKey(code) {
|
|
6980
|
+
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
6981
|
+
}
|
|
6982
|
+
function parseOne(code) {
|
|
6983
|
+
const key = parseCacheKey(code);
|
|
6984
|
+
const hit = cache.get(key);
|
|
6985
|
+
if (hit !== void 0) return Promise.resolve(hit);
|
|
6986
|
+
return new Promise((resolve8, reject) => {
|
|
6987
|
+
const worker = new Worker(fileURLToPath(workerUrl), { workerData: { code } });
|
|
6988
|
+
worker.once("message", (ast) => {
|
|
6989
|
+
cache.set(key, ast);
|
|
6990
|
+
resolve8(ast);
|
|
6991
|
+
});
|
|
6992
|
+
worker.once("error", reject);
|
|
6993
|
+
});
|
|
6994
|
+
}
|
|
6995
|
+
async function parseWithPool(codes) {
|
|
6996
|
+
const size = parsePoolSize();
|
|
6997
|
+
const out = new Array(codes.length);
|
|
6998
|
+
let next = 0;
|
|
6999
|
+
async function run() {
|
|
7000
|
+
for (; ; ) {
|
|
7001
|
+
const index = next;
|
|
7002
|
+
next += 1;
|
|
7003
|
+
if (index >= codes.length) return;
|
|
7004
|
+
const code = codes[index];
|
|
7005
|
+
if (code === void 0) return;
|
|
7006
|
+
out[index] = await parseOne(code);
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
const workers = Math.min(size, Math.max(1, codes.length));
|
|
7010
|
+
await Promise.all(Array.from({ length: workers }, () => run()));
|
|
7011
|
+
return out;
|
|
7012
|
+
}
|
|
7013
|
+
var workerUrl, cache;
|
|
7014
|
+
var init_parse_pool = __esm({
|
|
7015
|
+
"src/core/parse-pool.ts"() {
|
|
7016
|
+
init_version();
|
|
7017
|
+
workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7018
|
+
cache = /* @__PURE__ */ new Map();
|
|
7019
|
+
}
|
|
7020
|
+
});
|
|
6689
7021
|
async function parseSource(code) {
|
|
6690
7022
|
const { parse } = await import('@typescript-eslint/typescript-estree');
|
|
6691
7023
|
return parse(code, {
|
|
@@ -6801,6 +7133,7 @@ var init_engine = __esm({
|
|
|
6801
7133
|
init_xss();
|
|
6802
7134
|
init_phase29();
|
|
6803
7135
|
init_injection();
|
|
7136
|
+
init_parse_pool();
|
|
6804
7137
|
sastRules = [
|
|
6805
7138
|
sqliRule,
|
|
6806
7139
|
cmdiRule,
|
|
@@ -6838,7 +7171,8 @@ var init_engine = __esm({
|
|
|
6838
7171
|
continue;
|
|
6839
7172
|
}
|
|
6840
7173
|
try {
|
|
6841
|
-
const ast = await parseSource(source);
|
|
7174
|
+
const ast = parsePoolEnabled() ? (await parseWithPool([source]))[0] : await parseSource(source);
|
|
7175
|
+
if (ast === void 0) continue;
|
|
6842
7176
|
parsed.push({ file: rel, source, ast });
|
|
6843
7177
|
} catch {
|
|
6844
7178
|
}
|
|
@@ -7690,7 +8024,7 @@ function createAiClient(options) {
|
|
|
7690
8024
|
const useFixtures = options.useFixtures ?? !isLiveAi();
|
|
7691
8025
|
const model = options.model ?? process.env.FULLSHIELD_AI_MODEL ?? (useFixtures ? "recorded-fixture" : "qwen2.5-coder:7b");
|
|
7692
8026
|
const provider = options.provider ?? process.env.FULLSHIELD_AI_PROVIDER ?? (useFixtures ? "fixture" : "ollama");
|
|
7693
|
-
const
|
|
8027
|
+
const cache2 = /* @__PURE__ */ new Map();
|
|
7694
8028
|
let spentUsd = 0;
|
|
7695
8029
|
let networkCalls = 0;
|
|
7696
8030
|
const liveCost = provider === "ollama" ? 0 : FIXTURE_COST;
|
|
@@ -7710,7 +8044,7 @@ function createAiClient(options) {
|
|
|
7710
8044
|
const redactedPrompt = `${modeInstruction(req.mode)}
|
|
7711
8045
|
${wrapUntrustedCode(req.prompt)}`;
|
|
7712
8046
|
const key = cacheKey(req.model ?? model, redactedPrompt);
|
|
7713
|
-
const cached =
|
|
8047
|
+
const cached = cache2.get(key);
|
|
7714
8048
|
if (cached !== void 0) {
|
|
7715
8049
|
return { ...cached, cached: true, costUsd: 0 };
|
|
7716
8050
|
}
|
|
@@ -7724,7 +8058,7 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7724
8058
|
cached: false,
|
|
7725
8059
|
redactedPrompt
|
|
7726
8060
|
};
|
|
7727
|
-
|
|
8061
|
+
cache2.set(key, response2);
|
|
7728
8062
|
return response2;
|
|
7729
8063
|
}
|
|
7730
8064
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
@@ -7774,7 +8108,7 @@ ${wrapUntrustedCode(req.prompt)}`;
|
|
|
7774
8108
|
cached: false,
|
|
7775
8109
|
redactedPrompt
|
|
7776
8110
|
};
|
|
7777
|
-
|
|
8111
|
+
cache2.set(key, response);
|
|
7778
8112
|
return response;
|
|
7779
8113
|
}
|
|
7780
8114
|
};
|
|
@@ -8050,27 +8384,6 @@ var init_suppression = __esm({
|
|
|
8050
8384
|
IGNORE = /fullshield-ignore\s+([A-Z0-9-]+)\s*:\s*(\S+)/;
|
|
8051
8385
|
}
|
|
8052
8386
|
});
|
|
8053
|
-
function packageVersion() {
|
|
8054
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
8055
|
-
for (let depth = 0; depth < 6; depth += 1) {
|
|
8056
|
-
try {
|
|
8057
|
-
const raw = readFileSync(join(dir, "package.json"), "utf8");
|
|
8058
|
-
const pkg = JSON.parse(raw);
|
|
8059
|
-
if (pkg.name === "fullshield" && typeof pkg.version === "string") {
|
|
8060
|
-
return pkg.version;
|
|
8061
|
-
}
|
|
8062
|
-
} catch {
|
|
8063
|
-
}
|
|
8064
|
-
const parent = dirname(dir);
|
|
8065
|
-
if (parent === dir) break;
|
|
8066
|
-
dir = parent;
|
|
8067
|
-
}
|
|
8068
|
-
throw new Error("fullshield package.json not found");
|
|
8069
|
-
}
|
|
8070
|
-
var init_version = __esm({
|
|
8071
|
-
"src/core/version.ts"() {
|
|
8072
|
-
}
|
|
8073
|
-
});
|
|
8074
8387
|
|
|
8075
8388
|
// src/policy/prSummary.ts
|
|
8076
8389
|
function ruleDocsUrl(ruleId) {
|
|
@@ -10912,15 +11225,23 @@ function renderConsoleReport(result) {
|
|
|
10912
11225
|
);
|
|
10913
11226
|
}
|
|
10914
11227
|
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
11228
|
+
const ordered = bySeverityDesc.flatMap((severity) => findings.filter((f) => f.severity === severity));
|
|
11229
|
+
const shown = ordered.slice(0, 10);
|
|
11230
|
+
const hidden = ordered.length - shown.length;
|
|
10915
11231
|
for (const severity of bySeverityDesc) {
|
|
10916
|
-
const group =
|
|
11232
|
+
const group = shown.filter((f) => f.severity === severity);
|
|
10917
11233
|
if (group.length === 0) continue;
|
|
10918
11234
|
lines.push("");
|
|
10919
|
-
lines.push(SEVERITY_COLOR[severity](`${severity.toUpperCase()} (${
|
|
11235
|
+
lines.push(SEVERITY_COLOR[severity](`${severity.toUpperCase()} (${findings.filter((f) => f.severity === severity).length})`));
|
|
10920
11236
|
for (const finding6 of group) {
|
|
10921
11237
|
lines.push(...renderFinding(finding6));
|
|
11238
|
+
lines.push(` docs: rules/${finding6.ruleId}.md`);
|
|
10922
11239
|
}
|
|
10923
11240
|
}
|
|
11241
|
+
if (hidden > 0) {
|
|
11242
|
+
lines.push("");
|
|
11243
|
+
lines.push(`${hidden} more \u2014 use --json`);
|
|
11244
|
+
}
|
|
10924
11245
|
lines.push("");
|
|
10925
11246
|
if (summary.total === 0) {
|
|
10926
11247
|
lines.push(pc.green("\u2714 No findings."));
|
|
@@ -11803,6 +12124,98 @@ function stateForGrace(sshd) {
|
|
|
11803
12124
|
return sshd.loginGraceTimeZero ? "logingracetime 0" : "logingracetime not 0";
|
|
11804
12125
|
}
|
|
11805
12126
|
|
|
12127
|
+
// src/cli/commands/doctor.ts
|
|
12128
|
+
init_config2();
|
|
12129
|
+
var MIN_NODE = [22, 13, 0];
|
|
12130
|
+
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
12131
|
+
var FLAGS = [
|
|
12132
|
+
"FULLSHIELD_EXPERIMENTAL_SAST_GAPS",
|
|
12133
|
+
"FULLSHIELD_EXPERIMENTAL_FRAMEWORKS",
|
|
12134
|
+
"FULLSHIELD_EXPERIMENTAL_CLASSES",
|
|
12135
|
+
"FULLSHIELD_EXPERIMENTAL_SECRETS_ADV",
|
|
12136
|
+
"FULLSHIELD_EXPERIMENTAL_VEX",
|
|
12137
|
+
"FULLSHIELD_EXPERIMENTAL_DLP",
|
|
12138
|
+
"FULLSHIELD_EXPERIMENTAL_MCPCFG",
|
|
12139
|
+
"FULLSHIELD_EXPERIMENTAL_POLICY",
|
|
12140
|
+
"FULLSHIELD_EXPERIMENTAL_HOST",
|
|
12141
|
+
"FULLSHIELD_EXPERIMENTAL_TAINT_V2",
|
|
12142
|
+
"FULLSHIELD_PARSE_POOL"
|
|
12143
|
+
];
|
|
12144
|
+
function nodeOk(version) {
|
|
12145
|
+
const parts = version.split(".").map((part) => Number(part));
|
|
12146
|
+
for (let i = 0; i < MIN_NODE.length; i += 1) {
|
|
12147
|
+
const got = parts[i] ?? 0;
|
|
12148
|
+
const need = MIN_NODE[i] ?? 0;
|
|
12149
|
+
if (got > need) return true;
|
|
12150
|
+
if (got < need) return false;
|
|
12151
|
+
}
|
|
12152
|
+
return true;
|
|
12153
|
+
}
|
|
12154
|
+
async function runDoctor(cwd, options = {}) {
|
|
12155
|
+
const lines = [];
|
|
12156
|
+
const version = process.versions.node;
|
|
12157
|
+
lines.push(`node: ${version} ${nodeOk(version) ? "ok" : "too old (need >= 22.13.0)"}`);
|
|
12158
|
+
const locks = LOCKFILES.filter((name) => existsSync(join(cwd, name)));
|
|
12159
|
+
lines.push(locks.length > 0 ? `lockfile: ${locks.join(", ")}` : "lockfile: none");
|
|
12160
|
+
const cacheDir = join(homedir(), ".cache", "fullshield", "osv");
|
|
12161
|
+
let cacheFiles = 0;
|
|
12162
|
+
if (existsSync(cacheDir)) {
|
|
12163
|
+
cacheFiles = readdirSync(cacheDir).length;
|
|
12164
|
+
}
|
|
12165
|
+
lines.push(`cache: ${existsSync(cacheDir) ? cacheDir : "absent"} (${cacheFiles} files)`);
|
|
12166
|
+
const on = FLAGS.filter((name) => process.env[name] === "1");
|
|
12167
|
+
lines.push(on.length > 0 ? `flags: ${on.join(", ")}` : "flags: none");
|
|
12168
|
+
try {
|
|
12169
|
+
const config = await loadConfig(cwd, options.config);
|
|
12170
|
+
lines.push(`config: valid (failOn ${config.failOn}, offline ${config.deps.offline})`);
|
|
12171
|
+
} catch (error) {
|
|
12172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12173
|
+
lines.push(`config: invalid (${message})`);
|
|
12174
|
+
}
|
|
12175
|
+
const offline = options.offline === true;
|
|
12176
|
+
lines.push(
|
|
12177
|
+
offline ? "osv: network skipped (--offline); reachability uses the local lockfile only" : "osv: network allowed; reachability still uses the local lockfile"
|
|
12178
|
+
);
|
|
12179
|
+
return lines.join("\n");
|
|
12180
|
+
}
|
|
12181
|
+
|
|
12182
|
+
// src/cli/errors.ts
|
|
12183
|
+
var KNOWN = [
|
|
12184
|
+
{
|
|
12185
|
+
test: /invalid --fail-on/,
|
|
12186
|
+
code: "FS-E001",
|
|
12187
|
+
hint: "Use one of critical, high, medium, low, info."
|
|
12188
|
+
},
|
|
12189
|
+
{
|
|
12190
|
+
test: /unknown module/,
|
|
12191
|
+
code: "FS-E002",
|
|
12192
|
+
hint: "Pass a comma-separated list from `fullshield scan --help`."
|
|
12193
|
+
},
|
|
12194
|
+
{
|
|
12195
|
+
test: /not a directory/,
|
|
12196
|
+
code: "FS-E003",
|
|
12197
|
+
hint: "Pass the project directory to scan."
|
|
12198
|
+
},
|
|
12199
|
+
{
|
|
12200
|
+
test: /cannot read config/,
|
|
12201
|
+
code: "FS-E004",
|
|
12202
|
+
hint: "Check the path passed to --config."
|
|
12203
|
+
}
|
|
12204
|
+
];
|
|
12205
|
+
var DOCS = "https://github.com/kathanp/fullshield/blob/main/docs/errors.md";
|
|
12206
|
+
function formatCliError(error, debug = process.argv.includes("--debug")) {
|
|
12207
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12208
|
+
const known = KNOWN.find((item) => item.test.test(message));
|
|
12209
|
+
const code = known?.code ?? "FS-E000";
|
|
12210
|
+
const hint = known !== void 0 ? ` \u2014 ${known.hint} ${DOCS}#${code.toLowerCase()}` : "";
|
|
12211
|
+
const line = `fullshield: ${code} ${message}${hint}`;
|
|
12212
|
+
if (debug && error instanceof Error && error.stack !== void 0) {
|
|
12213
|
+
return `${line}
|
|
12214
|
+
${error.stack}`;
|
|
12215
|
+
}
|
|
12216
|
+
return line;
|
|
12217
|
+
}
|
|
12218
|
+
|
|
11806
12219
|
// src/cli/program.ts
|
|
11807
12220
|
var defaultIO = {
|
|
11808
12221
|
out: (text) => process.stdout.write(`${text}
|
|
@@ -11837,7 +12250,7 @@ function parseFailOn(value) {
|
|
|
11837
12250
|
}
|
|
11838
12251
|
function createProgram(io = defaultIO) {
|
|
11839
12252
|
const program = new Command();
|
|
11840
|
-
program.name("fullshield").description("Full-cycle security scanner for Node.js/TypeScript backends").version(packageVersion());
|
|
12253
|
+
program.name("fullshield").description("Full-cycle security scanner for Node.js/TypeScript backends").version(packageVersion()).option("--debug", "print stack traces on CLI errors");
|
|
11841
12254
|
program.command("scan").description("Scan a project directory for security findings").argument("[path]", "directory to scan", ".").option("--json", "machine-readable JSON output").option("--fail-on <severity>", "exit 1 if any finding is at or above this severity").option("--modules <modules>", "comma-separated subset of modules to run").option("--staged", "scan only files staged in git (pre-commit mode)").option("--offline", "use the OSV cache only (no network)").option("--offline-safe", "alias of --offline").option("--config <path>", "path to fullshield.config.json").option("--sarif <file>", "write a SARIF 2.1.0 report to this path").option("--markdown <file>", "write a Markdown report to this path").option("--html <file>", "write an HTML report to this path").option("--update-baseline", "record current findings as accepted (CI fails only on new ones)").option("--ai [mode]", "experimental: explain findings (off = zero AI network calls)").option("--emit-virtual-patches [file]", "experimental: write .fullshield/vpatches.json").option("--changed", "experimental: scan git staged/changed files only").option("--since <ref>", "experimental: scan files changed since a git ref").option(
|
|
11842
12255
|
"--experimental-sast-gaps",
|
|
11843
12256
|
"experimental: ORM raw / sh -c / execa / zx sinks (FULLSHIELD_EXPERIMENTAL_SAST_GAPS=1)"
|
|
@@ -11950,7 +12363,7 @@ function createProgram(io = defaultIO) {
|
|
|
11950
12363
|
process.exitCode = 1;
|
|
11951
12364
|
}
|
|
11952
12365
|
} catch (error) {
|
|
11953
|
-
io.err(
|
|
12366
|
+
io.err(formatCliError(error));
|
|
11954
12367
|
process.exitCode = 2;
|
|
11955
12368
|
}
|
|
11956
12369
|
});
|
|
@@ -11995,7 +12408,7 @@ function createProgram(io = defaultIO) {
|
|
|
11995
12408
|
io.out("Dry run. Re-run with --apply to write safe fixes.");
|
|
11996
12409
|
}
|
|
11997
12410
|
} catch (error) {
|
|
11998
|
-
io.err(
|
|
12411
|
+
io.err(formatCliError(error));
|
|
11999
12412
|
process.exitCode = 2;
|
|
12000
12413
|
}
|
|
12001
12414
|
});
|
|
@@ -12008,7 +12421,7 @@ function createProgram(io = defaultIO) {
|
|
|
12008
12421
|
io.out(`Wrote ${result.written.join(", ")}`);
|
|
12009
12422
|
}
|
|
12010
12423
|
} catch (error) {
|
|
12011
|
-
io.err(
|
|
12424
|
+
io.err(formatCliError(error));
|
|
12012
12425
|
process.exitCode = 2;
|
|
12013
12426
|
}
|
|
12014
12427
|
});
|
|
@@ -12037,7 +12450,7 @@ function createProgram(io = defaultIO) {
|
|
|
12037
12450
|
}
|
|
12038
12451
|
io.out(formatSummary(summary));
|
|
12039
12452
|
} catch (error) {
|
|
12040
|
-
io.err(
|
|
12453
|
+
io.err(formatCliError(error));
|
|
12041
12454
|
process.exitCode = 2;
|
|
12042
12455
|
}
|
|
12043
12456
|
});
|
|
@@ -12057,7 +12470,7 @@ function createProgram(io = defaultIO) {
|
|
|
12057
12470
|
io.out(json);
|
|
12058
12471
|
}
|
|
12059
12472
|
} catch (error) {
|
|
12060
|
-
io.err(
|
|
12473
|
+
io.err(formatCliError(error));
|
|
12061
12474
|
process.exitCode = 2;
|
|
12062
12475
|
}
|
|
12063
12476
|
});
|
|
@@ -12076,7 +12489,7 @@ function createProgram(io = defaultIO) {
|
|
|
12076
12489
|
}
|
|
12077
12490
|
if (findings.some((f) => meetsThreshold(f.severity, "high"))) process.exitCode = 1;
|
|
12078
12491
|
} catch (error) {
|
|
12079
|
-
io.err(
|
|
12492
|
+
io.err(formatCliError(error));
|
|
12080
12493
|
process.exitCode = 2;
|
|
12081
12494
|
}
|
|
12082
12495
|
});
|
|
@@ -12111,7 +12524,7 @@ function createProgram(io = defaultIO) {
|
|
|
12111
12524
|
const gate = failOn ?? result.config.failOn;
|
|
12112
12525
|
if (fresh.some((f) => meetsThreshold(f.severity, gate))) process.exitCode = 1;
|
|
12113
12526
|
} catch (error) {
|
|
12114
|
-
io.err(
|
|
12527
|
+
io.err(formatCliError(error));
|
|
12115
12528
|
process.exitCode = 2;
|
|
12116
12529
|
}
|
|
12117
12530
|
});
|
|
@@ -12124,7 +12537,7 @@ function createProgram(io = defaultIO) {
|
|
|
12124
12537
|
const model = buildThreatModel2(resolve(path), { experimental: true });
|
|
12125
12538
|
io.out(JSON.stringify({ routes: model.routes.length, stride: model.stride, mermaid: model.mermaid }));
|
|
12126
12539
|
} catch (error) {
|
|
12127
|
-
io.err(
|
|
12540
|
+
io.err(formatCliError(error));
|
|
12128
12541
|
process.exitCode = 2;
|
|
12129
12542
|
}
|
|
12130
12543
|
});
|
|
@@ -12154,7 +12567,7 @@ function createProgram(io = defaultIO) {
|
|
|
12154
12567
|
const started = await startDashboard2({ dir: resolve(path), experimental: true });
|
|
12155
12568
|
io.out(`dashboard listening on http://127.0.0.1:${started.port} (Host must be localhost or 127.0.0.1)`);
|
|
12156
12569
|
} catch (error) {
|
|
12157
|
-
io.err(
|
|
12570
|
+
io.err(formatCliError(error));
|
|
12158
12571
|
process.exitCode = 2;
|
|
12159
12572
|
}
|
|
12160
12573
|
});
|
|
@@ -12167,7 +12580,7 @@ function createProgram(io = defaultIO) {
|
|
|
12167
12580
|
const findings = scanIac2(resolve(path), { experimental: true });
|
|
12168
12581
|
io.out(JSON.stringify({ findings }));
|
|
12169
12582
|
} catch (error) {
|
|
12170
|
-
io.err(
|
|
12583
|
+
io.err(formatCliError(error));
|
|
12171
12584
|
process.exitCode = 2;
|
|
12172
12585
|
}
|
|
12173
12586
|
});
|
|
@@ -12196,7 +12609,7 @@ function createProgram(io = defaultIO) {
|
|
|
12196
12609
|
const req = JSON.parse(text);
|
|
12197
12610
|
io.out(JSON.stringify(await handleMcpRequest2(session, req)));
|
|
12198
12611
|
} catch (error) {
|
|
12199
|
-
io.err(
|
|
12612
|
+
io.err(formatCliError(error));
|
|
12200
12613
|
process.exitCode = 2;
|
|
12201
12614
|
}
|
|
12202
12615
|
});
|
|
@@ -12210,7 +12623,7 @@ function createProgram(io = defaultIO) {
|
|
|
12210
12623
|
const result = await confirmTarget2(url, { experimental: true, routes: [] });
|
|
12211
12624
|
io.out(JSON.stringify({ ok: true, confirmations: result.confirmations }));
|
|
12212
12625
|
} catch (error) {
|
|
12213
|
-
io.err(
|
|
12626
|
+
io.err(formatCliError(error));
|
|
12214
12627
|
process.exitCode = 2;
|
|
12215
12628
|
}
|
|
12216
12629
|
});
|
|
@@ -12233,7 +12646,7 @@ function createProgram(io = defaultIO) {
|
|
|
12233
12646
|
}
|
|
12234
12647
|
io.out(`static checks passed for ${pkg}; next step would be npm install --ignore-scripts`);
|
|
12235
12648
|
} catch (error) {
|
|
12236
|
-
io.err(
|
|
12649
|
+
io.err(formatCliError(error));
|
|
12237
12650
|
process.exitCode = 2;
|
|
12238
12651
|
}
|
|
12239
12652
|
});
|
|
@@ -12253,7 +12666,7 @@ function createProgram(io = defaultIO) {
|
|
|
12253
12666
|
});
|
|
12254
12667
|
io.out(JSON.stringify({ prs: result.prs, stoppedOnTestFailure: result.stoppedOnTestFailure, merged: result.merged }));
|
|
12255
12668
|
} catch (error) {
|
|
12256
|
-
io.err(
|
|
12669
|
+
io.err(formatCliError(error));
|
|
12257
12670
|
process.exitCode = 2;
|
|
12258
12671
|
}
|
|
12259
12672
|
});
|
|
@@ -12303,7 +12716,7 @@ function createProgram(io = defaultIO) {
|
|
|
12303
12716
|
}
|
|
12304
12717
|
if (result.findings.some((f) => meetsThreshold(f.severity, "high"))) process.exitCode = 1;
|
|
12305
12718
|
} catch (error) {
|
|
12306
|
-
io.err(
|
|
12719
|
+
io.err(formatCliError(error));
|
|
12307
12720
|
process.exitCode = 2;
|
|
12308
12721
|
}
|
|
12309
12722
|
});
|
|
@@ -12317,7 +12730,7 @@ function createProgram(io = defaultIO) {
|
|
|
12317
12730
|
io.out(`wrote honeytokens (.env.backup). decoys: /admin.php /.git/config /wp-login.php`);
|
|
12318
12731
|
io.out(`canary prefix ${canaries.awsKey.slice(0, 4)}****`);
|
|
12319
12732
|
} catch (error) {
|
|
12320
|
-
io.err(
|
|
12733
|
+
io.err(formatCliError(error));
|
|
12321
12734
|
process.exitCode = 2;
|
|
12322
12735
|
}
|
|
12323
12736
|
});
|
|
@@ -12353,7 +12766,15 @@ function createProgram(io = defaultIO) {
|
|
|
12353
12766
|
io.out(json);
|
|
12354
12767
|
}
|
|
12355
12768
|
} catch (error) {
|
|
12356
|
-
io.err(
|
|
12769
|
+
io.err(formatCliError(error));
|
|
12770
|
+
process.exitCode = 2;
|
|
12771
|
+
}
|
|
12772
|
+
});
|
|
12773
|
+
program.command("doctor").description("Report Node, lockfile, cache, flags, config, and whether OSV is offline").argument("[path]", "project directory", ".").option("--offline", "do not describe OSV as allowed to use the network").option("--config <file>", "explicit config file").action(async (path, opts) => {
|
|
12774
|
+
try {
|
|
12775
|
+
io.out(await runDoctor(resolve(path), opts));
|
|
12776
|
+
} catch (error) {
|
|
12777
|
+
io.err(formatCliError(error));
|
|
12357
12778
|
process.exitCode = 2;
|
|
12358
12779
|
}
|
|
12359
12780
|
});
|