@profullstack/threatcrush 0.7.0 → 0.7.1
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/dist/daemon.js +150 -129
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +154 -133
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -9580,111 +9580,7 @@ function workerId() {
|
|
|
9580
9580
|
return `${import_node_os3.default.hostname()}/${process.pid}`;
|
|
9581
9581
|
}
|
|
9582
9582
|
|
|
9583
|
-
//
|
|
9584
|
-
var import_node_fs5 = require("fs");
|
|
9585
|
-
var import_node_path2 = require("path");
|
|
9586
|
-
var LOCKFILES = [
|
|
9587
|
-
{ file: "package-lock.json", ecosystem: "npm" },
|
|
9588
|
-
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
9589
|
-
{ file: "yarn.lock", ecosystem: "npm" },
|
|
9590
|
-
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
9591
|
-
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
9592
|
-
];
|
|
9593
|
-
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
9594
|
-
async function scanDependencies(targetPath) {
|
|
9595
|
-
const findings = [];
|
|
9596
|
-
for (const { file, ecosystem } of LOCKFILES) {
|
|
9597
|
-
const lockPath = (0, import_node_path2.join)(targetPath, file);
|
|
9598
|
-
if (!(0, import_node_fs5.existsSync)(lockPath)) continue;
|
|
9599
|
-
let deps;
|
|
9600
|
-
try {
|
|
9601
|
-
deps = parseDependencies(lockPath, file);
|
|
9602
|
-
} catch {
|
|
9603
|
-
continue;
|
|
9604
|
-
}
|
|
9605
|
-
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
9606
|
-
let vulns;
|
|
9607
|
-
try {
|
|
9608
|
-
vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
9609
|
-
} catch {
|
|
9610
|
-
continue;
|
|
9611
|
-
}
|
|
9612
|
-
for (const vuln of vulns) {
|
|
9613
|
-
const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
|
|
9614
|
-
findings.push({
|
|
9615
|
-
ruleId: "dependency-known-vulnerability",
|
|
9616
|
-
title: "Dependency CVE",
|
|
9617
|
-
file,
|
|
9618
|
-
line: 1,
|
|
9619
|
-
severity: severityFromCvss(cvss),
|
|
9620
|
-
confidence: "evidence",
|
|
9621
|
-
message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
|
|
9622
|
-
consequence: "A published advisory exists for the exact version resolved in this lockfile.",
|
|
9623
|
-
excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
|
|
9624
|
-
category: "dependency"
|
|
9625
|
-
});
|
|
9626
|
-
}
|
|
9627
|
-
}
|
|
9628
|
-
}
|
|
9629
|
-
return findings;
|
|
9630
|
-
}
|
|
9631
|
-
function severityFromCvss(score) {
|
|
9632
|
-
if (!score) return "medium";
|
|
9633
|
-
const value = Number.parseFloat(score);
|
|
9634
|
-
if (Number.isNaN(value)) return "medium";
|
|
9635
|
-
if (value >= 9) return "critical";
|
|
9636
|
-
if (value >= 7) return "high";
|
|
9637
|
-
if (value >= 4) return "medium";
|
|
9638
|
-
return "low";
|
|
9639
|
-
}
|
|
9640
|
-
function parseDependencies(lockPath, filename) {
|
|
9641
|
-
const deps = [];
|
|
9642
|
-
if (filename === "package-lock.json") {
|
|
9643
|
-
const lock = JSON.parse((0, import_node_fs5.readFileSync)(lockPath, "utf-8"));
|
|
9644
|
-
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
9645
|
-
for (const [key, value] of Object.entries(packages)) {
|
|
9646
|
-
const name = key.replace(/^node_modules\//, "");
|
|
9647
|
-
const version = value?.version;
|
|
9648
|
-
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
9649
|
-
}
|
|
9650
|
-
return deps;
|
|
9651
|
-
}
|
|
9652
|
-
if (filename === "requirements.txt") {
|
|
9653
|
-
for (const line of (0, import_node_fs5.readFileSync)(lockPath, "utf-8").split("\n")) {
|
|
9654
|
-
const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
|
|
9655
|
-
if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
|
|
9656
|
-
}
|
|
9657
|
-
}
|
|
9658
|
-
return deps;
|
|
9659
|
-
}
|
|
9660
|
-
function isValidPackageName(name) {
|
|
9661
|
-
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
9662
|
-
}
|
|
9663
|
-
function isValidVersion(version) {
|
|
9664
|
-
return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
|
|
9665
|
-
}
|
|
9666
|
-
async function queryOsv(name, version, ecosystem) {
|
|
9667
|
-
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
9668
|
-
try {
|
|
9669
|
-
const response = await fetch("https://api.osv.dev/v1/query", {
|
|
9670
|
-
method: "POST",
|
|
9671
|
-
headers: { "Content-Type": "application/json" },
|
|
9672
|
-
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
9673
|
-
signal: AbortSignal.timeout(5e3)
|
|
9674
|
-
});
|
|
9675
|
-
if (!response.ok) return [];
|
|
9676
|
-
const data = await response.json();
|
|
9677
|
-
return data.vulns ?? [];
|
|
9678
|
-
} catch {
|
|
9679
|
-
return [];
|
|
9680
|
-
}
|
|
9681
|
-
}
|
|
9682
|
-
|
|
9683
|
-
// src/scan/engine.ts
|
|
9684
|
-
var import_node_fs6 = require("fs");
|
|
9685
|
-
var import_node_path3 = require("path");
|
|
9686
|
-
|
|
9687
|
-
// src/scan/types.ts
|
|
9583
|
+
// ../../packages/scan/src/types.ts
|
|
9688
9584
|
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
9689
9585
|
function severityRank(severity) {
|
|
9690
9586
|
const index = SEVERITY_ORDER.indexOf(severity);
|
|
@@ -9695,7 +9591,7 @@ function severityFor(declared, confidence) {
|
|
|
9695
9591
|
return severityRank(declared) > severityRank("medium") ? "medium" : declared;
|
|
9696
9592
|
}
|
|
9697
9593
|
|
|
9698
|
-
//
|
|
9594
|
+
// ../../packages/scan/src/code-rules.ts
|
|
9699
9595
|
var UNTRUSTED_JS = /\b(?:req|request|ctx|context)\s*\.\s*(?:body|query|params|param|headers|cookies|url|files)\b|\bprocess\.argv\b|\bwindow\.location\b|\bdocument\.location\b|\blocation\.(?:search|hash|href)\b|\bsearchParams\s*\.\s*(?:get|getAll|has|entries|keys|values|forEach)\b|\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetInputStream\s*\(/;
|
|
9700
9596
|
var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ\b\s*\[/;
|
|
9701
9597
|
var UNTRUSTED_RB = /\bparams\s*\[|\brequest\b|\bcookies\s*\[/;
|
|
@@ -10079,6 +9975,7 @@ var CODE_RULES = [
|
|
|
10079
9975
|
},
|
|
10080
9976
|
{
|
|
10081
9977
|
id: "tls-verification-disabled",
|
|
9978
|
+
inherent: true,
|
|
10082
9979
|
title: "TLS certificate verification disabled",
|
|
10083
9980
|
consequence: "Every connection made this way is trivially interceptable; the encryption is decorative.",
|
|
10084
9981
|
cwe: "CWE-295",
|
|
@@ -10172,6 +10069,7 @@ var CODE_RULES = [
|
|
|
10172
10069
|
// of privileged work actually happens, and they run as whoever invoked them.
|
|
10173
10070
|
{
|
|
10174
10071
|
id: "sh-remote-script-execution",
|
|
10072
|
+
inherent: true,
|
|
10175
10073
|
title: "network output piped into a shell",
|
|
10176
10074
|
consequence: "Whatever that URL serves at the moment this runs is executed as the invoking user. There is no version, no signature, and no review \u2014 a compromise of the host, or anyone able to answer for it, is a compromise of every machine that runs the script.",
|
|
10177
10075
|
cwe: "CWE-494",
|
|
@@ -10227,6 +10125,7 @@ var CODE_RULES = [
|
|
|
10227
10125
|
},
|
|
10228
10126
|
{
|
|
10229
10127
|
id: "sh-insecure-transport-flag",
|
|
10128
|
+
inherent: true,
|
|
10230
10129
|
title: "certificate verification disabled",
|
|
10231
10130
|
consequence: "Anyone positioned between this host and the server can substitute the response. When the response is a package, a key or a script, that is remote code execution with the transport doing nothing to stop it.",
|
|
10232
10131
|
cwe: "CWE-295",
|
|
@@ -10241,6 +10140,7 @@ var CODE_RULES = [
|
|
|
10241
10140
|
},
|
|
10242
10141
|
{
|
|
10243
10142
|
id: "sh-plaintext-download",
|
|
10143
|
+
inherent: true,
|
|
10244
10144
|
title: "download over plain HTTP",
|
|
10245
10145
|
consequence: "The response arrives unauthenticated over a channel any intermediary can rewrite. Where the payload is an archive, a package list or a key, substituting it is straightforward and leaves nothing for the script to notice.",
|
|
10246
10146
|
cwe: "CWE-319",
|
|
@@ -10252,6 +10152,7 @@ var CODE_RULES = [
|
|
|
10252
10152
|
},
|
|
10253
10153
|
{
|
|
10254
10154
|
id: "sh-world-writable-permissions",
|
|
10155
|
+
inherent: true,
|
|
10255
10156
|
title: "world-writable permissions",
|
|
10256
10157
|
consequence: "Any local account can rewrite the file. If it is a script, a config or anything on a privileged path, the next process to read it runs someone else\u2019s content.",
|
|
10257
10158
|
cwe: "CWE-732",
|
|
@@ -10417,6 +10318,7 @@ var CODE_RULES = [
|
|
|
10417
10318
|
},
|
|
10418
10319
|
{
|
|
10419
10320
|
id: "java-broken-cipher",
|
|
10321
|
+
inherent: true,
|
|
10420
10322
|
title: "broken cipher or ECB mode",
|
|
10421
10323
|
consequence: "DES, RC2, RC4 and Blowfish are broken or too small to rely on. ECB encrypts identical plaintext blocks to identical ciphertext blocks, so structure in the data survives encryption and is readable straight off the ciphertext.",
|
|
10422
10324
|
cwe: "CWE-327",
|
|
@@ -10503,6 +10405,9 @@ function fileTextOf(lines) {
|
|
|
10503
10405
|
}
|
|
10504
10406
|
return text;
|
|
10505
10407
|
}
|
|
10408
|
+
function withoutSingleQuoted(text) {
|
|
10409
|
+
return text.replace(/'[^'\n]*'/g, "''");
|
|
10410
|
+
}
|
|
10506
10411
|
function evaluateRule(rule, ctx) {
|
|
10507
10412
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
10508
10413
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -10517,13 +10422,15 @@ function evaluateRule(rule, ctx) {
|
|
|
10517
10422
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
10518
10423
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
10519
10424
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
10520
|
-
const
|
|
10425
|
+
const probeLine = ctx.language === "shell" ? withoutSingleQuoted(line) : line;
|
|
10426
|
+
const probeContext = ctx.language === "shell" ? withoutSingleQuoted(context) : context;
|
|
10427
|
+
const contextual = untrusted.test(probeLine) || untrusted.test(probeContext);
|
|
10521
10428
|
if (rule.needsContext && !contextual) return null;
|
|
10522
|
-
const confidence = contextual ? "contextual" : "pattern";
|
|
10429
|
+
const confidence = rule.inherent ? "evidence" : contextual ? "contextual" : "pattern";
|
|
10523
10430
|
return { rule, confidence, severity: severityFor(rule.severity, confidence) };
|
|
10524
10431
|
}
|
|
10525
10432
|
|
|
10526
|
-
//
|
|
10433
|
+
// ../../packages/scan/src/manifest-rules.ts
|
|
10527
10434
|
var POPULAR_NPM = [
|
|
10528
10435
|
"react",
|
|
10529
10436
|
"react-dom",
|
|
@@ -10798,7 +10705,7 @@ function scanRequirementsTxt(text) {
|
|
|
10798
10705
|
return findings;
|
|
10799
10706
|
}
|
|
10800
10707
|
|
|
10801
|
-
//
|
|
10708
|
+
// ../../packages/scan/src/secret-rules.ts
|
|
10802
10709
|
var SECRET_RULES = [
|
|
10803
10710
|
{
|
|
10804
10711
|
id: "secret-aws-access-key",
|
|
@@ -10981,7 +10888,15 @@ var SENSITIVE_FILES = [
|
|
|
10981
10888
|
// match. Reporting the file itself trades a real finding for a chore.
|
|
10982
10889
|
];
|
|
10983
10890
|
|
|
10984
|
-
//
|
|
10891
|
+
// ../../packages/scan/src/text.ts
|
|
10892
|
+
function baseNameOf(path) {
|
|
10893
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
10894
|
+
}
|
|
10895
|
+
function extensionOf(path) {
|
|
10896
|
+
const base = baseNameOf(path);
|
|
10897
|
+
const dot = base.lastIndexOf(".");
|
|
10898
|
+
return dot <= 0 ? "" : base.slice(dot);
|
|
10899
|
+
}
|
|
10985
10900
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
10986
10901
|
"node_modules",
|
|
10987
10902
|
".git",
|
|
@@ -11090,7 +11005,7 @@ var LANGUAGE_BY_EXTENSION = {
|
|
|
11090
11005
|
};
|
|
11091
11006
|
function languageOf(filename) {
|
|
11092
11007
|
if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
|
|
11093
|
-
return LANGUAGE_BY_EXTENSION[(
|
|
11008
|
+
return LANGUAGE_BY_EXTENSION[extensionOf(filename).toLowerCase()] ?? "other";
|
|
11094
11009
|
}
|
|
11095
11010
|
var LANGUAGE_BY_INTERPRETER = {
|
|
11096
11011
|
sh: "shell",
|
|
@@ -11110,10 +11025,12 @@ var LANGUAGE_BY_INTERPRETER = {
|
|
|
11110
11025
|
php: "php"
|
|
11111
11026
|
};
|
|
11112
11027
|
function languageOfShebang(firstLine) {
|
|
11113
|
-
const match = /^#!\s*(\S+)(?:\s+(
|
|
11028
|
+
const match = /^#!\s*(\S+)(?:\s+(.+?))?\s*$/.exec(firstLine);
|
|
11114
11029
|
if (!match) return null;
|
|
11115
|
-
const command = (
|
|
11116
|
-
const
|
|
11030
|
+
const command = baseNameOf(match[1]);
|
|
11031
|
+
const args = match[2]?.trim().split(/\s+/) ?? [];
|
|
11032
|
+
const splitString = args[0] === "-S" || args[0] === "--split-string";
|
|
11033
|
+
const name = command === "env" ? baseNameOf(args[splitString ? 1 : 0] ?? "") : command;
|
|
11117
11034
|
const exact = LANGUAGE_BY_INTERPRETER[name];
|
|
11118
11035
|
if (exact) return exact;
|
|
11119
11036
|
const stripped = name.replace(/[\d.]+$/, "");
|
|
@@ -11215,6 +11132,15 @@ function scanManifest(relativePath, filename, text) {
|
|
|
11215
11132
|
category: "manifest"
|
|
11216
11133
|
}));
|
|
11217
11134
|
}
|
|
11135
|
+
function meetsFailThreshold(findings, threshold) {
|
|
11136
|
+
if (threshold.length === 0) return false;
|
|
11137
|
+
const floor = Math.min(...threshold.map(severityRank));
|
|
11138
|
+
return findings.some((finding) => severityRank(finding.severity) >= floor);
|
|
11139
|
+
}
|
|
11140
|
+
|
|
11141
|
+
// ../../packages/scan/src/node/walk.ts
|
|
11142
|
+
var import_node_fs5 = require("fs");
|
|
11143
|
+
var import_node_path2 = require("path");
|
|
11218
11144
|
function scanPath(targetPath, options = {}) {
|
|
11219
11145
|
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
11220
11146
|
const allowed = options.categories ? new Set(options.categories) : null;
|
|
@@ -11224,15 +11150,15 @@ function scanPath(targetPath, options = {}) {
|
|
|
11224
11150
|
let suppressed = 0;
|
|
11225
11151
|
const rootIsDirectory = (() => {
|
|
11226
11152
|
try {
|
|
11227
|
-
return (0,
|
|
11153
|
+
return (0, import_node_fs5.statSync)(targetPath).isDirectory();
|
|
11228
11154
|
} catch {
|
|
11229
11155
|
return true;
|
|
11230
11156
|
}
|
|
11231
11157
|
})();
|
|
11232
|
-
const walkRoot = rootIsDirectory ? targetPath : (0,
|
|
11158
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path2.dirname)(targetPath);
|
|
11233
11159
|
const scanFile = (fullPath, filename) => {
|
|
11234
11160
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
11235
|
-
const extension = (0,
|
|
11161
|
+
const extension = (0, import_node_path2.extname)(filename).toLowerCase();
|
|
11236
11162
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
11237
11163
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
11238
11164
|
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
@@ -11244,26 +11170,26 @@ function scanPath(targetPath, options = {}) {
|
|
|
11244
11170
|
let handle;
|
|
11245
11171
|
let declared = null;
|
|
11246
11172
|
try {
|
|
11247
|
-
handle = (0,
|
|
11173
|
+
handle = (0, import_node_fs5.openSync)(fullPath, "r");
|
|
11248
11174
|
} catch {
|
|
11249
11175
|
unreadable.push(relativePath);
|
|
11250
11176
|
return;
|
|
11251
11177
|
}
|
|
11252
11178
|
try {
|
|
11253
|
-
if ((0,
|
|
11179
|
+
if ((0, import_node_fs5.fstatSync)(handle).size > maxFileBytes) return;
|
|
11254
11180
|
if (mayDeclareInterpreter) {
|
|
11255
11181
|
const prefix = Buffer.alloc(128);
|
|
11256
|
-
const read = (0,
|
|
11182
|
+
const read = (0, import_node_fs5.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
11257
11183
|
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
11258
11184
|
if (!declared) return;
|
|
11259
11185
|
}
|
|
11260
|
-
text = (0,
|
|
11186
|
+
text = (0, import_node_fs5.readFileSync)(handle, "utf-8");
|
|
11261
11187
|
} catch {
|
|
11262
11188
|
unreadable.push(relativePath);
|
|
11263
11189
|
return;
|
|
11264
11190
|
} finally {
|
|
11265
11191
|
try {
|
|
11266
|
-
(0,
|
|
11192
|
+
(0, import_node_fs5.closeSync)(handle);
|
|
11267
11193
|
} catch {
|
|
11268
11194
|
}
|
|
11269
11195
|
}
|
|
@@ -11280,13 +11206,13 @@ function scanPath(targetPath, options = {}) {
|
|
|
11280
11206
|
const walk = (currentPath) => {
|
|
11281
11207
|
let entries;
|
|
11282
11208
|
try {
|
|
11283
|
-
entries = (0,
|
|
11209
|
+
entries = (0, import_node_fs5.readdirSync)(currentPath, { withFileTypes: true });
|
|
11284
11210
|
} catch {
|
|
11285
11211
|
unreadable.push(toRelative(walkRoot, currentPath));
|
|
11286
11212
|
return;
|
|
11287
11213
|
}
|
|
11288
11214
|
for (const entry of entries) {
|
|
11289
|
-
const fullPath = (0,
|
|
11215
|
+
const fullPath = (0, import_node_path2.join)(currentPath, entry.name);
|
|
11290
11216
|
if (entry.isDirectory()) {
|
|
11291
11217
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
11292
11218
|
walk(fullPath);
|
|
@@ -11299,7 +11225,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
11299
11225
|
if (rootIsDirectory) {
|
|
11300
11226
|
walk(targetPath);
|
|
11301
11227
|
} else {
|
|
11302
|
-
scanFile(targetPath, (0,
|
|
11228
|
+
scanFile(targetPath, (0, import_node_path2.basename)(targetPath));
|
|
11303
11229
|
}
|
|
11304
11230
|
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
11305
11231
|
filtered.sort(
|
|
@@ -11330,16 +11256,111 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
|
11330
11256
|
}
|
|
11331
11257
|
}
|
|
11332
11258
|
function toRelative(base, target) {
|
|
11333
|
-
const rel = (0,
|
|
11334
|
-
return (rel === "" ? target : rel).split(
|
|
11259
|
+
const rel = (0, import_node_path2.relative)(base, target);
|
|
11260
|
+
return (rel === "" ? target : rel).split(import_node_path2.sep).join("/");
|
|
11335
11261
|
}
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11262
|
+
|
|
11263
|
+
// ../../packages/scan/src/node/dependencies.ts
|
|
11264
|
+
var import_node_fs6 = require("fs");
|
|
11265
|
+
var import_node_path3 = require("path");
|
|
11266
|
+
var LOCKFILES = [
|
|
11267
|
+
{ file: "package-lock.json", ecosystem: "npm" },
|
|
11268
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
11269
|
+
{ file: "yarn.lock", ecosystem: "npm" },
|
|
11270
|
+
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
11271
|
+
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
11272
|
+
];
|
|
11273
|
+
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
11274
|
+
async function scanDependencies(targetPath) {
|
|
11275
|
+
const findings = [];
|
|
11276
|
+
for (const { file, ecosystem } of LOCKFILES) {
|
|
11277
|
+
const lockPath = (0, import_node_path3.join)(targetPath, file);
|
|
11278
|
+
if (!(0, import_node_fs6.existsSync)(lockPath)) continue;
|
|
11279
|
+
let deps;
|
|
11280
|
+
try {
|
|
11281
|
+
deps = parseDependencies(lockPath, file);
|
|
11282
|
+
} catch {
|
|
11283
|
+
continue;
|
|
11284
|
+
}
|
|
11285
|
+
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
11286
|
+
let vulns;
|
|
11287
|
+
try {
|
|
11288
|
+
vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
11289
|
+
} catch {
|
|
11290
|
+
continue;
|
|
11291
|
+
}
|
|
11292
|
+
for (const vuln of vulns) {
|
|
11293
|
+
const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
|
|
11294
|
+
findings.push({
|
|
11295
|
+
ruleId: "dependency-known-vulnerability",
|
|
11296
|
+
title: "Dependency CVE",
|
|
11297
|
+
file,
|
|
11298
|
+
line: 1,
|
|
11299
|
+
severity: severityFromCvss(cvss),
|
|
11300
|
+
confidence: "evidence",
|
|
11301
|
+
message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
|
|
11302
|
+
consequence: "A published advisory exists for the exact version resolved in this lockfile.",
|
|
11303
|
+
excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
|
|
11304
|
+
category: "dependency"
|
|
11305
|
+
});
|
|
11306
|
+
}
|
|
11307
|
+
}
|
|
11308
|
+
}
|
|
11309
|
+
return findings;
|
|
11310
|
+
}
|
|
11311
|
+
function severityFromCvss(score) {
|
|
11312
|
+
if (!score) return "medium";
|
|
11313
|
+
const value = Number.parseFloat(score);
|
|
11314
|
+
if (Number.isNaN(value)) return "medium";
|
|
11315
|
+
if (value >= 9) return "critical";
|
|
11316
|
+
if (value >= 7) return "high";
|
|
11317
|
+
if (value >= 4) return "medium";
|
|
11318
|
+
return "low";
|
|
11319
|
+
}
|
|
11320
|
+
function parseDependencies(lockPath, filename) {
|
|
11321
|
+
const deps = [];
|
|
11322
|
+
if (filename === "package-lock.json") {
|
|
11323
|
+
const lock = JSON.parse((0, import_node_fs6.readFileSync)(lockPath, "utf-8"));
|
|
11324
|
+
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
11325
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
11326
|
+
const name = key.replace(/^node_modules\//, "");
|
|
11327
|
+
const version = value?.version;
|
|
11328
|
+
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
11329
|
+
}
|
|
11330
|
+
return deps;
|
|
11331
|
+
}
|
|
11332
|
+
if (filename === "requirements.txt") {
|
|
11333
|
+
for (const line of (0, import_node_fs6.readFileSync)(lockPath, "utf-8").split("\n")) {
|
|
11334
|
+
const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
|
|
11335
|
+
if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
|
|
11336
|
+
}
|
|
11337
|
+
}
|
|
11338
|
+
return deps;
|
|
11339
|
+
}
|
|
11340
|
+
function isValidPackageName(name) {
|
|
11341
|
+
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
11342
|
+
}
|
|
11343
|
+
function isValidVersion(version) {
|
|
11344
|
+
return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
|
|
11345
|
+
}
|
|
11346
|
+
async function queryOsv(name, version, ecosystem) {
|
|
11347
|
+
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
11348
|
+
try {
|
|
11349
|
+
const response = await fetch("https://api.osv.dev/v1/query", {
|
|
11350
|
+
method: "POST",
|
|
11351
|
+
headers: { "Content-Type": "application/json" },
|
|
11352
|
+
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
11353
|
+
signal: AbortSignal.timeout(5e3)
|
|
11354
|
+
});
|
|
11355
|
+
if (!response.ok) return [];
|
|
11356
|
+
const data = await response.json();
|
|
11357
|
+
return data.vulns ?? [];
|
|
11358
|
+
} catch {
|
|
11359
|
+
return [];
|
|
11360
|
+
}
|
|
11340
11361
|
}
|
|
11341
11362
|
|
|
11342
|
-
//
|
|
11363
|
+
// ../../packages/scan/src/node/sarif.ts
|
|
11343
11364
|
var import_node_crypto = require("crypto");
|
|
11344
11365
|
var import_node_path4 = require("path");
|
|
11345
11366
|
var FINGERPRINT_KEY = "threatcrush/contentHash/v1";
|