@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/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",
|
|
@@ -5147,6 +5048,7 @@ var CODE_RULES = [
|
|
|
5147
5048
|
},
|
|
5148
5049
|
{
|
|
5149
5050
|
id: "java-broken-cipher",
|
|
5051
|
+
inherent: true,
|
|
5150
5052
|
title: "broken cipher or ECB mode",
|
|
5151
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.",
|
|
5152
5054
|
cwe: "CWE-327",
|
|
@@ -5233,6 +5135,9 @@ function fileTextOf(lines) {
|
|
|
5233
5135
|
}
|
|
5234
5136
|
return text;
|
|
5235
5137
|
}
|
|
5138
|
+
function withoutSingleQuoted(text) {
|
|
5139
|
+
return text.replace(/'[^'\n]*'/g, "''");
|
|
5140
|
+
}
|
|
5236
5141
|
function evaluateRule(rule, ctx) {
|
|
5237
5142
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
5238
5143
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -5247,13 +5152,15 @@ function evaluateRule(rule, ctx) {
|
|
|
5247
5152
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
5248
5153
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
5249
5154
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
5250
|
-
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);
|
|
5251
5158
|
if (rule.needsContext && !contextual) return null;
|
|
5252
|
-
const confidence = contextual ? "contextual" : "pattern";
|
|
5159
|
+
const confidence = rule.inherent ? "evidence" : contextual ? "contextual" : "pattern";
|
|
5253
5160
|
return { rule, confidence, severity: severityFor(rule.severity, confidence) };
|
|
5254
5161
|
}
|
|
5255
5162
|
|
|
5256
|
-
//
|
|
5163
|
+
// ../../packages/scan/src/manifest-rules.ts
|
|
5257
5164
|
var POPULAR_NPM = [
|
|
5258
5165
|
"react",
|
|
5259
5166
|
"react-dom",
|
|
@@ -5528,7 +5435,7 @@ function scanRequirementsTxt(text) {
|
|
|
5528
5435
|
return findings;
|
|
5529
5436
|
}
|
|
5530
5437
|
|
|
5531
|
-
//
|
|
5438
|
+
// ../../packages/scan/src/secret-rules.ts
|
|
5532
5439
|
var SECRET_RULES = [
|
|
5533
5440
|
{
|
|
5534
5441
|
id: "secret-aws-access-key",
|
|
@@ -5711,7 +5618,15 @@ var SENSITIVE_FILES = [
|
|
|
5711
5618
|
// match. Reporting the file itself trades a real finding for a chore.
|
|
5712
5619
|
];
|
|
5713
5620
|
|
|
5714
|
-
//
|
|
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
|
+
}
|
|
5715
5630
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
5716
5631
|
"node_modules",
|
|
5717
5632
|
".git",
|
|
@@ -5820,7 +5735,7 @@ var LANGUAGE_BY_EXTENSION = {
|
|
|
5820
5735
|
};
|
|
5821
5736
|
function languageOf(filename) {
|
|
5822
5737
|
if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
|
|
5823
|
-
return LANGUAGE_BY_EXTENSION[(
|
|
5738
|
+
return LANGUAGE_BY_EXTENSION[extensionOf(filename).toLowerCase()] ?? "other";
|
|
5824
5739
|
}
|
|
5825
5740
|
var LANGUAGE_BY_INTERPRETER = {
|
|
5826
5741
|
sh: "shell",
|
|
@@ -5840,10 +5755,12 @@ var LANGUAGE_BY_INTERPRETER = {
|
|
|
5840
5755
|
php: "php"
|
|
5841
5756
|
};
|
|
5842
5757
|
function languageOfShebang(firstLine) {
|
|
5843
|
-
const match = /^#!\s*(\S+)(?:\s+(
|
|
5758
|
+
const match = /^#!\s*(\S+)(?:\s+(.+?))?\s*$/.exec(firstLine);
|
|
5844
5759
|
if (!match) return null;
|
|
5845
|
-
const command = (
|
|
5846
|
-
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;
|
|
5847
5764
|
const exact = LANGUAGE_BY_INTERPRETER[name];
|
|
5848
5765
|
if (exact) return exact;
|
|
5849
5766
|
const stripped = name.replace(/[\d.]+$/, "");
|
|
@@ -5945,6 +5862,10 @@ function scanManifest(relativePath, filename, text) {
|
|
|
5945
5862
|
category: "manifest"
|
|
5946
5863
|
}));
|
|
5947
5864
|
}
|
|
5865
|
+
|
|
5866
|
+
// ../../packages/scan/src/node/walk.ts
|
|
5867
|
+
var import_node_fs10 = require("fs");
|
|
5868
|
+
var import_node_path5 = require("path");
|
|
5948
5869
|
function scanPath(targetPath, options = {}) {
|
|
5949
5870
|
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
5950
5871
|
const allowed = options.categories ? new Set(options.categories) : null;
|
|
@@ -5954,15 +5875,15 @@ function scanPath(targetPath, options = {}) {
|
|
|
5954
5875
|
let suppressed = 0;
|
|
5955
5876
|
const rootIsDirectory = (() => {
|
|
5956
5877
|
try {
|
|
5957
|
-
return (0,
|
|
5878
|
+
return (0, import_node_fs10.statSync)(targetPath).isDirectory();
|
|
5958
5879
|
} catch {
|
|
5959
5880
|
return true;
|
|
5960
5881
|
}
|
|
5961
5882
|
})();
|
|
5962
|
-
const walkRoot = rootIsDirectory ? targetPath : (0,
|
|
5883
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path5.dirname)(targetPath);
|
|
5963
5884
|
const scanFile = (fullPath, filename) => {
|
|
5964
5885
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
5965
|
-
const extension = (0,
|
|
5886
|
+
const extension = (0, import_node_path5.extname)(filename).toLowerCase();
|
|
5966
5887
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
5967
5888
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
5968
5889
|
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
@@ -5974,26 +5895,26 @@ function scanPath(targetPath, options = {}) {
|
|
|
5974
5895
|
let handle;
|
|
5975
5896
|
let declared = null;
|
|
5976
5897
|
try {
|
|
5977
|
-
handle = (0,
|
|
5898
|
+
handle = (0, import_node_fs10.openSync)(fullPath, "r");
|
|
5978
5899
|
} catch {
|
|
5979
5900
|
unreadable.push(relativePath);
|
|
5980
5901
|
return;
|
|
5981
5902
|
}
|
|
5982
5903
|
try {
|
|
5983
|
-
if ((0,
|
|
5904
|
+
if ((0, import_node_fs10.fstatSync)(handle).size > maxFileBytes) return;
|
|
5984
5905
|
if (mayDeclareInterpreter) {
|
|
5985
5906
|
const prefix = Buffer.alloc(128);
|
|
5986
|
-
const read = (0,
|
|
5907
|
+
const read = (0, import_node_fs10.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
5987
5908
|
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
5988
5909
|
if (!declared) return;
|
|
5989
5910
|
}
|
|
5990
|
-
text = (0,
|
|
5911
|
+
text = (0, import_node_fs10.readFileSync)(handle, "utf-8");
|
|
5991
5912
|
} catch {
|
|
5992
5913
|
unreadable.push(relativePath);
|
|
5993
5914
|
return;
|
|
5994
5915
|
} finally {
|
|
5995
5916
|
try {
|
|
5996
|
-
(0,
|
|
5917
|
+
(0, import_node_fs10.closeSync)(handle);
|
|
5997
5918
|
} catch {
|
|
5998
5919
|
}
|
|
5999
5920
|
}
|
|
@@ -6010,13 +5931,13 @@ function scanPath(targetPath, options = {}) {
|
|
|
6010
5931
|
const walk = (currentPath) => {
|
|
6011
5932
|
let entries;
|
|
6012
5933
|
try {
|
|
6013
|
-
entries = (0,
|
|
5934
|
+
entries = (0, import_node_fs10.readdirSync)(currentPath, { withFileTypes: true });
|
|
6014
5935
|
} catch {
|
|
6015
5936
|
unreadable.push(toRelative(walkRoot, currentPath));
|
|
6016
5937
|
return;
|
|
6017
5938
|
}
|
|
6018
5939
|
for (const entry of entries) {
|
|
6019
|
-
const fullPath = (0,
|
|
5940
|
+
const fullPath = (0, import_node_path5.join)(currentPath, entry.name);
|
|
6020
5941
|
if (entry.isDirectory()) {
|
|
6021
5942
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
6022
5943
|
walk(fullPath);
|
|
@@ -6029,7 +5950,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
6029
5950
|
if (rootIsDirectory) {
|
|
6030
5951
|
walk(targetPath);
|
|
6031
5952
|
} else {
|
|
6032
|
-
scanFile(targetPath, (0,
|
|
5953
|
+
scanFile(targetPath, (0, import_node_path5.basename)(targetPath));
|
|
6033
5954
|
}
|
|
6034
5955
|
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
6035
5956
|
filtered.sort(
|
|
@@ -6060,11 +5981,111 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
|
6060
5981
|
}
|
|
6061
5982
|
}
|
|
6062
5983
|
function toRelative(base, target) {
|
|
6063
|
-
const rel = (0,
|
|
6064
|
-
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
|
+
}
|
|
6065
6086
|
}
|
|
6066
6087
|
|
|
6067
|
-
//
|
|
6088
|
+
// ../../packages/scan/src/node/sarif.ts
|
|
6068
6089
|
var import_node_crypto = require("crypto");
|
|
6069
6090
|
var import_node_path7 = require("path");
|
|
6070
6091
|
|