fullshield 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -0
- package/README.md +8 -3
- package/dist/agent.js +599 -64
- package/dist/cli/index.js +1074 -84
- package/dist/compliance.js +5 -0
- package/dist/dashboard.js +18 -1
- package/dist/iac.js +48 -0
- package/dist/index.d.ts +68 -5
- package/dist/index.js +882 -69
- package/dist/mcp.js +599 -64
- package/dist/supply.js +13 -3
- package/package.json +2 -2
- package/rules/SAST-SKIP-001.md +9 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
-
import { writeFile,
|
|
3
|
+
import { writeFile, readFile, mkdir, stat, mkdtemp, unlink, readdir } from 'fs/promises';
|
|
4
4
|
import { resolve, join, dirname, relative, basename, posix } from 'path';
|
|
5
5
|
import { readFileSync, existsSync, readdirSync, cpSync, mkdirSync, rmSync, statSync, writeFileSync, mkdtempSync, unlinkSync } from 'fs';
|
|
6
6
|
import { homedir, tmpdir, availableParallelism } from 'os';
|
|
@@ -113,7 +113,8 @@ function walk(node, visit) {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
function attachParents(node, parent) {
|
|
116
|
-
|
|
116
|
+
const current = node;
|
|
117
|
+
if (parent !== void 0 || current.parent === void 0) current.parent = parent;
|
|
117
118
|
for (const key of Object.keys(node)) {
|
|
118
119
|
if (SKIP_KEYS.has(String(key))) continue;
|
|
119
120
|
const value = node[key];
|
|
@@ -192,8 +193,18 @@ function mcpCfgEnabled() {
|
|
|
192
193
|
function taintV2Enabled() {
|
|
193
194
|
return process.env.FULLSHIELD_EXPERIMENTAL_TAINT_V2 === "1";
|
|
194
195
|
}
|
|
196
|
+
function licenseEnabled() {
|
|
197
|
+
return process.env[LICENSE_FLAG] === "1";
|
|
198
|
+
}
|
|
199
|
+
function diffEnabled() {
|
|
200
|
+
return process.env[DIFF_FLAG] === "1";
|
|
201
|
+
}
|
|
202
|
+
var CI_REPORTS_FLAG, DIFF_FLAG, LICENSE_FLAG;
|
|
195
203
|
var init_experimental = __esm({
|
|
196
204
|
"src/modules/sast/experimental.ts"() {
|
|
205
|
+
CI_REPORTS_FLAG = "FULLSHIELD_EXPERIMENTAL_CI_REPORTS";
|
|
206
|
+
DIFF_FLAG = "FULLSHIELD_EXPERIMENTAL_DIFF";
|
|
207
|
+
LICENSE_FLAG = "FULLSHIELD_EXPERIMENTAL_LICENSE";
|
|
197
208
|
}
|
|
198
209
|
});
|
|
199
210
|
function isSeverity(value) {
|
|
@@ -1267,6 +1278,29 @@ function shortestDependencyPath(target, starts, edges) {
|
|
|
1267
1278
|
}
|
|
1268
1279
|
return void 0;
|
|
1269
1280
|
}
|
|
1281
|
+
function collectDependencyPaths(target, starts, edges, limit = 8) {
|
|
1282
|
+
const out = [];
|
|
1283
|
+
const queue = [];
|
|
1284
|
+
let steps = 0;
|
|
1285
|
+
for (const start of starts) queue.push([start]);
|
|
1286
|
+
while (queue.length > 0 && out.length < limit && steps < 4e3) {
|
|
1287
|
+
steps += 1;
|
|
1288
|
+
const path = queue.shift();
|
|
1289
|
+
if (path === void 0) continue;
|
|
1290
|
+
const last = path[path.length - 1];
|
|
1291
|
+
if (last === void 0) continue;
|
|
1292
|
+
if (last === target) {
|
|
1293
|
+
if (path.length >= 2) out.push(path);
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
if (path.length >= 12) continue;
|
|
1297
|
+
for (const next of edges.get(last) ?? []) {
|
|
1298
|
+
if (path.includes(next)) continue;
|
|
1299
|
+
queue.push([...path, next]);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return out;
|
|
1303
|
+
}
|
|
1270
1304
|
function reachableFrom(target, roots, edges) {
|
|
1271
1305
|
if (roots.has(target)) return true;
|
|
1272
1306
|
return shortestDependencyPath(target, roots, edges) !== void 0;
|
|
@@ -1287,9 +1321,10 @@ function classifyReachability(pkgName, productionUsage, graph) {
|
|
|
1287
1321
|
}
|
|
1288
1322
|
if (graph !== void 0 && graph.edgesKnown) {
|
|
1289
1323
|
const roots = importedDirectNames(productionUsage, graph);
|
|
1290
|
-
const
|
|
1324
|
+
const paths = collectDependencyPaths(pkgName, roots, graph.edges);
|
|
1325
|
+
const path = paths[0];
|
|
1291
1326
|
if (path !== void 0 && path.length >= 2) {
|
|
1292
|
-
return { reachability: "reachable-via-dependency", path };
|
|
1327
|
+
return { reachability: "reachable-via-dependency", path, paths };
|
|
1293
1328
|
}
|
|
1294
1329
|
return { reachability: "not-imported" };
|
|
1295
1330
|
}
|
|
@@ -1319,8 +1354,99 @@ function isDevOnly(pkgName, ctx) {
|
|
|
1319
1354
|
if (inProd) return false;
|
|
1320
1355
|
return directDev.has(pkgName) || reachableFrom(pkgName, directDev, edges);
|
|
1321
1356
|
}
|
|
1357
|
+
function versionFromSnippet(snippet) {
|
|
1358
|
+
const name = packageNameFromSnippet(snippet);
|
|
1359
|
+
if (name === void 0) return snippet;
|
|
1360
|
+
return snippet.slice(name.length + 1);
|
|
1361
|
+
}
|
|
1362
|
+
function pathFields(classified) {
|
|
1363
|
+
const paths = classified.paths ?? (classified.path !== void 0 ? [classified.path] : []);
|
|
1364
|
+
if (paths.length === 0) return {};
|
|
1365
|
+
return {
|
|
1366
|
+
dependencyPath: paths[0],
|
|
1367
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
function appendOtherPaths(message, paths) {
|
|
1371
|
+
if (paths === void 0 || paths.length < 2) return message;
|
|
1372
|
+
const rest = paths.slice(1).map((p) => p.join(" > ")).join("; ");
|
|
1373
|
+
return `${message} Other paths: ${rest}.`;
|
|
1374
|
+
}
|
|
1375
|
+
function groupPackageAdvisories(findings) {
|
|
1376
|
+
const passthrough = [];
|
|
1377
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1378
|
+
for (const finding6 of findings) {
|
|
1379
|
+
if (finding6.ruleId !== "DEPS-VULN-001" && finding6.ruleId !== "DEPS-MALICIOUS-001") {
|
|
1380
|
+
passthrough.push(finding6);
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
const name = packageNameFromSnippet(finding6.snippet ?? "") ?? finding6.snippet ?? finding6.id;
|
|
1384
|
+
const advisory = finding6.references?.[0] ?? finding6.id;
|
|
1385
|
+
const key = `${finding6.ruleId}\0${name}\0${advisory}`;
|
|
1386
|
+
const list = groups.get(key);
|
|
1387
|
+
if (list === void 0) groups.set(key, [finding6]);
|
|
1388
|
+
else list.push(finding6);
|
|
1389
|
+
}
|
|
1390
|
+
const merged = [];
|
|
1391
|
+
for (const list of groups.values()) {
|
|
1392
|
+
const only = list[0];
|
|
1393
|
+
if (list.length === 1 && only !== void 0) {
|
|
1394
|
+
merged.push(only);
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
merged.push(mergeAdvisory(list));
|
|
1398
|
+
}
|
|
1399
|
+
return [...passthrough, ...merged];
|
|
1400
|
+
}
|
|
1401
|
+
function mergeAdvisory(list) {
|
|
1402
|
+
const ranked = [...list].sort(
|
|
1403
|
+
(a, b) => severityRank(b.severity) - severityRank(a.severity) || (REACH_RANK[a.reachability ?? "not-imported"] ?? 9) - (REACH_RANK[b.reachability ?? "not-imported"] ?? 9)
|
|
1404
|
+
);
|
|
1405
|
+
const primary = ranked[0];
|
|
1406
|
+
if (primary === void 0) {
|
|
1407
|
+
throw new Error("empty advisory group");
|
|
1408
|
+
}
|
|
1409
|
+
const name = packageNameFromSnippet(primary.snippet ?? "") ?? "package";
|
|
1410
|
+
const versions = [...new Set(list.map((f) => versionFromSnippet(f.snippet ?? "")))].filter((v) => v.length > 0).sort();
|
|
1411
|
+
const paths = [];
|
|
1412
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
1413
|
+
for (const finding6 of list) {
|
|
1414
|
+
const candidates = finding6.dependencyPaths ?? (finding6.dependencyPath !== void 0 ? [finding6.dependencyPath] : []);
|
|
1415
|
+
for (const path of candidates) {
|
|
1416
|
+
const key = path.join("\0");
|
|
1417
|
+
if (seenPath.has(key)) continue;
|
|
1418
|
+
seenPath.add(key);
|
|
1419
|
+
paths.push(path);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
paths.sort((a, b) => a.length - b.length || a.join(">").localeCompare(b.join(">")));
|
|
1423
|
+
const advisory = primary.references?.[0] ?? "";
|
|
1424
|
+
const onManifest = list.find((f) => f.file === "package.json");
|
|
1425
|
+
const file = onManifest?.file ?? primary.file;
|
|
1426
|
+
const line = onManifest?.line ?? primary.line;
|
|
1427
|
+
const snippet = versions.length > 0 ? `${name}@${versions.join(",")}` : primary.snippet ?? name;
|
|
1428
|
+
let message = primary.message;
|
|
1429
|
+
if (versions.length > 1) {
|
|
1430
|
+
message = `${message} Versions: ${versions.join(", ")}.`;
|
|
1431
|
+
}
|
|
1432
|
+
if (paths.length > 1) {
|
|
1433
|
+
message = `${message} Dependency paths: ${paths.map((p) => p.join(" > ")).join("; ")}.`;
|
|
1434
|
+
}
|
|
1435
|
+
return {
|
|
1436
|
+
...primary,
|
|
1437
|
+
id: makeFindingId(primary.ruleId, file, `${name}|${advisory}|${versions.join(",")}`),
|
|
1438
|
+
file,
|
|
1439
|
+
line,
|
|
1440
|
+
snippet,
|
|
1441
|
+
message,
|
|
1442
|
+
severity: primary.severity,
|
|
1443
|
+
reachability: primary.reachability,
|
|
1444
|
+
...paths[0] !== void 0 ? { dependencyPath: paths[0] } : {},
|
|
1445
|
+
...paths.length > 1 ? { dependencyPaths: paths } : {}
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1322
1448
|
function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
1323
|
-
|
|
1449
|
+
const mapped = findings.map((finding6) => {
|
|
1324
1450
|
if (finding6.ruleId === "DEPS-MALICIOUS-001") {
|
|
1325
1451
|
if (finding6.snippet === void 0) return finding6;
|
|
1326
1452
|
const pkg2 = packageNameFromSnippet(finding6.snippet);
|
|
@@ -1330,7 +1456,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
1330
1456
|
...finding6,
|
|
1331
1457
|
severity: "critical",
|
|
1332
1458
|
reachability: classified2.reachability,
|
|
1333
|
-
...classified2
|
|
1459
|
+
...pathFields(classified2)
|
|
1334
1460
|
};
|
|
1335
1461
|
}
|
|
1336
1462
|
if (finding6.ruleId !== "DEPS-VULN-001" || finding6.snippet === void 0) return finding6;
|
|
@@ -1340,7 +1466,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
1340
1466
|
const named = symbols.get(finding6.id) ?? [];
|
|
1341
1467
|
if (named.length > 0 && ctx.sources !== void 0) {
|
|
1342
1468
|
const symbolClass = classifySymbolUse(named, ctx.sources);
|
|
1343
|
-
classified = { reachability: symbolClass, path: classified.path };
|
|
1469
|
+
classified = { reachability: symbolClass, path: classified.path, paths: classified.paths };
|
|
1344
1470
|
}
|
|
1345
1471
|
const steps = reachabilitySteps(classified.reachability);
|
|
1346
1472
|
let severity = steps === 0 ? finding6.severity : downgradeSeverity(finding6.severity, steps);
|
|
@@ -1348,6 +1474,7 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
1348
1474
|
if (classified.path !== void 0) {
|
|
1349
1475
|
const shown = classified.path.join(" > ");
|
|
1350
1476
|
message = `${message} Reachable via ${shown}.`;
|
|
1477
|
+
message = appendOtherPaths(message, classified.paths);
|
|
1351
1478
|
}
|
|
1352
1479
|
const devOnly = isDevOnly(pkg, ctx);
|
|
1353
1480
|
if (devOnly) {
|
|
@@ -1357,16 +1484,27 @@ function applyReachability(findings, ctx, symbols = /* @__PURE__ */ new Map()) {
|
|
|
1357
1484
|
return {
|
|
1358
1485
|
...finding6,
|
|
1359
1486
|
reachability: classified.reachability,
|
|
1360
|
-
...classified
|
|
1487
|
+
...pathFields(classified),
|
|
1361
1488
|
severity,
|
|
1362
1489
|
message
|
|
1363
1490
|
};
|
|
1364
1491
|
});
|
|
1492
|
+
return groupPackageAdvisories(mapped);
|
|
1365
1493
|
}
|
|
1494
|
+
var REACH_RANK;
|
|
1366
1495
|
var init_reachability = __esm({
|
|
1367
1496
|
"src/modules/deps/reachability.ts"() {
|
|
1368
1497
|
init_finding();
|
|
1369
1498
|
init_symbol_reachability();
|
|
1499
|
+
REACH_RANK = {
|
|
1500
|
+
reachable: 0,
|
|
1501
|
+
"symbol-reachable": 1,
|
|
1502
|
+
"reachable-via-dependency": 2,
|
|
1503
|
+
"imported-not-called": 3,
|
|
1504
|
+
"symbol-not-called": 4,
|
|
1505
|
+
"reachability-unknown": 5,
|
|
1506
|
+
"not-imported": 6
|
|
1507
|
+
};
|
|
1370
1508
|
}
|
|
1371
1509
|
});
|
|
1372
1510
|
|
|
@@ -3294,6 +3432,62 @@ var init_experimental2 = __esm({
|
|
|
3294
3432
|
"src/supply/experimental.ts"() {
|
|
3295
3433
|
}
|
|
3296
3434
|
});
|
|
3435
|
+
function isCopyleftLicense(license) {
|
|
3436
|
+
return COPYLEFT.test(license);
|
|
3437
|
+
}
|
|
3438
|
+
function licenseText(value) {
|
|
3439
|
+
if (typeof value === "string") return value;
|
|
3440
|
+
if (typeof value === "object" && value !== null && "type" in value) {
|
|
3441
|
+
const type = value.type;
|
|
3442
|
+
if (typeof type === "string") return type;
|
|
3443
|
+
}
|
|
3444
|
+
return void 0;
|
|
3445
|
+
}
|
|
3446
|
+
async function readJson(path) {
|
|
3447
|
+
try {
|
|
3448
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
3449
|
+
} catch {
|
|
3450
|
+
return void 0;
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
async function scanCopyleftLicenses(cwd) {
|
|
3454
|
+
const manifest = await readJson(join(cwd, "package.json"));
|
|
3455
|
+
if (typeof manifest !== "object" || manifest === null) return [];
|
|
3456
|
+
const names = /* @__PURE__ */ new Set();
|
|
3457
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
3458
|
+
const block = manifest[field];
|
|
3459
|
+
if (typeof block === "object" && block !== null && !Array.isArray(block)) {
|
|
3460
|
+
for (const name of Object.keys(block)) names.add(name);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
const findings = [];
|
|
3464
|
+
for (const name of names) {
|
|
3465
|
+
const pkg = await readJson(join(cwd, "node_modules", name, "package.json"));
|
|
3466
|
+
if (typeof pkg !== "object" || pkg === null) continue;
|
|
3467
|
+
const license = licenseText(pkg.license);
|
|
3468
|
+
if (license === void 0 || !isCopyleftLicense(license)) continue;
|
|
3469
|
+
findings.push({
|
|
3470
|
+
id: makeFindingId("DEPS-LICENSE-001", "package.json", `${name}|${license}`),
|
|
3471
|
+
ruleId: "DEPS-LICENSE-001",
|
|
3472
|
+
module: "deps",
|
|
3473
|
+
title: `Copyleft dependency ${name}`,
|
|
3474
|
+
severity: "medium",
|
|
3475
|
+
confidence: "high",
|
|
3476
|
+
file: "package.json",
|
|
3477
|
+
snippet: `${name} ${license}`,
|
|
3478
|
+
message: `${name} is licensed ${license}.`,
|
|
3479
|
+
remediation: "Replace the dependency or get a legal review before shipping it in a commercial product."
|
|
3480
|
+
});
|
|
3481
|
+
}
|
|
3482
|
+
return findings;
|
|
3483
|
+
}
|
|
3484
|
+
var COPYLEFT;
|
|
3485
|
+
var init_license = __esm({
|
|
3486
|
+
"src/modules/deps/license.ts"() {
|
|
3487
|
+
init_finding();
|
|
3488
|
+
COPYLEFT = /(?:^|[^\w])(?:AGPL|GPL)(?:-|[^\w]|$)/i;
|
|
3489
|
+
}
|
|
3490
|
+
});
|
|
3297
3491
|
|
|
3298
3492
|
// src/fixer/eol.ts
|
|
3299
3493
|
function detectEol(text) {
|
|
@@ -3459,7 +3653,7 @@ function walkJs(dir, out = []) {
|
|
|
3459
3653
|
}
|
|
3460
3654
|
return out;
|
|
3461
3655
|
}
|
|
3462
|
-
function
|
|
3656
|
+
function readJson2(path) {
|
|
3463
3657
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
3464
3658
|
}
|
|
3465
3659
|
function installScriptKeys(scripts) {
|
|
@@ -3470,7 +3664,7 @@ function analyzePackageDir(dir) {
|
|
|
3470
3664
|
const findings = [];
|
|
3471
3665
|
const manifestPath = join(dir, "package.json");
|
|
3472
3666
|
if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) return findings;
|
|
3473
|
-
const pkg =
|
|
3667
|
+
const pkg = readJson2(manifestPath);
|
|
3474
3668
|
const scripts = pkg.scripts ?? {};
|
|
3475
3669
|
const keys = installScriptKeys(scripts);
|
|
3476
3670
|
if (keys.length > 0) {
|
|
@@ -3520,8 +3714,8 @@ function analyzePackageDir(dir) {
|
|
|
3520
3714
|
return findings;
|
|
3521
3715
|
}
|
|
3522
3716
|
function diffPackageVersions(oldDir, newDir) {
|
|
3523
|
-
const oldManifest = existsSync(join(oldDir, "package.json")) ?
|
|
3524
|
-
const nextManifest = existsSync(join(newDir, "package.json")) ?
|
|
3717
|
+
const oldManifest = existsSync(join(oldDir, "package.json")) ? readJson2(join(oldDir, "package.json")) : {};
|
|
3718
|
+
const nextManifest = existsSync(join(newDir, "package.json")) ? readJson2(join(newDir, "package.json")) : {};
|
|
3525
3719
|
const oldScripts = oldManifest.scripts ?? {};
|
|
3526
3720
|
const nextScripts = nextManifest.scripts ?? {};
|
|
3527
3721
|
const added = INSTALL_SCRIPT_KEYS.filter((k) => typeof nextScripts[k] === "string" && oldScripts[k] === void 0);
|
|
@@ -3562,14 +3756,24 @@ function checkLockfileIntegrity(lock) {
|
|
|
3562
3756
|
if (name === "") continue;
|
|
3563
3757
|
const resolved = entry.resolved;
|
|
3564
3758
|
if (!resolved) continue;
|
|
3759
|
+
if (entry.integrity === void 0 || entry.integrity.length === 0) {
|
|
3760
|
+
findings.push(
|
|
3761
|
+
finding(
|
|
3762
|
+
"SUPPLY-LOCKINTEGRITY-001",
|
|
3763
|
+
"Lockfile entry has no integrity hash",
|
|
3764
|
+
`${name} resolved without an integrity hash`,
|
|
3765
|
+
"package-lock.json"
|
|
3766
|
+
)
|
|
3767
|
+
);
|
|
3768
|
+
}
|
|
3565
3769
|
try {
|
|
3566
|
-
const
|
|
3567
|
-
if (
|
|
3770
|
+
const url = new URL(resolved);
|
|
3771
|
+
if (url.protocol !== "https:" || url.hostname !== REGISTRY_HOST) {
|
|
3568
3772
|
findings.push(
|
|
3569
3773
|
finding(
|
|
3570
3774
|
"SUPPLY-LOCKURL-001",
|
|
3571
3775
|
"Lockfile entry resolves from a non-npm registry host",
|
|
3572
|
-
`${name} resolved host ${
|
|
3776
|
+
`${name} resolved host ${url.hostname || resolved}`,
|
|
3573
3777
|
"package-lock.json"
|
|
3574
3778
|
)
|
|
3575
3779
|
);
|
|
@@ -3632,7 +3836,7 @@ function packageSources(dir) {
|
|
|
3632
3836
|
let scripts = "";
|
|
3633
3837
|
if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
|
|
3634
3838
|
try {
|
|
3635
|
-
const pkg =
|
|
3839
|
+
const pkg = readJson2(manifestPath);
|
|
3636
3840
|
const block = pkg.scripts ?? {};
|
|
3637
3841
|
scripts = Object.values(block).join("\n");
|
|
3638
3842
|
} catch {
|
|
@@ -3945,6 +4149,8 @@ var init_deps = __esm({
|
|
|
3945
4149
|
init_semver();
|
|
3946
4150
|
init_typosquat();
|
|
3947
4151
|
init_experimental2();
|
|
4152
|
+
init_experimental();
|
|
4153
|
+
init_license();
|
|
3948
4154
|
init_phase31();
|
|
3949
4155
|
LOCKFILE_NAMES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
3950
4156
|
MANIFEST_FIELDS2 = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
@@ -4026,6 +4232,7 @@ var init_deps = __esm({
|
|
|
4026
4232
|
reached = attachOverrideProposals(reached, graph, lockFile, manifestRaw);
|
|
4027
4233
|
reached.push(...scanInstalledPackages(ctx.cwd, packages));
|
|
4028
4234
|
}
|
|
4235
|
+
if (licenseEnabled()) reached.push(...await scanCopyleftLicenses(ctx.cwd));
|
|
4029
4236
|
return reached;
|
|
4030
4237
|
}
|
|
4031
4238
|
};
|
|
@@ -4939,7 +5146,7 @@ function seedTaintV2(functions, facts, seeds, returners, nameToFn) {
|
|
|
4939
5146
|
}
|
|
4940
5147
|
return changed;
|
|
4941
5148
|
}
|
|
4942
|
-
function analyzeFile(ast,
|
|
5149
|
+
function analyzeFile(ast, extraReturners2 = NO_RETURNERS) {
|
|
4943
5150
|
attachParents(ast);
|
|
4944
5151
|
const facts = /* @__PURE__ */ new WeakMap();
|
|
4945
5152
|
const functions = [];
|
|
@@ -4953,7 +5160,7 @@ function analyzeFile(ast, extraReturners = NO_RETURNERS) {
|
|
|
4953
5160
|
}
|
|
4954
5161
|
const seeds = /* @__PURE__ */ new Map();
|
|
4955
5162
|
for (const fn of functions) seeds.set(fn, /* @__PURE__ */ new Set());
|
|
4956
|
-
const returners = new Set(
|
|
5163
|
+
const returners = new Set(extraReturners2);
|
|
4957
5164
|
for (let hop = 0; hop < MAX_TAINT_HOPS; hop += 1) {
|
|
4958
5165
|
let changed = false;
|
|
4959
5166
|
facts.set(ast, analyzeScope(ast, [], returners));
|
|
@@ -6944,6 +7151,44 @@ var init_injection = __esm({
|
|
|
6944
7151
|
PHRASES = [/ignore previous/i, /mark as safe/i, /ai reviewer/i];
|
|
6945
7152
|
}
|
|
6946
7153
|
});
|
|
7154
|
+
|
|
7155
|
+
// src/runtime/lru.ts
|
|
7156
|
+
var LruMap;
|
|
7157
|
+
var init_lru = __esm({
|
|
7158
|
+
"src/runtime/lru.ts"() {
|
|
7159
|
+
LruMap = class {
|
|
7160
|
+
constructor(max) {
|
|
7161
|
+
this.max = max;
|
|
7162
|
+
if (max < 1) throw new Error("LruMap max must be >= 1");
|
|
7163
|
+
}
|
|
7164
|
+
max;
|
|
7165
|
+
map = /* @__PURE__ */ new Map();
|
|
7166
|
+
get size() {
|
|
7167
|
+
return this.map.size;
|
|
7168
|
+
}
|
|
7169
|
+
has(key) {
|
|
7170
|
+
return this.map.has(key);
|
|
7171
|
+
}
|
|
7172
|
+
get(key) {
|
|
7173
|
+
if (!this.map.has(key)) return void 0;
|
|
7174
|
+
const value = this.map.get(key);
|
|
7175
|
+
this.map.delete(key);
|
|
7176
|
+
this.map.set(key, value);
|
|
7177
|
+
return value;
|
|
7178
|
+
}
|
|
7179
|
+
set(key, value) {
|
|
7180
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
7181
|
+
this.map.set(key, value);
|
|
7182
|
+
while (this.map.size > this.max) {
|
|
7183
|
+
const oldest = this.map.keys().next().value;
|
|
7184
|
+
if (oldest === void 0) break;
|
|
7185
|
+
this.map.delete(oldest);
|
|
7186
|
+
}
|
|
7187
|
+
return this;
|
|
7188
|
+
}
|
|
7189
|
+
};
|
|
7190
|
+
}
|
|
7191
|
+
});
|
|
6947
7192
|
function packageVersion() {
|
|
6948
7193
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
6949
7194
|
for (let depth = 0; depth < 6; depth += 1) {
|
|
@@ -6972,9 +7217,15 @@ function parsePoolSize() {
|
|
|
6972
7217
|
const raw = process.env.FULLSHIELD_PARSE_WORKERS;
|
|
6973
7218
|
if (raw !== void 0 && raw !== "") {
|
|
6974
7219
|
const parsed = Number(raw);
|
|
6975
|
-
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7220
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.min(4, Math.floor(parsed));
|
|
6976
7221
|
}
|
|
6977
|
-
return Math.max(1, availableParallelism() - 1);
|
|
7222
|
+
return Math.min(2, Math.max(1, availableParallelism() - 1));
|
|
7223
|
+
}
|
|
7224
|
+
function workerMemoryMb() {
|
|
7225
|
+
const raw = process.env.FULLSHIELD_PARSE_WORKER_MB;
|
|
7226
|
+
const parsed = raw !== void 0 && raw !== "" ? Number(raw) : 512;
|
|
7227
|
+
if (Number.isFinite(parsed) && parsed >= 64) return Math.floor(parsed);
|
|
7228
|
+
return 512;
|
|
6978
7229
|
}
|
|
6979
7230
|
function parseCacheKey(code) {
|
|
6980
7231
|
return createHash("sha256").update(packageVersion()).update("\0").update(code).digest("hex");
|
|
@@ -6984,12 +7235,37 @@ function parseOne(code) {
|
|
|
6984
7235
|
const hit = cache.get(key);
|
|
6985
7236
|
if (hit !== void 0) return Promise.resolve(hit);
|
|
6986
7237
|
return new Promise((resolve8, reject) => {
|
|
6987
|
-
const worker = new Worker(fileURLToPath(workerUrl), {
|
|
6988
|
-
|
|
7238
|
+
const worker = new Worker(fileURLToPath(workerUrl), {
|
|
7239
|
+
workerData: { code },
|
|
7240
|
+
resourceLimits: { maxOldGenerationSizeMb: workerMemoryMb() }
|
|
7241
|
+
});
|
|
7242
|
+
let settled = false;
|
|
7243
|
+
const finish = () => {
|
|
7244
|
+
void worker.terminate();
|
|
7245
|
+
};
|
|
7246
|
+
const succeed = (ast) => {
|
|
7247
|
+
if (settled) return;
|
|
7248
|
+
settled = true;
|
|
6989
7249
|
cache.set(key, ast);
|
|
7250
|
+
finish();
|
|
6990
7251
|
resolve8(ast);
|
|
7252
|
+
};
|
|
7253
|
+
const fail = (error) => {
|
|
7254
|
+
if (settled) return;
|
|
7255
|
+
settled = true;
|
|
7256
|
+
finish();
|
|
7257
|
+
reject(error);
|
|
7258
|
+
};
|
|
7259
|
+
worker.once("message", (ast) => {
|
|
7260
|
+
succeed(ast);
|
|
7261
|
+
});
|
|
7262
|
+
worker.once("error", (error) => {
|
|
7263
|
+
fail(error);
|
|
7264
|
+
});
|
|
7265
|
+
worker.once("exit", (code2) => {
|
|
7266
|
+
if (settled) return;
|
|
7267
|
+
fail(new Error(`parse worker exited ${code2 ?? "null"}`));
|
|
6991
7268
|
});
|
|
6992
|
-
worker.once("error", reject);
|
|
6993
7269
|
});
|
|
6994
7270
|
}
|
|
6995
7271
|
async function parseWithPool(codes) {
|
|
@@ -7013,9 +7289,10 @@ async function parseWithPool(codes) {
|
|
|
7013
7289
|
var workerUrl, cache;
|
|
7014
7290
|
var init_parse_pool = __esm({
|
|
7015
7291
|
"src/core/parse-pool.ts"() {
|
|
7292
|
+
init_lru();
|
|
7016
7293
|
init_version();
|
|
7017
7294
|
workerUrl = new URL("./parse-worker.mjs", import.meta.url);
|
|
7018
|
-
cache =
|
|
7295
|
+
cache = new LruMap(64);
|
|
7019
7296
|
}
|
|
7020
7297
|
});
|
|
7021
7298
|
async function parseSource(code) {
|
|
@@ -7033,8 +7310,8 @@ function snippetOf(source, node) {
|
|
|
7033
7310
|
if (node.range === void 0) return "";
|
|
7034
7311
|
return source.slice(node.range[0], node.range[1]).replace(/\s+/g, " ").trim().slice(0, 120);
|
|
7035
7312
|
}
|
|
7036
|
-
function findingsFromAst(filename, source, ast,
|
|
7037
|
-
const facts = analyzeFile(ast,
|
|
7313
|
+
function findingsFromAst(filename, source, ast, extraReturners2 = /* @__PURE__ */ new Set(), projectCsrfProtected = false) {
|
|
7314
|
+
const facts = analyzeFile(ast, extraReturners2);
|
|
7038
7315
|
const findings = [];
|
|
7039
7316
|
for (const rule of sastRules) {
|
|
7040
7317
|
const ctx = {
|
|
@@ -7112,6 +7389,158 @@ function findingsFromTemplate(filename, source) {
|
|
|
7112
7389
|
xssRule.create(ctx);
|
|
7113
7390
|
return findings;
|
|
7114
7391
|
}
|
|
7392
|
+
function dedupeSastSinks(findings) {
|
|
7393
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7394
|
+
const out = [];
|
|
7395
|
+
for (const finding6 of findings) {
|
|
7396
|
+
if (finding6.module !== "sast" || finding6.line === void 0) {
|
|
7397
|
+
out.push(finding6);
|
|
7398
|
+
continue;
|
|
7399
|
+
}
|
|
7400
|
+
const key = `${finding6.ruleId}\0${finding6.file ?? ""}\0${finding6.line}`;
|
|
7401
|
+
if (seen.has(key)) continue;
|
|
7402
|
+
seen.add(key);
|
|
7403
|
+
out.push(finding6);
|
|
7404
|
+
}
|
|
7405
|
+
return out;
|
|
7406
|
+
}
|
|
7407
|
+
function sastStreamThreshold() {
|
|
7408
|
+
const raw = process.env.FULLSHIELD_SAST_STREAM_AT;
|
|
7409
|
+
if (raw !== void 0 && raw !== "") {
|
|
7410
|
+
const parsed = Number(raw);
|
|
7411
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7412
|
+
}
|
|
7413
|
+
return 4e3;
|
|
7414
|
+
}
|
|
7415
|
+
function sastBatchSize() {
|
|
7416
|
+
const raw = process.env.FULLSHIELD_SAST_BATCH;
|
|
7417
|
+
if (raw !== void 0 && raw !== "") {
|
|
7418
|
+
const parsed = Number(raw);
|
|
7419
|
+
if (Number.isFinite(parsed) && parsed >= 1) return Math.floor(parsed);
|
|
7420
|
+
}
|
|
7421
|
+
return 150;
|
|
7422
|
+
}
|
|
7423
|
+
function skipFinding(rel, message) {
|
|
7424
|
+
return {
|
|
7425
|
+
id: makeFindingId("SAST-SKIP-001", rel, message),
|
|
7426
|
+
ruleId: "SAST-SKIP-001",
|
|
7427
|
+
module: "sast",
|
|
7428
|
+
title: "File skipped by SAST",
|
|
7429
|
+
severity: "info",
|
|
7430
|
+
confidence: "high",
|
|
7431
|
+
file: rel,
|
|
7432
|
+
message,
|
|
7433
|
+
remediation: "Raise sast.maxFileBytes in fullshield.config.json to parse this file, or leave it skipped."
|
|
7434
|
+
};
|
|
7435
|
+
}
|
|
7436
|
+
function skipReason(source, maxBytes) {
|
|
7437
|
+
if (source.includes("\0")) return "it contains NUL bytes";
|
|
7438
|
+
const bytes = Buffer.byteLength(source);
|
|
7439
|
+
if (bytes > maxBytes) return `${bytes} bytes exceeds the SAST parse limit of ${maxBytes} bytes`;
|
|
7440
|
+
return void 0;
|
|
7441
|
+
}
|
|
7442
|
+
async function readScanSource(cwd, rel) {
|
|
7443
|
+
try {
|
|
7444
|
+
return await readFile(join(cwd, rel), "utf8");
|
|
7445
|
+
} catch {
|
|
7446
|
+
return void 0;
|
|
7447
|
+
}
|
|
7448
|
+
}
|
|
7449
|
+
function cloneAst(value) {
|
|
7450
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
7451
|
+
try {
|
|
7452
|
+
return structuredClone(value);
|
|
7453
|
+
} catch {
|
|
7454
|
+
return void 0;
|
|
7455
|
+
}
|
|
7456
|
+
}
|
|
7457
|
+
async function parseInProcess(source) {
|
|
7458
|
+
try {
|
|
7459
|
+
return await parseSource(source);
|
|
7460
|
+
} catch {
|
|
7461
|
+
return void 0;
|
|
7462
|
+
}
|
|
7463
|
+
}
|
|
7464
|
+
async function parseCode(source) {
|
|
7465
|
+
if (!parsePoolEnabled()) return parseInProcess(source);
|
|
7466
|
+
try {
|
|
7467
|
+
const cloned = cloneAst((await parseWithPool([source]))[0]);
|
|
7468
|
+
if (cloned !== void 0) return cloned;
|
|
7469
|
+
} catch {
|
|
7470
|
+
}
|
|
7471
|
+
return parseInProcess(source);
|
|
7472
|
+
}
|
|
7473
|
+
function releaseParsed(parsed) {
|
|
7474
|
+
for (const entry of parsed) {
|
|
7475
|
+
entry.source = "";
|
|
7476
|
+
entry.ast = void 0;
|
|
7477
|
+
}
|
|
7478
|
+
parsed.length = 0;
|
|
7479
|
+
}
|
|
7480
|
+
function extraReturners(rel, ast, files, returning) {
|
|
7481
|
+
const ns = collectRequires(rel, ast, files);
|
|
7482
|
+
const extra = new Set(returning.get(rel));
|
|
7483
|
+
for (const [local, spec] of ns) {
|
|
7484
|
+
const hash = spec.indexOf("#");
|
|
7485
|
+
const file = hash >= 0 ? spec.slice(0, hash) : spec;
|
|
7486
|
+
const exp = hash >= 0 ? spec.slice(hash + 1) : local;
|
|
7487
|
+
if (returning.get(file)?.has(exp)) extra.add(local);
|
|
7488
|
+
}
|
|
7489
|
+
return extra;
|
|
7490
|
+
}
|
|
7491
|
+
async function runSastStreaming(ctx) {
|
|
7492
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
7493
|
+
const findings = [];
|
|
7494
|
+
const returning = /* @__PURE__ */ new Map();
|
|
7495
|
+
const parseable = [];
|
|
7496
|
+
const fileSet = /* @__PURE__ */ new Set();
|
|
7497
|
+
let projectCsrf = false;
|
|
7498
|
+
for (const rel of ctx.files) {
|
|
7499
|
+
const isSource = SOURCE_EXT.test(rel);
|
|
7500
|
+
const isTemplate = TEMPLATE_EXT.test(rel);
|
|
7501
|
+
if (!isSource && !isTemplate) continue;
|
|
7502
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
7503
|
+
if (source === void 0) continue;
|
|
7504
|
+
const why = skipReason(source, maxBytes);
|
|
7505
|
+
if (why !== void 0) {
|
|
7506
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
7507
|
+
continue;
|
|
7508
|
+
}
|
|
7509
|
+
if (sourceHasCsrfGuard(source)) projectCsrf = true;
|
|
7510
|
+
if (isTemplate) {
|
|
7511
|
+
findings.push(...findingsFromTemplate(rel, source));
|
|
7512
|
+
continue;
|
|
7513
|
+
}
|
|
7514
|
+
const ast = await parseCode(source);
|
|
7515
|
+
if (ast === void 0) continue;
|
|
7516
|
+
returning.set(rel, functionsThatReturnTaint(ast));
|
|
7517
|
+
parseable.push(rel);
|
|
7518
|
+
fileSet.add(rel);
|
|
7519
|
+
}
|
|
7520
|
+
for (const rel of parseable) {
|
|
7521
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
7522
|
+
if (source === void 0) continue;
|
|
7523
|
+
const ast = await parseCode(source);
|
|
7524
|
+
if (ast === void 0) continue;
|
|
7525
|
+
findings.push(
|
|
7526
|
+
...findingsFromAst(rel, source, ast, extraReturners(rel, ast, fileSet, returning), projectCsrf)
|
|
7527
|
+
);
|
|
7528
|
+
}
|
|
7529
|
+
const batch = sastBatchSize();
|
|
7530
|
+
for (let i = 0; i < parseable.length; i += batch) {
|
|
7531
|
+
const parsed = [];
|
|
7532
|
+
for (const rel of parseable.slice(i, i + batch)) {
|
|
7533
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
7534
|
+
if (source === void 0) continue;
|
|
7535
|
+
const ast = await parseCode(source);
|
|
7536
|
+
if (ast === void 0) continue;
|
|
7537
|
+
parsed.push({ file: rel, source, ast });
|
|
7538
|
+
}
|
|
7539
|
+
findings.push(...runInterproc(parsed));
|
|
7540
|
+
releaseParsed(parsed);
|
|
7541
|
+
}
|
|
7542
|
+
return dedupeSastSinks(findings);
|
|
7543
|
+
}
|
|
7115
7544
|
var sastRules, SOURCE_EXT, sastModule;
|
|
7116
7545
|
var init_engine = __esm({
|
|
7117
7546
|
"src/modules/sast/engine.ts"() {
|
|
@@ -7153,49 +7582,48 @@ var init_engine = __esm({
|
|
|
7153
7582
|
sastModule = {
|
|
7154
7583
|
name: "sast",
|
|
7155
7584
|
async run(ctx) {
|
|
7585
|
+
const candidates = ctx.files.filter((rel) => SOURCE_EXT.test(rel) || TEMPLATE_EXT.test(rel));
|
|
7586
|
+
if (candidates.length > sastStreamThreshold()) return runSastStreaming(ctx);
|
|
7587
|
+
const maxBytes = ctx.config.sast.maxFileBytes;
|
|
7156
7588
|
const findings = [];
|
|
7157
7589
|
const parsed = [];
|
|
7158
|
-
for (const rel of
|
|
7159
|
-
const
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
source = await readFile(join(ctx.cwd, rel), "utf8");
|
|
7165
|
-
} catch {
|
|
7590
|
+
for (const rel of candidates) {
|
|
7591
|
+
const source = await readScanSource(ctx.cwd, rel);
|
|
7592
|
+
if (source === void 0) continue;
|
|
7593
|
+
const why = skipReason(source, maxBytes);
|
|
7594
|
+
if (why !== void 0) {
|
|
7595
|
+
findings.push(skipFinding(rel, `Skipped ${rel}: ${why}.`));
|
|
7166
7596
|
continue;
|
|
7167
7597
|
}
|
|
7168
|
-
if (
|
|
7169
|
-
if (isTemplate) {
|
|
7598
|
+
if (TEMPLATE_EXT.test(rel)) {
|
|
7170
7599
|
findings.push(...findingsFromTemplate(rel, source));
|
|
7171
7600
|
continue;
|
|
7172
7601
|
}
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
parsed.push({ file: rel, source, ast });
|
|
7177
|
-
} catch {
|
|
7178
|
-
}
|
|
7602
|
+
const ast = await parseCode(source);
|
|
7603
|
+
if (ast === void 0) continue;
|
|
7604
|
+
parsed.push({ file: rel, source, ast });
|
|
7179
7605
|
}
|
|
7180
7606
|
const files = new Set(parsed.map((p) => p.file));
|
|
7181
7607
|
const projectCsrf = parsed.some((p) => sourceHasCsrfGuard(p.source));
|
|
7182
7608
|
const returning = /* @__PURE__ */ new Map();
|
|
7183
|
-
for (const
|
|
7184
|
-
returning.set(
|
|
7609
|
+
for (const entry of parsed) {
|
|
7610
|
+
returning.set(entry.file, functionsThatReturnTaint(entry.ast));
|
|
7185
7611
|
}
|
|
7186
|
-
for (const
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7612
|
+
for (const entry of parsed) {
|
|
7613
|
+
findings.push(
|
|
7614
|
+
...findingsFromAst(
|
|
7615
|
+
entry.file,
|
|
7616
|
+
entry.source,
|
|
7617
|
+
entry.ast,
|
|
7618
|
+
extraReturners(entry.file, entry.ast, files, returning),
|
|
7619
|
+
projectCsrf
|
|
7620
|
+
)
|
|
7621
|
+
);
|
|
7196
7622
|
}
|
|
7197
7623
|
findings.push(...runInterproc(parsed));
|
|
7198
|
-
|
|
7624
|
+
const out = dedupeSastSinks(findings);
|
|
7625
|
+
releaseParsed(parsed);
|
|
7626
|
+
return out;
|
|
7199
7627
|
}
|
|
7200
7628
|
};
|
|
7201
7629
|
}
|
|
@@ -8121,16 +8549,37 @@ var init_provider = __esm({
|
|
|
8121
8549
|
FIXTURE_COST = 0.01;
|
|
8122
8550
|
}
|
|
8123
8551
|
});
|
|
8552
|
+
function isIsoDate(value) {
|
|
8553
|
+
if (!/^\d{4}-\d{2}-\d{2}(T[\d:.+\-Z]+)?$/.test(value)) return false;
|
|
8554
|
+
return Number.isFinite(Date.parse(value));
|
|
8555
|
+
}
|
|
8556
|
+
function expiresAtMs(expires) {
|
|
8557
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8558
|
+
return Date.parse(expires);
|
|
8559
|
+
}
|
|
8560
|
+
function validSuppression(value) {
|
|
8561
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
8562
|
+
const row = value;
|
|
8563
|
+
if (typeof row.ruleId !== "string" || row.ruleId.length === 0) return void 0;
|
|
8564
|
+
if (typeof row.owner !== "string" || row.owner.length === 0) return void 0;
|
|
8565
|
+
if (typeof row.reason !== "string" || row.reason.length === 0) return void 0;
|
|
8566
|
+
if (typeof row.expires !== "string" || !isIsoDate(row.expires)) return void 0;
|
|
8567
|
+
const file = row.file === void 0 ? void 0 : row.file.replace(/\\/g, "/");
|
|
8568
|
+
if (row.file !== void 0 && (typeof row.file !== "string" || row.file.length === 0)) return void 0;
|
|
8569
|
+
return { ruleId: row.ruleId, file, owner: row.owner, reason: row.reason, expires: row.expires };
|
|
8570
|
+
}
|
|
8124
8571
|
async function readBaseline(path) {
|
|
8125
8572
|
try {
|
|
8126
8573
|
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
8127
8574
|
if (raw.version !== 1 || !Array.isArray(raw.findings)) return void 0;
|
|
8575
|
+
if (raw.suppressions !== void 0 && !Array.isArray(raw.suppressions)) return void 0;
|
|
8128
8576
|
return raw;
|
|
8129
8577
|
} catch {
|
|
8130
8578
|
return void 0;
|
|
8131
8579
|
}
|
|
8132
8580
|
}
|
|
8133
8581
|
async function writeBaseline(path, findings) {
|
|
8582
|
+
const existing = await readBaseline(path);
|
|
8134
8583
|
const doc = {
|
|
8135
8584
|
version: 1,
|
|
8136
8585
|
findings: findings.map((f) => ({
|
|
@@ -8138,7 +8587,8 @@ async function writeBaseline(path, findings) {
|
|
|
8138
8587
|
ruleId: f.ruleId,
|
|
8139
8588
|
file: f.file,
|
|
8140
8589
|
line: f.line
|
|
8141
|
-
}))
|
|
8590
|
+
})),
|
|
8591
|
+
...existing?.suppressions !== void 0 ? { suppressions: existing.suppressions } : {}
|
|
8142
8592
|
};
|
|
8143
8593
|
await writeFile(path, `${JSON.stringify(doc, null, 2)}
|
|
8144
8594
|
`, "utf8");
|
|
@@ -8148,6 +8598,30 @@ function applyBaseline(findings, baseline) {
|
|
|
8148
8598
|
const known = new Set(baseline.findings.map((f) => f.id));
|
|
8149
8599
|
return findings.filter((f) => !known.has(f.id));
|
|
8150
8600
|
}
|
|
8601
|
+
function applySuppressions(findings, rows, now = /* @__PURE__ */ new Date()) {
|
|
8602
|
+
if (rows === void 0 || rows.length === 0) return { findings: [...findings], errors: [] };
|
|
8603
|
+
const errors = [];
|
|
8604
|
+
const active = [];
|
|
8605
|
+
rows.forEach((row, index) => {
|
|
8606
|
+
const parsed = validSuppression(row);
|
|
8607
|
+
if (parsed === void 0) {
|
|
8608
|
+
errors.push(`suppression ${index + 1} needs ruleId, owner, reason, and an ISO expires date`);
|
|
8609
|
+
return;
|
|
8610
|
+
}
|
|
8611
|
+
if (expiresAtMs(parsed.expires) < now.getTime()) {
|
|
8612
|
+
errors.push(
|
|
8613
|
+
`suppression for ${parsed.ruleId} expired on ${parsed.expires} (owner: ${parsed.owner}); renew or remove it`
|
|
8614
|
+
);
|
|
8615
|
+
return;
|
|
8616
|
+
}
|
|
8617
|
+
active.push(parsed);
|
|
8618
|
+
});
|
|
8619
|
+
const kept = findings.filter((finding6) => {
|
|
8620
|
+
const file = (finding6.file ?? "").replace(/\\/g, "/");
|
|
8621
|
+
return !active.some((row) => row.ruleId === finding6.ruleId && (row.file === void 0 || row.file === file));
|
|
8622
|
+
});
|
|
8623
|
+
return { findings: kept, errors };
|
|
8624
|
+
}
|
|
8151
8625
|
var init_baseline = __esm({
|
|
8152
8626
|
"src/core/baseline.ts"() {
|
|
8153
8627
|
}
|
|
@@ -8253,6 +8727,32 @@ function validate(raw, source) {
|
|
|
8253
8727
|
}
|
|
8254
8728
|
out.ai = ai;
|
|
8255
8729
|
}
|
|
8730
|
+
if (record.defaultExcludes !== void 0) {
|
|
8731
|
+
if (typeof record.defaultExcludes !== "boolean") {
|
|
8732
|
+
throw new ConfigError(`${source}: "defaultExcludes" must be a boolean`);
|
|
8733
|
+
}
|
|
8734
|
+
out.defaultExcludes = record.defaultExcludes;
|
|
8735
|
+
}
|
|
8736
|
+
if (record.excludes !== void 0) {
|
|
8737
|
+
if (!Array.isArray(record.excludes) || record.excludes.some((p) => typeof p !== "string")) {
|
|
8738
|
+
throw new ConfigError(`${source}: "excludes" must be an array of glob strings`);
|
|
8739
|
+
}
|
|
8740
|
+
out.excludes = record.excludes;
|
|
8741
|
+
}
|
|
8742
|
+
if (record.sast !== void 0) {
|
|
8743
|
+
if (typeof record.sast !== "object" || record.sast === null || Array.isArray(record.sast)) {
|
|
8744
|
+
throw new ConfigError(`${source}: "sast" must be an object`);
|
|
8745
|
+
}
|
|
8746
|
+
const rawSast = record.sast;
|
|
8747
|
+
const sast = { maxFileBytes: 1024 * 1024 };
|
|
8748
|
+
if (rawSast.maxFileBytes !== void 0) {
|
|
8749
|
+
if (typeof rawSast.maxFileBytes !== "number" || rawSast.maxFileBytes < 1) {
|
|
8750
|
+
throw new ConfigError(`${source}: "sast.maxFileBytes" must be a positive number`);
|
|
8751
|
+
}
|
|
8752
|
+
sast.maxFileBytes = rawSast.maxFileBytes;
|
|
8753
|
+
}
|
|
8754
|
+
out.sast = sast;
|
|
8755
|
+
}
|
|
8256
8756
|
return out;
|
|
8257
8757
|
}
|
|
8258
8758
|
async function loadConfig(cwd, configPath) {
|
|
@@ -8287,19 +8787,35 @@ var init_config2 = __esm({
|
|
|
8287
8787
|
modules: [...MODULE_NAMES],
|
|
8288
8788
|
failOn: "high",
|
|
8289
8789
|
ignore: ["dist/**", "**/*.test.ts"],
|
|
8790
|
+
defaultExcludes: true,
|
|
8290
8791
|
baseline: ".fullshield-baseline.json",
|
|
8291
8792
|
notify: { slackWebhookEnv: "FULLSHIELD_SLACK_WEBHOOK" },
|
|
8292
8793
|
deps: { offline: false },
|
|
8293
|
-
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 }
|
|
8794
|
+
ai: { enabled: false, maxCostUsd: 1, maxFindings: 50 },
|
|
8795
|
+
sast: { maxFileBytes: 1024 * 1024 }
|
|
8294
8796
|
};
|
|
8295
8797
|
ConfigError = class extends Error {
|
|
8296
8798
|
};
|
|
8297
|
-
KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
8799
|
+
KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
8800
|
+
"modules",
|
|
8801
|
+
"failOn",
|
|
8802
|
+
"ignore",
|
|
8803
|
+
"defaultExcludes",
|
|
8804
|
+
"excludes",
|
|
8805
|
+
"baseline",
|
|
8806
|
+
"notify",
|
|
8807
|
+
"deps",
|
|
8808
|
+
"ai",
|
|
8809
|
+
"sast"
|
|
8810
|
+
]);
|
|
8298
8811
|
}
|
|
8299
8812
|
});
|
|
8300
8813
|
async function walkFiles(options) {
|
|
8301
8814
|
const ig = ignoreFactory();
|
|
8302
|
-
ig.add(
|
|
8815
|
+
ig.add(HARD_IGNORED);
|
|
8816
|
+
if (options.defaultExcludes !== false) {
|
|
8817
|
+
ig.add(options.excludes ?? DEFAULT_EXCLUDES);
|
|
8818
|
+
}
|
|
8303
8819
|
try {
|
|
8304
8820
|
ig.add(await readFile(join(options.cwd, ".gitignore"), "utf8"));
|
|
8305
8821
|
} catch {
|
|
@@ -8325,11 +8841,24 @@ async function walkFiles(options) {
|
|
|
8325
8841
|
await walk5("");
|
|
8326
8842
|
return files.sort();
|
|
8327
8843
|
}
|
|
8328
|
-
var ignoreFactory,
|
|
8844
|
+
var ignoreFactory, HARD_IGNORED, DEFAULT_EXCLUDES;
|
|
8329
8845
|
var init_fileWalker = __esm({
|
|
8330
8846
|
"src/core/fileWalker.ts"() {
|
|
8331
8847
|
ignoreFactory = ignoreModule.default;
|
|
8332
|
-
|
|
8848
|
+
HARD_IGNORED = [".git/", ".fullshield/"];
|
|
8849
|
+
DEFAULT_EXCLUDES = [
|
|
8850
|
+
"node_modules/",
|
|
8851
|
+
"dist/",
|
|
8852
|
+
"build/",
|
|
8853
|
+
".next/",
|
|
8854
|
+
"coverage/",
|
|
8855
|
+
"vendor/",
|
|
8856
|
+
"vendored/",
|
|
8857
|
+
"third_party/",
|
|
8858
|
+
"third-party/",
|
|
8859
|
+
"compiled/",
|
|
8860
|
+
"*.min.js"
|
|
8861
|
+
];
|
|
8333
8862
|
}
|
|
8334
8863
|
});
|
|
8335
8864
|
async function listStagedFiles(cwd) {
|
|
@@ -8543,7 +9072,7 @@ function resolvePolicyBranch(cwd, env = process.env) {
|
|
|
8543
9072
|
return "*";
|
|
8544
9073
|
}
|
|
8545
9074
|
}
|
|
8546
|
-
function
|
|
9075
|
+
function expiresAtMs2(expires) {
|
|
8547
9076
|
if (/^\d{4}-\d{2}-\d{2}$/.test(expires)) {
|
|
8548
9077
|
return Date.parse(`${expires}T23:59:59.999Z`);
|
|
8549
9078
|
}
|
|
@@ -8565,7 +9094,7 @@ function applyPolicy(findings, policy, ctx = {}) {
|
|
|
8565
9094
|
});
|
|
8566
9095
|
const suppressed = /* @__PURE__ */ new Set();
|
|
8567
9096
|
for (const exception of policy.exceptions ?? []) {
|
|
8568
|
-
if (
|
|
9097
|
+
if (expiresAtMs2(exception.expires) < now.getTime()) {
|
|
8569
9098
|
errors.push(
|
|
8570
9099
|
`policy exception for ${exception.ruleId} expired on ${exception.expires} (owner: ${exception.owner}); renew or remove it`
|
|
8571
9100
|
);
|
|
@@ -8634,6 +9163,7 @@ var init_incremental = __esm({
|
|
|
8634
9163
|
// src/modules/secrets/history.ts
|
|
8635
9164
|
var history_exports = {};
|
|
8636
9165
|
__export(history_exports, {
|
|
9166
|
+
historyCommitLimit: () => historyCommitLimit,
|
|
8637
9167
|
scanGitHistory: () => scanGitHistory
|
|
8638
9168
|
});
|
|
8639
9169
|
function gitError(error) {
|
|
@@ -8676,7 +9206,13 @@ function remapHit(hit, file) {
|
|
|
8676
9206
|
secret: hit.secret
|
|
8677
9207
|
};
|
|
8678
9208
|
}
|
|
8679
|
-
|
|
9209
|
+
function historyCommitLimit(raw = process.env.FULLSHIELD_HISTORY_MAX) {
|
|
9210
|
+
if (raw === void 0 || raw.trim() === "") return 500;
|
|
9211
|
+
const parsed = Number(raw);
|
|
9212
|
+
if (!Number.isInteger(parsed) || parsed < 1) return 500;
|
|
9213
|
+
return Math.min(parsed, 5e3);
|
|
9214
|
+
}
|
|
9215
|
+
async function streamLogChanges(cwd, maxCommits) {
|
|
8680
9216
|
const changes = [];
|
|
8681
9217
|
await gitOk(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
8682
9218
|
const child = spawn(
|
|
@@ -8693,6 +9229,8 @@ async function streamLogChanges(cwd) {
|
|
|
8693
9229
|
"--format=FSCOMMIT:%H",
|
|
8694
9230
|
"--no-textconv",
|
|
8695
9231
|
"--no-ext-diff",
|
|
9232
|
+
"-n",
|
|
9233
|
+
String(maxCommits),
|
|
8696
9234
|
"HEAD"
|
|
8697
9235
|
],
|
|
8698
9236
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
@@ -8761,8 +9299,14 @@ async function headSecretKeys(cwd) {
|
|
|
8761
9299
|
}
|
|
8762
9300
|
return keys;
|
|
8763
9301
|
}
|
|
8764
|
-
async function
|
|
8765
|
-
const
|
|
9302
|
+
async function commitCount(cwd) {
|
|
9303
|
+
const raw = await gitOk(cwd, ["rev-list", "--count", "HEAD"]);
|
|
9304
|
+
const total = Number(raw.trim());
|
|
9305
|
+
return Number.isInteger(total) && total >= 0 ? total : 0;
|
|
9306
|
+
}
|
|
9307
|
+
async function scanGitHistory(cwd, maxCommits = historyCommitLimit()) {
|
|
9308
|
+
const total = await commitCount(cwd);
|
|
9309
|
+
const changes = await streamLogChanges(cwd, maxCommits);
|
|
8766
9310
|
const blobHits = /* @__PURE__ */ new Map();
|
|
8767
9311
|
const first = /* @__PURE__ */ new Map();
|
|
8768
9312
|
for (const change of changes) {
|
|
@@ -8799,7 +9343,13 @@ async function scanGitHistory(cwd) {
|
|
|
8799
9343
|
secret: hit.secret
|
|
8800
9344
|
});
|
|
8801
9345
|
}
|
|
8802
|
-
|
|
9346
|
+
const scanned = Math.min(maxCommits, total);
|
|
9347
|
+
const history = total > maxCommits ? `scanned ${scanned} of ${total} commits` : void 0;
|
|
9348
|
+
return {
|
|
9349
|
+
findings: hits.map((h) => h.finding),
|
|
9350
|
+
hits,
|
|
9351
|
+
...history !== void 0 ? { history } : {}
|
|
9352
|
+
};
|
|
8803
9353
|
}
|
|
8804
9354
|
var execFileAsync2, ZERO_HASH, MAX_BLOB, RAW_LINE;
|
|
8805
9355
|
var init_history = __esm({
|
|
@@ -9094,7 +9644,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9094
9644
|
options.modules ?? config.modules,
|
|
9095
9645
|
loadedPolicy?.requiredModules
|
|
9096
9646
|
);
|
|
9097
|
-
let files = await walkFiles({
|
|
9647
|
+
let files = await walkFiles({
|
|
9648
|
+
cwd,
|
|
9649
|
+
ignore: config.ignore,
|
|
9650
|
+
defaultExcludes: config.defaultExcludes,
|
|
9651
|
+
excludes: config.excludes
|
|
9652
|
+
});
|
|
9098
9653
|
if (options.staged === true) {
|
|
9099
9654
|
const staged = new Set(await listStagedFiles(cwd));
|
|
9100
9655
|
files = files.filter((file) => staged.has(file));
|
|
@@ -9128,6 +9683,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9128
9683
|
);
|
|
9129
9684
|
}
|
|
9130
9685
|
const extraHits = [];
|
|
9686
|
+
let historyNotice;
|
|
9131
9687
|
const secretsWanted = modulesRequested.includes("secrets");
|
|
9132
9688
|
if (secretsAdvEnabled() && secretsWanted) {
|
|
9133
9689
|
if (options.history === true) {
|
|
@@ -9135,6 +9691,7 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9135
9691
|
const hist = await scanGitHistory2(cwd);
|
|
9136
9692
|
extraHits.push(...hist.hits);
|
|
9137
9693
|
collected.push(...hist.findings);
|
|
9694
|
+
historyNotice = hist.history;
|
|
9138
9695
|
}
|
|
9139
9696
|
if (options.bundle !== void 0 && options.bundle.length > 0) {
|
|
9140
9697
|
const { scanBundle: scanBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
@@ -9155,7 +9712,11 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9155
9712
|
if (options.updateBaseline === true) {
|
|
9156
9713
|
await writeBaseline(baselinePath, unsuppressed);
|
|
9157
9714
|
}
|
|
9158
|
-
|
|
9715
|
+
const baselineDoc = await readBaseline(baselinePath);
|
|
9716
|
+
let findings = sortFindings(applyBaseline(unsuppressed, baselineDoc));
|
|
9717
|
+
const suppressed = applySuppressions(findings, baselineDoc?.suppressions);
|
|
9718
|
+
findings = sortFindings(suppressed.findings);
|
|
9719
|
+
const suppressionErrors = suppressed.errors;
|
|
9159
9720
|
if (secretsAdvEnabled() && secretsWanted && options.verifySecrets === true && options.iOwnTheseCredentials === true) {
|
|
9160
9721
|
const { applyVerification: applyVerification2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
9161
9722
|
const { scanTextForSecretHits: scanTextForSecretHits2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
@@ -9227,10 +9788,12 @@ async function runScan(options = {}, modules = builtinModules) {
|
|
|
9227
9788
|
cwd,
|
|
9228
9789
|
filesScanned: files.length,
|
|
9229
9790
|
modulesRequested: [...modulesRequested],
|
|
9230
|
-
modulesRun
|
|
9791
|
+
modulesRun,
|
|
9792
|
+
...historyNotice !== void 0 ? { history: historyNotice } : {}
|
|
9231
9793
|
},
|
|
9232
9794
|
config,
|
|
9233
|
-
policy: policyResult
|
|
9795
|
+
policy: policyResult,
|
|
9796
|
+
...suppressionErrors.length > 0 ? { suppressionErrors } : {}
|
|
9234
9797
|
};
|
|
9235
9798
|
}
|
|
9236
9799
|
function scan(options = {}) {
|
|
@@ -9546,6 +10109,32 @@ function finding4(ruleId, title, message, file, severity = "high") {
|
|
|
9546
10109
|
remediation: "Pin actions by SHA, drop pull_request_target, tighten permissions, close 0.0.0.0/0, encrypt storage, drop privileged."
|
|
9547
10110
|
};
|
|
9548
10111
|
}
|
|
10112
|
+
function runBodies(text) {
|
|
10113
|
+
const bodies = [];
|
|
10114
|
+
const lines = text.split(/\r?\n/);
|
|
10115
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
10116
|
+
const match = /^(\s*)(?:-\s+)?run:\s*(.*)$/.exec(lines[i] ?? "");
|
|
10117
|
+
if (match === null) continue;
|
|
10118
|
+
const indent = match[1]?.length ?? 0;
|
|
10119
|
+
let body = match[2] ?? "";
|
|
10120
|
+
if (body === "|" || body === ">") {
|
|
10121
|
+
const block = [];
|
|
10122
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
10123
|
+
const line = lines[j] ?? "";
|
|
10124
|
+
if (line.trim() === "") {
|
|
10125
|
+
block.push(line);
|
|
10126
|
+
continue;
|
|
10127
|
+
}
|
|
10128
|
+
const lead = line.match(/^\s*/)?.[0].length ?? 0;
|
|
10129
|
+
if (lead <= indent) break;
|
|
10130
|
+
block.push(line);
|
|
10131
|
+
}
|
|
10132
|
+
body = block.join("\n");
|
|
10133
|
+
}
|
|
10134
|
+
bodies.push(body);
|
|
10135
|
+
}
|
|
10136
|
+
return bodies;
|
|
10137
|
+
}
|
|
9549
10138
|
function walk3(dir, out = []) {
|
|
9550
10139
|
if (!existsSync(dir)) return out;
|
|
9551
10140
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
@@ -9573,6 +10162,28 @@ function scanIac(dir, opts = {}) {
|
|
|
9573
10162
|
if (/pull_request_target/.test(text)) {
|
|
9574
10163
|
findings.push(finding4("IAC-GHA-PRTARGET-001", "pull_request_target workflow", "untrusted PR can run with base secrets", rel, "critical"));
|
|
9575
10164
|
}
|
|
10165
|
+
if (/pull_request_target/.test(text) && /actions\/checkout/.test(text) && /github\.event\.pull_request/.test(text)) {
|
|
10166
|
+
findings.push(
|
|
10167
|
+
finding4(
|
|
10168
|
+
"IAC-GHA-PRCHECKOUT-001",
|
|
10169
|
+
"pull_request_target checks out pull-request code",
|
|
10170
|
+
"untrusted head can run with the base repository token",
|
|
10171
|
+
rel,
|
|
10172
|
+
"critical"
|
|
10173
|
+
)
|
|
10174
|
+
);
|
|
10175
|
+
}
|
|
10176
|
+
if (runBodies(text).some((body) => /\$\{\{\s*github\.event\./.test(body))) {
|
|
10177
|
+
findings.push(
|
|
10178
|
+
finding4(
|
|
10179
|
+
"IAC-GHA-INJECT-001",
|
|
10180
|
+
"Workflow run step interpolates github.event",
|
|
10181
|
+
"untrusted event text is passed to the shell",
|
|
10182
|
+
rel,
|
|
10183
|
+
"critical"
|
|
10184
|
+
)
|
|
10185
|
+
);
|
|
10186
|
+
}
|
|
9576
10187
|
if (/permissions:\s*\n\s+contents:\s*write/.test(text) || /permissions:\s+write-all/.test(text)) {
|
|
9577
10188
|
findings.push(finding4("IAC-GHA-PERMS-001", "Overly broad GitHub Actions permissions", "write-all or contents: write", rel));
|
|
9578
10189
|
}
|
|
@@ -9723,6 +10334,7 @@ var init_compliance = __esm({
|
|
|
9723
10334
|
ROW("SUPPLY-BEHAVIOR-001", "A06:2021 Vulnerable Components", "CWE-829", "V14.2.1", "PCI DSS 6.3.3"),
|
|
9724
10335
|
ROW("SUPPLY-COOLDOWN-001", "A06:2021 Vulnerable Components", "CWE-1104", "V14.2.1", "PCI DSS 6.3.3"),
|
|
9725
10336
|
ROW("SUPPLY-LOCKURL-001", "A08:2021 Integrity Failures", "CWE-494", "V14.2.3", "PCI DSS 6.3.3"),
|
|
10337
|
+
ROW("SUPPLY-LOCKINTEGRITY-001", "A08:2021 Integrity Failures", "CWE-353", "V14.2.3", "PCI DSS 6.3.3"),
|
|
9726
10338
|
ROW("SUPPLY-CONFUSION-001", "A08:2021 Integrity Failures", "CWE-494", "V14.2.3", "PCI DSS 6.3.3"),
|
|
9727
10339
|
ROW("SUPPLY-PROVENANCE-001", "A08:2021 Integrity Failures", "CWE-494", "V14.2.3", "PCI DSS 6.3.3"),
|
|
9728
10340
|
ROW("SUPPLY-WORM-001", "A06:2021 Vulnerable Components", "CWE-506", "V14.2.1", "PCI DSS 6.3.3 / 12.10"),
|
|
@@ -9731,6 +10343,9 @@ var init_compliance = __esm({
|
|
|
9731
10343
|
ROW("IAC-GHA-PRTARGET-001", "A01:2021 Broken Access Control", "CWE-269", "V4.1.1", "PCI DSS 7.2"),
|
|
9732
10344
|
ROW("IAC-GHA-PERMS-001", "A01:2021 Broken Access Control", "CWE-250", "V4.1.1", "PCI DSS 7.2"),
|
|
9733
10345
|
ROW("IAC-GHA-SECRETLOG-001", "A09:2021 Security Logging Failures", "CWE-532", "V7.1.1", "PCI DSS 3.2 / 10.2"),
|
|
10346
|
+
ROW("IAC-GHA-INJECT-001", "A03:2021 Injection", "CWE-94", "V5.2.4", "PCI DSS 6.2.4"),
|
|
10347
|
+
ROW("IAC-GHA-PRCHECKOUT-001", "A01:2021 Broken Access Control", "CWE-829", "V4.1.1", "PCI DSS 6.2.4"),
|
|
10348
|
+
ROW("DEPS-LICENSE-001", "A06:2021 Vulnerable Components", "CWE-1104", "V14.2.1", "PCI DSS 6.3.3"),
|
|
9734
10349
|
ROW("IAC-TF-S3PUBLIC-001", "A01:2021 Broken Access Control", "CWE-284", "V12.1.1", "PCI DSS 1.3 / 3.2"),
|
|
9735
10350
|
ROW("IAC-TF-SGOPEN-001", "A05:2021 Security Misconfiguration", "CWE-284", "V1.4.1", "PCI DSS 1.2 / 1.3"),
|
|
9736
10351
|
ROW("IAC-TF-UNENCRYPTED-001", "A02:2021 Cryptographic Failures", "CWE-311", "V6.2.1", "PCI DSS 3.4 / 4.2"),
|
|
@@ -9757,6 +10372,7 @@ var init_compliance = __esm({
|
|
|
9757
10372
|
ROW("SAST-TIMING-001", "A07:2021 Identification Failures", "CWE-208", "V2.10.3", "PCI DSS 8.3"),
|
|
9758
10373
|
ROW("SAST-HASH-001", "A02:2021 Cryptographic Failures", "CWE-916", "V6.2.2", "PCI DSS 8.3.1"),
|
|
9759
10374
|
ROW("SAST-GQL-001", "A04:2021 Insecure Design", "CWE-770/200", "V13.1.1", "PCI DSS 6.2.4"),
|
|
10375
|
+
ROW("SAST-SKIP-001", "A05:2021 Security Misconfiguration", "CWE-693", "V1.1.1", "PCI DSS 6.2.4"),
|
|
9760
10376
|
ROW("CONFIG-TRUSTPROXY-001", "A07:2021 Identification Failures", "CWE-290", "V2.2.1", "PCI DSS 8.3.4"),
|
|
9761
10377
|
ROW("SECRET-BUNDLE-001", "A02:2021 Cryptographic Failures", "CWE-798", "V6.4.1", "PCI DSS 3.2 / 8.3"),
|
|
9762
10378
|
ROW("DLP-SECRET-001", "A02:2021 Cryptographic Failures", "CWE-200", "V8.3.4", "PCI DSS 3.2 / 8.3"),
|
|
@@ -9776,6 +10392,23 @@ var init_compliance = __esm({
|
|
|
9776
10392
|
byId = new Map(RULE_MAP.map((r) => [r.ruleId, r]));
|
|
9777
10393
|
}
|
|
9778
10394
|
});
|
|
10395
|
+
function nodeSqliteAvailable() {
|
|
10396
|
+
try {
|
|
10397
|
+
require2("node:sqlite");
|
|
10398
|
+
return true;
|
|
10399
|
+
} catch {
|
|
10400
|
+
return false;
|
|
10401
|
+
}
|
|
10402
|
+
}
|
|
10403
|
+
function nodeSqliteUnavailableMessage(feature) {
|
|
10404
|
+
return `${feature} requires Node 22.13+ (node:sqlite is not available on Node ${process.versions.node})`;
|
|
10405
|
+
}
|
|
10406
|
+
var require2;
|
|
10407
|
+
var init_sqlite = __esm({
|
|
10408
|
+
"src/core/sqlite.ts"() {
|
|
10409
|
+
require2 = createRequire(import.meta.url);
|
|
10410
|
+
}
|
|
10411
|
+
});
|
|
9779
10412
|
|
|
9780
10413
|
// src/dashboard/index.ts
|
|
9781
10414
|
var dashboard_exports = {};
|
|
@@ -9789,7 +10422,7 @@ __export(dashboard_exports, {
|
|
|
9789
10422
|
startDashboard: () => startDashboard
|
|
9790
10423
|
});
|
|
9791
10424
|
function sqliteDb(file) {
|
|
9792
|
-
const { DatabaseSync } =
|
|
10425
|
+
const { DatabaseSync } = require3("node:sqlite");
|
|
9793
10426
|
let db;
|
|
9794
10427
|
try {
|
|
9795
10428
|
db = new DatabaseSync(file);
|
|
@@ -9840,6 +10473,9 @@ function sqliteDb(file) {
|
|
|
9840
10473
|
};
|
|
9841
10474
|
}
|
|
9842
10475
|
function openHistory(dir) {
|
|
10476
|
+
if (!nodeSqliteAvailable()) {
|
|
10477
|
+
throw new Error(nodeSqliteUnavailableMessage("history"));
|
|
10478
|
+
}
|
|
9843
10479
|
const root = join(dir, ".fullshield");
|
|
9844
10480
|
mkdirSync(root, { recursive: true });
|
|
9845
10481
|
return sqliteDb(join(root, "history.db"));
|
|
@@ -9941,10 +10577,11 @@ function startDashboard(opts) {
|
|
|
9941
10577
|
});
|
|
9942
10578
|
});
|
|
9943
10579
|
}
|
|
9944
|
-
var
|
|
10580
|
+
var require3;
|
|
9945
10581
|
var init_dashboard = __esm({
|
|
9946
10582
|
"src/dashboard/index.ts"() {
|
|
9947
|
-
|
|
10583
|
+
init_sqlite();
|
|
10584
|
+
require3 = createRequire(import.meta.url);
|
|
9948
10585
|
}
|
|
9949
10586
|
});
|
|
9950
10587
|
function finding5(ruleId, title, message, file, severity = "high") {
|
|
@@ -11169,6 +11806,153 @@ ${target.message}`
|
|
|
11169
11806
|
// src/cli/program.ts
|
|
11170
11807
|
init_version();
|
|
11171
11808
|
|
|
11809
|
+
// src/core/fingerprint.ts
|
|
11810
|
+
init_finding();
|
|
11811
|
+
function lineIndependentFingerprint(finding6) {
|
|
11812
|
+
const file = (finding6.file ?? "").replace(/\\/g, "/");
|
|
11813
|
+
const text = (finding6.snippet ?? finding6.message ?? "").replace(/\s+/g, " ").trim();
|
|
11814
|
+
return makeFindingId(finding6.ruleId, file, `${finding6.severity}\0${text}`);
|
|
11815
|
+
}
|
|
11816
|
+
|
|
11817
|
+
// src/reporters/ci.ts
|
|
11818
|
+
init_finding();
|
|
11819
|
+
|
|
11820
|
+
// src/reporters/redact.ts
|
|
11821
|
+
init_mask();
|
|
11822
|
+
var SECRET = /\b(?:AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|sk_live_[A-Za-z0-9]{10,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g;
|
|
11823
|
+
function redactSecrets2(text) {
|
|
11824
|
+
return text.replace(SECRET, (match) => maskSecret(match));
|
|
11825
|
+
}
|
|
11826
|
+
function redactFinding(finding6) {
|
|
11827
|
+
return {
|
|
11828
|
+
...finding6,
|
|
11829
|
+
title: redactSecrets2(finding6.title),
|
|
11830
|
+
message: redactSecrets2(finding6.message),
|
|
11831
|
+
remediation: redactSecrets2(finding6.remediation),
|
|
11832
|
+
snippet: finding6.snippet === void 0 ? void 0 : redactSecrets2(finding6.snippet)
|
|
11833
|
+
};
|
|
11834
|
+
}
|
|
11835
|
+
function redactScan(result) {
|
|
11836
|
+
return { ...result, findings: result.findings.map(redactFinding) };
|
|
11837
|
+
}
|
|
11838
|
+
|
|
11839
|
+
// src/reporters/ci.ts
|
|
11840
|
+
var MESSAGE_CAP = 4e3;
|
|
11841
|
+
function posix2(file) {
|
|
11842
|
+
return file.replace(/\\/g, "/");
|
|
11843
|
+
}
|
|
11844
|
+
function clip(text) {
|
|
11845
|
+
return text.length <= MESSAGE_CAP ? text : `${text.slice(0, MESSAGE_CAP)}\u2026`;
|
|
11846
|
+
}
|
|
11847
|
+
function githubEscape(value, property) {
|
|
11848
|
+
let out = value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A");
|
|
11849
|
+
if (property) out = out.replace(/,/g, "%2C");
|
|
11850
|
+
return out;
|
|
11851
|
+
}
|
|
11852
|
+
function githubLevel(severity) {
|
|
11853
|
+
if (severity === "critical" || severity === "high") return "error";
|
|
11854
|
+
if (severity === "medium") return "warning";
|
|
11855
|
+
return "notice";
|
|
11856
|
+
}
|
|
11857
|
+
function gitlabSeverity(severity) {
|
|
11858
|
+
if (severity === "info") return "info";
|
|
11859
|
+
if (severity === "low") return "minor";
|
|
11860
|
+
if (severity === "medium") return "major";
|
|
11861
|
+
if (severity === "high") return "critical";
|
|
11862
|
+
return "blocker";
|
|
11863
|
+
}
|
|
11864
|
+
function xmlEscape(value) {
|
|
11865
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
11866
|
+
}
|
|
11867
|
+
function oneLine(value) {
|
|
11868
|
+
return value.replace(/\s+/g, " ").trim();
|
|
11869
|
+
}
|
|
11870
|
+
function renderGithubAnnotations(input) {
|
|
11871
|
+
const result = redactScan(input);
|
|
11872
|
+
if (result.findings.length === 0) {
|
|
11873
|
+
return "::notice title=FullShield::No findings\n";
|
|
11874
|
+
}
|
|
11875
|
+
const lines = result.findings.map((finding6) => {
|
|
11876
|
+
const props = [];
|
|
11877
|
+
if (finding6.file !== void 0) props.push(`file=${githubEscape(posix2(finding6.file), true)}`);
|
|
11878
|
+
if (finding6.line !== void 0) props.push(`line=${finding6.line}`);
|
|
11879
|
+
if (finding6.column !== void 0) props.push(`col=${finding6.column + 1}`);
|
|
11880
|
+
props.push(`title=${githubEscape(finding6.ruleId, true)}`);
|
|
11881
|
+
const message = githubEscape(clip(finding6.message), false);
|
|
11882
|
+
return `::${githubLevel(finding6.severity)} ${props.join(",")}::${message}`;
|
|
11883
|
+
});
|
|
11884
|
+
return `${lines.join("\n")}
|
|
11885
|
+
`;
|
|
11886
|
+
}
|
|
11887
|
+
function junitCase(finding6) {
|
|
11888
|
+
const file = finding6.file !== void 0 ? posix2(finding6.file) : "fullshield";
|
|
11889
|
+
const attrs = [
|
|
11890
|
+
`name="${xmlEscape(finding6.ruleId)}"`,
|
|
11891
|
+
`classname="${xmlEscape(file)}"`,
|
|
11892
|
+
`file="${xmlEscape(file)}"`
|
|
11893
|
+
];
|
|
11894
|
+
if (finding6.line !== void 0) attrs.push(`line="${finding6.line}"`);
|
|
11895
|
+
const open = `<testcase ${attrs.join(" ")}>`;
|
|
11896
|
+
if (!meetsThreshold(finding6.severity, "medium")) {
|
|
11897
|
+
return `${open}<skipped message="${xmlEscape(oneLine(clip(finding6.message)))}"/></testcase>`;
|
|
11898
|
+
}
|
|
11899
|
+
const detail = clip(`${finding6.message}${finding6.remediation.length > 0 ? `
|
|
11900
|
+
Fix: ${finding6.remediation}` : ""}`);
|
|
11901
|
+
return `${open}<failure type="${finding6.severity}" message="${xmlEscape(oneLine(clip(finding6.message)))}">${xmlEscape(detail)}</failure></testcase>`;
|
|
11902
|
+
}
|
|
11903
|
+
function renderJunitReport(input) {
|
|
11904
|
+
const result = redactScan(input);
|
|
11905
|
+
const header = '<?xml version="1.0" encoding="UTF-8"?>';
|
|
11906
|
+
if (result.findings.length === 0) {
|
|
11907
|
+
return `${header}
|
|
11908
|
+
<testsuites name="fullshield" tests="1" failures="0" skipped="0"><testsuite name="fullshield" tests="1" failures="0" skipped="0"><testcase name="scan" classname="fullshield"/></testsuite></testsuites>
|
|
11909
|
+
`;
|
|
11910
|
+
}
|
|
11911
|
+
const groups = /* @__PURE__ */ new Map();
|
|
11912
|
+
for (const finding6 of result.findings) {
|
|
11913
|
+
const list = groups.get(finding6.module) ?? [];
|
|
11914
|
+
list.push(finding6);
|
|
11915
|
+
groups.set(finding6.module, list);
|
|
11916
|
+
}
|
|
11917
|
+
let failures = 0;
|
|
11918
|
+
let skipped = 0;
|
|
11919
|
+
const suites = [];
|
|
11920
|
+
for (const [name, findings] of groups) {
|
|
11921
|
+
let suiteFailures = 0;
|
|
11922
|
+
let suiteSkipped = 0;
|
|
11923
|
+
const cases = findings.map((finding6) => {
|
|
11924
|
+
if (meetsThreshold(finding6.severity, "medium")) suiteFailures += 1;
|
|
11925
|
+
else suiteSkipped += 1;
|
|
11926
|
+
return junitCase(finding6);
|
|
11927
|
+
});
|
|
11928
|
+
failures += suiteFailures;
|
|
11929
|
+
skipped += suiteSkipped;
|
|
11930
|
+
suites.push(
|
|
11931
|
+
`<testsuite name="${xmlEscape(name)}" tests="${findings.length}" failures="${suiteFailures}" skipped="${suiteSkipped}">${cases.join("")}</testsuite>`
|
|
11932
|
+
);
|
|
11933
|
+
}
|
|
11934
|
+
return `${header}
|
|
11935
|
+
<testsuites name="fullshield" tests="${result.findings.length}" failures="${failures}" skipped="${skipped}">${suites.join("")}</testsuites>
|
|
11936
|
+
`;
|
|
11937
|
+
}
|
|
11938
|
+
function renderGitlabCodeQuality(input) {
|
|
11939
|
+
const result = redactScan(input);
|
|
11940
|
+
const report = result.findings.map((finding6) => {
|
|
11941
|
+
const path = finding6.file !== void 0 ? posix2(finding6.file) : ".";
|
|
11942
|
+
const location4 = { path };
|
|
11943
|
+
if (finding6.line !== void 0) location4.lines = { begin: finding6.line };
|
|
11944
|
+
return {
|
|
11945
|
+
description: clip(finding6.message),
|
|
11946
|
+
check_name: finding6.ruleId,
|
|
11947
|
+
fingerprint: lineIndependentFingerprint(finding6),
|
|
11948
|
+
severity: gitlabSeverity(finding6.severity),
|
|
11949
|
+
location: location4
|
|
11950
|
+
};
|
|
11951
|
+
});
|
|
11952
|
+
return `${JSON.stringify(report, null, 2)}
|
|
11953
|
+
`;
|
|
11954
|
+
}
|
|
11955
|
+
|
|
11172
11956
|
// src/reporters/console.ts
|
|
11173
11957
|
init_finding();
|
|
11174
11958
|
var SEVERITY_COLOR = {
|
|
@@ -11210,7 +11994,8 @@ function renderFinding(finding6) {
|
|
|
11210
11994
|
}
|
|
11211
11995
|
return lines;
|
|
11212
11996
|
}
|
|
11213
|
-
function renderConsoleReport(
|
|
11997
|
+
function renderConsoleReport(input) {
|
|
11998
|
+
const result = redactScan(input);
|
|
11214
11999
|
const { findings, summary, metadata } = result;
|
|
11215
12000
|
const lines = [];
|
|
11216
12001
|
lines.push(
|
|
@@ -11218,6 +12003,7 @@ function renderConsoleReport(result) {
|
|
|
11218
12003
|
` \u2014 scanned ${metadata.filesScanned} files in ${metadata.durationMs}ms (modules: ${metadata.modulesRun.length > 0 ? metadata.modulesRun.join(", ") : "none"})`
|
|
11219
12004
|
)
|
|
11220
12005
|
);
|
|
12006
|
+
if (metadata.history !== void 0) lines.push(metadata.history);
|
|
11221
12007
|
if (metadata.modulesRun.length < metadata.modulesRequested.length) {
|
|
11222
12008
|
const pending = metadata.modulesRequested.filter((m) => !metadata.modulesRun.includes(m));
|
|
11223
12009
|
lines.push(
|
|
@@ -11261,7 +12047,8 @@ function location2(finding6) {
|
|
|
11261
12047
|
if (finding6.file === void 0) return "(project)";
|
|
11262
12048
|
return finding6.line !== void 0 ? `${finding6.file}:${finding6.line}` : finding6.file;
|
|
11263
12049
|
}
|
|
11264
|
-
function renderHtmlReport(
|
|
12050
|
+
function renderHtmlReport(input) {
|
|
12051
|
+
const result = redactScan(input);
|
|
11265
12052
|
const sections = [];
|
|
11266
12053
|
const bySeverityDesc = [...SEVERITIES].reverse();
|
|
11267
12054
|
for (const severity of bySeverityDesc) {
|
|
@@ -11308,7 +12095,7 @@ ${items}`);
|
|
|
11308
12095
|
|
|
11309
12096
|
// src/reporters/json.ts
|
|
11310
12097
|
function publicFinding(finding6) {
|
|
11311
|
-
const copy =
|
|
12098
|
+
const copy = redactFinding(finding6);
|
|
11312
12099
|
delete copy.aiVerdict;
|
|
11313
12100
|
delete copy.exploitScenario;
|
|
11314
12101
|
delete copy.aiReasoning;
|
|
@@ -11332,7 +12119,8 @@ function location3(finding6) {
|
|
|
11332
12119
|
if (finding6.file === void 0) return "(project)";
|
|
11333
12120
|
return finding6.line !== void 0 ? `${finding6.file}:${finding6.line}` : finding6.file;
|
|
11334
12121
|
}
|
|
11335
|
-
function renderMarkdownReport(
|
|
12122
|
+
function renderMarkdownReport(input) {
|
|
12123
|
+
const result = redactScan(input);
|
|
11336
12124
|
const lines = [
|
|
11337
12125
|
"# FullShield report",
|
|
11338
12126
|
"",
|
|
@@ -11384,7 +12172,8 @@ function sarifLevel(severity) {
|
|
|
11384
12172
|
function uri(file) {
|
|
11385
12173
|
return (file ?? "").replace(/\\/g, "/");
|
|
11386
12174
|
}
|
|
11387
|
-
function renderSarifReport(
|
|
12175
|
+
function renderSarifReport(input) {
|
|
12176
|
+
const result = redactScan(input);
|
|
11388
12177
|
const rules = /* @__PURE__ */ new Map();
|
|
11389
12178
|
for (const finding6 of result.findings) {
|
|
11390
12179
|
if (!rules.has(finding6.ruleId)) {
|
|
@@ -11451,7 +12240,137 @@ function renderSarifReport(result) {
|
|
|
11451
12240
|
return JSON.stringify(doc, null, 2);
|
|
11452
12241
|
}
|
|
11453
12242
|
|
|
12243
|
+
// src/diff/index.ts
|
|
12244
|
+
init_finding();
|
|
12245
|
+
init_experimental();
|
|
12246
|
+
function posix3(file) {
|
|
12247
|
+
return (file ?? "").replace(/\\/g, "/");
|
|
12248
|
+
}
|
|
12249
|
+
function diffKey(finding6) {
|
|
12250
|
+
return lineIndependentFingerprint(finding6);
|
|
12251
|
+
}
|
|
12252
|
+
function groupById(findings) {
|
|
12253
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12254
|
+
for (const finding6 of findings) {
|
|
12255
|
+
if (finding6.id === void 0 || finding6.id.length === 0) continue;
|
|
12256
|
+
const list = groups.get(finding6.id) ?? [];
|
|
12257
|
+
list.push(finding6);
|
|
12258
|
+
groups.set(finding6.id, list);
|
|
12259
|
+
}
|
|
12260
|
+
return groups;
|
|
12261
|
+
}
|
|
12262
|
+
function pairMoves(removed, added) {
|
|
12263
|
+
const removedIds = groupById(removed);
|
|
12264
|
+
const addedIds = groupById(added);
|
|
12265
|
+
const moved = [];
|
|
12266
|
+
for (const [id, group] of removedIds) {
|
|
12267
|
+
const match = addedIds.get(id);
|
|
12268
|
+
if (group.length === 1 && match?.length === 1 && group[0] !== void 0 && match[0] !== void 0) {
|
|
12269
|
+
moved.push({ before: group[0], after: match[0] });
|
|
12270
|
+
}
|
|
12271
|
+
}
|
|
12272
|
+
return moved;
|
|
12273
|
+
}
|
|
12274
|
+
function byLocation(a, b) {
|
|
12275
|
+
return (a.line ?? 0) - (b.line ?? 0) || a.id.localeCompare(b.id) || a.ruleId.localeCompare(b.ruleId);
|
|
12276
|
+
}
|
|
12277
|
+
function groupFindings(findings) {
|
|
12278
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12279
|
+
for (const finding6 of findings) {
|
|
12280
|
+
const key = diffKey(finding6);
|
|
12281
|
+
const list = groups.get(key) ?? [];
|
|
12282
|
+
list.push(finding6);
|
|
12283
|
+
groups.set(key, list);
|
|
12284
|
+
}
|
|
12285
|
+
return groups;
|
|
12286
|
+
}
|
|
12287
|
+
function diffScans(before, after) {
|
|
12288
|
+
const beforeGroups = groupFindings(before);
|
|
12289
|
+
const afterGroups = groupFindings(after);
|
|
12290
|
+
const unchanged = [];
|
|
12291
|
+
const added = [];
|
|
12292
|
+
const removed = [];
|
|
12293
|
+
const moved = [];
|
|
12294
|
+
const keys = [];
|
|
12295
|
+
for (const key of beforeGroups.keys()) keys.push(key);
|
|
12296
|
+
for (const key of afterGroups.keys()) {
|
|
12297
|
+
if (!beforeGroups.has(key)) keys.push(key);
|
|
12298
|
+
}
|
|
12299
|
+
for (const key of keys) {
|
|
12300
|
+
const left = [...beforeGroups.get(key) ?? []].sort(byLocation);
|
|
12301
|
+
const right = [...afterGroups.get(key) ?? []].sort(byLocation);
|
|
12302
|
+
const paired = Math.min(left.length, right.length);
|
|
12303
|
+
for (let i = 0; i < paired; i += 1) {
|
|
12304
|
+
const earlier = left[i];
|
|
12305
|
+
const later = right[i];
|
|
12306
|
+
if (earlier === void 0 || later === void 0) continue;
|
|
12307
|
+
if ((earlier.line ?? 0) === (later.line ?? 0)) unchanged.push(later);
|
|
12308
|
+
else moved.push({ before: earlier, after: later });
|
|
12309
|
+
}
|
|
12310
|
+
for (const finding6 of left.slice(paired)) removed.push(finding6);
|
|
12311
|
+
for (const finding6 of right.slice(paired)) added.push(finding6);
|
|
12312
|
+
}
|
|
12313
|
+
const idMoves = pairMoves(removed, added);
|
|
12314
|
+
const movedBefore = new Set(idMoves.map((item) => item.before));
|
|
12315
|
+
const movedAfter = new Set(idMoves.map((item) => item.after));
|
|
12316
|
+
return {
|
|
12317
|
+
unchanged,
|
|
12318
|
+
added: added.filter((finding6) => !movedAfter.has(finding6)),
|
|
12319
|
+
removed: removed.filter((finding6) => !movedBefore.has(finding6)),
|
|
12320
|
+
moved: [...moved, ...idMoves]
|
|
12321
|
+
};
|
|
12322
|
+
}
|
|
12323
|
+
function parseScanDocument(text) {
|
|
12324
|
+
let parsed;
|
|
12325
|
+
try {
|
|
12326
|
+
parsed = JSON.parse(text);
|
|
12327
|
+
} catch {
|
|
12328
|
+
throw new Error("scan JSON must be an object with a findings array or a findings array");
|
|
12329
|
+
}
|
|
12330
|
+
const list = Array.isArray(parsed) ? parsed : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.findings) ? parsed.findings : void 0;
|
|
12331
|
+
if (list === void 0) {
|
|
12332
|
+
throw new Error("scan JSON must be an object with a findings array or a findings array");
|
|
12333
|
+
}
|
|
12334
|
+
return list.map((item) => {
|
|
12335
|
+
if (typeof item !== "object" || item === null) {
|
|
12336
|
+
throw new Error("scan JSON finding is missing ruleId or severity");
|
|
12337
|
+
}
|
|
12338
|
+
const record = item;
|
|
12339
|
+
const ruleId = record.ruleId;
|
|
12340
|
+
const severity = record.severity;
|
|
12341
|
+
if (typeof ruleId !== "string" || ruleId.length === 0 || typeof severity !== "string") {
|
|
12342
|
+
throw new Error("scan JSON finding is missing ruleId or severity");
|
|
12343
|
+
}
|
|
12344
|
+
if (!isSeverity(severity)) {
|
|
12345
|
+
throw new Error(`scan JSON finding has an unknown severity "${severity}"`);
|
|
12346
|
+
}
|
|
12347
|
+
if (record.line !== void 0 && (typeof record.line !== "number" || !Number.isInteger(record.line) || record.line < 0)) {
|
|
12348
|
+
throw new Error("scan JSON finding line must be an integer >= 0");
|
|
12349
|
+
}
|
|
12350
|
+
return item;
|
|
12351
|
+
});
|
|
12352
|
+
}
|
|
12353
|
+
function addedFails(diff, failOn) {
|
|
12354
|
+
return diff.added.some((finding6) => finding6.source !== "ai" && meetsThreshold(finding6.severity, failOn));
|
|
12355
|
+
}
|
|
12356
|
+
var ADDED_CAP = 20;
|
|
12357
|
+
function addedLine(finding6) {
|
|
12358
|
+
const where = finding6.file !== void 0 ? ` ${posix3(finding6.file)}:${finding6.line ?? 0}` : "";
|
|
12359
|
+
return `${finding6.severity} ${finding6.ruleId}${where}`;
|
|
12360
|
+
}
|
|
12361
|
+
function renderDiffText(diff) {
|
|
12362
|
+
const lines = [
|
|
12363
|
+
`fullshield diff: unchanged ${diff.unchanged.length}, added ${diff.added.length}, removed ${diff.removed.length}, moved ${diff.moved.length}`
|
|
12364
|
+
];
|
|
12365
|
+
if (diff.added.length === 0) return lines.join("\n");
|
|
12366
|
+
lines.push("added:");
|
|
12367
|
+
for (const finding6 of diff.added.slice(0, ADDED_CAP)) lines.push(addedLine(finding6));
|
|
12368
|
+
if (diff.added.length > ADDED_CAP) lines.push(`${diff.added.length - ADDED_CAP} more \u2014 use --json`);
|
|
12369
|
+
return lines.join("\n");
|
|
12370
|
+
}
|
|
12371
|
+
|
|
11454
12372
|
// src/cli/program.ts
|
|
12373
|
+
init_experimental();
|
|
11455
12374
|
init_policy();
|
|
11456
12375
|
|
|
11457
12376
|
// src/sbom/index.ts
|
|
@@ -12126,7 +13045,7 @@ function stateForGrace(sshd) {
|
|
|
12126
13045
|
|
|
12127
13046
|
// src/cli/commands/doctor.ts
|
|
12128
13047
|
init_config2();
|
|
12129
|
-
var MIN_NODE = [
|
|
13048
|
+
var MIN_NODE = [20, 11, 0];
|
|
12130
13049
|
var LOCKFILES = ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"];
|
|
12131
13050
|
var FLAGS = [
|
|
12132
13051
|
"FULLSHIELD_EXPERIMENTAL_SAST_GAPS",
|
|
@@ -12139,6 +13058,9 @@ var FLAGS = [
|
|
|
12139
13058
|
"FULLSHIELD_EXPERIMENTAL_POLICY",
|
|
12140
13059
|
"FULLSHIELD_EXPERIMENTAL_HOST",
|
|
12141
13060
|
"FULLSHIELD_EXPERIMENTAL_TAINT_V2",
|
|
13061
|
+
"FULLSHIELD_EXPERIMENTAL_CI_REPORTS",
|
|
13062
|
+
"FULLSHIELD_EXPERIMENTAL_DIFF",
|
|
13063
|
+
"FULLSHIELD_EXPERIMENTAL_LICENSE",
|
|
12142
13064
|
"FULLSHIELD_PARSE_POOL"
|
|
12143
13065
|
];
|
|
12144
13066
|
function nodeOk(version) {
|
|
@@ -12154,7 +13076,7 @@ function nodeOk(version) {
|
|
|
12154
13076
|
async function runDoctor(cwd, options = {}) {
|
|
12155
13077
|
const lines = [];
|
|
12156
13078
|
const version = process.versions.node;
|
|
12157
|
-
lines.push(`node: ${version} ${nodeOk(version) ? "ok" : "too old (need >=
|
|
13079
|
+
lines.push(`node: ${version} ${nodeOk(version) ? "ok" : "too old (need >= 20.11.0)"}`);
|
|
12158
13080
|
const locks = LOCKFILES.filter((name) => existsSync(join(cwd, name)));
|
|
12159
13081
|
lines.push(locks.length > 0 ? `lockfile: ${locks.join(", ")}` : "lockfile: none");
|
|
12160
13082
|
const cacheDir = join(homedir(), ".cache", "fullshield", "osv");
|
|
@@ -12200,6 +13122,16 @@ var KNOWN = [
|
|
|
12200
13122
|
test: /cannot read config/,
|
|
12201
13123
|
code: "FS-E004",
|
|
12202
13124
|
hint: "Check the path passed to --config."
|
|
13125
|
+
},
|
|
13126
|
+
{
|
|
13127
|
+
test: /FULLSHIELD_EXPERIMENTAL_CI_REPORTS/,
|
|
13128
|
+
code: "FS-E005",
|
|
13129
|
+
hint: "Set FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1 or pass --experimental-ci-reports."
|
|
13130
|
+
},
|
|
13131
|
+
{
|
|
13132
|
+
test: /FULLSHIELD_EXPERIMENTAL_DIFF|scan JSON|cannot read diff/,
|
|
13133
|
+
code: "FS-E006",
|
|
13134
|
+
hint: "Set FULLSHIELD_EXPERIMENTAL_DIFF=1 or pass --experimental-diff. Both inputs must be scan --json files."
|
|
12203
13135
|
}
|
|
12204
13136
|
];
|
|
12205
13137
|
var DOCS = "https://github.com/kathanp/fullshield/blob/main/docs/errors.md";
|
|
@@ -12251,7 +13183,16 @@ function parseFailOn(value) {
|
|
|
12251
13183
|
function createProgram(io = defaultIO) {
|
|
12252
13184
|
const program = new Command();
|
|
12253
13185
|
program.name("fullshield").description("Full-cycle security scanner for Node.js/TypeScript backends").version(packageVersion()).option("--debug", "print stack traces on CLI errors");
|
|
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(
|
|
13186
|
+
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(
|
|
13187
|
+
"--experimental-ci-reports",
|
|
13188
|
+
"experimental: GitHub annotations, JUnit, and GitLab Code Quality (FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1)"
|
|
13189
|
+
).option(
|
|
13190
|
+
"--github-annotations <file>",
|
|
13191
|
+
"experimental: GitHub Actions workflow commands (requires FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1)"
|
|
13192
|
+
).option("--junit <file>", "experimental: JUnit XML (requires FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1)").option(
|
|
13193
|
+
"--gitlab <file>",
|
|
13194
|
+
"experimental: GitLab Code Quality JSON (requires FULLSHIELD_EXPERIMENTAL_CI_REPORTS=1)"
|
|
13195
|
+
).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(
|
|
12255
13196
|
"--experimental-sast-gaps",
|
|
12256
13197
|
"experimental: ORM raw / sh -c / execa / zx sinks (FULLSHIELD_EXPERIMENTAL_SAST_GAPS=1)"
|
|
12257
13198
|
).option(
|
|
@@ -12310,6 +13251,15 @@ function createProgram(io = defaultIO) {
|
|
|
12310
13251
|
if (opts.prSummary === true && process.env[POLICY_FLAG] !== "1") {
|
|
12311
13252
|
throw new Error(`--pr-summary requires ${POLICY_FLAG}=1`);
|
|
12312
13253
|
}
|
|
13254
|
+
if (opts.experimentalCiReports === true) {
|
|
13255
|
+
process.env[CI_REPORTS_FLAG] = "1";
|
|
13256
|
+
}
|
|
13257
|
+
const wantsCiReport = opts.githubAnnotations !== void 0 || opts.junit !== void 0 || opts.gitlab !== void 0;
|
|
13258
|
+
if (wantsCiReport && process.env[CI_REPORTS_FLAG] !== "1") {
|
|
13259
|
+
throw new Error(
|
|
13260
|
+
`--github-annotations, --junit, and --gitlab require ${CI_REPORTS_FLAG}=1`
|
|
13261
|
+
);
|
|
13262
|
+
}
|
|
12313
13263
|
const result = await scan({
|
|
12314
13264
|
cwd: resolve(path),
|
|
12315
13265
|
modules,
|
|
@@ -12337,6 +13287,15 @@ function createProgram(io = defaultIO) {
|
|
|
12337
13287
|
if (opts.html !== void 0) {
|
|
12338
13288
|
await writeFile(resolve(opts.html), renderHtmlReport(result), "utf8");
|
|
12339
13289
|
}
|
|
13290
|
+
if (opts.githubAnnotations !== void 0) {
|
|
13291
|
+
await writeFile(resolve(opts.githubAnnotations), renderGithubAnnotations(result), "utf8");
|
|
13292
|
+
}
|
|
13293
|
+
if (opts.junit !== void 0) {
|
|
13294
|
+
await writeFile(resolve(opts.junit), renderJunitReport(result), "utf8");
|
|
13295
|
+
}
|
|
13296
|
+
if (opts.gitlab !== void 0) {
|
|
13297
|
+
await writeFile(resolve(opts.gitlab), renderGitlabCodeQuality(result), "utf8");
|
|
13298
|
+
}
|
|
12340
13299
|
await writeLastScan(resolve(path), result);
|
|
12341
13300
|
if (opts.emitVirtualPatches !== void 0) {
|
|
12342
13301
|
if (process.env.FULLSHIELD_EXPERIMENTAL_VPATCH !== "1") {
|
|
@@ -12358,6 +13317,12 @@ function createProgram(io = defaultIO) {
|
|
|
12358
13317
|
}
|
|
12359
13318
|
process.exitCode = 1;
|
|
12360
13319
|
}
|
|
13320
|
+
if (result.suppressionErrors !== void 0 && result.suppressionErrors.length > 0) {
|
|
13321
|
+
for (const message of result.suppressionErrors) {
|
|
13322
|
+
io.err(`fullshield: ${message}`);
|
|
13323
|
+
}
|
|
13324
|
+
process.exitCode = 1;
|
|
13325
|
+
}
|
|
12361
13326
|
const failOn = failOnOverride ?? result.policy?.failOn ?? result.config.failOn;
|
|
12362
13327
|
if (result.findings.some((f) => failsCi(f, failOn))) {
|
|
12363
13328
|
process.exitCode = 1;
|
|
@@ -12748,8 +13713,8 @@ function createProgram(io = defaultIO) {
|
|
|
12748
13713
|
let name = "app";
|
|
12749
13714
|
let version = "0.0.0";
|
|
12750
13715
|
try {
|
|
12751
|
-
const { readFile:
|
|
12752
|
-
const pkg = JSON.parse(await
|
|
13716
|
+
const { readFile: readFile21 } = await import('fs/promises');
|
|
13717
|
+
const pkg = JSON.parse(await readFile21(join(cwd, "package.json"), "utf8"));
|
|
12753
13718
|
if (typeof pkg.name === "string") name = pkg.name;
|
|
12754
13719
|
if (typeof pkg.version === "string") version = pkg.version;
|
|
12755
13720
|
} catch {
|
|
@@ -12770,6 +13735,31 @@ function createProgram(io = defaultIO) {
|
|
|
12770
13735
|
process.exitCode = 2;
|
|
12771
13736
|
}
|
|
12772
13737
|
});
|
|
13738
|
+
program.command("diff").description("Experimental comparison of two scan --json files (FULLSHIELD_EXPERIMENTAL_DIFF=1)").argument("<before>", "earlier scan JSON").argument("<after>", "later scan JSON").option("--experimental-diff", "enable this command (FULLSHIELD_EXPERIMENTAL_DIFF=1)").option("--json", "print the machine diff").option("--out <file>", "write the machine diff to this file").option("--fail-on <severity>", "exit 1 when an added finding is at or above this severity", "high").action(
|
|
13739
|
+
async (beforePath, afterPath, opts) => {
|
|
13740
|
+
try {
|
|
13741
|
+
if (opts.experimentalDiff === true) process.env[DIFF_FLAG] = "1";
|
|
13742
|
+
if (!diffEnabled()) throw new Error(`diff requires ${DIFF_FLAG}=1`);
|
|
13743
|
+
const failOn = parseFailOn(opts.failOn ?? "high");
|
|
13744
|
+
let beforeText;
|
|
13745
|
+
let afterText;
|
|
13746
|
+
try {
|
|
13747
|
+
beforeText = await readFile(resolve(beforePath), "utf8");
|
|
13748
|
+
afterText = await readFile(resolve(afterPath), "utf8");
|
|
13749
|
+
} catch {
|
|
13750
|
+
throw new Error("cannot read diff input");
|
|
13751
|
+
}
|
|
13752
|
+
const diff = diffScans(parseScanDocument(beforeText), parseScanDocument(afterText));
|
|
13753
|
+
const json = JSON.stringify(diff, null, 2);
|
|
13754
|
+
if (opts.out !== void 0) await writeFile(resolve(opts.out), json, "utf8");
|
|
13755
|
+
io.out(opts.json === true ? json : renderDiffText(diff));
|
|
13756
|
+
if (addedFails(diff, failOn)) process.exitCode = 1;
|
|
13757
|
+
} catch (error) {
|
|
13758
|
+
io.err(formatCliError(error));
|
|
13759
|
+
process.exitCode = 2;
|
|
13760
|
+
}
|
|
13761
|
+
}
|
|
13762
|
+
);
|
|
12773
13763
|
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
13764
|
try {
|
|
12775
13765
|
io.out(await runDoctor(resolve(path), opts));
|