@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/daemon.js
CHANGED
|
@@ -4310,111 +4310,7 @@ function workerId() {
|
|
|
4310
4310
|
return `${import_node_os4.default.hostname()}/${process.pid}`;
|
|
4311
4311
|
}
|
|
4312
4312
|
|
|
4313
|
-
//
|
|
4314
|
-
var import_node_fs10 = require("fs");
|
|
4315
|
-
var import_node_path5 = require("path");
|
|
4316
|
-
var LOCKFILES = [
|
|
4317
|
-
{ file: "package-lock.json", ecosystem: "npm" },
|
|
4318
|
-
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
4319
|
-
{ file: "yarn.lock", ecosystem: "npm" },
|
|
4320
|
-
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
4321
|
-
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
4322
|
-
];
|
|
4323
|
-
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
4324
|
-
async function scanDependencies(targetPath) {
|
|
4325
|
-
const findings = [];
|
|
4326
|
-
for (const { file, ecosystem } of LOCKFILES) {
|
|
4327
|
-
const lockPath = (0, import_node_path5.join)(targetPath, file);
|
|
4328
|
-
if (!(0, import_node_fs10.existsSync)(lockPath)) continue;
|
|
4329
|
-
let deps;
|
|
4330
|
-
try {
|
|
4331
|
-
deps = parseDependencies(lockPath, file);
|
|
4332
|
-
} catch {
|
|
4333
|
-
continue;
|
|
4334
|
-
}
|
|
4335
|
-
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
4336
|
-
let vulns;
|
|
4337
|
-
try {
|
|
4338
|
-
vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
4339
|
-
} catch {
|
|
4340
|
-
continue;
|
|
4341
|
-
}
|
|
4342
|
-
for (const vuln of vulns) {
|
|
4343
|
-
const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
|
|
4344
|
-
findings.push({
|
|
4345
|
-
ruleId: "dependency-known-vulnerability",
|
|
4346
|
-
title: "Dependency CVE",
|
|
4347
|
-
file,
|
|
4348
|
-
line: 1,
|
|
4349
|
-
severity: severityFromCvss(cvss),
|
|
4350
|
-
confidence: "evidence",
|
|
4351
|
-
message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
|
|
4352
|
-
consequence: "A published advisory exists for the exact version resolved in this lockfile.",
|
|
4353
|
-
excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
|
|
4354
|
-
category: "dependency"
|
|
4355
|
-
});
|
|
4356
|
-
}
|
|
4357
|
-
}
|
|
4358
|
-
}
|
|
4359
|
-
return findings;
|
|
4360
|
-
}
|
|
4361
|
-
function severityFromCvss(score) {
|
|
4362
|
-
if (!score) return "medium";
|
|
4363
|
-
const value = Number.parseFloat(score);
|
|
4364
|
-
if (Number.isNaN(value)) return "medium";
|
|
4365
|
-
if (value >= 9) return "critical";
|
|
4366
|
-
if (value >= 7) return "high";
|
|
4367
|
-
if (value >= 4) return "medium";
|
|
4368
|
-
return "low";
|
|
4369
|
-
}
|
|
4370
|
-
function parseDependencies(lockPath, filename) {
|
|
4371
|
-
const deps = [];
|
|
4372
|
-
if (filename === "package-lock.json") {
|
|
4373
|
-
const lock = JSON.parse((0, import_node_fs10.readFileSync)(lockPath, "utf-8"));
|
|
4374
|
-
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
4375
|
-
for (const [key, value] of Object.entries(packages)) {
|
|
4376
|
-
const name = key.replace(/^node_modules\//, "");
|
|
4377
|
-
const version = value?.version;
|
|
4378
|
-
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
4379
|
-
}
|
|
4380
|
-
return deps;
|
|
4381
|
-
}
|
|
4382
|
-
if (filename === "requirements.txt") {
|
|
4383
|
-
for (const line of (0, import_node_fs10.readFileSync)(lockPath, "utf-8").split("\n")) {
|
|
4384
|
-
const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
|
|
4385
|
-
if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
|
|
4386
|
-
}
|
|
4387
|
-
}
|
|
4388
|
-
return deps;
|
|
4389
|
-
}
|
|
4390
|
-
function isValidPackageName(name) {
|
|
4391
|
-
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
4392
|
-
}
|
|
4393
|
-
function isValidVersion(version) {
|
|
4394
|
-
return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
|
|
4395
|
-
}
|
|
4396
|
-
async function queryOsv(name, version, ecosystem) {
|
|
4397
|
-
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
4398
|
-
try {
|
|
4399
|
-
const response = await fetch("https://api.osv.dev/v1/query", {
|
|
4400
|
-
method: "POST",
|
|
4401
|
-
headers: { "Content-Type": "application/json" },
|
|
4402
|
-
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
4403
|
-
signal: AbortSignal.timeout(5e3)
|
|
4404
|
-
});
|
|
4405
|
-
if (!response.ok) return [];
|
|
4406
|
-
const data = await response.json();
|
|
4407
|
-
return data.vulns ?? [];
|
|
4408
|
-
} catch {
|
|
4409
|
-
return [];
|
|
4410
|
-
}
|
|
4411
|
-
}
|
|
4412
|
-
|
|
4413
|
-
// src/scan/engine.ts
|
|
4414
|
-
var import_node_fs11 = require("fs");
|
|
4415
|
-
var import_node_path6 = require("path");
|
|
4416
|
-
|
|
4417
|
-
// src/scan/types.ts
|
|
4313
|
+
// ../../packages/scan/src/types.ts
|
|
4418
4314
|
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
4419
4315
|
function severityRank(severity) {
|
|
4420
4316
|
const index = SEVERITY_ORDER.indexOf(severity);
|
|
@@ -4425,7 +4321,7 @@ function severityFor(declared, confidence) {
|
|
|
4425
4321
|
return severityRank(declared) > severityRank("medium") ? "medium" : declared;
|
|
4426
4322
|
}
|
|
4427
4323
|
|
|
4428
|
-
//
|
|
4324
|
+
// ../../packages/scan/src/code-rules.ts
|
|
4429
4325
|
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*\(/;
|
|
4430
4326
|
var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ\b\s*\[/;
|
|
4431
4327
|
var UNTRUSTED_RB = /\bparams\s*\[|\brequest\b|\bcookies\s*\[/;
|
|
@@ -4809,6 +4705,7 @@ var CODE_RULES = [
|
|
|
4809
4705
|
},
|
|
4810
4706
|
{
|
|
4811
4707
|
id: "tls-verification-disabled",
|
|
4708
|
+
inherent: true,
|
|
4812
4709
|
title: "TLS certificate verification disabled",
|
|
4813
4710
|
consequence: "Every connection made this way is trivially interceptable; the encryption is decorative.",
|
|
4814
4711
|
cwe: "CWE-295",
|
|
@@ -4902,6 +4799,7 @@ var CODE_RULES = [
|
|
|
4902
4799
|
// of privileged work actually happens, and they run as whoever invoked them.
|
|
4903
4800
|
{
|
|
4904
4801
|
id: "sh-remote-script-execution",
|
|
4802
|
+
inherent: true,
|
|
4905
4803
|
title: "network output piped into a shell",
|
|
4906
4804
|
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.",
|
|
4907
4805
|
cwe: "CWE-494",
|
|
@@ -4957,6 +4855,7 @@ var CODE_RULES = [
|
|
|
4957
4855
|
},
|
|
4958
4856
|
{
|
|
4959
4857
|
id: "sh-insecure-transport-flag",
|
|
4858
|
+
inherent: true,
|
|
4960
4859
|
title: "certificate verification disabled",
|
|
4961
4860
|
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.",
|
|
4962
4861
|
cwe: "CWE-295",
|
|
@@ -4971,6 +4870,7 @@ var CODE_RULES = [
|
|
|
4971
4870
|
},
|
|
4972
4871
|
{
|
|
4973
4872
|
id: "sh-plaintext-download",
|
|
4873
|
+
inherent: true,
|
|
4974
4874
|
title: "download over plain HTTP",
|
|
4975
4875
|
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.",
|
|
4976
4876
|
cwe: "CWE-319",
|
|
@@ -4982,6 +4882,7 @@ var CODE_RULES = [
|
|
|
4982
4882
|
},
|
|
4983
4883
|
{
|
|
4984
4884
|
id: "sh-world-writable-permissions",
|
|
4885
|
+
inherent: true,
|
|
4985
4886
|
title: "world-writable permissions",
|
|
4986
4887
|
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.",
|
|
4987
4888
|
cwe: "CWE-732",
|
|
@@ -5099,6 +5000,91 @@ var CODE_RULES = [
|
|
|
5099
5000
|
languages: ["php"],
|
|
5100
5001
|
pattern: /\bextract\s*\(\s*\$_(?:GET|POST|REQUEST|COOKIE)\b|\bimport_request_variables\s*\(/,
|
|
5101
5002
|
guard: false
|
|
5003
|
+
},
|
|
5004
|
+
// ── Java and Go: classes the other languages already had ─────────────────
|
|
5005
|
+
//
|
|
5006
|
+
// Command injection, SSRF and path traversal were implemented for JavaScript
|
|
5007
|
+
// and, in part, for Go, and never for Java — so the same defect in the same
|
|
5008
|
+
// codebase was reported or not depending on which file it lived in. TLS
|
|
5009
|
+
// verification, weak hashing and insecure randomness are deliberately absent
|
|
5010
|
+
// here: `tls-verification-disabled`, `weak-hash-on-credential` and
|
|
5011
|
+
// `insecure-randomness-for-secret` are language-agnostic and already cover
|
|
5012
|
+
// both, including Go's `InsecureSkipVerify` and Java's `MessageDigest`.
|
|
5013
|
+
{
|
|
5014
|
+
id: "java-runtime-exec-concatenation",
|
|
5015
|
+
title: "Runtime.exec with a concatenated command",
|
|
5016
|
+
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.",
|
|
5017
|
+
cwe: "CWE-78",
|
|
5018
|
+
severity: "critical",
|
|
5019
|
+
languages: ["java"],
|
|
5020
|
+
// The array form — `exec(new String[]{"git", arg})` — passes argv and is
|
|
5021
|
+
// the fix, so it is not matched: the `+` has to be inside the string
|
|
5022
|
+
// argument for this to fire.
|
|
5023
|
+
pattern: /\b(?:Runtime\s*\.\s*getRuntime\s*\(\s*\)\s*\.\s*exec|ProcessBuilder)\s*\(\s*(?:"[^"\n]*"\s*\+|\w+\s*\+\s*")/
|
|
5024
|
+
},
|
|
5025
|
+
{
|
|
5026
|
+
id: "java-ssrf-outbound-request",
|
|
5027
|
+
title: "outbound request to a computed URL",
|
|
5028
|
+
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.",
|
|
5029
|
+
cwe: "CWE-918",
|
|
5030
|
+
severity: "high",
|
|
5031
|
+
languages: ["java"],
|
|
5032
|
+
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)]/,
|
|
5033
|
+
needsContext: true
|
|
5034
|
+
},
|
|
5035
|
+
{
|
|
5036
|
+
id: "java-request-path-traversal",
|
|
5037
|
+
title: "file path built from request data",
|
|
5038
|
+
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.",
|
|
5039
|
+
cwe: "CWE-22",
|
|
5040
|
+
severity: "high",
|
|
5041
|
+
languages: ["java"],
|
|
5042
|
+
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]*\+/,
|
|
5043
|
+
// `getCanonicalPath().startsWith(base)` is the check that makes this safe,
|
|
5044
|
+
// and it is normally a line or two below the construction.
|
|
5045
|
+
guard: /getCanonicalPath|toRealPath|normalize\s*\(\s*\)|\bstartsWith\s*\(/,
|
|
5046
|
+
guardForward: 4,
|
|
5047
|
+
needsContext: true
|
|
5048
|
+
},
|
|
5049
|
+
{
|
|
5050
|
+
id: "java-broken-cipher",
|
|
5051
|
+
inherent: true,
|
|
5052
|
+
title: "broken cipher or ECB mode",
|
|
5053
|
+
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.",
|
|
5054
|
+
cwe: "CWE-327",
|
|
5055
|
+
severity: "high",
|
|
5056
|
+
languages: ["java"],
|
|
5057
|
+
// Bare `"AES"` is included: the JCE resolves it to `AES/ECB/PKCS5Padding`,
|
|
5058
|
+
// so the default is the mode this rule exists to catch.
|
|
5059
|
+
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*\)/,
|
|
5060
|
+
guard: false
|
|
5061
|
+
},
|
|
5062
|
+
{
|
|
5063
|
+
id: "go-request-path-traversal",
|
|
5064
|
+
title: "file path built from request data",
|
|
5065
|
+
consequence: "A `../` sequence in the value walks out of the intended directory, and the handler serves or writes whatever it reaches.",
|
|
5066
|
+
cwe: "CWE-22",
|
|
5067
|
+
severity: "high",
|
|
5068
|
+
languages: ["go"],
|
|
5069
|
+
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)/,
|
|
5070
|
+
// `filepath.Clean` alone does not bound the result to a directory, so it is
|
|
5071
|
+
// not a guard here — the containment check is.
|
|
5072
|
+
guard: /\bstrings\s*\.\s*HasPrefix\s*\(|\bfilepath\s*\.\s*Rel\s*\(|\bfs\s*\.\s*ValidPath\s*\(|\bhttp\s*\.\s*Dir\b/,
|
|
5073
|
+
guardBack: 5,
|
|
5074
|
+
guardForward: 3,
|
|
5075
|
+
needsContext: true
|
|
5076
|
+
},
|
|
5077
|
+
{
|
|
5078
|
+
id: "go-template-escaping-bypass",
|
|
5079
|
+
title: "value marked as pre-escaped HTML",
|
|
5080
|
+
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.",
|
|
5081
|
+
cwe: "CWE-79",
|
|
5082
|
+
severity: "high",
|
|
5083
|
+
languages: ["go"],
|
|
5084
|
+
// A conversion of a *variable*. `template.HTML("<br>")` on a literal is a
|
|
5085
|
+
// constant the author wrote and is not a finding.
|
|
5086
|
+
pattern: /\btemplate\s*\.\s*(?:HTML|JS|CSS|HTMLAttr|URL|Srcset)\s*\(\s*(?!\s*[`"])/,
|
|
5087
|
+
needsContext: true
|
|
5102
5088
|
}
|
|
5103
5089
|
];
|
|
5104
5090
|
var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
|
|
@@ -5149,6 +5135,9 @@ function fileTextOf(lines) {
|
|
|
5149
5135
|
}
|
|
5150
5136
|
return text;
|
|
5151
5137
|
}
|
|
5138
|
+
function withoutSingleQuoted(text) {
|
|
5139
|
+
return text.replace(/'[^'\n]*'/g, "''");
|
|
5140
|
+
}
|
|
5152
5141
|
function evaluateRule(rule, ctx) {
|
|
5153
5142
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
5154
5143
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -5163,13 +5152,15 @@ function evaluateRule(rule, ctx) {
|
|
|
5163
5152
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
5164
5153
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
5165
5154
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
5166
|
-
const
|
|
5155
|
+
const probeLine = ctx.language === "shell" ? withoutSingleQuoted(line) : line;
|
|
5156
|
+
const probeContext = ctx.language === "shell" ? withoutSingleQuoted(context) : context;
|
|
5157
|
+
const contextual = untrusted.test(probeLine) || untrusted.test(probeContext);
|
|
5167
5158
|
if (rule.needsContext && !contextual) return null;
|
|
5168
|
-
const confidence = contextual ? "contextual" : "pattern";
|
|
5159
|
+
const confidence = rule.inherent ? "evidence" : contextual ? "contextual" : "pattern";
|
|
5169
5160
|
return { rule, confidence, severity: severityFor(rule.severity, confidence) };
|
|
5170
5161
|
}
|
|
5171
5162
|
|
|
5172
|
-
//
|
|
5163
|
+
// ../../packages/scan/src/manifest-rules.ts
|
|
5173
5164
|
var POPULAR_NPM = [
|
|
5174
5165
|
"react",
|
|
5175
5166
|
"react-dom",
|
|
@@ -5444,7 +5435,7 @@ function scanRequirementsTxt(text) {
|
|
|
5444
5435
|
return findings;
|
|
5445
5436
|
}
|
|
5446
5437
|
|
|
5447
|
-
//
|
|
5438
|
+
// ../../packages/scan/src/secret-rules.ts
|
|
5448
5439
|
var SECRET_RULES = [
|
|
5449
5440
|
{
|
|
5450
5441
|
id: "secret-aws-access-key",
|
|
@@ -5627,7 +5618,15 @@ var SENSITIVE_FILES = [
|
|
|
5627
5618
|
// match. Reporting the file itself trades a real finding for a chore.
|
|
5628
5619
|
];
|
|
5629
5620
|
|
|
5630
|
-
//
|
|
5621
|
+
// ../../packages/scan/src/text.ts
|
|
5622
|
+
function baseNameOf(path) {
|
|
5623
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
5624
|
+
}
|
|
5625
|
+
function extensionOf(path) {
|
|
5626
|
+
const base = baseNameOf(path);
|
|
5627
|
+
const dot = base.lastIndexOf(".");
|
|
5628
|
+
return dot <= 0 ? "" : base.slice(dot);
|
|
5629
|
+
}
|
|
5631
5630
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5632
5631
|
"node_modules",
|
|
5633
5632
|
".git",
|
|
@@ -5736,7 +5735,7 @@ var LANGUAGE_BY_EXTENSION = {
|
|
|
5736
5735
|
};
|
|
5737
5736
|
function languageOf(filename) {
|
|
5738
5737
|
if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
|
|
5739
|
-
return LANGUAGE_BY_EXTENSION[(
|
|
5738
|
+
return LANGUAGE_BY_EXTENSION[extensionOf(filename).toLowerCase()] ?? "other";
|
|
5740
5739
|
}
|
|
5741
5740
|
var LANGUAGE_BY_INTERPRETER = {
|
|
5742
5741
|
sh: "shell",
|
|
@@ -5756,10 +5755,12 @@ var LANGUAGE_BY_INTERPRETER = {
|
|
|
5756
5755
|
php: "php"
|
|
5757
5756
|
};
|
|
5758
5757
|
function languageOfShebang(firstLine) {
|
|
5759
|
-
const match = /^#!\s*(\S+)(?:\s+(
|
|
5758
|
+
const match = /^#!\s*(\S+)(?:\s+(.+?))?\s*$/.exec(firstLine);
|
|
5760
5759
|
if (!match) return null;
|
|
5761
|
-
const command = (
|
|
5762
|
-
const
|
|
5760
|
+
const command = baseNameOf(match[1]);
|
|
5761
|
+
const args = match[2]?.trim().split(/\s+/) ?? [];
|
|
5762
|
+
const splitString = args[0] === "-S" || args[0] === "--split-string";
|
|
5763
|
+
const name = command === "env" ? baseNameOf(args[splitString ? 1 : 0] ?? "") : command;
|
|
5763
5764
|
const exact = LANGUAGE_BY_INTERPRETER[name];
|
|
5764
5765
|
if (exact) return exact;
|
|
5765
5766
|
const stripped = name.replace(/[\d.]+$/, "");
|
|
@@ -5861,6 +5862,10 @@ function scanManifest(relativePath, filename, text) {
|
|
|
5861
5862
|
category: "manifest"
|
|
5862
5863
|
}));
|
|
5863
5864
|
}
|
|
5865
|
+
|
|
5866
|
+
// ../../packages/scan/src/node/walk.ts
|
|
5867
|
+
var import_node_fs10 = require("fs");
|
|
5868
|
+
var import_node_path5 = require("path");
|
|
5864
5869
|
function scanPath(targetPath, options = {}) {
|
|
5865
5870
|
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
5866
5871
|
const allowed = options.categories ? new Set(options.categories) : null;
|
|
@@ -5870,15 +5875,15 @@ function scanPath(targetPath, options = {}) {
|
|
|
5870
5875
|
let suppressed = 0;
|
|
5871
5876
|
const rootIsDirectory = (() => {
|
|
5872
5877
|
try {
|
|
5873
|
-
return (0,
|
|
5878
|
+
return (0, import_node_fs10.statSync)(targetPath).isDirectory();
|
|
5874
5879
|
} catch {
|
|
5875
5880
|
return true;
|
|
5876
5881
|
}
|
|
5877
5882
|
})();
|
|
5878
|
-
const walkRoot = rootIsDirectory ? targetPath : (0,
|
|
5883
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path5.dirname)(targetPath);
|
|
5879
5884
|
const scanFile = (fullPath, filename) => {
|
|
5880
5885
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
5881
|
-
const extension = (0,
|
|
5886
|
+
const extension = (0, import_node_path5.extname)(filename).toLowerCase();
|
|
5882
5887
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
5883
5888
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
5884
5889
|
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
@@ -5890,26 +5895,26 @@ function scanPath(targetPath, options = {}) {
|
|
|
5890
5895
|
let handle;
|
|
5891
5896
|
let declared = null;
|
|
5892
5897
|
try {
|
|
5893
|
-
handle = (0,
|
|
5898
|
+
handle = (0, import_node_fs10.openSync)(fullPath, "r");
|
|
5894
5899
|
} catch {
|
|
5895
5900
|
unreadable.push(relativePath);
|
|
5896
5901
|
return;
|
|
5897
5902
|
}
|
|
5898
5903
|
try {
|
|
5899
|
-
if ((0,
|
|
5904
|
+
if ((0, import_node_fs10.fstatSync)(handle).size > maxFileBytes) return;
|
|
5900
5905
|
if (mayDeclareInterpreter) {
|
|
5901
5906
|
const prefix = Buffer.alloc(128);
|
|
5902
|
-
const read = (0,
|
|
5907
|
+
const read = (0, import_node_fs10.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
5903
5908
|
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
5904
5909
|
if (!declared) return;
|
|
5905
5910
|
}
|
|
5906
|
-
text = (0,
|
|
5911
|
+
text = (0, import_node_fs10.readFileSync)(handle, "utf-8");
|
|
5907
5912
|
} catch {
|
|
5908
5913
|
unreadable.push(relativePath);
|
|
5909
5914
|
return;
|
|
5910
5915
|
} finally {
|
|
5911
5916
|
try {
|
|
5912
|
-
(0,
|
|
5917
|
+
(0, import_node_fs10.closeSync)(handle);
|
|
5913
5918
|
} catch {
|
|
5914
5919
|
}
|
|
5915
5920
|
}
|
|
@@ -5926,13 +5931,13 @@ function scanPath(targetPath, options = {}) {
|
|
|
5926
5931
|
const walk = (currentPath) => {
|
|
5927
5932
|
let entries;
|
|
5928
5933
|
try {
|
|
5929
|
-
entries = (0,
|
|
5934
|
+
entries = (0, import_node_fs10.readdirSync)(currentPath, { withFileTypes: true });
|
|
5930
5935
|
} catch {
|
|
5931
5936
|
unreadable.push(toRelative(walkRoot, currentPath));
|
|
5932
5937
|
return;
|
|
5933
5938
|
}
|
|
5934
5939
|
for (const entry of entries) {
|
|
5935
|
-
const fullPath = (0,
|
|
5940
|
+
const fullPath = (0, import_node_path5.join)(currentPath, entry.name);
|
|
5936
5941
|
if (entry.isDirectory()) {
|
|
5937
5942
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
5938
5943
|
walk(fullPath);
|
|
@@ -5945,7 +5950,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
5945
5950
|
if (rootIsDirectory) {
|
|
5946
5951
|
walk(targetPath);
|
|
5947
5952
|
} else {
|
|
5948
|
-
scanFile(targetPath, (0,
|
|
5953
|
+
scanFile(targetPath, (0, import_node_path5.basename)(targetPath));
|
|
5949
5954
|
}
|
|
5950
5955
|
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
5951
5956
|
filtered.sort(
|
|
@@ -5976,11 +5981,111 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
|
5976
5981
|
}
|
|
5977
5982
|
}
|
|
5978
5983
|
function toRelative(base, target) {
|
|
5979
|
-
const rel = (0,
|
|
5980
|
-
return (rel === "" ? target : rel).split(
|
|
5984
|
+
const rel = (0, import_node_path5.relative)(base, target);
|
|
5985
|
+
return (rel === "" ? target : rel).split(import_node_path5.sep).join("/");
|
|
5986
|
+
}
|
|
5987
|
+
|
|
5988
|
+
// ../../packages/scan/src/node/dependencies.ts
|
|
5989
|
+
var import_node_fs11 = require("fs");
|
|
5990
|
+
var import_node_path6 = require("path");
|
|
5991
|
+
var LOCKFILES = [
|
|
5992
|
+
{ file: "package-lock.json", ecosystem: "npm" },
|
|
5993
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
5994
|
+
{ file: "yarn.lock", ecosystem: "npm" },
|
|
5995
|
+
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
5996
|
+
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
5997
|
+
];
|
|
5998
|
+
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
5999
|
+
async function scanDependencies(targetPath) {
|
|
6000
|
+
const findings = [];
|
|
6001
|
+
for (const { file, ecosystem } of LOCKFILES) {
|
|
6002
|
+
const lockPath = (0, import_node_path6.join)(targetPath, file);
|
|
6003
|
+
if (!(0, import_node_fs11.existsSync)(lockPath)) continue;
|
|
6004
|
+
let deps;
|
|
6005
|
+
try {
|
|
6006
|
+
deps = parseDependencies(lockPath, file);
|
|
6007
|
+
} catch {
|
|
6008
|
+
continue;
|
|
6009
|
+
}
|
|
6010
|
+
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
6011
|
+
let vulns;
|
|
6012
|
+
try {
|
|
6013
|
+
vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
6014
|
+
} catch {
|
|
6015
|
+
continue;
|
|
6016
|
+
}
|
|
6017
|
+
for (const vuln of vulns) {
|
|
6018
|
+
const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
|
|
6019
|
+
findings.push({
|
|
6020
|
+
ruleId: "dependency-known-vulnerability",
|
|
6021
|
+
title: "Dependency CVE",
|
|
6022
|
+
file,
|
|
6023
|
+
line: 1,
|
|
6024
|
+
severity: severityFromCvss(cvss),
|
|
6025
|
+
confidence: "evidence",
|
|
6026
|
+
message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
|
|
6027
|
+
consequence: "A published advisory exists for the exact version resolved in this lockfile.",
|
|
6028
|
+
excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
|
|
6029
|
+
category: "dependency"
|
|
6030
|
+
});
|
|
6031
|
+
}
|
|
6032
|
+
}
|
|
6033
|
+
}
|
|
6034
|
+
return findings;
|
|
6035
|
+
}
|
|
6036
|
+
function severityFromCvss(score) {
|
|
6037
|
+
if (!score) return "medium";
|
|
6038
|
+
const value = Number.parseFloat(score);
|
|
6039
|
+
if (Number.isNaN(value)) return "medium";
|
|
6040
|
+
if (value >= 9) return "critical";
|
|
6041
|
+
if (value >= 7) return "high";
|
|
6042
|
+
if (value >= 4) return "medium";
|
|
6043
|
+
return "low";
|
|
6044
|
+
}
|
|
6045
|
+
function parseDependencies(lockPath, filename) {
|
|
6046
|
+
const deps = [];
|
|
6047
|
+
if (filename === "package-lock.json") {
|
|
6048
|
+
const lock = JSON.parse((0, import_node_fs11.readFileSync)(lockPath, "utf-8"));
|
|
6049
|
+
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
6050
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
6051
|
+
const name = key.replace(/^node_modules\//, "");
|
|
6052
|
+
const version = value?.version;
|
|
6053
|
+
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
6054
|
+
}
|
|
6055
|
+
return deps;
|
|
6056
|
+
}
|
|
6057
|
+
if (filename === "requirements.txt") {
|
|
6058
|
+
for (const line of (0, import_node_fs11.readFileSync)(lockPath, "utf-8").split("\n")) {
|
|
6059
|
+
const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
|
|
6060
|
+
if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
|
|
6061
|
+
}
|
|
6062
|
+
}
|
|
6063
|
+
return deps;
|
|
6064
|
+
}
|
|
6065
|
+
function isValidPackageName(name) {
|
|
6066
|
+
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
6067
|
+
}
|
|
6068
|
+
function isValidVersion(version) {
|
|
6069
|
+
return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
|
|
6070
|
+
}
|
|
6071
|
+
async function queryOsv(name, version, ecosystem) {
|
|
6072
|
+
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
6073
|
+
try {
|
|
6074
|
+
const response = await fetch("https://api.osv.dev/v1/query", {
|
|
6075
|
+
method: "POST",
|
|
6076
|
+
headers: { "Content-Type": "application/json" },
|
|
6077
|
+
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
6078
|
+
signal: AbortSignal.timeout(5e3)
|
|
6079
|
+
});
|
|
6080
|
+
if (!response.ok) return [];
|
|
6081
|
+
const data = await response.json();
|
|
6082
|
+
return data.vulns ?? [];
|
|
6083
|
+
} catch {
|
|
6084
|
+
return [];
|
|
6085
|
+
}
|
|
5981
6086
|
}
|
|
5982
6087
|
|
|
5983
|
-
//
|
|
6088
|
+
// ../../packages/scan/src/node/sarif.ts
|
|
5984
6089
|
var import_node_crypto = require("crypto");
|
|
5985
6090
|
var import_node_path7 = require("path");
|
|
5986
6091
|
|