@profullstack/threatcrush 0.6.2 → 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 +234 -129
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +238 -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",
|
|
@@ -10369,6 +10270,91 @@ var CODE_RULES = [
|
|
|
10369
10270
|
languages: ["php"],
|
|
10370
10271
|
pattern: /\bextract\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)\b|\bimport_request_variables\s*\(/,
|
|
10371
10272
|
guard: false
|
|
10273
|
+
},
|
|
10274
|
+
// ── Java and Go: classes the other languages already had ─────────────────
|
|
10275
|
+
//
|
|
10276
|
+
// Command injection, SSRF and path traversal were implemented for JavaScript
|
|
10277
|
+
// and, in part, for Go, and never for Java — so the same defect in the same
|
|
10278
|
+
// codebase was reported or not depending on which file it lived in. TLS
|
|
10279
|
+
// verification, weak hashing and insecure randomness are deliberately absent
|
|
10280
|
+
// here: `tls-verification-disabled`, `weak-hash-on-credential` and
|
|
10281
|
+
// `insecure-randomness-for-secret` are language-agnostic and already cover
|
|
10282
|
+
// both, including Go's `InsecureSkipVerify` and Java's `MessageDigest`.
|
|
10283
|
+
{
|
|
10284
|
+
id: "java-runtime-exec-concatenation",
|
|
10285
|
+
title: "Runtime.exec with a concatenated command",
|
|
10286
|
+
consequence: "The single-string form of `exec` is split on whitespace and handed to the OS. A value carrying a space becomes extra arguments, and where a shell is invoked, `;` and `$(\u2026)` become extra commands.",
|
|
10287
|
+
cwe: "CWE-78",
|
|
10288
|
+
severity: "critical",
|
|
10289
|
+
languages: ["java"],
|
|
10290
|
+
// The array form — `exec(new String[]{"git", arg})` — passes argv and is
|
|
10291
|
+
// the fix, so it is not matched: the `+` has to be inside the string
|
|
10292
|
+
// argument for this to fire.
|
|
10293
|
+
pattern: /\b(?:Runtime\s*\.\s*getRuntime\s*\(\s*\)\s*\.\s*exec|ProcessBuilder)\s*\(\s*(?:"[^"\n]*"\s*\+|\w+\s*\+\s*")/
|
|
10294
|
+
},
|
|
10295
|
+
{
|
|
10296
|
+
id: "java-ssrf-outbound-request",
|
|
10297
|
+
title: "outbound request to a computed URL",
|
|
10298
|
+
consequence: "The destination is chosen by the caller, so the request can be aimed at internal services and cloud metadata endpoints that are reachable from this host and from nowhere else.",
|
|
10299
|
+
cwe: "CWE-918",
|
|
10300
|
+
severity: "high",
|
|
10301
|
+
languages: ["java"],
|
|
10302
|
+
pattern: /\bnew\s+URL\s*\(\s*(?!\s*"[a-z]+:\/\/[^"\n]*"\s*\))[^)\n]*\w|\bHttpRequest\s*\.\s*newBuilder\s*\(\s*\)\s*\.\s*uri\s*\(\s*URI\s*\.\s*create\s*\(\s*[^"\n)]/,
|
|
10303
|
+
needsContext: true
|
|
10304
|
+
},
|
|
10305
|
+
{
|
|
10306
|
+
id: "java-request-path-traversal",
|
|
10307
|
+
title: "file path built from request data",
|
|
10308
|
+
consequence: "A `../` sequence in the value walks out of the intended directory. The process then reads or writes wherever it lands, with its own privileges.",
|
|
10309
|
+
cwe: "CWE-22",
|
|
10310
|
+
severity: "high",
|
|
10311
|
+
languages: ["java"],
|
|
10312
|
+
pattern: /\b(?:new\s+File|new\s+FileInputStream|new\s+FileOutputStream|Paths\s*\.\s*get|Files\s*\.\s*(?:readAllBytes|newInputStream|newOutputStream|copy|delete))\s*\([^)\n]*\+/,
|
|
10313
|
+
// `getCanonicalPath().startsWith(base)` is the check that makes this safe,
|
|
10314
|
+
// and it is normally a line or two below the construction.
|
|
10315
|
+
guard: /getCanonicalPath|toRealPath|normalize\s*\(\s*\)|\bstartsWith\s*\(/,
|
|
10316
|
+
guardForward: 4,
|
|
10317
|
+
needsContext: true
|
|
10318
|
+
},
|
|
10319
|
+
{
|
|
10320
|
+
id: "java-broken-cipher",
|
|
10321
|
+
inherent: true,
|
|
10322
|
+
title: "broken cipher or ECB mode",
|
|
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.",
|
|
10324
|
+
cwe: "CWE-327",
|
|
10325
|
+
severity: "high",
|
|
10326
|
+
languages: ["java"],
|
|
10327
|
+
// Bare `"AES"` is included: the JCE resolves it to `AES/ECB/PKCS5Padding`,
|
|
10328
|
+
// so the default is the mode this rule exists to catch.
|
|
10329
|
+
pattern: /\bCipher\s*\.\s*getInstance\s*\(\s*"(?:DES|DESede|RC2|RC4|ARCFOUR|Blowfish)(?:\/|")|\bCipher\s*\.\s*getInstance\s*\(\s*"[^"\n]*\/ECB\/|\bCipher\s*\.\s*getInstance\s*\(\s*"AES"\s*\)/,
|
|
10330
|
+
guard: false
|
|
10331
|
+
},
|
|
10332
|
+
{
|
|
10333
|
+
id: "go-request-path-traversal",
|
|
10334
|
+
title: "file path built from request data",
|
|
10335
|
+
consequence: "A `../` sequence in the value walks out of the intended directory, and the handler serves or writes whatever it reaches.",
|
|
10336
|
+
cwe: "CWE-22",
|
|
10337
|
+
severity: "high",
|
|
10338
|
+
languages: ["go"],
|
|
10339
|
+
pattern: /\b(?:os\s*\.\s*(?:Open|OpenFile|ReadFile|Create|Remove|WriteFile)|ioutil\s*\.\s*(?:ReadFile|WriteFile)|http\s*\.\s*ServeFile)\s*\([^)\n]*(?:r\s*\.\s*URL|FormValue|Query\s*\(\s*\)\s*\.\s*Get|mux\s*\.\s*Vars|\bfilepath\s*\.\s*Join\s*\([^)\n]*\w)/,
|
|
10340
|
+
// `filepath.Clean` alone does not bound the result to a directory, so it is
|
|
10341
|
+
// not a guard here — the containment check is.
|
|
10342
|
+
guard: /\bstrings\s*\.\s*HasPrefix\s*\(|\bfilepath\s*\.\s*Rel\s*\(|\bfs\s*\.\s*ValidPath\s*\(|\bhttp\s*\.\s*Dir\b/,
|
|
10343
|
+
guardBack: 5,
|
|
10344
|
+
guardForward: 3,
|
|
10345
|
+
needsContext: true
|
|
10346
|
+
},
|
|
10347
|
+
{
|
|
10348
|
+
id: "go-template-escaping-bypass",
|
|
10349
|
+
title: "value marked as pre-escaped HTML",
|
|
10350
|
+
consequence: "`template.HTML` tells `html/template` the value is already safe, which switches off the contextual escaping that makes the package worth using. Markup in the value reaches the page intact.",
|
|
10351
|
+
cwe: "CWE-79",
|
|
10352
|
+
severity: "high",
|
|
10353
|
+
languages: ["go"],
|
|
10354
|
+
// A conversion of a *variable*. `template.HTML("<br>")` on a literal is a
|
|
10355
|
+
// constant the author wrote and is not a finding.
|
|
10356
|
+
pattern: /\btemplate\s*\.\s*(?:HTML|JS|CSS|HTMLAttr|URL|Srcset)\s*\(\s*(?!\s*[`"])/,
|
|
10357
|
+
needsContext: true
|
|
10372
10358
|
}
|
|
10373
10359
|
];
|
|
10374
10360
|
var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
|
|
@@ -10419,6 +10405,9 @@ function fileTextOf(lines) {
|
|
|
10419
10405
|
}
|
|
10420
10406
|
return text;
|
|
10421
10407
|
}
|
|
10408
|
+
function withoutSingleQuoted(text) {
|
|
10409
|
+
return text.replace(/'[^'\n]*'/g, "''");
|
|
10410
|
+
}
|
|
10422
10411
|
function evaluateRule(rule, ctx) {
|
|
10423
10412
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
10424
10413
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -10433,13 +10422,15 @@ function evaluateRule(rule, ctx) {
|
|
|
10433
10422
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
10434
10423
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
10435
10424
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
10436
|
-
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);
|
|
10437
10428
|
if (rule.needsContext && !contextual) return null;
|
|
10438
|
-
const confidence = contextual ? "contextual" : "pattern";
|
|
10429
|
+
const confidence = rule.inherent ? "evidence" : contextual ? "contextual" : "pattern";
|
|
10439
10430
|
return { rule, confidence, severity: severityFor(rule.severity, confidence) };
|
|
10440
10431
|
}
|
|
10441
10432
|
|
|
10442
|
-
//
|
|
10433
|
+
// ../../packages/scan/src/manifest-rules.ts
|
|
10443
10434
|
var POPULAR_NPM = [
|
|
10444
10435
|
"react",
|
|
10445
10436
|
"react-dom",
|
|
@@ -10714,7 +10705,7 @@ function scanRequirementsTxt(text) {
|
|
|
10714
10705
|
return findings;
|
|
10715
10706
|
}
|
|
10716
10707
|
|
|
10717
|
-
//
|
|
10708
|
+
// ../../packages/scan/src/secret-rules.ts
|
|
10718
10709
|
var SECRET_RULES = [
|
|
10719
10710
|
{
|
|
10720
10711
|
id: "secret-aws-access-key",
|
|
@@ -10897,7 +10888,15 @@ var SENSITIVE_FILES = [
|
|
|
10897
10888
|
// match. Reporting the file itself trades a real finding for a chore.
|
|
10898
10889
|
];
|
|
10899
10890
|
|
|
10900
|
-
//
|
|
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
|
+
}
|
|
10901
10900
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
10902
10901
|
"node_modules",
|
|
10903
10902
|
".git",
|
|
@@ -11006,7 +11005,7 @@ var LANGUAGE_BY_EXTENSION = {
|
|
|
11006
11005
|
};
|
|
11007
11006
|
function languageOf(filename) {
|
|
11008
11007
|
if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
|
|
11009
|
-
return LANGUAGE_BY_EXTENSION[(
|
|
11008
|
+
return LANGUAGE_BY_EXTENSION[extensionOf(filename).toLowerCase()] ?? "other";
|
|
11010
11009
|
}
|
|
11011
11010
|
var LANGUAGE_BY_INTERPRETER = {
|
|
11012
11011
|
sh: "shell",
|
|
@@ -11026,10 +11025,12 @@ var LANGUAGE_BY_INTERPRETER = {
|
|
|
11026
11025
|
php: "php"
|
|
11027
11026
|
};
|
|
11028
11027
|
function languageOfShebang(firstLine) {
|
|
11029
|
-
const match = /^#!\s*(\S+)(?:\s+(
|
|
11028
|
+
const match = /^#!\s*(\S+)(?:\s+(.+?))?\s*$/.exec(firstLine);
|
|
11030
11029
|
if (!match) return null;
|
|
11031
|
-
const command = (
|
|
11032
|
-
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;
|
|
11033
11034
|
const exact = LANGUAGE_BY_INTERPRETER[name];
|
|
11034
11035
|
if (exact) return exact;
|
|
11035
11036
|
const stripped = name.replace(/[\d.]+$/, "");
|
|
@@ -11131,6 +11132,15 @@ function scanManifest(relativePath, filename, text) {
|
|
|
11131
11132
|
category: "manifest"
|
|
11132
11133
|
}));
|
|
11133
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");
|
|
11134
11144
|
function scanPath(targetPath, options = {}) {
|
|
11135
11145
|
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
11136
11146
|
const allowed = options.categories ? new Set(options.categories) : null;
|
|
@@ -11140,15 +11150,15 @@ function scanPath(targetPath, options = {}) {
|
|
|
11140
11150
|
let suppressed = 0;
|
|
11141
11151
|
const rootIsDirectory = (() => {
|
|
11142
11152
|
try {
|
|
11143
|
-
return (0,
|
|
11153
|
+
return (0, import_node_fs5.statSync)(targetPath).isDirectory();
|
|
11144
11154
|
} catch {
|
|
11145
11155
|
return true;
|
|
11146
11156
|
}
|
|
11147
11157
|
})();
|
|
11148
|
-
const walkRoot = rootIsDirectory ? targetPath : (0,
|
|
11158
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path2.dirname)(targetPath);
|
|
11149
11159
|
const scanFile = (fullPath, filename) => {
|
|
11150
11160
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
11151
|
-
const extension = (0,
|
|
11161
|
+
const extension = (0, import_node_path2.extname)(filename).toLowerCase();
|
|
11152
11162
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
11153
11163
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
11154
11164
|
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
@@ -11160,26 +11170,26 @@ function scanPath(targetPath, options = {}) {
|
|
|
11160
11170
|
let handle;
|
|
11161
11171
|
let declared = null;
|
|
11162
11172
|
try {
|
|
11163
|
-
handle = (0,
|
|
11173
|
+
handle = (0, import_node_fs5.openSync)(fullPath, "r");
|
|
11164
11174
|
} catch {
|
|
11165
11175
|
unreadable.push(relativePath);
|
|
11166
11176
|
return;
|
|
11167
11177
|
}
|
|
11168
11178
|
try {
|
|
11169
|
-
if ((0,
|
|
11179
|
+
if ((0, import_node_fs5.fstatSync)(handle).size > maxFileBytes) return;
|
|
11170
11180
|
if (mayDeclareInterpreter) {
|
|
11171
11181
|
const prefix = Buffer.alloc(128);
|
|
11172
|
-
const read = (0,
|
|
11182
|
+
const read = (0, import_node_fs5.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
11173
11183
|
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
11174
11184
|
if (!declared) return;
|
|
11175
11185
|
}
|
|
11176
|
-
text = (0,
|
|
11186
|
+
text = (0, import_node_fs5.readFileSync)(handle, "utf-8");
|
|
11177
11187
|
} catch {
|
|
11178
11188
|
unreadable.push(relativePath);
|
|
11179
11189
|
return;
|
|
11180
11190
|
} finally {
|
|
11181
11191
|
try {
|
|
11182
|
-
(0,
|
|
11192
|
+
(0, import_node_fs5.closeSync)(handle);
|
|
11183
11193
|
} catch {
|
|
11184
11194
|
}
|
|
11185
11195
|
}
|
|
@@ -11196,13 +11206,13 @@ function scanPath(targetPath, options = {}) {
|
|
|
11196
11206
|
const walk = (currentPath) => {
|
|
11197
11207
|
let entries;
|
|
11198
11208
|
try {
|
|
11199
|
-
entries = (0,
|
|
11209
|
+
entries = (0, import_node_fs5.readdirSync)(currentPath, { withFileTypes: true });
|
|
11200
11210
|
} catch {
|
|
11201
11211
|
unreadable.push(toRelative(walkRoot, currentPath));
|
|
11202
11212
|
return;
|
|
11203
11213
|
}
|
|
11204
11214
|
for (const entry of entries) {
|
|
11205
|
-
const fullPath = (0,
|
|
11215
|
+
const fullPath = (0, import_node_path2.join)(currentPath, entry.name);
|
|
11206
11216
|
if (entry.isDirectory()) {
|
|
11207
11217
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
11208
11218
|
walk(fullPath);
|
|
@@ -11215,7 +11225,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
11215
11225
|
if (rootIsDirectory) {
|
|
11216
11226
|
walk(targetPath);
|
|
11217
11227
|
} else {
|
|
11218
|
-
scanFile(targetPath, (0,
|
|
11228
|
+
scanFile(targetPath, (0, import_node_path2.basename)(targetPath));
|
|
11219
11229
|
}
|
|
11220
11230
|
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
11221
11231
|
filtered.sort(
|
|
@@ -11246,16 +11256,111 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
|
11246
11256
|
}
|
|
11247
11257
|
}
|
|
11248
11258
|
function toRelative(base, target) {
|
|
11249
|
-
const rel = (0,
|
|
11250
|
-
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("/");
|
|
11251
11261
|
}
|
|
11252
|
-
|
|
11253
|
-
|
|
11254
|
-
|
|
11255
|
-
|
|
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
|
+
}
|
|
11256
11361
|
}
|
|
11257
11362
|
|
|
11258
|
-
//
|
|
11363
|
+
// ../../packages/scan/src/node/sarif.ts
|
|
11259
11364
|
var import_node_crypto = require("crypto");
|
|
11260
11365
|
var import_node_path4 = require("path");
|
|
11261
11366
|
var FINGERPRINT_KEY = "threatcrush/contentHash/v1";
|