@profullstack/threatcrush 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/daemon.js +1461 -222
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1998 -473
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/dist/index.js
CHANGED
|
@@ -1074,7 +1074,7 @@ var require_suggestSimilar = __commonJS({
|
|
|
1074
1074
|
"../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/suggestSimilar.js"(exports2) {
|
|
1075
1075
|
"use strict";
|
|
1076
1076
|
var maxDistance = 3;
|
|
1077
|
-
function
|
|
1077
|
+
function editDistance2(a, b) {
|
|
1078
1078
|
if (Math.abs(a.length - b.length) > maxDistance)
|
|
1079
1079
|
return Math.max(a.length, b.length);
|
|
1080
1080
|
const d = [];
|
|
@@ -1120,7 +1120,7 @@ var require_suggestSimilar = __commonJS({
|
|
|
1120
1120
|
const minSimilarity = 0.4;
|
|
1121
1121
|
candidates.forEach((candidate) => {
|
|
1122
1122
|
if (candidate.length <= 1) return;
|
|
1123
|
-
const distance =
|
|
1123
|
+
const distance = editDistance2(word, candidate);
|
|
1124
1124
|
const length = Math.max(word.length, candidate.length);
|
|
1125
1125
|
const similarity = (length - distance) / length;
|
|
1126
1126
|
if (similarity > minSimilarity) {
|
|
@@ -3917,7 +3917,7 @@ var init_ipc_client = __esm({
|
|
|
3917
3917
|
if (!(0, import_node_fs2.existsSync)(this.socketPath)) {
|
|
3918
3918
|
throw new Error(`threatcrushd socket not found at ${this.socketPath}`);
|
|
3919
3919
|
}
|
|
3920
|
-
return new Promise((
|
|
3920
|
+
return new Promise((resolve5, reject) => {
|
|
3921
3921
|
const sock = (0, import_node_net.createConnection)(this.socketPath);
|
|
3922
3922
|
const timer = setTimeout(() => {
|
|
3923
3923
|
sock.destroy();
|
|
@@ -3930,7 +3930,7 @@ var init_ipc_client = __esm({
|
|
|
3930
3930
|
sock.on("data", (chunk) => this.onData(chunk.toString()));
|
|
3931
3931
|
sock.on("close", () => this.onClose());
|
|
3932
3932
|
sock.on("error", () => this.onClose());
|
|
3933
|
-
|
|
3933
|
+
resolve5();
|
|
3934
3934
|
});
|
|
3935
3935
|
sock.once("error", (err) => {
|
|
3936
3936
|
clearTimeout(timer);
|
|
@@ -3946,8 +3946,8 @@ var init_ipc_client = __esm({
|
|
|
3946
3946
|
if (!this.socket) throw new Error("not connected");
|
|
3947
3947
|
const id = this.nextId++;
|
|
3948
3948
|
const frame = params ? { id, method, params } : { id, method };
|
|
3949
|
-
return new Promise((
|
|
3950
|
-
this.pending.set(id, { resolve:
|
|
3949
|
+
return new Promise((resolve5, reject) => {
|
|
3950
|
+
this.pending.set(id, { resolve: resolve5, reject });
|
|
3951
3951
|
this.socket.write(JSON.stringify(frame) + "\n", (err) => {
|
|
3952
3952
|
if (err) {
|
|
3953
3953
|
this.pending.delete(id);
|
|
@@ -7363,20 +7363,20 @@ var require_parse_async = __commonJS({
|
|
|
7363
7363
|
const index = 0;
|
|
7364
7364
|
const blocksize = opts.blocksize || 40960;
|
|
7365
7365
|
const parser = new TOMLParser();
|
|
7366
|
-
return new Promise((
|
|
7367
|
-
setImmediate(parseAsyncNext, index, blocksize,
|
|
7366
|
+
return new Promise((resolve5, reject) => {
|
|
7367
|
+
setImmediate(parseAsyncNext, index, blocksize, resolve5, reject);
|
|
7368
7368
|
});
|
|
7369
|
-
function parseAsyncNext(index2, blocksize2,
|
|
7369
|
+
function parseAsyncNext(index2, blocksize2, resolve5, reject) {
|
|
7370
7370
|
if (index2 >= str.length) {
|
|
7371
7371
|
try {
|
|
7372
|
-
return
|
|
7372
|
+
return resolve5(parser.finish());
|
|
7373
7373
|
} catch (err) {
|
|
7374
7374
|
return reject(prettyError(err, str));
|
|
7375
7375
|
}
|
|
7376
7376
|
}
|
|
7377
7377
|
try {
|
|
7378
7378
|
parser.parse(str.slice(index2, index2 + blocksize2));
|
|
7379
|
-
setImmediate(parseAsyncNext, index2 + blocksize2, blocksize2,
|
|
7379
|
+
setImmediate(parseAsyncNext, index2 + blocksize2, blocksize2, resolve5, reject);
|
|
7380
7380
|
} catch (err) {
|
|
7381
7381
|
reject(prettyError(err, str));
|
|
7382
7382
|
}
|
|
@@ -7402,7 +7402,7 @@ var require_parse_stream = __commonJS({
|
|
|
7402
7402
|
function parseReadable(stm) {
|
|
7403
7403
|
const parser = new TOMLParser();
|
|
7404
7404
|
stm.setEncoding("utf8");
|
|
7405
|
-
return new Promise((
|
|
7405
|
+
return new Promise((resolve5, reject) => {
|
|
7406
7406
|
let readable;
|
|
7407
7407
|
let ended = false;
|
|
7408
7408
|
let errored = false;
|
|
@@ -7410,7 +7410,7 @@ var require_parse_stream = __commonJS({
|
|
|
7410
7410
|
ended = true;
|
|
7411
7411
|
if (readable) return;
|
|
7412
7412
|
try {
|
|
7413
|
-
|
|
7413
|
+
resolve5(parser.finish());
|
|
7414
7414
|
} catch (err) {
|
|
7415
7415
|
reject(err);
|
|
7416
7416
|
}
|
|
@@ -8260,8 +8260,8 @@ var source_default = chalk;
|
|
|
8260
8260
|
// src/index.ts
|
|
8261
8261
|
var import_readline = __toESM(require("readline"));
|
|
8262
8262
|
var import_node_child_process10 = require("child_process");
|
|
8263
|
-
var
|
|
8264
|
-
var
|
|
8263
|
+
var import_node_fs29 = require("fs");
|
|
8264
|
+
var import_node_path19 = require("path");
|
|
8265
8265
|
var import_node_os8 = require("os");
|
|
8266
8266
|
|
|
8267
8267
|
// src/commands/monitor.ts
|
|
@@ -8323,6 +8323,7 @@ var NGINX_REGEX = /^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) \S+" (\d{3}) (\d+) "
|
|
|
8323
8323
|
var AUTH_REGEX = /^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+(\S+?)(?:\[\d+\])?:\s+(.*)/;
|
|
8324
8324
|
var SYSLOG_REGEX = /^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+(\S+?)(?:\[\d+\])?:\s+(.*)/;
|
|
8325
8325
|
var IP_REGEX = /(?:from|FROM)\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/;
|
|
8326
|
+
var INVALID_USER_REGEX = /(?:for\s+invalid\s+user)\s+(\S+?)(?:\s+from|\s*$)/;
|
|
8326
8327
|
var USER_REGEX = /(?:for|user)\s+(\S+?)(?:\s+from|\s*$)/;
|
|
8327
8328
|
var ATTACK_PATTERNS = {
|
|
8328
8329
|
sqli: [
|
|
@@ -8376,7 +8377,7 @@ function parseAuthLog(line) {
|
|
|
8376
8377
|
const match = line.match(AUTH_REGEX);
|
|
8377
8378
|
if (!match) return null;
|
|
8378
8379
|
const ipMatch = match[3].match(IP_REGEX);
|
|
8379
|
-
const userMatch = match[3].match(USER_REGEX);
|
|
8380
|
+
const userMatch = match[3].match(INVALID_USER_REGEX) || match[3].match(USER_REGEX);
|
|
8380
8381
|
return {
|
|
8381
8382
|
timestamp: parseSyslogTimestamp(match[1]),
|
|
8382
8383
|
raw: line,
|
|
@@ -8404,10 +8405,25 @@ function parseSyslog(line) {
|
|
|
8404
8405
|
};
|
|
8405
8406
|
}
|
|
8406
8407
|
function detectAttackPattern(path) {
|
|
8408
|
+
const candidates = /* @__PURE__ */ new Set([path]);
|
|
8409
|
+
let current = path;
|
|
8410
|
+
for (let i = 0; i < 2; i++) {
|
|
8411
|
+
let decoded = null;
|
|
8412
|
+
try {
|
|
8413
|
+
decoded = decodeURIComponent(current);
|
|
8414
|
+
} catch {
|
|
8415
|
+
decoded = null;
|
|
8416
|
+
}
|
|
8417
|
+
if (decoded === null || decoded === current) break;
|
|
8418
|
+
candidates.add(decoded);
|
|
8419
|
+
current = decoded;
|
|
8420
|
+
}
|
|
8407
8421
|
for (const [type, patterns] of Object.entries(ATTACK_PATTERNS)) {
|
|
8408
8422
|
for (const pattern of patterns) {
|
|
8409
|
-
|
|
8410
|
-
|
|
8423
|
+
for (const candidate of candidates) {
|
|
8424
|
+
if (pattern.test(candidate)) {
|
|
8425
|
+
return type;
|
|
8426
|
+
}
|
|
8411
8427
|
}
|
|
8412
8428
|
}
|
|
8413
8429
|
}
|
|
@@ -8421,20 +8437,19 @@ function autoDetectParser(line) {
|
|
|
8421
8437
|
return parseSyslog(line);
|
|
8422
8438
|
}
|
|
8423
8439
|
function parseNginxTimestamp(s) {
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
} catch {
|
|
8428
|
-
return /* @__PURE__ */ new Date();
|
|
8429
|
-
}
|
|
8440
|
+
const cleaned = s.replace(/(\d{2})\/(\w{3})\/(\d{4}):/, "$2 $1, $3 ");
|
|
8441
|
+
const d = new Date(cleaned);
|
|
8442
|
+
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
8430
8443
|
}
|
|
8431
8444
|
function parseSyslogTimestamp(s) {
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8445
|
+
const now = /* @__PURE__ */ new Date();
|
|
8446
|
+
let d = /* @__PURE__ */ new Date(`${s} ${now.getFullYear()}`);
|
|
8447
|
+
if (Number.isNaN(d.getTime())) return now;
|
|
8448
|
+
if (d.getTime() > now.getTime()) {
|
|
8449
|
+
const prev = /* @__PURE__ */ new Date(`${s} ${now.getFullYear() - 1}`);
|
|
8450
|
+
if (!Number.isNaN(prev.getTime())) d = prev;
|
|
8437
8451
|
}
|
|
8452
|
+
return d;
|
|
8438
8453
|
}
|
|
8439
8454
|
|
|
8440
8455
|
// src/commands/monitor.ts
|
|
@@ -8624,8 +8639,8 @@ async function runDemoMode() {
|
|
|
8624
8639
|
}
|
|
8625
8640
|
|
|
8626
8641
|
// src/commands/scan.ts
|
|
8627
|
-
var
|
|
8628
|
-
var
|
|
8642
|
+
var import_node_fs7 = require("fs");
|
|
8643
|
+
var import_node_path5 = require("path");
|
|
8629
8644
|
|
|
8630
8645
|
// ../../node_modules/.pnpm/ora@8.2.0/node_modules/ora/index.js
|
|
8631
8646
|
var import_node_process7 = __toESM(require("process"), 1);
|
|
@@ -9565,58 +9580,1134 @@ function workerId() {
|
|
|
9565
9580
|
return `${import_node_os3.default.hostname()}/${process.pid}`;
|
|
9566
9581
|
}
|
|
9567
9582
|
|
|
9568
|
-
// src/
|
|
9569
|
-
var
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
{
|
|
9573
|
-
{
|
|
9574
|
-
{
|
|
9575
|
-
{
|
|
9576
|
-
{
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9583
|
+
// src/scan/dependencies.ts
|
|
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
|
|
9688
|
+
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
9689
|
+
function severityRank(severity) {
|
|
9690
|
+
const index = SEVERITY_ORDER.indexOf(severity);
|
|
9691
|
+
return index === -1 ? 0 : index;
|
|
9692
|
+
}
|
|
9693
|
+
function severityFor(declared, confidence) {
|
|
9694
|
+
if (confidence !== "pattern") return declared;
|
|
9695
|
+
return severityRank(declared) > severityRank("medium") ? "medium" : declared;
|
|
9696
|
+
}
|
|
9697
|
+
|
|
9698
|
+
// src/scan/code-rules.ts
|
|
9699
|
+
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
|
+
var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ\b\s*\[/;
|
|
9701
|
+
var UNTRUSTED_RB = /\bparams\s*\[|\brequest\b|\bcookies\s*\[/;
|
|
9702
|
+
var UNTRUSTED_GO = /\br\s*\.\s*(?:URL|Form|Body|Header|PostForm)\b|\bFormValue\s*\(|\bQuery\s*\(\s*\)\s*\.\s*Get\s*\(|\bmux\.Vars\s*\(/;
|
|
9703
|
+
var UNTRUSTED_JAVA = /\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetHeader\s*\(|\bgetInputStream\s*\(|\bgetCookies\s*\(|\b@RequestParam\b|\b@PathVariable\b/;
|
|
9704
|
+
function untrustedPatternFor(language) {
|
|
9705
|
+
switch (language) {
|
|
9706
|
+
case "python":
|
|
9707
|
+
return UNTRUSTED_PY;
|
|
9708
|
+
case "ruby":
|
|
9709
|
+
return UNTRUSTED_RB;
|
|
9710
|
+
case "go":
|
|
9711
|
+
return UNTRUSTED_GO;
|
|
9712
|
+
case "java":
|
|
9713
|
+
return UNTRUSTED_JAVA;
|
|
9714
|
+
default:
|
|
9715
|
+
return UNTRUSTED_JS;
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9718
|
+
var GENERIC_GUARD = (
|
|
9719
|
+
// `esc(`, `aEsc(`, `htmlEscape(`, `escapeHtml(` — the escaper is almost
|
|
9720
|
+
// never *named* `escapeHtml` in real code. It gets aliased to something
|
|
9721
|
+
// short because it is called on nearly every interpolation, so matching only
|
|
9722
|
+
// the long spellings reported the codebases that escape most rigorously.
|
|
9723
|
+
//
|
|
9724
|
+
// The identifier must END at the escaper (with at most a known output-context
|
|
9725
|
+
// suffix). An earlier, looser form also matched `describe(`, which would have
|
|
9726
|
+
// silenced findings across every test file in every repository.
|
|
9727
|
+
/\ballow(?:ed|list|_list|ed_hosts)?\b|\bwhitelist\b|\b\w{0,6}[Ee]sc(?:ape)?(?:[Hh]tml|HTML|[Xx]ml|XML|[Ss]ql|[Aa]ttr|[Jj]s|[Uu]ri|[Uu]rl)?\s*\(|\bhtml_escape\b|\bhtmlspecialchars\s*\(|\bsanitiz\w*\b|\bencoded\b|\brealpath\b|\bcommonpath\b|\bresolve\(\)\.startsWith\b|\bprocess\.env\b|\bos\.environ\b|\bgetenv\b|\bENV\s*\[|setObjectInputFilter|ObjectInputFilter/i
|
|
9728
|
+
);
|
|
9729
|
+
var XXE_GUARD = /FEATURE_SECURE_PROCESSING|setExpandEntityReferences|disallow-doctype-decl|external-general-entities|external-parameter-entities|setXIncludeAware\s*\(\s*false/;
|
|
9730
|
+
var CODE_SINK = /\bglobalThis\s*\[|\bconstructor\b|\beval\b|\bFunction\b|\brun\s*\(|\bvm\s*\.\s*run/;
|
|
9731
|
+
var EXFIL_SINK = /\bconsole\s*\.\s*(?:log|debug|info|warn|error)\s*\(|\bfetch\s*\(|\baxios\b|\brequest\s*\(|\.\s*send\s*\(/;
|
|
9732
|
+
var SQL_KEYWORDS = "SELECT|INSERT\\s+INTO|INSERT|UPDATE|DELETE\\s+FROM|DELETE|DROP|UNION\\s+SELECT";
|
|
9733
|
+
var SQL_IN_DOUBLE = `"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*"`;
|
|
9734
|
+
var SQL_IN_SINGLE = `'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*'`;
|
|
9735
|
+
var SQL_STRING = `(?:${SQL_IN_DOUBLE}|${SQL_IN_SINGLE})`;
|
|
9736
|
+
var CODE_RULES = [
|
|
9737
|
+
// ── Injection: SQL ───────────────────────────────────────────────────────
|
|
9738
|
+
{
|
|
9739
|
+
id: "sql-string-concatenation",
|
|
9740
|
+
title: "SQL assembled by concatenation or interpolation",
|
|
9741
|
+
consequence: "A quote in the interpolated value changes the query\u2019s meaning \u2014 the query runs as the attacker wrote it, not as you wrote it.",
|
|
9742
|
+
cwe: "CWE-89",
|
|
9743
|
+
severity: "critical",
|
|
9744
|
+
// The tail is what distinguishes assembly from parameterisation. A bound
|
|
9745
|
+
// query leaves a comma after the closing quote (`"… = $1", [id]`) and
|
|
9746
|
+
// matches none of these.
|
|
9747
|
+
pattern: new RegExp(
|
|
9748
|
+
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"[^"\\n]*|'[^'\\n]*)(?:WHERE|ORDER\\s+BY|VALUES|SET)\\b`,
|
|
9749
|
+
"i"
|
|
9750
|
+
)
|
|
9751
|
+
},
|
|
9752
|
+
{
|
|
9753
|
+
id: "sql-template-interpolation",
|
|
9754
|
+
title: "SQL built from a template literal or f-string",
|
|
9755
|
+
consequence: "Template interpolation is string concatenation with nicer syntax \u2014 it binds nothing and escapes nothing.",
|
|
9756
|
+
cwe: "CWE-89",
|
|
9757
|
+
severity: "critical",
|
|
9758
|
+
pattern: new RegExp(
|
|
9759
|
+
`\`[^\`\\n]*(?:${SQL_KEYWORDS})\\b[^\`\\n]*\\$\\{|\\bf"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*\\{|\\bf'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*\\{`,
|
|
9760
|
+
"i"
|
|
9761
|
+
)
|
|
9762
|
+
},
|
|
9763
|
+
{
|
|
9764
|
+
id: "sql-format-call",
|
|
9765
|
+
title: "SQL text produced by a format helper",
|
|
9766
|
+
consequence: "`Sprintf`/`String.format` substitute without quoting; the resulting string is concatenated SQL by another name.",
|
|
9767
|
+
cwe: "CWE-89",
|
|
9768
|
+
severity: "critical",
|
|
9769
|
+
languages: ["go", "java"],
|
|
9770
|
+
pattern: new RegExp(
|
|
9771
|
+
`\\b(?:fmt\\.Sprintf|String\\.format)\\s*\\(\\s*"[^"\\n]*(?:${SQL_KEYWORDS})\\b`,
|
|
9772
|
+
"i"
|
|
9773
|
+
)
|
|
9774
|
+
},
|
|
9775
|
+
{
|
|
9776
|
+
id: "rb-sql-interpolation",
|
|
9777
|
+
title: "ActiveRecord query built by string interpolation",
|
|
9778
|
+
consequence: '`where("\u2026 #{value}")` interpolates before the adapter sees it, so no binding ever happens.',
|
|
9779
|
+
cwe: "CWE-89",
|
|
9780
|
+
severity: "critical",
|
|
9781
|
+
languages: ["ruby"],
|
|
9782
|
+
pattern: /\b(?:where|find_by_sql|execute|select_all|select_values|order|group|pluck)\s*[( ]\s*(?:"[^"\n]*|'[^'\n]*)#\{/
|
|
9783
|
+
},
|
|
9784
|
+
// ── Injection: OS command ────────────────────────────────────────────────
|
|
9785
|
+
{
|
|
9786
|
+
id: "js-shell-exec-interpolation",
|
|
9787
|
+
title: "shell execution with an interpolated string",
|
|
9788
|
+
consequence: "A `;` or `$(\u2026)` in the interpolated value runs as the server user.",
|
|
9789
|
+
cwe: "CWE-78",
|
|
9790
|
+
severity: "critical",
|
|
9791
|
+
languages: ["javascript", "typescript"],
|
|
9792
|
+
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/
|
|
9793
|
+
},
|
|
9794
|
+
{
|
|
9795
|
+
id: "py-shell-command-string",
|
|
9796
|
+
title: "shell command built from a string",
|
|
9797
|
+
consequence: "`os.system` and `shell=True` hand the string to `/bin/sh`, which happily interprets metacharacters.",
|
|
9798
|
+
cwe: "CWE-78",
|
|
9799
|
+
severity: "critical",
|
|
9800
|
+
languages: ["python"],
|
|
9801
|
+
pattern: /\bos\.(?:system|popen)\s*\(\s*(?:f?['"][^'"]*['"]\s*(?:\+|%|\.\s*format)|f['"]|[a-zA-Z_]\w*\s*[,)])|\bsubprocess\.(?:run|call|check_call|check_output|Popen)\s*\([^)]*\bshell\s*=\s*True/
|
|
9802
|
+
},
|
|
9803
|
+
{
|
|
9804
|
+
id: "go-shell-exec-command",
|
|
9805
|
+
title: "exec.Command invoking a shell",
|
|
9806
|
+
consequence: "Passing `sh -c` re-introduces the shell that `exec.Command`\u2019s argv interface exists to avoid.",
|
|
9807
|
+
cwe: "CWE-78",
|
|
9808
|
+
severity: "critical",
|
|
9809
|
+
languages: ["go"],
|
|
9810
|
+
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"/
|
|
9811
|
+
},
|
|
9812
|
+
{
|
|
9813
|
+
id: "rb-backtick-interpolation",
|
|
9814
|
+
title: "backtick command with interpolation",
|
|
9815
|
+
consequence: "Ruby backticks are a shell invocation; `#{}` inside one is command injection.",
|
|
9816
|
+
cwe: "CWE-78",
|
|
9817
|
+
severity: "critical",
|
|
9818
|
+
languages: ["ruby"],
|
|
9819
|
+
pattern: /`[^`\n]*#\{|\bsystem\s*\(\s*["'][^"'\n]*#\{|%x\[[^\]]*#\{/
|
|
9820
|
+
},
|
|
9821
|
+
// ── Injection: dynamic code ──────────────────────────────────────────────
|
|
9822
|
+
{
|
|
9823
|
+
id: "js-dynamic-code-execution",
|
|
9824
|
+
title: "dynamic code execution",
|
|
9825
|
+
consequence: "Any string reaching this call executes as code with the process\u2019 privileges.",
|
|
9826
|
+
cwe: "CWE-95",
|
|
9827
|
+
severity: "critical",
|
|
9828
|
+
languages: ["javascript", "typescript"],
|
|
9829
|
+
pattern: /\beval\s*\(|\bnew\s+Function\s*\(|\bvm\s*\.\s*run(?:InThisContext|InNewContext|InContext)\s*\(|\bset(?:Timeout|Interval)\s*\(\s*(?:['"`]|(?:req|request|ctx|params|query|body)\b)/
|
|
9830
|
+
},
|
|
9831
|
+
{
|
|
9832
|
+
id: "js-indirect-code-sink",
|
|
9833
|
+
title: "code sink reached indirectly",
|
|
9834
|
+
consequence: "Resolving `eval`/`Function` through `globalThis[\u2026]` or `.constructor` hides the sink from literal matching. Legitimate code has no reason to.",
|
|
9835
|
+
cwe: "CWE-506",
|
|
9836
|
+
severity: "high",
|
|
9837
|
+
languages: ["javascript", "typescript"],
|
|
9838
|
+
pattern: /\bglobalThis\s*\[\s*[a-zA-Z_$][\w$]*\s*\]|\(\s*function\s*\(\s*\)\s*\{\s*\}\s*\)\s*\.\s*constructor/
|
|
9839
|
+
},
|
|
9840
|
+
{
|
|
9841
|
+
id: "js-encoded-payload-execution",
|
|
9842
|
+
title: "encoded blob decoded next to a code sink",
|
|
9843
|
+
consequence: "A base64 literal that is decoded and executed is the standard shape of a planted backdoor; the encoding exists to defeat review.",
|
|
9844
|
+
cwe: "CWE-506",
|
|
9845
|
+
severity: "critical",
|
|
9846
|
+
languages: ["javascript", "typescript"],
|
|
9847
|
+
pattern: /\bBuffer\.from\s*\(\s*[\w.$]+\s*,\s*['"]base64['"]\s*\)|\batob\s*\(\s*[\w.$]+\s*\)/,
|
|
9848
|
+
requires: CODE_SINK,
|
|
9849
|
+
guardBack: 6,
|
|
9850
|
+
guardForward: 3
|
|
9851
|
+
},
|
|
9852
|
+
{
|
|
9853
|
+
id: "py-dynamic-code-execution",
|
|
9854
|
+
title: "dynamic code execution",
|
|
9855
|
+
consequence: "Any string reaching this call executes as Python with the process\u2019 privileges.",
|
|
9856
|
+
cwe: "CWE-95",
|
|
9857
|
+
severity: "critical",
|
|
9858
|
+
languages: ["python"],
|
|
9859
|
+
pattern: /\b(?:eval|exec)\s*\(\s*(?!['"]\s*\))[a-zA-Z_(f'"]/,
|
|
9860
|
+
needsContext: true
|
|
9861
|
+
},
|
|
9862
|
+
{
|
|
9863
|
+
id: "rb-dynamic-dispatch",
|
|
9864
|
+
title: "dynamic code execution or unrestricted #send",
|
|
9865
|
+
consequence: "`eval` runs arbitrary Ruby; unrestricted `#send` lets the caller invoke any method on the receiver, including private ones.",
|
|
9866
|
+
cwe: "CWE-95",
|
|
9867
|
+
severity: "critical",
|
|
9868
|
+
languages: ["ruby"],
|
|
9869
|
+
pattern: /\beval\s*\(|\binstance_eval\s*\(|\bclass_eval\s*\(|\.\s*send\s*\(\s*(?:params|request|args)\b/
|
|
9870
|
+
},
|
|
9871
|
+
// ── Cross-site scripting ─────────────────────────────────────────────────
|
|
9872
|
+
{
|
|
9873
|
+
id: "js-unescaped-html-sink",
|
|
9874
|
+
title: "unescaped HTML rendering",
|
|
9875
|
+
consequence: "A script tag in the value executes in the victim\u2019s session \u2014 stored or reflected XSS.",
|
|
9876
|
+
cwe: "CWE-79",
|
|
9877
|
+
severity: "high",
|
|
9878
|
+
languages: ["javascript", "typescript"],
|
|
9879
|
+
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=\s*(?!\s*['"`]\s*['"`]\s*;?\s*$)|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/,
|
|
9880
|
+
/**
|
|
9881
|
+
* A whole-statement assignment of a string with no interpolation and no
|
|
9882
|
+
* concatenation carries no data, so it cannot carry attacker data. This
|
|
9883
|
+
* was the single largest source of noise: a codebase that builds its UI
|
|
9884
|
+
* with innerHTML reports every static heading and spinner as XSS, and a
|
|
9885
|
+
* rule that flags 40 safe lines to catch one real one gets switched off.
|
|
9886
|
+
*
|
|
9887
|
+
* Line-scoped on purpose — see `lineGuard`.
|
|
9888
|
+
*/
|
|
9889
|
+
lineGuard: /(?:innerHTML|outerHTML)\s*=\s*(?:'[^'\\]*'|"[^"\\]*"|`[^`$\\]*`)\s*;?\s*$/
|
|
9890
|
+
},
|
|
9891
|
+
{
|
|
9892
|
+
id: "java-html-writer-concatenation",
|
|
9893
|
+
title: "HTML written to the response by concatenation",
|
|
9894
|
+
consequence: "The servlet writer performs no encoding; a value concatenated into markup is rendered as markup.",
|
|
9895
|
+
cwe: "CWE-79",
|
|
9896
|
+
severity: "high",
|
|
9897
|
+
languages: ["java"],
|
|
9898
|
+
pattern: /\b(?:println|print|write)\s*\(\s*"[^"\n]*<[^"\n]*"\s*\+/
|
|
9899
|
+
},
|
|
9900
|
+
{
|
|
9901
|
+
id: "rb-unescaped-output",
|
|
9902
|
+
title: "Rails output escaping bypassed",
|
|
9903
|
+
consequence: "`html_safe` and `raw` tell Rails the string is already safe. If it came from a parameter, it is not.",
|
|
9904
|
+
cwe: "CWE-79",
|
|
9905
|
+
severity: "high",
|
|
9906
|
+
languages: ["ruby"],
|
|
9907
|
+
pattern: /\.\s*html_safe\b|\braw\s*\(\s*(?:params|request|@)|\blink_to\s+[^,\n]+,\s*params\s*\[/
|
|
9908
|
+
},
|
|
9909
|
+
{
|
|
9910
|
+
id: "py-template-autoescape-off",
|
|
9911
|
+
title: "template rendering with escaping disabled",
|
|
9912
|
+
consequence: "With autoescape off \u2014 or a `|safe` filter \u2014 every interpolated value is rendered as markup.",
|
|
9913
|
+
cwe: "CWE-79",
|
|
9914
|
+
severity: "high",
|
|
9915
|
+
languages: ["python"],
|
|
9916
|
+
pattern: /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False|\|\s*safe\b|\bMarkup\s*\(\s*(?!['"])/
|
|
9917
|
+
},
|
|
9918
|
+
{
|
|
9919
|
+
id: "py-template-from-input",
|
|
9920
|
+
title: "template compiled from a non-literal source",
|
|
9921
|
+
consequence: "Server-side template injection. In Jinja2 the sandbox is escapable, so this escalates from XSS to remote code execution.",
|
|
9922
|
+
cwe: "CWE-1336",
|
|
9923
|
+
severity: "critical",
|
|
9924
|
+
languages: ["python"],
|
|
9925
|
+
pattern: /\bTemplate\s*\(\s*(?!['"])[a-zA-Z_]/,
|
|
9926
|
+
needsContext: true
|
|
9927
|
+
},
|
|
9928
|
+
// ── Server-side request forgery ──────────────────────────────────────────
|
|
9929
|
+
{
|
|
9930
|
+
id: "js-ssrf-outbound-request",
|
|
9931
|
+
title: "outbound request to a non-constant URL",
|
|
9932
|
+
consequence: "An attacker who controls the URL reaches the cloud metadata endpoint, localhost, and every service on the private network.",
|
|
9933
|
+
cwe: "CWE-918",
|
|
9934
|
+
severity: "high",
|
|
9935
|
+
languages: ["javascript", "typescript"],
|
|
9936
|
+
pattern: /\bfetch\s*\(\s*[a-zA-Z_$][\w$]*\s*[,)]|\bhttps?\s*\.\s*(?:get|request)\s*\(\s*[a-zA-Z_$][\w$]*\s*[,)]|\baxios\s*\.\s*get\s*\(\s*[a-zA-Z_$][\w$]*\s*[,)]/,
|
|
9937
|
+
needsContext: true
|
|
9938
|
+
},
|
|
9939
|
+
{
|
|
9940
|
+
id: "py-ssrf-outbound-request",
|
|
9941
|
+
title: "outbound request to a non-constant URL",
|
|
9942
|
+
consequence: "An attacker who controls the URL reaches the cloud metadata endpoint, localhost, and every service on the private network.",
|
|
9943
|
+
cwe: "CWE-918",
|
|
9944
|
+
severity: "high",
|
|
9945
|
+
languages: ["python"],
|
|
9946
|
+
pattern: /\brequests\.(?:get|request|head)\s*\(\s*[a-zA-Z_]\w*\s*[,)]|\burlopen\s*\(\s*[a-zA-Z_]\w*\s*[,)]|\bhttpx\.get\s*\(\s*[a-zA-Z_]\w*\s*[,)]/,
|
|
9947
|
+
needsContext: true
|
|
9948
|
+
},
|
|
9949
|
+
{
|
|
9950
|
+
id: "go-ssrf-outbound-request",
|
|
9951
|
+
title: "outbound request to a non-constant URL",
|
|
9952
|
+
consequence: "An attacker who controls the URL reaches the cloud metadata endpoint, localhost, and every service on the private network.",
|
|
9953
|
+
cwe: "CWE-918",
|
|
9954
|
+
severity: "high",
|
|
9955
|
+
languages: ["go"],
|
|
9956
|
+
pattern: /\bhttp\.(?:Get|Post|Head)\s*\(\s*(?:[a-zA-Z_]\w*\s*[,)]|"[^"]*"\s*\+)/,
|
|
9957
|
+
needsContext: true
|
|
9958
|
+
},
|
|
9959
|
+
// ── Open redirect ────────────────────────────────────────────────────────
|
|
9960
|
+
{
|
|
9961
|
+
id: "js-open-redirect",
|
|
9962
|
+
title: "redirect to a non-constant destination",
|
|
9963
|
+
consequence: "Your domain becomes the credible first hop of a phishing chain; the victim sees your hostname in the link they clicked.",
|
|
9964
|
+
cwe: "CWE-601",
|
|
9965
|
+
severity: "medium",
|
|
9966
|
+
languages: ["javascript", "typescript"],
|
|
9967
|
+
pattern: /\b(?:res|response)\s*\.\s*redirect\s*\(\s*[a-zA-Z_$][\w$]*\s*\)|\bwindow\s*\.\s*location(?:\s*\.\s*(?:href|replace))?\s*(?:=\s*[a-zA-Z_$]|\(\s*[a-zA-Z_$][\w$]*\s*\))/,
|
|
9968
|
+
needsContext: true
|
|
9969
|
+
},
|
|
9970
|
+
// ── Deserialisation ──────────────────────────────────────────────────────
|
|
9971
|
+
{
|
|
9972
|
+
id: "py-unsafe-deserialization",
|
|
9973
|
+
title: "deserialisation of untrusted data",
|
|
9974
|
+
consequence: "`pickle` and `yaml.load` instantiate arbitrary types during parsing \u2014 a crafted payload is remote code execution, not a parse error.",
|
|
9975
|
+
cwe: "CWE-502",
|
|
9976
|
+
severity: "critical",
|
|
9977
|
+
languages: ["python"],
|
|
9978
|
+
pattern: /\bpickle\.loads?\s*\(|\bcPickle\.loads?\s*\(|\bmarshal\.loads\s*\(|\byaml\.load\s*\(|\bjsonpickle\.decode\s*\(/
|
|
9979
|
+
},
|
|
9980
|
+
{
|
|
9981
|
+
id: "java-unsafe-deserialization",
|
|
9982
|
+
title: "Java deserialisation without a class filter",
|
|
9983
|
+
consequence: "A gadget chain in the classpath turns `readObject()` on attacker bytes into remote code execution.",
|
|
9984
|
+
cwe: "CWE-502",
|
|
9985
|
+
severity: "critical",
|
|
9986
|
+
languages: ["java"],
|
|
9987
|
+
pattern: /\breadObject\s*\(\s*\)|\bnew\s+ObjectInputStream\s*\(/,
|
|
9988
|
+
guardBack: 8,
|
|
9989
|
+
// The stream is constructed, *then* filtered. Without a forward window the
|
|
9990
|
+
// guarded case matches on its constructor line and reports a correct
|
|
9991
|
+
// implementation as a finding.
|
|
9992
|
+
guardForward: 6
|
|
9993
|
+
},
|
|
9994
|
+
{
|
|
9995
|
+
id: "js-unsafe-yaml-load",
|
|
9996
|
+
title: "YAML parsed with type resolution enabled",
|
|
9997
|
+
consequence: "A crafted document can instantiate arbitrary types during parsing.",
|
|
9998
|
+
cwe: "CWE-502",
|
|
9999
|
+
severity: "high",
|
|
10000
|
+
languages: ["javascript", "typescript"],
|
|
10001
|
+
pattern: /\byaml\s*\.\s*load\s*\((?![^)]*safe)|\bloadAll\s*\([^)]*unsafe/i
|
|
10002
|
+
},
|
|
10003
|
+
// ── XML external entities ────────────────────────────────────────────────
|
|
10004
|
+
{
|
|
10005
|
+
id: "java-xxe-parser-defaults",
|
|
10006
|
+
title: "XML parser left on its insecure defaults",
|
|
10007
|
+
consequence: "External entity expansion reads local files and makes outbound requests on the parser\u2019s behalf \u2014 file disclosure and SSRF from a document.",
|
|
10008
|
+
cwe: "CWE-611",
|
|
10009
|
+
severity: "high",
|
|
10010
|
+
languages: ["java"],
|
|
10011
|
+
pattern: /\b(?:DocumentBuilderFactory|SAXParserFactory|XMLInputFactory|TransformerFactory|SchemaFactory)\s*\.\s*newInstance\s*\(\s*\)/,
|
|
10012
|
+
guard: XXE_GUARD,
|
|
10013
|
+
guardBack: 4,
|
|
10014
|
+
guardForward: 8
|
|
10015
|
+
},
|
|
10016
|
+
{
|
|
10017
|
+
id: "java-xxe-parse-call",
|
|
10018
|
+
title: "XML parsed by a builder that was never hardened",
|
|
10019
|
+
consequence: "The expansion happens at `parse()`. Flagging only the factory misses the line where the document is actually read.",
|
|
10020
|
+
cwe: "CWE-611",
|
|
10021
|
+
severity: "high",
|
|
10022
|
+
languages: ["java"],
|
|
10023
|
+
// Receiver-qualified so `LocalDate.parse(s)` and friends stay out of it.
|
|
10024
|
+
pattern: /\b\w*(?:[Bb]uilder|[Pp]arser|[Rr]eader)\s*\.\s*parse\s*\(/,
|
|
10025
|
+
guard: XXE_GUARD,
|
|
10026
|
+
guardBack: 6,
|
|
10027
|
+
guardForward: 4
|
|
10028
|
+
},
|
|
10029
|
+
// ── Path traversal ───────────────────────────────────────────────────────
|
|
10030
|
+
{
|
|
10031
|
+
id: "py-path-traversal",
|
|
10032
|
+
title: "file opened at a path built from input",
|
|
10033
|
+
consequence: "A `../` sequence \u2014 or an absolute path \u2014 reads or writes outside the intended directory.",
|
|
10034
|
+
cwe: "CWE-22",
|
|
10035
|
+
severity: "high",
|
|
10036
|
+
languages: ["python"],
|
|
10037
|
+
pattern: /\bopen\s*\(\s*(?:os\.path\.join\s*\(|[a-zA-Z_]\w*\s*\+|f['"])/,
|
|
10038
|
+
needsContext: true
|
|
10039
|
+
},
|
|
10040
|
+
{
|
|
10041
|
+
id: "js-path-traversal",
|
|
10042
|
+
title: "file path built from a variable",
|
|
10043
|
+
consequence: "A `../` sequence in the value reads or writes outside the intended directory.",
|
|
10044
|
+
cwe: "CWE-22",
|
|
10045
|
+
severity: "medium",
|
|
10046
|
+
languages: ["javascript", "typescript"],
|
|
10047
|
+
pattern: /\b(?:readFile|readFileSync|writeFile|writeFileSync|createReadStream|createWriteStream|unlink|unlinkSync|sendFile)\s*\(\s*(?:`[^`]*\$\{|[a-zA-Z_$][\w$]*\s*\+|path\.join\s*\([^)]*(?:req|request)\b)/,
|
|
10048
|
+
needsContext: true
|
|
10049
|
+
},
|
|
10050
|
+
// ── Cryptography, tokens, randomness ─────────────────────────────────────
|
|
10051
|
+
{
|
|
10052
|
+
id: "js-jwt-decode-without-verify",
|
|
10053
|
+
title: "JWT decoded without verifying the signature",
|
|
10054
|
+
consequence: "`decode` parses the claims and checks nothing. Anyone can mint a token with any `sub` and any `role`.",
|
|
10055
|
+
cwe: "CWE-347",
|
|
10056
|
+
severity: "critical",
|
|
10057
|
+
languages: ["javascript", "typescript"],
|
|
10058
|
+
pattern: /\bjwt\s*\.\s*decode\s*\(|\bjsonwebtoken\s*\.\s*decode\s*\(|\bdecodeJwt\s*\(/
|
|
10059
|
+
},
|
|
10060
|
+
{
|
|
10061
|
+
id: "tls-verification-disabled",
|
|
10062
|
+
title: "TLS certificate verification disabled",
|
|
10063
|
+
consequence: "Every connection made this way is trivially interceptable; the encryption is decorative.",
|
|
10064
|
+
cwe: "CWE-295",
|
|
10065
|
+
severity: "high",
|
|
10066
|
+
pattern: /rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*[=:]\s*['"]?0|strictSSL\s*:\s*false|\bverify\s*=\s*False\b|InsecureSkipVerify\s*:\s*true/
|
|
10067
|
+
},
|
|
10068
|
+
{
|
|
10069
|
+
id: "weak-hash-on-credential",
|
|
10070
|
+
title: "broken hash used on a credential",
|
|
10071
|
+
consequence: "MD5 and SHA-1 are fast and collision-prone; hashed passwords are recoverable.",
|
|
10072
|
+
cwe: "CWE-327",
|
|
10073
|
+
severity: "high",
|
|
10074
|
+
pattern: /(?:createHash|hashlib|MessageDigest\.getInstance|Digest::)\s*[.(]?\s*['"]?(?:md5|MD5|sha1|SHA-?1)['"]?\s*\)?[\s\S]{0,80}(?:password|passwd|secret|token|credential)/i
|
|
10075
|
+
},
|
|
10076
|
+
{
|
|
10077
|
+
id: "insecure-randomness-for-secret",
|
|
10078
|
+
title: "predictable randomness used for a security value",
|
|
10079
|
+
consequence: "`Math.random`/`random.random` are predictable; tokens, session ids and reset codes built from them are guessable.",
|
|
10080
|
+
cwe: "CWE-338",
|
|
10081
|
+
severity: "high",
|
|
10082
|
+
pattern: /(?:token|secret|password|salt|nonce|session|otp|reset|apikey|api_key)[\w]*\s*[:=][^;\n]{0,60}(?:Math\s*\.\s*random\s*\(|\brandom\s*\.\s*(?:random|randint|choice)\s*\(|\brand\s*\()/i
|
|
10083
|
+
},
|
|
10084
|
+
{
|
|
10085
|
+
id: "redos-nested-quantifier",
|
|
10086
|
+
title: "regex with nested unbounded quantifiers",
|
|
10087
|
+
consequence: "Catastrophic backtracking: a crafted input of a few dozen characters pins a CPU core for minutes.",
|
|
10088
|
+
cwe: "CWE-1333",
|
|
10089
|
+
severity: "medium",
|
|
10090
|
+
pattern: /\([^)\n]*[+*]\s*\)\s*[+*]|\([^)\n]*\{\d+,\}\s*\)\s*[+*{]/
|
|
10091
|
+
},
|
|
10092
|
+
// ── Temporary files ──────────────────────────────────────────────────────
|
|
10093
|
+
{
|
|
10094
|
+
id: "insecure-temp-file",
|
|
10095
|
+
title: "predictable temporary file path",
|
|
10096
|
+
consequence: "A predictable name in a world-writable directory is a symlink attack: an attacker pre-creates the path and your process writes through it.",
|
|
10097
|
+
cwe: "CWE-377",
|
|
10098
|
+
severity: "medium",
|
|
10099
|
+
// A hardcoded path under /tmp is the finding whether or not it is
|
|
10100
|
+
// formatted: `"/tmp/application.log.tmp"` is worse than the PID-based one,
|
|
10101
|
+
// because every process on the host can predict it exactly.
|
|
10102
|
+
pattern: /\btempfile\.mktemp\s*\(|\bos\.tmpnam\s*\(|['"]\/tmp\/[^'"\n]+['"]|['"]\/tmp\/[^'"\n]*\{|\bFile\.createTempFile\s*\(/
|
|
10103
|
+
},
|
|
10104
|
+
// ── Information exposure ─────────────────────────────────────────────────
|
|
10105
|
+
{
|
|
10106
|
+
id: "py-stack-trace-returned",
|
|
10107
|
+
title: "stack trace returned to the caller",
|
|
10108
|
+
consequence: "Tracebacks leak absolute paths, dependency versions and source fragments \u2014 the reconnaissance an attacker would otherwise have to guess at.",
|
|
10109
|
+
cwe: "CWE-209",
|
|
10110
|
+
severity: "medium",
|
|
10111
|
+
languages: ["python"],
|
|
10112
|
+
pattern: /\breturn\b[^\n]*\btraceback\.(?:format_exc|format_exception|print_exc)\s*\(|\breturn\b[^\n]*\bstr\s*\(\s*e\s*\)/
|
|
10113
|
+
},
|
|
10114
|
+
{
|
|
10115
|
+
id: "js-environment-exfiltration",
|
|
10116
|
+
title: "process environment serialised into a payload",
|
|
10117
|
+
consequence: "The environment is where every secret lives. Serialising it whole into a request body is credential exfiltration regardless of the endpoint.",
|
|
10118
|
+
cwe: "CWE-532",
|
|
10119
|
+
severity: "critical",
|
|
10120
|
+
languages: ["javascript", "typescript"],
|
|
10121
|
+
pattern: /\bJSON\.stringify\s*\(\s*\{?[^)]*\bprocess\.env\b(?!\s*\.)/,
|
|
10122
|
+
guard: false
|
|
10123
|
+
},
|
|
10124
|
+
{
|
|
10125
|
+
id: "js-credential-logged",
|
|
10126
|
+
title: "credential read from the environment into a log sink",
|
|
10127
|
+
consequence: "CI retains job logs, and on public forks it publishes them. A token printed once is a token leaked permanently.",
|
|
10128
|
+
cwe: "CWE-532",
|
|
10129
|
+
severity: "high",
|
|
10130
|
+
languages: ["javascript", "typescript"],
|
|
10131
|
+
pattern: /\b(?:token|apiKey|api_key|secret|password|credential|auth)\w*\s*:\s*process\.env\.\w+/i,
|
|
10132
|
+
requires: EXFIL_SINK,
|
|
10133
|
+
guard: false,
|
|
10134
|
+
guardBack: 4,
|
|
10135
|
+
guardForward: 1
|
|
10136
|
+
},
|
|
10137
|
+
// ── Prototype pollution ──────────────────────────────────────────────────
|
|
10138
|
+
{
|
|
10139
|
+
id: "js-prototype-pollution",
|
|
10140
|
+
title: "write to a prototype-reachable key",
|
|
10141
|
+
consequence: "An attacker-supplied `__proto__` key changes behaviour for every object in the process, including ones it never touched.",
|
|
10142
|
+
cwe: "CWE-1321",
|
|
10143
|
+
severity: "high",
|
|
10144
|
+
languages: ["javascript", "typescript"],
|
|
10145
|
+
pattern: /\[\s*['"]__proto__['"]\s*\]|\bObject\s*\.\s*assign\s*\(\s*[\w.$]*\.prototype\b|\.\s*__proto__\s*=/
|
|
10146
|
+
}
|
|
10147
|
+
];
|
|
10148
|
+
var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
|
|
10149
|
+
var DEFINITION_PREFIX = /^\s*(?:(?:export|public|private|protected|static|final|async|abstract)\s+)*(?:def|function|func|class|module|interface|struct|impl|fn|sub)\b/;
|
|
10150
|
+
function isComment(line) {
|
|
10151
|
+
return COMMENT_PREFIX.test(line);
|
|
10152
|
+
}
|
|
10153
|
+
function proseLines(lines) {
|
|
10154
|
+
const inside = /* @__PURE__ */ new Set();
|
|
10155
|
+
let delimiter = null;
|
|
10156
|
+
lines.forEach((line, index) => {
|
|
10157
|
+
if (delimiter) {
|
|
10158
|
+
inside.add(index);
|
|
10159
|
+
if (line.includes(delimiter)) delimiter = null;
|
|
10160
|
+
return;
|
|
10161
|
+
}
|
|
10162
|
+
for (const candidate of ['"""', "'''"]) {
|
|
10163
|
+
const start = line.indexOf(candidate);
|
|
10164
|
+
if (start === -1) continue;
|
|
10165
|
+
if (line.indexOf(candidate, start + candidate.length) !== -1) return;
|
|
10166
|
+
delimiter = candidate;
|
|
10167
|
+
inside.add(index);
|
|
10168
|
+
return;
|
|
10169
|
+
}
|
|
10170
|
+
});
|
|
10171
|
+
return inside;
|
|
10172
|
+
}
|
|
10173
|
+
function skippable(line, index, prose) {
|
|
10174
|
+
return isComment(line) || DEFINITION_PREFIX.test(line) || (prose?.has(index) ?? false);
|
|
10175
|
+
}
|
|
10176
|
+
function windowText(lines, index, back, forward, prose) {
|
|
10177
|
+
const from = Math.max(0, index - back);
|
|
10178
|
+
const to = Math.min(lines.length - 1, index + forward);
|
|
10179
|
+
const collected = [];
|
|
10180
|
+
for (let i = from; i <= to; i += 1) {
|
|
10181
|
+
const line = lines[i] ?? "";
|
|
10182
|
+
if (i !== index && skippable(line, i, prose)) continue;
|
|
10183
|
+
collected.push(line);
|
|
10184
|
+
}
|
|
10185
|
+
return collected.join("\n");
|
|
10186
|
+
}
|
|
10187
|
+
function evaluateRule(rule, ctx) {
|
|
10188
|
+
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
10189
|
+
const line = ctx.lines[ctx.index] ?? "";
|
|
10190
|
+
if (isComment(line) || ctx.prose?.has(ctx.index)) return null;
|
|
10191
|
+
if (!rule.pattern.test(line)) return null;
|
|
10192
|
+
const back = rule.guardBack ?? 8;
|
|
10193
|
+
const forward = rule.guardForward ?? 0;
|
|
10194
|
+
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
10195
|
+
if (rule.requires && !rule.requires.test(context)) return null;
|
|
10196
|
+
if (rule.lineGuard?.test(line)) return null;
|
|
10197
|
+
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
10198
|
+
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
10199
|
+
const untrusted = untrustedPatternFor(ctx.language);
|
|
10200
|
+
const contextual = untrusted.test(line) || untrusted.test(context);
|
|
10201
|
+
if (rule.needsContext && !contextual) return null;
|
|
10202
|
+
const confidence = contextual ? "contextual" : "pattern";
|
|
10203
|
+
return { rule, confidence, severity: severityFor(rule.severity, confidence) };
|
|
10204
|
+
}
|
|
10205
|
+
|
|
10206
|
+
// src/scan/manifest-rules.ts
|
|
10207
|
+
var POPULAR_NPM = [
|
|
10208
|
+
"react",
|
|
10209
|
+
"react-dom",
|
|
10210
|
+
"lodash",
|
|
10211
|
+
"express",
|
|
10212
|
+
"axios",
|
|
10213
|
+
"chalk",
|
|
10214
|
+
"commander",
|
|
10215
|
+
"debug",
|
|
10216
|
+
"moment",
|
|
10217
|
+
"dayjs",
|
|
10218
|
+
"uuid",
|
|
10219
|
+
"dotenv",
|
|
10220
|
+
"typescript",
|
|
10221
|
+
"webpack",
|
|
10222
|
+
"vite",
|
|
10223
|
+
"rollup",
|
|
10224
|
+
"eslint",
|
|
10225
|
+
"prettier",
|
|
10226
|
+
"jest",
|
|
10227
|
+
"vitest",
|
|
10228
|
+
"mocha",
|
|
10229
|
+
"chai",
|
|
10230
|
+
"sinon",
|
|
10231
|
+
"request",
|
|
10232
|
+
"node-fetch",
|
|
10233
|
+
"cross-env",
|
|
10234
|
+
"rimraf",
|
|
10235
|
+
"glob",
|
|
10236
|
+
"minimist",
|
|
10237
|
+
"yargs",
|
|
10238
|
+
"inquirer",
|
|
10239
|
+
"colors",
|
|
10240
|
+
"ora",
|
|
10241
|
+
"semver",
|
|
10242
|
+
"ws",
|
|
10243
|
+
"socket.io",
|
|
10244
|
+
"mongoose",
|
|
10245
|
+
"sequelize",
|
|
10246
|
+
"knex",
|
|
10247
|
+
"pg",
|
|
10248
|
+
"mysql",
|
|
10249
|
+
"mysql2",
|
|
10250
|
+
"redis",
|
|
10251
|
+
"ioredis",
|
|
10252
|
+
"jsonwebtoken",
|
|
10253
|
+
"bcrypt",
|
|
10254
|
+
"passport",
|
|
10255
|
+
"cors",
|
|
10256
|
+
"helmet",
|
|
10257
|
+
"morgan",
|
|
10258
|
+
"body-parser",
|
|
10259
|
+
"multer",
|
|
10260
|
+
"nodemailer",
|
|
10261
|
+
"puppeteer",
|
|
10262
|
+
"playwright",
|
|
10263
|
+
"cheerio",
|
|
10264
|
+
"sharp",
|
|
10265
|
+
"canvas",
|
|
10266
|
+
"esbuild",
|
|
10267
|
+
"babel",
|
|
10268
|
+
"postcss",
|
|
10269
|
+
"tailwindcss",
|
|
10270
|
+
"next",
|
|
10271
|
+
"nuxt",
|
|
10272
|
+
"vue",
|
|
10273
|
+
"svelte",
|
|
10274
|
+
"angular",
|
|
10275
|
+
"rxjs",
|
|
10276
|
+
"zod"
|
|
10277
|
+
];
|
|
10278
|
+
var POPULAR_PYPI = [
|
|
10279
|
+
"requests",
|
|
10280
|
+
"urllib3",
|
|
10281
|
+
"numpy",
|
|
10282
|
+
"pandas",
|
|
10283
|
+
"scipy",
|
|
10284
|
+
"flask",
|
|
10285
|
+
"django",
|
|
10286
|
+
"fastapi",
|
|
10287
|
+
"sqlalchemy",
|
|
10288
|
+
"pydantic",
|
|
10289
|
+
"click",
|
|
10290
|
+
"jinja2",
|
|
10291
|
+
"pyyaml",
|
|
10292
|
+
"boto3",
|
|
10293
|
+
"botocore",
|
|
10294
|
+
"setuptools",
|
|
10295
|
+
"wheel",
|
|
10296
|
+
"pip",
|
|
10297
|
+
"six",
|
|
10298
|
+
"certifi",
|
|
10299
|
+
"idna",
|
|
10300
|
+
"chardet",
|
|
10301
|
+
"attrs",
|
|
10302
|
+
"python-dateutil",
|
|
10303
|
+
"pytz",
|
|
10304
|
+
"pytest",
|
|
10305
|
+
"tox",
|
|
10306
|
+
"black",
|
|
10307
|
+
"flake8",
|
|
10308
|
+
"mypy",
|
|
10309
|
+
"isort",
|
|
10310
|
+
"beautifulsoup4",
|
|
10311
|
+
"lxml",
|
|
10312
|
+
"pillow",
|
|
10313
|
+
"matplotlib",
|
|
10314
|
+
"seaborn",
|
|
10315
|
+
"scikit-learn",
|
|
10316
|
+
"tensorflow",
|
|
10317
|
+
"torch",
|
|
10318
|
+
"transformers",
|
|
10319
|
+
"openai",
|
|
10320
|
+
"anthropic",
|
|
10321
|
+
"httpx",
|
|
10322
|
+
"aiohttp",
|
|
10323
|
+
"celery",
|
|
10324
|
+
"redis",
|
|
10325
|
+
"psycopg2",
|
|
10326
|
+
"pymongo",
|
|
10327
|
+
"cryptography",
|
|
10328
|
+
"paramiko",
|
|
10329
|
+
"colorama"
|
|
10330
|
+
];
|
|
10331
|
+
var INTERNAL_MARKER = /(?:^|[-_/@])(?:internal|private|corp|intranet|inhouse|confidential)(?:$|[-_/])/i;
|
|
10332
|
+
function normalizeName(name) {
|
|
10333
|
+
return name.toLowerCase().replace(/^@/, "").replace(/[-_.\s]/g, "");
|
|
10334
|
+
}
|
|
10335
|
+
function editDistance(a, b, cap = 3) {
|
|
10336
|
+
if (a === b) return 0;
|
|
10337
|
+
if (Math.abs(a.length - b.length) > cap) return cap + 1;
|
|
10338
|
+
const rows = [];
|
|
10339
|
+
for (let i = 0; i <= a.length; i += 1) {
|
|
10340
|
+
rows.push(new Array(b.length + 1).fill(0));
|
|
10341
|
+
rows[i][0] = i;
|
|
10342
|
+
}
|
|
10343
|
+
for (let j = 0; j <= b.length; j += 1) rows[0][j] = j;
|
|
10344
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
10345
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
10346
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
10347
|
+
let best = Math.min(
|
|
10348
|
+
rows[i - 1][j] + 1,
|
|
10349
|
+
rows[i][j - 1] + 1,
|
|
10350
|
+
rows[i - 1][j - 1] + cost
|
|
10351
|
+
);
|
|
10352
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
10353
|
+
best = Math.min(best, rows[i - 2][j - 2] + 1);
|
|
10354
|
+
}
|
|
10355
|
+
rows[i][j] = best;
|
|
10356
|
+
}
|
|
10357
|
+
}
|
|
10358
|
+
return rows[a.length][b.length];
|
|
10359
|
+
}
|
|
10360
|
+
function detectTyposquat(name, ecosystem) {
|
|
10361
|
+
const popular = ecosystem === "npm" ? POPULAR_NPM : POPULAR_PYPI;
|
|
10362
|
+
const lower = name.toLowerCase().replace(/^@[^/]+\//, "");
|
|
10363
|
+
if (popular.includes(lower)) return null;
|
|
10364
|
+
if (lower.length < 4) return null;
|
|
10365
|
+
const normalized = normalizeName(lower);
|
|
10366
|
+
for (const candidate of popular) {
|
|
10367
|
+
const candidateNormalized = normalizeName(candidate);
|
|
10368
|
+
if (normalized === candidateNormalized) return { impersonates: candidate, kind: "separator" };
|
|
10369
|
+
if (editDistance(normalized, candidateNormalized, 1) === 1) {
|
|
10370
|
+
return { impersonates: candidate, kind: "edit" };
|
|
10371
|
+
}
|
|
10372
|
+
}
|
|
10373
|
+
return null;
|
|
10374
|
+
}
|
|
10375
|
+
var LIFECYCLE_SCRIPTS = ["preinstall", "install", "postinstall", "prepare", "prepublish"];
|
|
10376
|
+
function scanPackageJson(text) {
|
|
10377
|
+
const findings = [];
|
|
10378
|
+
const lines = text.split("\n");
|
|
10379
|
+
let parsed;
|
|
10380
|
+
try {
|
|
10381
|
+
parsed = JSON.parse(text);
|
|
10382
|
+
} catch {
|
|
10383
|
+
return findings;
|
|
10384
|
+
}
|
|
10385
|
+
const lineOf = (needle) => {
|
|
10386
|
+
const index = lines.findIndex((line) => line.includes(`"${needle}"`));
|
|
10387
|
+
return index === -1 ? 1 : index + 1;
|
|
10388
|
+
};
|
|
10389
|
+
const depBuckets = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
10390
|
+
for (const bucket of depBuckets) {
|
|
10391
|
+
const deps = parsed[bucket];
|
|
10392
|
+
if (!deps || typeof deps !== "object") continue;
|
|
10393
|
+
for (const name of Object.keys(deps)) {
|
|
10394
|
+
const line = lineOf(name);
|
|
10395
|
+
if (INTERNAL_MARKER.test(name)) {
|
|
10396
|
+
findings.push({
|
|
10397
|
+
ruleId: "manifest-dependency-confusion",
|
|
10398
|
+
title: "internal-looking package resolved from a public registry",
|
|
10399
|
+
line,
|
|
10400
|
+
severity: "critical",
|
|
10401
|
+
cwe: "CWE-1357",
|
|
10402
|
+
message: `"${name}" names itself as internal but carries no registry pin`,
|
|
10403
|
+
consequence: "Whoever registers this name publicly first wins the resolution, and their code runs in your build.",
|
|
10404
|
+
excerpt: (lines[line - 1] ?? "").trim()
|
|
10405
|
+
});
|
|
10406
|
+
continue;
|
|
10407
|
+
}
|
|
10408
|
+
const squat = detectTyposquat(name, "npm");
|
|
10409
|
+
if (squat) {
|
|
10410
|
+
findings.push({
|
|
10411
|
+
ruleId: "manifest-typosquat",
|
|
10412
|
+
title: "dependency name close to a popular package",
|
|
10413
|
+
line,
|
|
10414
|
+
severity: "high",
|
|
10415
|
+
cwe: "CWE-1357",
|
|
10416
|
+
message: squat.kind === "separator" ? `"${name}" differs from "${squat.impersonates}" only in punctuation` : `"${name}" is one edit from "${squat.impersonates}"`,
|
|
10417
|
+
consequence: "A reviewer scanning a diff reads the name they expected. The package that installs is the one that was written.",
|
|
10418
|
+
excerpt: (lines[line - 1] ?? "").trim()
|
|
10419
|
+
});
|
|
10420
|
+
}
|
|
10421
|
+
}
|
|
10422
|
+
}
|
|
10423
|
+
const scripts = parsed.scripts;
|
|
10424
|
+
if (scripts && typeof scripts === "object") {
|
|
10425
|
+
for (const [name, body] of Object.entries(scripts)) {
|
|
10426
|
+
if (!LIFECYCLE_SCRIPTS.includes(name)) continue;
|
|
10427
|
+
findings.push({
|
|
10428
|
+
ruleId: "manifest-install-lifecycle-script",
|
|
10429
|
+
title: "install-time lifecycle script",
|
|
10430
|
+
line: lineOf(name),
|
|
10431
|
+
severity: "medium",
|
|
10432
|
+
cwe: "CWE-506",
|
|
10433
|
+
message: `"${name}" runs automatically on install: ${String(body).slice(0, 120)}`,
|
|
10434
|
+
consequence: "Lifecycle scripts run with the installing user\u2019s privileges and network access, before any code is reviewed. It is the execution vector every notable npm compromise has used.",
|
|
10435
|
+
excerpt: (lines[lineOf(name) - 1] ?? "").trim()
|
|
10436
|
+
});
|
|
10437
|
+
}
|
|
10438
|
+
}
|
|
10439
|
+
return findings;
|
|
10440
|
+
}
|
|
10441
|
+
function scanRequirementsTxt(text) {
|
|
10442
|
+
const findings = [];
|
|
10443
|
+
const lines = text.split("\n");
|
|
10444
|
+
lines.forEach((raw, index) => {
|
|
10445
|
+
const line = raw.trim();
|
|
10446
|
+
if (!line || line.startsWith("#") || line.startsWith("-")) return;
|
|
10447
|
+
const match = /^([A-Za-z0-9_.-]+)\s*(?:[=<>!~]=|@|$)/.exec(line);
|
|
10448
|
+
const name = match?.[1];
|
|
10449
|
+
if (!name) return;
|
|
10450
|
+
if (INTERNAL_MARKER.test(name)) {
|
|
10451
|
+
findings.push({
|
|
10452
|
+
ruleId: "manifest-dependency-confusion",
|
|
10453
|
+
title: "internal-looking package resolved from a public index",
|
|
10454
|
+
line: index + 1,
|
|
10455
|
+
severity: "critical",
|
|
10456
|
+
cwe: "CWE-1357",
|
|
10457
|
+
message: `"${name}" names itself as internal but carries no index pin`,
|
|
10458
|
+
consequence: "pip resolves the highest version across every configured index, so a public package of the same name shadows the private one.",
|
|
10459
|
+
excerpt: line
|
|
10460
|
+
});
|
|
10461
|
+
return;
|
|
10462
|
+
}
|
|
10463
|
+
const squat = detectTyposquat(name, "pypi");
|
|
10464
|
+
if (squat) {
|
|
10465
|
+
findings.push({
|
|
10466
|
+
ruleId: "manifest-typosquat",
|
|
10467
|
+
title: "dependency name close to a popular package",
|
|
10468
|
+
line: index + 1,
|
|
10469
|
+
severity: "high",
|
|
10470
|
+
cwe: "CWE-1357",
|
|
10471
|
+
message: squat.kind === "separator" ? `"${name}" differs from "${squat.impersonates}" only in punctuation` : `"${name}" is one edit from "${squat.impersonates}"`,
|
|
10472
|
+
consequence: "A reviewer scanning a diff reads the name they expected. The package that installs is the one that was written.",
|
|
10473
|
+
excerpt: line
|
|
10474
|
+
});
|
|
10475
|
+
}
|
|
10476
|
+
});
|
|
10477
|
+
return findings;
|
|
10478
|
+
}
|
|
10479
|
+
|
|
10480
|
+
// src/scan/secret-rules.ts
|
|
10481
|
+
var SECRET_RULES = [
|
|
10482
|
+
{
|
|
10483
|
+
id: "secret-aws-access-key",
|
|
10484
|
+
name: "AWS Access Key",
|
|
10485
|
+
pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/,
|
|
10486
|
+
severity: "critical",
|
|
10487
|
+
cwe: "CWE-798",
|
|
10488
|
+
consequence: "Paired with a secret key, grants the API access of whatever IAM principal issued it."
|
|
10489
|
+
},
|
|
10490
|
+
{
|
|
10491
|
+
id: "secret-aws-secret-key",
|
|
10492
|
+
name: "AWS Secret Access Key",
|
|
10493
|
+
pattern: /(?:aws_secret_access_key|AWS_SECRET(?:_ACCESS_KEY)?)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/i,
|
|
10494
|
+
severity: "critical",
|
|
10495
|
+
cwe: "CWE-798",
|
|
10496
|
+
consequence: "The other half of an AWS credential pair; on its own it is still the hard half to guess."
|
|
10497
|
+
},
|
|
10498
|
+
{
|
|
10499
|
+
id: "secret-github-token",
|
|
10500
|
+
name: "GitHub Token",
|
|
10501
|
+
pattern: /\b(?:ghp_[A-Za-z0-9]{36}|gho_[A-Za-z0-9]{36}|ghu_[A-Za-z0-9]{36}|ghs_[A-Za-z0-9]{36}|ghr_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{60,})\b/,
|
|
10502
|
+
severity: "critical",
|
|
10503
|
+
cwe: "CWE-798",
|
|
10504
|
+
consequence: "Repository read or write as the issuing account, including the ability to push workflow changes."
|
|
10505
|
+
},
|
|
10506
|
+
{
|
|
10507
|
+
id: "secret-npm-token",
|
|
10508
|
+
name: "npm Token",
|
|
10509
|
+
pattern: /\bnpm_[A-Za-z0-9]{36}\b/,
|
|
10510
|
+
severity: "critical",
|
|
10511
|
+
cwe: "CWE-798",
|
|
10512
|
+
consequence: "Publish rights to every package the account owns \u2014 a supply-chain compromise in one command."
|
|
10513
|
+
},
|
|
10514
|
+
{
|
|
10515
|
+
id: "secret-private-key",
|
|
10516
|
+
name: "Private Key",
|
|
10517
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/,
|
|
10518
|
+
severity: "critical",
|
|
10519
|
+
cwe: "CWE-798",
|
|
10520
|
+
consequence: "Key material, committed. Rotation is the only remediation."
|
|
10521
|
+
},
|
|
10522
|
+
{
|
|
10523
|
+
id: "secret-slack-token",
|
|
10524
|
+
name: "Slack Token",
|
|
10525
|
+
pattern: /\bxox[bpoasr]-[A-Za-z0-9-]{10,}/,
|
|
10526
|
+
severity: "critical",
|
|
10527
|
+
cwe: "CWE-798",
|
|
10528
|
+
consequence: "Read and post access to the workspace as the installing app."
|
|
10529
|
+
},
|
|
10530
|
+
{
|
|
10531
|
+
id: "secret-slack-webhook",
|
|
10532
|
+
name: "Slack Webhook URL",
|
|
10533
|
+
pattern: /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_+\/-]{6,}/,
|
|
10534
|
+
severity: "high",
|
|
10535
|
+
cwe: "CWE-798",
|
|
10536
|
+
consequence: "The URL *is* the credential \u2014 anyone holding it can post to that channel."
|
|
10537
|
+
},
|
|
10538
|
+
{
|
|
10539
|
+
id: "secret-stripe-key",
|
|
10540
|
+
name: "Stripe Key",
|
|
10541
|
+
pattern: /\b(?:sk_live_|rk_live_|sk_test_|rk_test_)[A-Za-z0-9]{20,}\b/,
|
|
10542
|
+
severity: "critical",
|
|
10543
|
+
cwe: "CWE-798",
|
|
10544
|
+
consequence: "Charge, refund and customer-data access against the account."
|
|
10545
|
+
},
|
|
10546
|
+
{
|
|
10547
|
+
id: "secret-sendgrid-key",
|
|
10548
|
+
name: "SendGrid API Key",
|
|
10549
|
+
pattern: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/,
|
|
10550
|
+
severity: "critical",
|
|
10551
|
+
cwe: "CWE-798",
|
|
10552
|
+
consequence: "Send mail as the domain \u2014 the credential behind most convincing phishing from a real sender."
|
|
10553
|
+
},
|
|
10554
|
+
{
|
|
10555
|
+
id: "secret-google-api-key",
|
|
10556
|
+
name: "Google API Key",
|
|
10557
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/,
|
|
10558
|
+
severity: "high",
|
|
10559
|
+
cwe: "CWE-798",
|
|
10560
|
+
consequence: "Quota theft at minimum; API access to whatever the key was scoped to at worst."
|
|
10561
|
+
},
|
|
10562
|
+
{
|
|
10563
|
+
id: "secret-openai-key",
|
|
10564
|
+
name: "OpenAI API Key",
|
|
10565
|
+
pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/,
|
|
10566
|
+
severity: "critical",
|
|
10567
|
+
cwe: "CWE-798",
|
|
10568
|
+
consequence: "Billed inference against the owner\u2019s account, with no per-key spend limit by default."
|
|
10569
|
+
},
|
|
10570
|
+
{
|
|
10571
|
+
id: "secret-anthropic-key",
|
|
10572
|
+
name: "Anthropic API Key",
|
|
10573
|
+
pattern: /\bsk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{32,}\b/,
|
|
10574
|
+
severity: "critical",
|
|
10575
|
+
cwe: "CWE-798",
|
|
10576
|
+
consequence: "Billed inference against the owner\u2019s account."
|
|
10577
|
+
},
|
|
10578
|
+
{
|
|
10579
|
+
id: "secret-database-url",
|
|
10580
|
+
name: "Database URL with credentials",
|
|
10581
|
+
// Requires a credential segment before the `@` — `postgres://localhost/db`
|
|
10582
|
+
// is a hostname, not a secret.
|
|
10583
|
+
pattern: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|clickhouse):\/\/[^\s'"@\/]*:[^\s'"@\/]*@[^\s'"]+/i,
|
|
10584
|
+
severity: "high",
|
|
10585
|
+
cwe: "CWE-798",
|
|
10586
|
+
consequence: "Direct database access, usually bypassing every application-level authorisation check."
|
|
10587
|
+
},
|
|
10588
|
+
{
|
|
10589
|
+
id: "secret-jwt",
|
|
10590
|
+
name: "JSON Web Token",
|
|
10591
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
|
|
10592
|
+
severity: "medium",
|
|
10593
|
+
cwe: "CWE-798",
|
|
10594
|
+
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived."
|
|
10595
|
+
},
|
|
10596
|
+
{
|
|
10597
|
+
id: "secret-generic-api-key",
|
|
10598
|
+
name: "Generic API Key",
|
|
10599
|
+
// Quoted assignment only. An unquoted value in a `.env` is covered by the
|
|
10600
|
+
// vendor-prefixed rules above; matching it here is what starts flagging
|
|
10601
|
+
// ARNs and parameter-store paths.
|
|
10602
|
+
pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
|
|
10603
|
+
severity: "high",
|
|
10604
|
+
cwe: "CWE-798",
|
|
10605
|
+
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid."
|
|
10606
|
+
},
|
|
10607
|
+
{
|
|
10608
|
+
id: "secret-generic-credential",
|
|
10609
|
+
name: "Hardcoded Credential",
|
|
10610
|
+
pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
|
|
10611
|
+
severity: "high",
|
|
10612
|
+
cwe: "CWE-798",
|
|
10613
|
+
consequence: "A password in source is a password in every clone, fork and CI cache of that source."
|
|
10614
|
+
},
|
|
10615
|
+
{
|
|
10616
|
+
id: "secret-hex-token",
|
|
10617
|
+
name: "High-entropy Hex Token",
|
|
10618
|
+
pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
|
|
10619
|
+
severity: "medium",
|
|
10620
|
+
cwe: "CWE-798",
|
|
10621
|
+
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign."
|
|
10622
|
+
}
|
|
9582
10623
|
];
|
|
9583
|
-
var
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
10624
|
+
var KNOWN_PLACEHOLDERS = [
|
|
10625
|
+
// Deliberately NOT here: AWS's published documentation key/secret pair
|
|
10626
|
+
// (`AKIAIOSFODNN7EXAMPLE`, `wJalrXUtnFEMI/…`). GitHub allow-lists them, and
|
|
10627
|
+
// the argument for following suit is that they authenticate nothing. The
|
|
10628
|
+
// argument against is stronger: they appear in a repository because someone
|
|
10629
|
+
// pasted a credentials template and left it there, and the remediation —
|
|
10630
|
+
// move this to the secret manager — is identical to the one for a live key.
|
|
10631
|
+
// Exempting them means the scanner goes quiet on the file most likely to
|
|
10632
|
+
// acquire a real key next.
|
|
10633
|
+
/\bEXAMPLE_?KEY\b/i,
|
|
10634
|
+
/\bYOUR_[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD)\b/,
|
|
10635
|
+
/\b(?:xxx+|X{4,}|\*{4,}|<[a-z-]+>)\b/,
|
|
10636
|
+
/\bchangeme\b/i
|
|
9592
10637
|
];
|
|
10638
|
+
function isKnownPlaceholder(text) {
|
|
10639
|
+
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
10640
|
+
}
|
|
10641
|
+
function redactSecret(line) {
|
|
10642
|
+
return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
|
|
10643
|
+
if (match.length <= 12) return match;
|
|
10644
|
+
return `${match.slice(0, 3)}${"*".repeat(Math.min(16, match.length - 3))}`;
|
|
10645
|
+
});
|
|
10646
|
+
}
|
|
10647
|
+
var SENSITIVE_FILES = [
|
|
10648
|
+
{ pattern: ".env", message: "Environment file committed \u2014 the usual home of every runtime credential", severity: "high" },
|
|
10649
|
+
{ pattern: ".env.local", message: "Local environment file committed", severity: "high" },
|
|
10650
|
+
{ pattern: ".env.production", message: "Production environment file committed", severity: "critical" },
|
|
10651
|
+
{ pattern: "id_rsa", message: "Private SSH key committed", severity: "critical" },
|
|
10652
|
+
{ pattern: "id_ed25519", message: "Private SSH key committed", severity: "critical" },
|
|
10653
|
+
{ pattern: "id_ecdsa", message: "Private SSH key committed", severity: "critical" },
|
|
10654
|
+
{ pattern: ".pem", message: "PEM certificate or key file committed", severity: "high" },
|
|
10655
|
+
{ pattern: ".p12", message: "PKCS#12 keystore committed", severity: "high" },
|
|
10656
|
+
{ pattern: ".pfx", message: "PKCS#12 keystore committed", severity: "high" },
|
|
10657
|
+
{ pattern: ".keystore", message: "Java keystore committed", severity: "high" }
|
|
10658
|
+
// Deliberately not `.npmrc`. Its presence is normal; only an `_authToken`
|
|
10659
|
+
// line in it is a credential, and that is a content match, not a filename
|
|
10660
|
+
// match. Reporting the file itself trades a real finding for a chore.
|
|
10661
|
+
];
|
|
10662
|
+
|
|
10663
|
+
// src/scan/engine.ts
|
|
9593
10664
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
9594
10665
|
"node_modules",
|
|
9595
10666
|
".git",
|
|
9596
10667
|
".next",
|
|
10668
|
+
".nuxt",
|
|
9597
10669
|
"dist",
|
|
9598
10670
|
"build",
|
|
10671
|
+
"out",
|
|
9599
10672
|
"__pycache__",
|
|
9600
10673
|
".venv",
|
|
10674
|
+
"venv",
|
|
9601
10675
|
"vendor",
|
|
9602
10676
|
".terraform",
|
|
9603
10677
|
"coverage",
|
|
9604
|
-
".cache"
|
|
10678
|
+
".cache",
|
|
10679
|
+
".pnpm-store",
|
|
10680
|
+
"target",
|
|
10681
|
+
".gradle",
|
|
10682
|
+
".idea",
|
|
10683
|
+
".vscode",
|
|
10684
|
+
"bower_components",
|
|
10685
|
+
".svelte-kit"
|
|
9605
10686
|
]);
|
|
9606
10687
|
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
9607
10688
|
".ts",
|
|
9608
10689
|
".js",
|
|
9609
10690
|
".tsx",
|
|
9610
10691
|
".jsx",
|
|
10692
|
+
".mjs",
|
|
10693
|
+
".cjs",
|
|
10694
|
+
".mts",
|
|
10695
|
+
".cts",
|
|
9611
10696
|
".py",
|
|
9612
10697
|
".rb",
|
|
9613
10698
|
".go",
|
|
9614
10699
|
".java",
|
|
10700
|
+
".kt",
|
|
10701
|
+
".scala",
|
|
9615
10702
|
".php",
|
|
9616
10703
|
".rs",
|
|
9617
10704
|
".c",
|
|
10705
|
+
".cc",
|
|
9618
10706
|
".cpp",
|
|
9619
10707
|
".h",
|
|
10708
|
+
".hpp",
|
|
10709
|
+
".cs",
|
|
10710
|
+
".swift",
|
|
9620
10711
|
".yml",
|
|
9621
10712
|
".yaml",
|
|
9622
10713
|
".json",
|
|
@@ -9627,283 +10718,628 @@ var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
9627
10718
|
".env",
|
|
9628
10719
|
".sh",
|
|
9629
10720
|
".bash",
|
|
10721
|
+
".zsh",
|
|
9630
10722
|
".tf",
|
|
9631
10723
|
".hcl",
|
|
9632
10724
|
".xml",
|
|
9633
10725
|
".properties",
|
|
9634
|
-
".gradle"
|
|
10726
|
+
".gradle",
|
|
10727
|
+
".txt",
|
|
10728
|
+
".md",
|
|
10729
|
+
".sql",
|
|
10730
|
+
".erb",
|
|
10731
|
+
".ejs",
|
|
10732
|
+
".vue",
|
|
10733
|
+
".svelte"
|
|
9635
10734
|
]);
|
|
9636
|
-
|
|
10735
|
+
var LANGUAGE_BY_EXTENSION = {
|
|
10736
|
+
".js": "javascript",
|
|
10737
|
+
".jsx": "javascript",
|
|
10738
|
+
".mjs": "javascript",
|
|
10739
|
+
".cjs": "javascript",
|
|
10740
|
+
".ts": "typescript",
|
|
10741
|
+
".tsx": "typescript",
|
|
10742
|
+
".mts": "typescript",
|
|
10743
|
+
".cts": "typescript",
|
|
10744
|
+
".vue": "javascript",
|
|
10745
|
+
".svelte": "javascript",
|
|
10746
|
+
".ejs": "javascript",
|
|
10747
|
+
".py": "python",
|
|
10748
|
+
".rb": "ruby",
|
|
10749
|
+
".erb": "ruby",
|
|
10750
|
+
".go": "go",
|
|
10751
|
+
".java": "java",
|
|
10752
|
+
".kt": "java",
|
|
10753
|
+
".scala": "java",
|
|
10754
|
+
".php": "php",
|
|
10755
|
+
".sh": "shell",
|
|
10756
|
+
".bash": "shell",
|
|
10757
|
+
".zsh": "shell",
|
|
10758
|
+
".yml": "config",
|
|
10759
|
+
".yaml": "config",
|
|
10760
|
+
".json": "config",
|
|
10761
|
+
".toml": "config",
|
|
10762
|
+
".ini": "config",
|
|
10763
|
+
".cfg": "config",
|
|
10764
|
+
".conf": "config",
|
|
10765
|
+
".env": "config",
|
|
10766
|
+
".tf": "config",
|
|
10767
|
+
".hcl": "config",
|
|
10768
|
+
".properties": "config"
|
|
10769
|
+
};
|
|
10770
|
+
function languageOf(filename) {
|
|
10771
|
+
if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
|
|
10772
|
+
return LANGUAGE_BY_EXTENSION[(0, import_node_path3.extname)(filename).toLowerCase()] ?? "other";
|
|
10773
|
+
}
|
|
10774
|
+
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
10775
|
+
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
10776
|
+
function collectSuppressions(lines) {
|
|
10777
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
10778
|
+
let count = 0;
|
|
10779
|
+
const add = (index, ruleId) => {
|
|
10780
|
+
const existing = byLine.get(index) ?? /* @__PURE__ */ new Set();
|
|
10781
|
+
existing.add(ruleId ?? "*");
|
|
10782
|
+
byLine.set(index, existing);
|
|
10783
|
+
count += 1;
|
|
10784
|
+
};
|
|
10785
|
+
lines.forEach((line, index) => {
|
|
10786
|
+
const next = SUPPRESS_NEXT.exec(line);
|
|
10787
|
+
if (next) add(index + 1, next[1]);
|
|
10788
|
+
const same = SUPPRESS_LINE.exec(line);
|
|
10789
|
+
if (same && !next) add(index, same[1]);
|
|
10790
|
+
});
|
|
10791
|
+
return { byLine, count };
|
|
10792
|
+
}
|
|
10793
|
+
function isSuppressed(suppressions, index, ruleId) {
|
|
10794
|
+
const rules = suppressions.byLine.get(index);
|
|
10795
|
+
if (!rules) return false;
|
|
10796
|
+
return rules.has("*") || rules.has(ruleId);
|
|
10797
|
+
}
|
|
10798
|
+
function isTestPath(relativePath) {
|
|
10799
|
+
const p = relativePath.replace(/\\/g, "/");
|
|
10800
|
+
return /(?:^|\/)(?:tests?|__tests__|__mocks__|spec|specs|fixtures?|mocks?|e2e|testdata)\//i.test(p) || /(?:^|\/)(?:test|conftest)_[^/]+$/i.test(p) || /[._-](?:test|spec)\.[a-z]+$/i.test(p) || /_test\.[a-z]+$/i.test(p);
|
|
10801
|
+
}
|
|
10802
|
+
function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
9637
10803
|
const findings = [];
|
|
9638
|
-
|
|
9639
|
-
|
|
10804
|
+
const lines = text.split("\n");
|
|
10805
|
+
const suppressions = collectSuppressions(lines);
|
|
10806
|
+
const inTests = isTestPath(relativePath);
|
|
10807
|
+
lines.forEach((line, index) => {
|
|
10808
|
+
for (const rule of SECRET_RULES) {
|
|
10809
|
+
const match = rule.pattern.exec(line);
|
|
10810
|
+
if (!match) continue;
|
|
10811
|
+
if (isKnownPlaceholder(match[0])) continue;
|
|
10812
|
+
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
10813
|
+
findings.push({
|
|
10814
|
+
ruleId: rule.id,
|
|
10815
|
+
title: rule.name,
|
|
10816
|
+
file: relativePath,
|
|
10817
|
+
line: index + 1,
|
|
10818
|
+
// Reported but not blocking in tests — see isTestPath.
|
|
10819
|
+
severity: inTests ? "low" : rule.severity,
|
|
10820
|
+
// A matched credential format is the finding, not a proxy for one.
|
|
10821
|
+
confidence: "evidence",
|
|
10822
|
+
message: inTests ? `Possible ${rule.name} detected in a test file \u2014 usually a fixture, still worth confirming it is not a live credential` : `Possible ${rule.name} detected`,
|
|
10823
|
+
consequence: rule.consequence,
|
|
10824
|
+
cwe: rule.cwe,
|
|
10825
|
+
excerpt: redactSecret(line.trim()).slice(0, 200),
|
|
10826
|
+
sensitive: true,
|
|
10827
|
+
category: "secret"
|
|
10828
|
+
});
|
|
10829
|
+
}
|
|
10830
|
+
});
|
|
10831
|
+
const prose = proseLines(lines);
|
|
10832
|
+
lines.forEach((_line, index) => {
|
|
10833
|
+
for (const rule of CODE_RULES) {
|
|
10834
|
+
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
10835
|
+
const match = evaluateRule(rule, { lines, index, language, prose });
|
|
10836
|
+
if (!match) continue;
|
|
10837
|
+
findings.push({
|
|
10838
|
+
ruleId: rule.id,
|
|
10839
|
+
title: rule.title,
|
|
10840
|
+
file: relativePath,
|
|
10841
|
+
line: index + 1,
|
|
10842
|
+
severity: match.severity,
|
|
10843
|
+
confidence: match.confidence,
|
|
10844
|
+
message: `${rule.title} (${rule.cwe})`,
|
|
10845
|
+
consequence: rule.consequence,
|
|
10846
|
+
cwe: rule.cwe,
|
|
10847
|
+
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
10848
|
+
category: "code"
|
|
10849
|
+
});
|
|
10850
|
+
}
|
|
10851
|
+
});
|
|
10852
|
+
return findings;
|
|
10853
|
+
}
|
|
10854
|
+
function scanManifest(relativePath, filename, text) {
|
|
10855
|
+
const manifestFindings = filename === "package.json" ? scanPackageJson(text) : filename === "requirements.txt" ? scanRequirementsTxt(text) : [];
|
|
10856
|
+
return manifestFindings.map((finding) => ({
|
|
10857
|
+
ruleId: finding.ruleId,
|
|
10858
|
+
title: finding.title,
|
|
10859
|
+
file: relativePath,
|
|
10860
|
+
line: finding.line,
|
|
10861
|
+
severity: finding.severity,
|
|
10862
|
+
confidence: "evidence",
|
|
10863
|
+
message: finding.message,
|
|
10864
|
+
consequence: finding.consequence,
|
|
10865
|
+
cwe: finding.cwe,
|
|
10866
|
+
excerpt: finding.excerpt.slice(0, 200),
|
|
10867
|
+
category: "manifest"
|
|
10868
|
+
}));
|
|
10869
|
+
}
|
|
10870
|
+
function scanPath(targetPath, options = {}) {
|
|
10871
|
+
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
10872
|
+
const allowed = options.categories ? new Set(options.categories) : null;
|
|
10873
|
+
const findings = [];
|
|
10874
|
+
const unreadable = [];
|
|
10875
|
+
let filesScanned = 0;
|
|
10876
|
+
let suppressed = 0;
|
|
10877
|
+
const rootIsDirectory = (() => {
|
|
10878
|
+
try {
|
|
10879
|
+
return (0, import_node_fs6.statSync)(targetPath).isDirectory();
|
|
10880
|
+
} catch {
|
|
10881
|
+
return true;
|
|
10882
|
+
}
|
|
10883
|
+
})();
|
|
10884
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path3.dirname)(targetPath);
|
|
10885
|
+
const scanFile = (fullPath, filename) => {
|
|
10886
|
+
const relativePath = toRelative(walkRoot, fullPath);
|
|
10887
|
+
const extension = (0, import_node_path3.extname)(filename).toLowerCase();
|
|
10888
|
+
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
10889
|
+
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
10890
|
+
if (!scannable && !isManifest) {
|
|
10891
|
+
recordSensitiveFile(filename, relativePath, findings, []);
|
|
10892
|
+
return;
|
|
10893
|
+
}
|
|
10894
|
+
let text;
|
|
10895
|
+
let handle;
|
|
10896
|
+
try {
|
|
10897
|
+
handle = (0, import_node_fs6.openSync)(fullPath, "r");
|
|
10898
|
+
} catch {
|
|
10899
|
+
unreadable.push(relativePath);
|
|
10900
|
+
return;
|
|
10901
|
+
}
|
|
10902
|
+
try {
|
|
10903
|
+
if ((0, import_node_fs6.fstatSync)(handle).size > maxFileBytes) return;
|
|
10904
|
+
text = (0, import_node_fs6.readFileSync)(handle, "utf-8");
|
|
10905
|
+
} catch {
|
|
10906
|
+
unreadable.push(relativePath);
|
|
10907
|
+
return;
|
|
10908
|
+
} finally {
|
|
10909
|
+
try {
|
|
10910
|
+
(0, import_node_fs6.closeSync)(handle);
|
|
10911
|
+
} catch {
|
|
10912
|
+
}
|
|
10913
|
+
}
|
|
10914
|
+
filesScanned += 1;
|
|
10915
|
+
options.onFile?.(relativePath);
|
|
10916
|
+
suppressed += collectSuppressions(text.split("\n")).count;
|
|
10917
|
+
const fileFindings = [
|
|
10918
|
+
...scanText(relativePath, text, languageOf(filename)),
|
|
10919
|
+
...isManifest ? scanManifest(relativePath, filename, text) : []
|
|
10920
|
+
];
|
|
10921
|
+
findings.push(...fileFindings);
|
|
10922
|
+
recordSensitiveFile(filename, relativePath, findings, fileFindings);
|
|
10923
|
+
};
|
|
10924
|
+
const walk = (currentPath) => {
|
|
10925
|
+
let entries;
|
|
10926
|
+
try {
|
|
10927
|
+
entries = (0, import_node_fs6.readdirSync)(currentPath, { withFileTypes: true });
|
|
10928
|
+
} catch {
|
|
10929
|
+
unreadable.push(toRelative(walkRoot, currentPath));
|
|
10930
|
+
return;
|
|
10931
|
+
}
|
|
10932
|
+
for (const entry of entries) {
|
|
10933
|
+
const fullPath = (0, import_node_path3.join)(currentPath, entry.name);
|
|
10934
|
+
if (entry.isDirectory()) {
|
|
10935
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
10936
|
+
walk(fullPath);
|
|
10937
|
+
continue;
|
|
10938
|
+
}
|
|
10939
|
+
if (!entry.isFile()) continue;
|
|
10940
|
+
scanFile(fullPath, entry.name);
|
|
10941
|
+
}
|
|
10942
|
+
};
|
|
10943
|
+
if (rootIsDirectory) {
|
|
10944
|
+
walk(targetPath);
|
|
10945
|
+
} else {
|
|
10946
|
+
scanFile(targetPath, (0, import_node_path3.basename)(targetPath));
|
|
10947
|
+
}
|
|
10948
|
+
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
10949
|
+
filtered.sort(
|
|
10950
|
+
(a, b) => severityRank(b.severity) - severityRank(a.severity) || a.file.localeCompare(b.file) || a.line - b.line
|
|
10951
|
+
);
|
|
10952
|
+
return { findings: filtered, filesScanned, unreadable, suppressed, root: walkRoot };
|
|
10953
|
+
}
|
|
10954
|
+
function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
10955
|
+
if (fileFindings.length > 0) return;
|
|
10956
|
+
for (const sensitive of SENSITIVE_FILES) {
|
|
10957
|
+
const matches = filename === sensitive.pattern || filename.endsWith(sensitive.pattern);
|
|
10958
|
+
if (!matches) continue;
|
|
10959
|
+
sink.push({
|
|
10960
|
+
ruleId: "sensitive-file-committed",
|
|
10961
|
+
title: "Sensitive file",
|
|
10962
|
+
file: relativePath,
|
|
10963
|
+
line: 1,
|
|
10964
|
+
severity: sensitive.severity,
|
|
10965
|
+
confidence: "evidence",
|
|
10966
|
+
message: sensitive.message,
|
|
10967
|
+
consequence: "Anything in this file is in every clone, fork and CI cache of the repository.",
|
|
10968
|
+
cwe: "CWE-538",
|
|
10969
|
+
excerpt: "",
|
|
10970
|
+
sensitive: true,
|
|
10971
|
+
category: "file"
|
|
9640
10972
|
});
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
10973
|
+
return;
|
|
10974
|
+
}
|
|
10975
|
+
}
|
|
10976
|
+
function toRelative(base, target) {
|
|
10977
|
+
const rel = (0, import_node_path3.relative)(base, target);
|
|
10978
|
+
return (rel === "" ? target : rel).split(import_node_path3.sep).join("/");
|
|
10979
|
+
}
|
|
10980
|
+
function meetsFailThreshold(findings, threshold) {
|
|
10981
|
+
if (threshold.length === 0) return false;
|
|
10982
|
+
const floor = Math.min(...threshold.map(severityRank));
|
|
10983
|
+
return findings.some((finding) => severityRank(finding.severity) >= floor);
|
|
10984
|
+
}
|
|
10985
|
+
|
|
10986
|
+
// src/scan/sarif.ts
|
|
10987
|
+
var import_node_path4 = require("path");
|
|
10988
|
+
var SARIF_VERSION = "2.1.0";
|
|
10989
|
+
var SARIF_SCHEMA = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json";
|
|
10990
|
+
function sarifLevel(severity) {
|
|
10991
|
+
switch (severity) {
|
|
10992
|
+
case "critical":
|
|
10993
|
+
case "high":
|
|
10994
|
+
return "error";
|
|
10995
|
+
case "medium":
|
|
10996
|
+
return "warning";
|
|
10997
|
+
case "low":
|
|
10998
|
+
return "note";
|
|
10999
|
+
default:
|
|
11000
|
+
return "none";
|
|
11001
|
+
}
|
|
11002
|
+
}
|
|
11003
|
+
function securitySeverity(severity) {
|
|
11004
|
+
switch (severity) {
|
|
11005
|
+
case "critical":
|
|
11006
|
+
return "9.0";
|
|
11007
|
+
case "high":
|
|
11008
|
+
return "7.0";
|
|
11009
|
+
case "medium":
|
|
11010
|
+
return "5.0";
|
|
11011
|
+
case "low":
|
|
11012
|
+
return "3.0";
|
|
11013
|
+
default:
|
|
11014
|
+
return "1.0";
|
|
11015
|
+
}
|
|
11016
|
+
}
|
|
11017
|
+
function toArtifactUri(filePath, base, prefix = "", root = base) {
|
|
11018
|
+
const absolute = (0, import_node_path4.isAbsolute)(filePath) ? filePath : (0, import_node_path4.resolve)(root, filePath);
|
|
11019
|
+
const relativePath = (0, import_node_path4.relative)(base, absolute);
|
|
11020
|
+
const escapedOut = relativePath.startsWith("..") || relativePath === "";
|
|
11021
|
+
const chosen = escapedOut ? absolute : relativePath;
|
|
11022
|
+
const posix = chosen.split(import_node_path4.sep).join("/").replace(/^\.\//, "");
|
|
11023
|
+
if (!prefix || escapedOut) return posix;
|
|
11024
|
+
const trimmed = prefix.replace(/^\/+|\/+$/g, "");
|
|
11025
|
+
return trimmed ? `${trimmed}/${posix}` : posix;
|
|
11026
|
+
}
|
|
11027
|
+
function buildSarif(findings, options) {
|
|
11028
|
+
const rules = /* @__PURE__ */ new Map();
|
|
11029
|
+
for (const finding of findings) {
|
|
11030
|
+
if (rules.has(finding.ruleId)) continue;
|
|
11031
|
+
const tags = ["security"];
|
|
11032
|
+
if (finding.cwe) tags.push(`external/cwe/${finding.cwe.toLowerCase()}`);
|
|
11033
|
+
tags.push(`threatcrush/${finding.category}`);
|
|
11034
|
+
const description = finding.consequence ? `${finding.title}. ${finding.consequence}` : finding.title;
|
|
11035
|
+
rules.set(finding.ruleId, {
|
|
11036
|
+
id: finding.ruleId,
|
|
11037
|
+
name: finding.ruleId,
|
|
11038
|
+
shortDescription: { text: finding.title },
|
|
11039
|
+
fullDescription: { text: description },
|
|
11040
|
+
help: {
|
|
11041
|
+
text: description,
|
|
11042
|
+
markdown: finding.consequence ? `**${finding.title}**
|
|
11043
|
+
|
|
11044
|
+
${finding.consequence}` : `**${finding.title}**`
|
|
11045
|
+
},
|
|
11046
|
+
defaultConfiguration: { level: sarifLevel(finding.severity) },
|
|
11047
|
+
properties: {
|
|
11048
|
+
tags,
|
|
11049
|
+
"security-severity": securitySeverity(finding.severity),
|
|
11050
|
+
// SARIF's vocabulary for how much the rule is claiming. It lines up
|
|
11051
|
+
// with the confidence model: a bare construct match is `medium`, a
|
|
11052
|
+
// match with visible untrusted input is `high`.
|
|
11053
|
+
precision: finding.confidence === "pattern" ? "medium" : "high"
|
|
11054
|
+
}
|
|
11055
|
+
});
|
|
11056
|
+
}
|
|
11057
|
+
const base = options.base ?? process.cwd();
|
|
11058
|
+
const root = options.root ?? base;
|
|
11059
|
+
const results = findings.map((finding) => ({
|
|
11060
|
+
ruleId: finding.ruleId,
|
|
11061
|
+
level: sarifLevel(finding.severity),
|
|
11062
|
+
message: { text: finding.message },
|
|
11063
|
+
locations: [
|
|
11064
|
+
{
|
|
11065
|
+
physicalLocation: {
|
|
11066
|
+
artifactLocation: {
|
|
11067
|
+
uri: toArtifactUri(finding.file, base, options.pathPrefix, root),
|
|
11068
|
+
uriBaseId: "%SRCROOT%"
|
|
11069
|
+
},
|
|
11070
|
+
region: {
|
|
11071
|
+
// Clamped, never 0. A whole-file finding has no line; SARIF has no
|
|
11072
|
+
// way to say that, and 0 fails validation outright.
|
|
11073
|
+
startLine: Math.max(1, finding.line),
|
|
11074
|
+
snippet: { text: finding.excerpt }
|
|
11075
|
+
}
|
|
11076
|
+
}
|
|
11077
|
+
}
|
|
11078
|
+
],
|
|
11079
|
+
partialFingerprints: {
|
|
11080
|
+
primaryLocationLineHash: `${finding.ruleId}:${finding.file}:${Math.max(1, finding.line)}`
|
|
11081
|
+
},
|
|
11082
|
+
properties: {
|
|
11083
|
+
severity: finding.severity,
|
|
11084
|
+
confidence: finding.confidence,
|
|
11085
|
+
category: finding.category,
|
|
11086
|
+
...finding.cwe ? { cwe: finding.cwe } : {}
|
|
11087
|
+
}
|
|
11088
|
+
}));
|
|
11089
|
+
return {
|
|
11090
|
+
$schema: SARIF_SCHEMA,
|
|
11091
|
+
version: SARIF_VERSION,
|
|
11092
|
+
runs: [
|
|
11093
|
+
{
|
|
11094
|
+
tool: {
|
|
11095
|
+
driver: {
|
|
11096
|
+
name: "ThreatCrush",
|
|
11097
|
+
version: options.toolVersion,
|
|
11098
|
+
informationUri: "https://threatcrush.com",
|
|
11099
|
+
rules: [...rules.values()]
|
|
11100
|
+
}
|
|
11101
|
+
},
|
|
11102
|
+
results,
|
|
11103
|
+
columnKind: "utf16CodeUnits"
|
|
11104
|
+
}
|
|
11105
|
+
]
|
|
11106
|
+
};
|
|
11107
|
+
}
|
|
11108
|
+
|
|
11109
|
+
// src/commands/scan.ts
|
|
11110
|
+
function readVersion() {
|
|
11111
|
+
for (const candidate of [
|
|
11112
|
+
(0, import_node_path5.join)(__dirname, "..", "package.json"),
|
|
11113
|
+
(0, import_node_path5.join)(__dirname, "..", "..", "package.json")
|
|
11114
|
+
]) {
|
|
11115
|
+
try {
|
|
11116
|
+
return JSON.parse((0, import_node_fs7.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
|
|
11117
|
+
} catch {
|
|
11118
|
+
}
|
|
11119
|
+
}
|
|
11120
|
+
return "0.0.0";
|
|
11121
|
+
}
|
|
11122
|
+
var PKG_VERSION = readVersion();
|
|
11123
|
+
function parseFailOn(raw) {
|
|
11124
|
+
if (!raw) return [];
|
|
11125
|
+
const requested = raw.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
11126
|
+
const unknown = requested.filter((name) => !SEVERITY_ORDER.includes(name));
|
|
11127
|
+
if (unknown.length > 0) {
|
|
11128
|
+
throw new Error(
|
|
11129
|
+
`unknown severity in --fail-on: ${unknown.join(", ")} (expected ${SEVERITY_ORDER.join(", ")})`
|
|
11130
|
+
);
|
|
9652
11131
|
}
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
11132
|
+
return requested;
|
|
11133
|
+
}
|
|
11134
|
+
function toRunResult(targetPath, findings, filesScanned) {
|
|
11135
|
+
const structured = findings.map((finding) => ({
|
|
11136
|
+
type: finding.title,
|
|
11137
|
+
severity: finding.severity,
|
|
11138
|
+
message: finding.message,
|
|
11139
|
+
location: `${finding.file}:${finding.line}`,
|
|
11140
|
+
details: {
|
|
11141
|
+
file: finding.file,
|
|
11142
|
+
line: finding.line,
|
|
11143
|
+
snippet: finding.excerpt,
|
|
11144
|
+
ruleId: finding.ruleId,
|
|
11145
|
+
confidence: finding.confidence,
|
|
11146
|
+
...finding.cwe ? { cwe: finding.cwe } : {}
|
|
11147
|
+
}
|
|
9659
11148
|
}));
|
|
9660
|
-
const
|
|
11149
|
+
const counts = summarize(structured);
|
|
9661
11150
|
return {
|
|
9662
11151
|
type: "scan",
|
|
9663
11152
|
target: targetPath,
|
|
9664
11153
|
findings: structured,
|
|
9665
|
-
severity_summary:
|
|
9666
|
-
summary: findings.length === 0 ?
|
|
11154
|
+
severity_summary: counts,
|
|
11155
|
+
summary: findings.length === 0 ? `No issues found across ${filesScanned} files` : `${findings.length} issue(s): ${counts.critical}C ${counts.high}H ${counts.medium}M ${counts.low}L`
|
|
9667
11156
|
};
|
|
9668
11157
|
}
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
11158
|
+
function failedResult(targetPath, message) {
|
|
11159
|
+
return {
|
|
11160
|
+
type: "scan",
|
|
11161
|
+
target: targetPath,
|
|
11162
|
+
findings: [],
|
|
11163
|
+
severity_summary: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
11164
|
+
summary: `Scan failed: ${message}`,
|
|
11165
|
+
error: message
|
|
11166
|
+
};
|
|
11167
|
+
}
|
|
11168
|
+
async function runScan(targetPath) {
|
|
11169
|
+
try {
|
|
11170
|
+
const report = scanPath(targetPath);
|
|
11171
|
+
const findings = [...report.findings, ...await scanDependencies(targetPath)];
|
|
11172
|
+
return toRunResult(targetPath, findings, report.filesScanned);
|
|
11173
|
+
} catch (err) {
|
|
11174
|
+
return failedResult(targetPath, err.message);
|
|
11175
|
+
}
|
|
11176
|
+
}
|
|
11177
|
+
async function scanCommand(targetPath, options = {}) {
|
|
11178
|
+
const format = options.format ?? "text";
|
|
11179
|
+
const machineReadable = format !== "text";
|
|
11180
|
+
const say = machineReadable ? (line) => process.stderr.write(`${line}
|
|
11181
|
+
`) : (line) => process.stdout.write(`${line}
|
|
9672
11182
|
`);
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
11183
|
+
if (!(0, import_node_fs7.existsSync)(targetPath)) {
|
|
11184
|
+
say(source_default.red(`Scan target does not exist: ${targetPath}`));
|
|
11185
|
+
process.exitCode = 2;
|
|
11186
|
+
return failedResult(targetPath, `no such path: ${targetPath}`);
|
|
11187
|
+
}
|
|
11188
|
+
if (!machineReadable) {
|
|
11189
|
+
banner();
|
|
11190
|
+
logger.info(`Scanning ${source_default.white(targetPath)} for security issues...
|
|
11191
|
+
`);
|
|
11192
|
+
}
|
|
11193
|
+
const spinner = machineReadable ? null : ora({ text: "Scanning files...", color: "green" }).start();
|
|
11194
|
+
let outcome;
|
|
9676
11195
|
try {
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
11196
|
+
let seen = 0;
|
|
11197
|
+
const report = scanPath(targetPath, {
|
|
11198
|
+
onFile: () => {
|
|
11199
|
+
seen += 1;
|
|
11200
|
+
if (spinner) spinner.text = `Scanning files... (${seen} files)`;
|
|
11201
|
+
}
|
|
9680
11202
|
});
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
11203
|
+
if (options.dependencies) {
|
|
11204
|
+
if (spinner) spinner.text = "Querying OSV.dev for dependency advisories...";
|
|
11205
|
+
report.findings.push(...await scanDependencies(targetPath));
|
|
11206
|
+
}
|
|
11207
|
+
outcome = {
|
|
11208
|
+
result: toRunResult(targetPath, report.findings, report.filesScanned),
|
|
11209
|
+
findings: report.findings,
|
|
11210
|
+
filesScanned: report.filesScanned,
|
|
11211
|
+
unreadable: report.unreadable,
|
|
11212
|
+
suppressed: report.suppressed,
|
|
11213
|
+
root: report.root
|
|
9690
11214
|
};
|
|
11215
|
+
} catch (err) {
|
|
11216
|
+
spinner?.fail(`Scan failed: ${err.message}`);
|
|
11217
|
+
process.exitCode = 2;
|
|
11218
|
+
return failedResult(targetPath, err.message);
|
|
9691
11219
|
}
|
|
9692
|
-
spinner
|
|
11220
|
+
spinner?.succeed(`Scanned ${outcome.filesScanned} files
|
|
9693
11221
|
`);
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
11222
|
+
if (outcome.unreadable.length > 0) {
|
|
11223
|
+
say(
|
|
11224
|
+
source_default.yellow(
|
|
11225
|
+
` ! ${outcome.unreadable.length} path(s) could not be read and were NOT scanned`
|
|
11226
|
+
)
|
|
11227
|
+
);
|
|
11228
|
+
if (options.verbose) {
|
|
11229
|
+
for (const path of outcome.unreadable) say(source_default.gray(` ${path}`));
|
|
11230
|
+
}
|
|
11231
|
+
}
|
|
11232
|
+
if (outcome.suppressed > 0) {
|
|
11233
|
+
say(
|
|
11234
|
+
source_default.gray(
|
|
11235
|
+
` \xB7 ${outcome.suppressed} finding(s) suppressed by inline threatcrush-disable comments`
|
|
11236
|
+
)
|
|
11237
|
+
);
|
|
11238
|
+
}
|
|
11239
|
+
if (machineReadable) {
|
|
11240
|
+
emitMachineReadable(format, outcome, targetPath, options, say);
|
|
11241
|
+
} else {
|
|
11242
|
+
printHuman(outcome);
|
|
11243
|
+
}
|
|
11244
|
+
const failOn = options.failOn ?? [];
|
|
11245
|
+
if (meetsFailThreshold(outcome.findings, failOn)) {
|
|
11246
|
+
say(
|
|
11247
|
+
source_default.red(
|
|
11248
|
+
`
|
|
11249
|
+
\u2717 findings at or above ${[...failOn].join("/")} \u2014 failing as requested by --fail-on`
|
|
11250
|
+
)
|
|
11251
|
+
);
|
|
11252
|
+
process.exitCode = 1;
|
|
11253
|
+
}
|
|
11254
|
+
return outcome.result;
|
|
11255
|
+
}
|
|
11256
|
+
function emitMachineReadable(format, outcome, targetPath, options, say) {
|
|
11257
|
+
const payload = format === "sarif" ? buildSarif(outcome.findings, {
|
|
11258
|
+
toolVersion: PKG_VERSION,
|
|
11259
|
+
pathPrefix: options.pathPrefix,
|
|
11260
|
+
// Relative to the working directory, NOT the scan root. `threatcrush
|
|
11261
|
+
// scan vulns` from a repo root must emit `vulns/secrets/x.env`, not
|
|
11262
|
+
// `secrets/x.env` — the second form matches nothing in the
|
|
11263
|
+
// consumer's view of the repository, so every finding lands
|
|
11264
|
+
// "outside" whatever it scoped to and a working scan reads as 0%.
|
|
11265
|
+
// This is the single most expensive mistake in the whole pipeline
|
|
11266
|
+
// and it fails silently. `--path-prefix` covers the remaining case:
|
|
11267
|
+
// a scan run from inside the subdirectory it is scanning.
|
|
11268
|
+
base: process.cwd(),
|
|
11269
|
+
root: (0, import_node_path5.resolve)(outcome.root)
|
|
11270
|
+
}) : {
|
|
11271
|
+
tool: "threatcrush",
|
|
11272
|
+
version: PKG_VERSION,
|
|
11273
|
+
target: targetPath,
|
|
11274
|
+
filesScanned: outcome.filesScanned,
|
|
11275
|
+
unreadable: outcome.unreadable,
|
|
11276
|
+
suppressed: outcome.suppressed,
|
|
11277
|
+
summary: outcome.result.severity_summary,
|
|
11278
|
+
findings: outcome.findings
|
|
11279
|
+
};
|
|
11280
|
+
const serialized = `${JSON.stringify(payload, null, 2)}
|
|
11281
|
+
`;
|
|
11282
|
+
if (options.output) {
|
|
11283
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)((0, import_node_path5.resolve)(options.output)), { recursive: true });
|
|
11284
|
+
(0, import_node_fs7.writeFileSync)(options.output, serialized, "utf-8");
|
|
11285
|
+
say(
|
|
11286
|
+
source_default.gray(
|
|
11287
|
+
` ${format.toUpperCase()} written to ${options.output} (${outcome.findings.length} finding(s))`
|
|
11288
|
+
)
|
|
11289
|
+
);
|
|
11290
|
+
return;
|
|
11291
|
+
}
|
|
11292
|
+
process.stdout.write(serialized);
|
|
11293
|
+
}
|
|
11294
|
+
function printHuman(outcome) {
|
|
11295
|
+
const { findings, filesScanned } = outcome;
|
|
9702
11296
|
if (findings.length === 0) {
|
|
9703
11297
|
console.log(source_default.green.bold(" \u2713 No security issues found!"));
|
|
9704
11298
|
console.log();
|
|
9705
|
-
return
|
|
9706
|
-
type: "scan",
|
|
9707
|
-
target: targetPath,
|
|
9708
|
-
findings: [],
|
|
9709
|
-
severity_summary: sevCounts,
|
|
9710
|
-
summary: `No issues found across ${filesScanned} files`
|
|
9711
|
-
};
|
|
11299
|
+
return;
|
|
9712
11300
|
}
|
|
9713
|
-
const
|
|
9714
|
-
const high = findings.filter((f) => f.severity === "high");
|
|
9715
|
-
const medium = findings.filter((f) => f.severity === "medium");
|
|
9716
|
-
const low = findings.filter((f) => f.severity === "low");
|
|
11301
|
+
const counts = outcome.result.severity_summary;
|
|
9717
11302
|
console.log(source_default.white.bold(" Scan Results"));
|
|
9718
11303
|
console.log(source_default.gray(" " + "\u2500".repeat(70)));
|
|
9719
11304
|
console.log(
|
|
9720
|
-
` ${source_default.red.bold(critical
|
|
11305
|
+
` ${source_default.red.bold(counts.critical + " critical")} ${source_default.red(counts.high + " high")} ${source_default.yellow(counts.medium + " medium")} ${source_default.gray(counts.low + " low")}`
|
|
9721
11306
|
);
|
|
9722
11307
|
console.log(source_default.gray(" " + "\u2500".repeat(70)));
|
|
9723
11308
|
console.log();
|
|
9724
|
-
const
|
|
9725
|
-
|
|
9726
|
-
const
|
|
9727
|
-
console.log(` ${
|
|
9728
|
-
console.log(
|
|
11309
|
+
for (const finding of findings) {
|
|
11310
|
+
const label = finding.severity.toUpperCase();
|
|
11311
|
+
const badge = finding.severity === "critical" ? source_default.bgRed.white.bold(` ${label} `) : finding.severity === "high" ? source_default.red(`[${label}]`) : finding.severity === "medium" ? source_default.yellow(`[${label}]`) : source_default.gray(`[${label}]`);
|
|
11312
|
+
console.log(` ${badge} ${source_default.white.bold(finding.title)}`);
|
|
11313
|
+
console.log(
|
|
11314
|
+
` ${source_default.gray("File:")} ${source_default.cyan(finding.file)}:${source_default.yellow(String(finding.line))}`
|
|
11315
|
+
);
|
|
9729
11316
|
console.log(` ${source_default.gray("Info:")} ${finding.message}`);
|
|
9730
|
-
if (finding.
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
);
|
|
9735
|
-
console.log(` ${source_default.gray("Code:")} ${redacted.trim()}`);
|
|
11317
|
+
if (finding.consequence) {
|
|
11318
|
+
console.log(` ${source_default.gray("Risk:")} ${source_default.dim(finding.consequence)}`);
|
|
11319
|
+
}
|
|
11320
|
+
if (finding.excerpt) {
|
|
11321
|
+
console.log(` ${source_default.gray("Code:")} ${finding.excerpt}`);
|
|
9736
11322
|
}
|
|
11323
|
+
console.log(
|
|
11324
|
+
` ${source_default.gray("Rule:")} ${source_default.dim(finding.ruleId)}` + (finding.cwe ? source_default.dim(` \xB7 ${finding.cwe}`) : "") + source_default.dim(` \xB7 confidence: ${finding.confidence}`)
|
|
11325
|
+
);
|
|
9737
11326
|
console.log();
|
|
9738
11327
|
}
|
|
9739
11328
|
console.log(source_default.gray(" " + "\u2500".repeat(70)));
|
|
9740
|
-
console.log(
|
|
11329
|
+
console.log(
|
|
11330
|
+
` ${source_default.white.bold(`${findings.length} issue(s) found`)} across ${filesScanned} files`
|
|
11331
|
+
);
|
|
9741
11332
|
console.log();
|
|
9742
|
-
return {
|
|
9743
|
-
type: "scan",
|
|
9744
|
-
target: targetPath,
|
|
9745
|
-
findings: structured,
|
|
9746
|
-
severity_summary: sevCounts,
|
|
9747
|
-
summary: `${findings.length} issue(s): ${sevCounts.critical}C ${sevCounts.high}H ${sevCounts.medium}M ${sevCounts.low}L`
|
|
9748
|
-
};
|
|
9749
|
-
}
|
|
9750
|
-
function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
9751
|
-
let entries;
|
|
9752
|
-
try {
|
|
9753
|
-
entries = (0, import_node_fs5.readdirSync)(currentPath, { withFileTypes: true });
|
|
9754
|
-
} catch {
|
|
9755
|
-
return;
|
|
9756
|
-
}
|
|
9757
|
-
for (const entry of entries) {
|
|
9758
|
-
const fullPath = (0, import_node_path2.join)(currentPath, entry.name);
|
|
9759
|
-
if (entry.isDirectory()) {
|
|
9760
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
9761
|
-
scanDirectory(basePath, fullPath, findings, onFile);
|
|
9762
|
-
continue;
|
|
9763
|
-
}
|
|
9764
|
-
if (!entry.isFile()) continue;
|
|
9765
|
-
for (const mc of MISCONFIG_FILES) {
|
|
9766
|
-
if (entry.name === mc.pattern || entry.name.endsWith(mc.pattern)) {
|
|
9767
|
-
findings.push({
|
|
9768
|
-
file: (0, import_node_path2.relative)(basePath, fullPath),
|
|
9769
|
-
line: 0,
|
|
9770
|
-
type: "Sensitive File",
|
|
9771
|
-
severity: "high",
|
|
9772
|
-
message: mc.message,
|
|
9773
|
-
snippet: ""
|
|
9774
|
-
});
|
|
9775
|
-
}
|
|
9776
|
-
}
|
|
9777
|
-
const ext = (0, import_node_path2.extname)(entry.name).toLowerCase();
|
|
9778
|
-
if (!SCAN_EXTENSIONS.has(ext) && !entry.name.startsWith(".env")) continue;
|
|
9779
|
-
try {
|
|
9780
|
-
const stat = (0, import_node_fs5.statSync)(fullPath);
|
|
9781
|
-
if (stat.size > 1024 * 1024) continue;
|
|
9782
|
-
} catch {
|
|
9783
|
-
continue;
|
|
9784
|
-
}
|
|
9785
|
-
onFile();
|
|
9786
|
-
let content;
|
|
9787
|
-
try {
|
|
9788
|
-
content = (0, import_node_fs5.readFileSync)(fullPath, "utf-8");
|
|
9789
|
-
} catch {
|
|
9790
|
-
continue;
|
|
9791
|
-
}
|
|
9792
|
-
const lines = content.split("\n");
|
|
9793
|
-
for (let i = 0; i < lines.length; i++) {
|
|
9794
|
-
const line = lines[i];
|
|
9795
|
-
if (line.trim().startsWith("//") && !line.includes("password") && !line.includes("secret")) continue;
|
|
9796
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
9797
|
-
pattern.pattern.lastIndex = 0;
|
|
9798
|
-
if (pattern.pattern.test(line)) {
|
|
9799
|
-
findings.push({
|
|
9800
|
-
file: (0, import_node_path2.relative)(basePath, fullPath),
|
|
9801
|
-
line: i + 1,
|
|
9802
|
-
type: pattern.name,
|
|
9803
|
-
severity: pattern.severity,
|
|
9804
|
-
message: `Possible ${pattern.name} detected`,
|
|
9805
|
-
snippet: line.length > 120 ? line.slice(0, 120) + "..." : line
|
|
9806
|
-
});
|
|
9807
|
-
}
|
|
9808
|
-
}
|
|
9809
|
-
}
|
|
9810
|
-
}
|
|
9811
|
-
}
|
|
9812
|
-
async function scanDependencies(targetPath) {
|
|
9813
|
-
const findings = [];
|
|
9814
|
-
const lockfiles = [
|
|
9815
|
-
{ file: "package-lock.json", ecosystem: "npm" },
|
|
9816
|
-
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
9817
|
-
{ file: "yarn.lock", ecosystem: "npm" },
|
|
9818
|
-
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
9819
|
-
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
9820
|
-
];
|
|
9821
|
-
for (const { file, ecosystem } of lockfiles) {
|
|
9822
|
-
const lockPath = (0, import_node_path2.join)(targetPath, file);
|
|
9823
|
-
if (!(0, import_node_fs5.existsSync)(lockPath)) continue;
|
|
9824
|
-
try {
|
|
9825
|
-
const deps = parseDependencies(lockPath, file, ecosystem);
|
|
9826
|
-
for (const dep of deps.slice(0, 50)) {
|
|
9827
|
-
try {
|
|
9828
|
-
const vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
9829
|
-
for (const vuln of vulns) {
|
|
9830
|
-
const cvssScore = vuln.severity?.find((s) => s.type === "CVSS_V3")?.score;
|
|
9831
|
-
const severity = cvssScore ? parseFloat(cvssScore) >= 9 ? "critical" : parseFloat(cvssScore) >= 7 ? "high" : parseFloat(cvssScore) >= 4 ? "medium" : "low" : "medium";
|
|
9832
|
-
findings.push({
|
|
9833
|
-
file,
|
|
9834
|
-
line: 0,
|
|
9835
|
-
type: "Dependency CVE",
|
|
9836
|
-
severity,
|
|
9837
|
-
message: `${dep.name}@${dep.version}: ${vuln.summary || vuln.id}`,
|
|
9838
|
-
snippet: `${vuln.id}${cvssScore ? ` (CVSS: ${cvssScore})` : ""}`
|
|
9839
|
-
});
|
|
9840
|
-
}
|
|
9841
|
-
} catch {
|
|
9842
|
-
}
|
|
9843
|
-
}
|
|
9844
|
-
} catch {
|
|
9845
|
-
}
|
|
9846
|
-
}
|
|
9847
|
-
return findings;
|
|
9848
|
-
}
|
|
9849
|
-
function parseDependencies(lockPath, filename, ecosystem) {
|
|
9850
|
-
const deps = [];
|
|
9851
|
-
if (filename === "package-lock.json") {
|
|
9852
|
-
try {
|
|
9853
|
-
const lock = JSON.parse((0, import_node_fs5.readFileSync)(lockPath, "utf-8"));
|
|
9854
|
-
const packages = lock.packages || lock.dependencies || {};
|
|
9855
|
-
for (const [key, value] of Object.entries(packages)) {
|
|
9856
|
-
const name = key.replace(/^node_modules\//, "");
|
|
9857
|
-
const version = value.version;
|
|
9858
|
-
if (name && version && !name.startsWith(".")) {
|
|
9859
|
-
deps.push({ name, version });
|
|
9860
|
-
}
|
|
9861
|
-
}
|
|
9862
|
-
} catch {
|
|
9863
|
-
}
|
|
9864
|
-
} else if (filename === "requirements.txt") {
|
|
9865
|
-
try {
|
|
9866
|
-
const content = (0, import_node_fs5.readFileSync)(lockPath, "utf-8");
|
|
9867
|
-
for (const line of content.split("\n")) {
|
|
9868
|
-
const match = line.match(/^([a-zA-Z0-9_.-]+)==([0-9.]+)/);
|
|
9869
|
-
if (match) deps.push({ name: match[1], version: match[2] });
|
|
9870
|
-
}
|
|
9871
|
-
} catch {
|
|
9872
|
-
}
|
|
9873
|
-
}
|
|
9874
|
-
return deps;
|
|
9875
|
-
}
|
|
9876
|
-
function isValidPackageName(name) {
|
|
9877
|
-
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
9878
|
-
}
|
|
9879
|
-
function isValidVersion(version) {
|
|
9880
|
-
return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
|
|
9881
|
-
}
|
|
9882
|
-
async function queryOsv(name, version, ecosystem) {
|
|
9883
|
-
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
9884
|
-
try {
|
|
9885
|
-
const res = await fetch("https://api.osv.dev/v1/query", {
|
|
9886
|
-
method: "POST",
|
|
9887
|
-
headers: { "Content-Type": "application/json" },
|
|
9888
|
-
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
9889
|
-
signal: AbortSignal.timeout(5e3)
|
|
9890
|
-
});
|
|
9891
|
-
if (!res.ok) return [];
|
|
9892
|
-
const data = await res.json();
|
|
9893
|
-
return data.vulns || [];
|
|
9894
|
-
} catch {
|
|
9895
|
-
return [];
|
|
9896
|
-
}
|
|
9897
11333
|
}
|
|
9898
11334
|
|
|
9899
11335
|
// src/commands/init.ts
|
|
9900
|
-
var
|
|
11336
|
+
var import_node_fs10 = require("fs");
|
|
9901
11337
|
var import_node_child_process = require("child_process");
|
|
9902
11338
|
var import_node_readline3 = __toESM(require("readline"));
|
|
9903
11339
|
|
|
9904
11340
|
// src/core/config.ts
|
|
9905
|
-
var
|
|
9906
|
-
var
|
|
11341
|
+
var import_node_fs8 = require("fs");
|
|
11342
|
+
var import_node_path6 = require("path");
|
|
9907
11343
|
var import_toml = __toESM(require_toml());
|
|
9908
11344
|
var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
|
|
9909
11345
|
var DEFAULT_CONFDIR = "/etc/threatcrush/threatcrushd.conf.d";
|
|
@@ -9929,11 +11365,11 @@ var DEFAULT_CONFIG = {
|
|
|
9929
11365
|
};
|
|
9930
11366
|
function loadConfig(configPath) {
|
|
9931
11367
|
const path = configPath || DEFAULT_CONFIG_PATH;
|
|
9932
|
-
if (!(0,
|
|
11368
|
+
if (!(0, import_node_fs8.existsSync)(path)) {
|
|
9933
11369
|
return { ...DEFAULT_CONFIG };
|
|
9934
11370
|
}
|
|
9935
11371
|
try {
|
|
9936
|
-
const raw = (0,
|
|
11372
|
+
const raw = (0, import_node_fs8.readFileSync)(path, "utf-8");
|
|
9937
11373
|
const parsed = import_toml.default.parse(raw);
|
|
9938
11374
|
return {
|
|
9939
11375
|
daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
|
|
@@ -9949,13 +11385,13 @@ function loadConfig(configPath) {
|
|
|
9949
11385
|
function loadModuleConfigs(confDir) {
|
|
9950
11386
|
const dir = confDir || DEFAULT_CONFDIR;
|
|
9951
11387
|
const configs = /* @__PURE__ */ new Map();
|
|
9952
|
-
if (!(0,
|
|
11388
|
+
if (!(0, import_node_fs8.existsSync)(dir)) {
|
|
9953
11389
|
return configs;
|
|
9954
11390
|
}
|
|
9955
|
-
const files = (0,
|
|
11391
|
+
const files = (0, import_node_fs8.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
|
|
9956
11392
|
for (const file of files) {
|
|
9957
11393
|
try {
|
|
9958
|
-
const raw = (0,
|
|
11394
|
+
const raw = (0, import_node_fs8.readFileSync)((0, import_node_path6.join)(dir, file), "utf-8");
|
|
9959
11395
|
const parsed = import_toml.default.parse(raw);
|
|
9960
11396
|
for (const [name, config] of Object.entries(parsed)) {
|
|
9961
11397
|
configs.set(name, config);
|
|
@@ -9984,23 +11420,23 @@ function generateModuleConfig(moduleName, defaults = {}) {
|
|
|
9984
11420
|
}
|
|
9985
11421
|
|
|
9986
11422
|
// src/core/cli-config.ts
|
|
9987
|
-
var
|
|
9988
|
-
var
|
|
11423
|
+
var import_node_fs9 = require("fs");
|
|
11424
|
+
var import_node_path7 = require("path");
|
|
9989
11425
|
var import_node_os4 = require("os");
|
|
9990
|
-
var CLI_CONFIG_DIR = (0,
|
|
9991
|
-
var CLI_CONFIG_PATH = (0,
|
|
11426
|
+
var CLI_CONFIG_DIR = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".threatcrush");
|
|
11427
|
+
var CLI_CONFIG_PATH = (0, import_node_path7.join)(CLI_CONFIG_DIR, "config.json");
|
|
9992
11428
|
function readCliConfig() {
|
|
9993
11429
|
try {
|
|
9994
|
-
return JSON.parse((0,
|
|
11430
|
+
return JSON.parse((0, import_node_fs9.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
|
|
9995
11431
|
} catch {
|
|
9996
11432
|
return {};
|
|
9997
11433
|
}
|
|
9998
11434
|
}
|
|
9999
11435
|
function writeCliConfig(config) {
|
|
10000
|
-
if (!(0,
|
|
10001
|
-
(0,
|
|
11436
|
+
if (!(0, import_node_fs9.existsSync)(CLI_CONFIG_DIR)) (0, import_node_fs9.mkdirSync)(CLI_CONFIG_DIR, { recursive: true });
|
|
11437
|
+
(0, import_node_fs9.writeFileSync)(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
10002
11438
|
try {
|
|
10003
|
-
(0,
|
|
11439
|
+
(0, import_node_fs9.chmodSync)(CLI_CONFIG_PATH, 384);
|
|
10004
11440
|
} catch {
|
|
10005
11441
|
}
|
|
10006
11442
|
}
|
|
@@ -10038,13 +11474,13 @@ var import_node_stream = require("stream");
|
|
|
10038
11474
|
var API_URL = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
10039
11475
|
function prompt(question) {
|
|
10040
11476
|
const rl = import_node_readline2.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
10041
|
-
return new Promise((
|
|
11477
|
+
return new Promise((resolve5) => rl.question(question, (answer) => {
|
|
10042
11478
|
rl.close();
|
|
10043
|
-
|
|
11479
|
+
resolve5(answer.trim());
|
|
10044
11480
|
}));
|
|
10045
11481
|
}
|
|
10046
11482
|
function promptPassword(question) {
|
|
10047
|
-
return new Promise((
|
|
11483
|
+
return new Promise((resolve5) => {
|
|
10048
11484
|
const muted = new import_node_stream.Writable({
|
|
10049
11485
|
write(_chunk, _enc, cb) {
|
|
10050
11486
|
cb();
|
|
@@ -10055,7 +11491,7 @@ function promptPassword(question) {
|
|
|
10055
11491
|
rl.question("", (answer) => {
|
|
10056
11492
|
rl.close();
|
|
10057
11493
|
process.stdout.write("\n");
|
|
10058
|
-
|
|
11494
|
+
resolve5(answer);
|
|
10059
11495
|
});
|
|
10060
11496
|
});
|
|
10061
11497
|
}
|
|
@@ -10245,16 +11681,16 @@ function binaryExists(name) {
|
|
|
10245
11681
|
}
|
|
10246
11682
|
}
|
|
10247
11683
|
function findLogPath(paths) {
|
|
10248
|
-
return paths.find((p) => (0,
|
|
11684
|
+
return paths.find((p) => (0, import_node_fs10.existsSync)(p));
|
|
10249
11685
|
}
|
|
10250
11686
|
async function promptYesNo(question, fallback2) {
|
|
10251
11687
|
const rl = import_node_readline3.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
10252
11688
|
const hint = fallback2 ? "(Y/n)" : "(y/N)";
|
|
10253
|
-
return new Promise((
|
|
11689
|
+
return new Promise((resolve5) => rl.question(`${question} ${hint}: `, (answer) => {
|
|
10254
11690
|
rl.close();
|
|
10255
11691
|
const trimmed = answer.trim().toLowerCase();
|
|
10256
|
-
if (!trimmed) return
|
|
10257
|
-
|
|
11692
|
+
if (!trimmed) return resolve5(fallback2);
|
|
11693
|
+
resolve5(trimmed === "y" || trimmed === "yes");
|
|
10258
11694
|
}));
|
|
10259
11695
|
}
|
|
10260
11696
|
async function ensureLoggedIn() {
|
|
@@ -10361,11 +11797,11 @@ async function initCommand() {
|
|
|
10361
11797
|
}
|
|
10362
11798
|
} else {
|
|
10363
11799
|
const spinner2 = ora({ text: "Writing configuration files...", color: "green" }).start();
|
|
10364
|
-
(0,
|
|
10365
|
-
(0,
|
|
10366
|
-
(0,
|
|
11800
|
+
(0, import_node_fs10.mkdirSync)(confDDir, { recursive: true });
|
|
11801
|
+
(0, import_node_fs10.mkdirSync)("/var/log/threatcrush", { recursive: true });
|
|
11802
|
+
(0, import_node_fs10.mkdirSync)("/var/lib/threatcrush", { recursive: true });
|
|
10367
11803
|
const mainConfig = generateDefaultConfig(detected.map((d) => d.name));
|
|
10368
|
-
(0,
|
|
11804
|
+
(0, import_node_fs10.writeFileSync)(`${configDir}/threatcrushd.conf`, mainConfig);
|
|
10369
11805
|
for (const svc of detected) {
|
|
10370
11806
|
const svcDef = SERVICES_TO_DETECT.find((s) => s.name === svc.name);
|
|
10371
11807
|
if (!svcDef) continue;
|
|
@@ -10374,7 +11810,7 @@ async function initCommand() {
|
|
|
10374
11810
|
...svcDef.moduleConfig,
|
|
10375
11811
|
log_path: svc.logPath || svcDef.logPaths[0]
|
|
10376
11812
|
});
|
|
10377
|
-
(0,
|
|
11813
|
+
(0, import_node_fs10.writeFileSync)(`${confDDir}/${modName}.conf`, modConfig);
|
|
10378
11814
|
}
|
|
10379
11815
|
spinner2.succeed("Configuration written successfully");
|
|
10380
11816
|
console.log();
|
|
@@ -10392,8 +11828,8 @@ async function initCommand() {
|
|
|
10392
11828
|
}
|
|
10393
11829
|
function checkWriteAccess(dir) {
|
|
10394
11830
|
try {
|
|
10395
|
-
if (!(0,
|
|
10396
|
-
(0,
|
|
11831
|
+
if (!(0, import_node_fs10.existsSync)(dir)) {
|
|
11832
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
10397
11833
|
}
|
|
10398
11834
|
return true;
|
|
10399
11835
|
} catch {
|
|
@@ -10402,8 +11838,8 @@ function checkWriteAccess(dir) {
|
|
|
10402
11838
|
}
|
|
10403
11839
|
|
|
10404
11840
|
// src/core/module-loader.ts
|
|
10405
|
-
var
|
|
10406
|
-
var
|
|
11841
|
+
var import_node_fs11 = require("fs");
|
|
11842
|
+
var import_node_path8 = require("path");
|
|
10407
11843
|
var import_toml2 = __toESM(require_toml());
|
|
10408
11844
|
init_paths();
|
|
10409
11845
|
function discoverModules(moduleDir, confDir) {
|
|
@@ -10411,22 +11847,22 @@ function discoverModules(moduleDir, confDir) {
|
|
|
10411
11847
|
const configs = loadModuleConfigs(confDir || PATHS.confD);
|
|
10412
11848
|
const searchPaths = [
|
|
10413
11849
|
moduleDir || PATHS.moduleDir,
|
|
10414
|
-
(0,
|
|
11850
|
+
(0, import_node_path8.resolve)(process.cwd(), "modules")
|
|
10415
11851
|
];
|
|
10416
|
-
const builtinDir = (0,
|
|
10417
|
-
if ((0,
|
|
11852
|
+
const builtinDir = (0, import_node_path8.resolve)(__dirname || ".", "..", "modules");
|
|
11853
|
+
if ((0, import_node_fs11.existsSync)(builtinDir)) {
|
|
10418
11854
|
searchPaths.push(builtinDir);
|
|
10419
11855
|
}
|
|
10420
11856
|
for (const basePath of searchPaths) {
|
|
10421
|
-
if (!(0,
|
|
10422
|
-
const entries = (0,
|
|
11857
|
+
if (!(0, import_node_fs11.existsSync)(basePath)) continue;
|
|
11858
|
+
const entries = (0, import_node_fs11.readdirSync)(basePath, { withFileTypes: true });
|
|
10423
11859
|
for (const entry of entries) {
|
|
10424
11860
|
if (!entry.isDirectory()) continue;
|
|
10425
|
-
const modPath = (0,
|
|
10426
|
-
const manifestPath = (0,
|
|
10427
|
-
if (!(0,
|
|
11861
|
+
const modPath = (0, import_node_path8.join)(basePath, entry.name);
|
|
11862
|
+
const manifestPath = (0, import_node_path8.join)(modPath, "mod.toml");
|
|
11863
|
+
if (!(0, import_node_fs11.existsSync)(manifestPath)) continue;
|
|
10428
11864
|
try {
|
|
10429
|
-
const raw = (0,
|
|
11865
|
+
const raw = (0, import_node_fs11.readFileSync)(manifestPath, "utf-8");
|
|
10430
11866
|
const manifest = import_toml2.default.parse(raw);
|
|
10431
11867
|
const config = configs.get(manifest.module.name) || { enabled: true };
|
|
10432
11868
|
modules.push({
|
|
@@ -10545,8 +11981,8 @@ function formatUptime(seconds) {
|
|
|
10545
11981
|
|
|
10546
11982
|
// src/commands/modules.ts
|
|
10547
11983
|
var import_node_child_process2 = require("child_process");
|
|
10548
|
-
var
|
|
10549
|
-
var
|
|
11984
|
+
var import_node_fs12 = require("fs");
|
|
11985
|
+
var import_node_path9 = require("path");
|
|
10550
11986
|
var import_toml3 = __toESM(require_toml());
|
|
10551
11987
|
init_paths();
|
|
10552
11988
|
init_pidfile();
|
|
@@ -10555,13 +11991,32 @@ function modulesDir() {
|
|
|
10555
11991
|
ensureRuntimeDirs();
|
|
10556
11992
|
return PATHS.moduleDir;
|
|
10557
11993
|
}
|
|
11994
|
+
function safeModuleDirName(name, label = "module name") {
|
|
11995
|
+
if (!/^[A-Za-z0-9._@-]+$/.test(name) || name === "." || name === "..") {
|
|
11996
|
+
throw new Error(`Unsafe ${label}: ${name}`);
|
|
11997
|
+
}
|
|
11998
|
+
return name;
|
|
11999
|
+
}
|
|
12000
|
+
function moduleDestination(dir, name, label) {
|
|
12001
|
+
return (0, import_node_path9.join)(dir, safeModuleDirName(name, label));
|
|
12002
|
+
}
|
|
12003
|
+
function assertSafeTarballEntries(tarPath) {
|
|
12004
|
+
const listing = (0, import_node_child_process2.execFileSync)("tar", ["-tzf", tarPath], { encoding: "utf-8" });
|
|
12005
|
+
for (const entry of listing.split("\n").map((line) => line.trim()).filter(Boolean)) {
|
|
12006
|
+
const normalized = entry.replace(/\\/g, "/");
|
|
12007
|
+
const segments = normalized.split("/");
|
|
12008
|
+
if (normalized.startsWith("/") || segments.includes("..")) {
|
|
12009
|
+
throw new Error(`Unsafe tarball entry: ${entry}`);
|
|
12010
|
+
}
|
|
12011
|
+
}
|
|
12012
|
+
}
|
|
10558
12013
|
function validateManifest(modPath) {
|
|
10559
|
-
const manifestPath = (0,
|
|
10560
|
-
if (!(0,
|
|
12014
|
+
const manifestPath = (0, import_node_path9.join)(modPath, "mod.toml");
|
|
12015
|
+
if (!(0, import_node_fs12.existsSync)(manifestPath)) {
|
|
10561
12016
|
return { ok: false, error: `mod.toml not found at ${manifestPath}` };
|
|
10562
12017
|
}
|
|
10563
12018
|
try {
|
|
10564
|
-
const raw = (0,
|
|
12019
|
+
const raw = (0, import_node_fs12.readFileSync)(manifestPath, "utf-8");
|
|
10565
12020
|
const parsed = import_toml3.default.parse(raw);
|
|
10566
12021
|
const name = parsed.module?.name;
|
|
10567
12022
|
const version = parsed.module?.version;
|
|
@@ -10615,8 +12070,8 @@ async function modulesInstallCommand(source) {
|
|
|
10615
12070
|
console.log();
|
|
10616
12071
|
const dir = modulesDir();
|
|
10617
12072
|
if (source.startsWith("./") || source.startsWith("/") || source.startsWith("~")) {
|
|
10618
|
-
const absPath = (0,
|
|
10619
|
-
if (!(0,
|
|
12073
|
+
const absPath = (0, import_node_path9.resolve)(source.startsWith("~") ? source.replace("~", process.env.HOME || "") : source);
|
|
12074
|
+
if (!(0, import_node_fs12.existsSync)(absPath)) {
|
|
10620
12075
|
console.log(source_default.red(` \u2717 Path not found: ${absPath}
|
|
10621
12076
|
`));
|
|
10622
12077
|
return;
|
|
@@ -10627,8 +12082,15 @@ async function modulesInstallCommand(source) {
|
|
|
10627
12082
|
`));
|
|
10628
12083
|
return;
|
|
10629
12084
|
}
|
|
10630
|
-
|
|
10631
|
-
|
|
12085
|
+
let dest2;
|
|
12086
|
+
try {
|
|
12087
|
+
dest2 = moduleDestination(dir, check.name);
|
|
12088
|
+
} catch (err) {
|
|
12089
|
+
console.log(source_default.red(` x ${err.message}
|
|
12090
|
+
`));
|
|
12091
|
+
return;
|
|
12092
|
+
}
|
|
12093
|
+
if ((0, import_node_fs12.existsSync)(dest2)) {
|
|
10632
12094
|
console.log(source_default.yellow(` ! ${check.name} is already installed at ${dest2}`));
|
|
10633
12095
|
console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${check.name}`)} first.
|
|
10634
12096
|
`));
|
|
@@ -10636,7 +12098,7 @@ async function modulesInstallCommand(source) {
|
|
|
10636
12098
|
}
|
|
10637
12099
|
const spinner2 = ora({ text: `Copying module files...`, color: "green" }).start();
|
|
10638
12100
|
try {
|
|
10639
|
-
(0,
|
|
12101
|
+
(0, import_node_fs12.cpSync)(absPath, dest2, { recursive: true, dereference: true });
|
|
10640
12102
|
spinner2.succeed(`Installed ${source_default.white(check.name)} v${check.version} \u2192 ${dest2}`);
|
|
10641
12103
|
} catch (err) {
|
|
10642
12104
|
spinner2.fail(`Copy failed: ${err.message}`);
|
|
@@ -10648,9 +12110,17 @@ async function modulesInstallCommand(source) {
|
|
|
10648
12110
|
}
|
|
10649
12111
|
if (source.startsWith("github:") || source.startsWith("https://") || source.startsWith("git@") || source.endsWith(".git")) {
|
|
10650
12112
|
const gitUrl = source.startsWith("github:") ? `https://github.com/${source.slice("github:".length)}.git` : source;
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
12113
|
+
let name;
|
|
12114
|
+
let dest2;
|
|
12115
|
+
try {
|
|
12116
|
+
name = safeModuleDirName((0, import_node_path9.basename)(gitUrl.replace(/\.git$/, "")), "repository name");
|
|
12117
|
+
dest2 = moduleDestination(dir, name);
|
|
12118
|
+
} catch (err) {
|
|
12119
|
+
console.log(source_default.red(` x ${err.message}
|
|
12120
|
+
`));
|
|
12121
|
+
return;
|
|
12122
|
+
}
|
|
12123
|
+
if ((0, import_node_fs12.existsSync)(dest2)) {
|
|
10654
12124
|
console.log(source_default.yellow(` ! ${name} is already installed at ${dest2}`));
|
|
10655
12125
|
console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${name}`)} first.
|
|
10656
12126
|
`));
|
|
@@ -10658,7 +12128,7 @@ async function modulesInstallCommand(source) {
|
|
|
10658
12128
|
}
|
|
10659
12129
|
const spinner2 = ora({ text: `Cloning ${gitUrl}...`, color: "green" }).start();
|
|
10660
12130
|
try {
|
|
10661
|
-
(0, import_node_child_process2.
|
|
12131
|
+
(0, import_node_child_process2.execFileSync)("git", ["clone", "--depth", "1", "--", gitUrl, dest2], { stdio: "pipe" });
|
|
10662
12132
|
} catch (err) {
|
|
10663
12133
|
spinner2.fail(`Clone failed: ${err.message}`);
|
|
10664
12134
|
return;
|
|
@@ -10667,7 +12137,7 @@ async function modulesInstallCommand(source) {
|
|
|
10667
12137
|
if (!check.ok) {
|
|
10668
12138
|
spinner2.fail(check.error);
|
|
10669
12139
|
try {
|
|
10670
|
-
(0,
|
|
12140
|
+
(0, import_node_fs12.rmSync)(dest2, { recursive: true, force: true });
|
|
10671
12141
|
} catch {
|
|
10672
12142
|
}
|
|
10673
12143
|
return;
|
|
@@ -10704,8 +12174,15 @@ async function modulesInstallCommand(source) {
|
|
|
10704
12174
|
}
|
|
10705
12175
|
spinner.succeed(`Found ${mod.name} v${mod.version}`);
|
|
10706
12176
|
const install = mod.install;
|
|
10707
|
-
|
|
10708
|
-
|
|
12177
|
+
let dest;
|
|
12178
|
+
try {
|
|
12179
|
+
dest = moduleDestination(dir, mod.slug, "module slug");
|
|
12180
|
+
} catch (err) {
|
|
12181
|
+
console.log(source_default.red(` x ${err.message}
|
|
12182
|
+
`));
|
|
12183
|
+
return;
|
|
12184
|
+
}
|
|
12185
|
+
if ((0, import_node_fs12.existsSync)(dest)) {
|
|
10709
12186
|
console.log(source_default.yellow(` ! ${mod.slug} is already installed at ${dest}
|
|
10710
12187
|
`));
|
|
10711
12188
|
return;
|
|
@@ -10713,7 +12190,7 @@ async function modulesInstallCommand(source) {
|
|
|
10713
12190
|
if (install.npm_package) {
|
|
10714
12191
|
const npmSpinner = ora({ text: `Installing ${install.npm_package}...`, color: "green" }).start();
|
|
10715
12192
|
try {
|
|
10716
|
-
(0, import_node_child_process2.
|
|
12193
|
+
(0, import_node_child_process2.execFileSync)("npm", ["install", "-g", "--", install.npm_package], { stdio: "pipe" });
|
|
10717
12194
|
npmSpinner.succeed(`Package ${install.npm_package} installed globally`);
|
|
10718
12195
|
} catch (err) {
|
|
10719
12196
|
npmSpinner.fail(`npm install failed: ${err.message}`);
|
|
@@ -10722,7 +12199,7 @@ async function modulesInstallCommand(source) {
|
|
|
10722
12199
|
} else if (install.git_url) {
|
|
10723
12200
|
const cloneSpinner = ora({ text: "Cloning module repository...", color: "green" }).start();
|
|
10724
12201
|
try {
|
|
10725
|
-
(0, import_node_child_process2.
|
|
12202
|
+
(0, import_node_child_process2.execFileSync)("git", ["clone", "--depth", "1", "--", install.git_url, dest], { stdio: "pipe" });
|
|
10726
12203
|
} catch (err) {
|
|
10727
12204
|
cloneSpinner.fail(`Clone failed: ${err.message}`);
|
|
10728
12205
|
return;
|
|
@@ -10731,7 +12208,7 @@ async function modulesInstallCommand(source) {
|
|
|
10731
12208
|
if (!check.ok) {
|
|
10732
12209
|
cloneSpinner.fail(check.error);
|
|
10733
12210
|
try {
|
|
10734
|
-
(0,
|
|
12211
|
+
(0, import_node_fs12.rmSync)(dest, { recursive: true, force: true });
|
|
10735
12212
|
} catch {
|
|
10736
12213
|
}
|
|
10737
12214
|
return;
|
|
@@ -10745,10 +12222,11 @@ async function modulesInstallCommand(source) {
|
|
|
10745
12222
|
dlSpinner.fail(`HTTP ${res.status}`);
|
|
10746
12223
|
return;
|
|
10747
12224
|
}
|
|
10748
|
-
const tar = (0,
|
|
10749
|
-
(0,
|
|
10750
|
-
(
|
|
10751
|
-
(0,
|
|
12225
|
+
const tar = (0, import_node_path9.join)(dir, `${mod.slug}.tar.gz`);
|
|
12226
|
+
(0, import_node_fs12.writeFileSync)(tar, Buffer.from(await res.arrayBuffer()));
|
|
12227
|
+
assertSafeTarballEntries(tar);
|
|
12228
|
+
(0, import_node_child_process2.execFileSync)("tar", ["-xzf", tar, "-C", dir], { stdio: "pipe" });
|
|
12229
|
+
(0, import_node_fs12.rmSync)(tar, { force: true });
|
|
10752
12230
|
const check = validateManifest(dest);
|
|
10753
12231
|
if (!check.ok) {
|
|
10754
12232
|
dlSpinner.fail(check.error);
|
|
@@ -10771,8 +12249,15 @@ async function modulesRemoveCommand(name) {
|
|
|
10771
12249
|
console.log(source_default.green.bold(" Module Removal"));
|
|
10772
12250
|
console.log(source_default.gray(" " + "\u2500".repeat(50)));
|
|
10773
12251
|
const dir = modulesDir();
|
|
10774
|
-
|
|
10775
|
-
|
|
12252
|
+
let target;
|
|
12253
|
+
try {
|
|
12254
|
+
target = moduleDestination(dir, name);
|
|
12255
|
+
} catch (err) {
|
|
12256
|
+
console.log(source_default.red(` x ${err.message}
|
|
12257
|
+
`));
|
|
12258
|
+
return;
|
|
12259
|
+
}
|
|
12260
|
+
if (!(0, import_node_fs12.existsSync)(target)) {
|
|
10776
12261
|
console.log(source_default.yellow(` ! No module named "${name}" in ${dir}
|
|
10777
12262
|
`));
|
|
10778
12263
|
return;
|
|
@@ -10782,7 +12267,7 @@ async function modulesRemoveCommand(name) {
|
|
|
10782
12267
|
console.log(source_default.yellow(` ! Directory name "${name}" does not match manifest name "${check.name}"`));
|
|
10783
12268
|
}
|
|
10784
12269
|
try {
|
|
10785
|
-
(0,
|
|
12270
|
+
(0, import_node_fs12.rmSync)(target, { recursive: true, force: true });
|
|
10786
12271
|
console.log(source_default.green(` \u2713 Removed ${name} from ${dir}
|
|
10787
12272
|
`));
|
|
10788
12273
|
} catch (err) {
|
|
@@ -11155,21 +12640,21 @@ async function pentestCommand(targetUrl) {
|
|
|
11155
12640
|
|
|
11156
12641
|
// src/commands/orgs.ts
|
|
11157
12642
|
var import_node_os5 = require("os");
|
|
11158
|
-
var
|
|
11159
|
-
var
|
|
12643
|
+
var import_node_fs13 = require("fs");
|
|
12644
|
+
var import_node_path10 = require("path");
|
|
11160
12645
|
var API_URL3 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
11161
|
-
var CONFIG_PATH = (0,
|
|
12646
|
+
var CONFIG_PATH = (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".threatcrush", "config.json");
|
|
11162
12647
|
function readConfig() {
|
|
11163
12648
|
try {
|
|
11164
|
-
return JSON.parse((0,
|
|
12649
|
+
return JSON.parse((0, import_node_fs13.readFileSync)(CONFIG_PATH, "utf-8"));
|
|
11165
12650
|
} catch {
|
|
11166
12651
|
return {};
|
|
11167
12652
|
}
|
|
11168
12653
|
}
|
|
11169
12654
|
function writeConfig(config) {
|
|
11170
|
-
const dir = (0,
|
|
11171
|
-
if (!(0,
|
|
11172
|
-
(0,
|
|
12655
|
+
const dir = (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".threatcrush");
|
|
12656
|
+
if (!(0, import_node_fs13.existsSync)(dir)) (0, import_node_fs13.mkdirSync)(dir, { recursive: true });
|
|
12657
|
+
(0, import_node_fs13.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
11173
12658
|
}
|
|
11174
12659
|
function getAuthHeaders() {
|
|
11175
12660
|
const config = readConfig();
|
|
@@ -11305,13 +12790,13 @@ async function useOrganization(slug) {
|
|
|
11305
12790
|
|
|
11306
12791
|
// src/commands/servers.ts
|
|
11307
12792
|
var import_node_os6 = require("os");
|
|
11308
|
-
var
|
|
11309
|
-
var
|
|
12793
|
+
var import_node_fs14 = require("fs");
|
|
12794
|
+
var import_node_path11 = require("path");
|
|
11310
12795
|
var API_URL4 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
11311
|
-
var CONFIG_PATH2 = (0,
|
|
12796
|
+
var CONFIG_PATH2 = (0, import_node_path11.join)((0, import_node_os6.homedir)(), ".threatcrush", "config.json");
|
|
11312
12797
|
function readConfig2() {
|
|
11313
12798
|
try {
|
|
11314
|
-
return JSON.parse((0,
|
|
12799
|
+
return JSON.parse((0, import_node_fs14.readFileSync)(CONFIG_PATH2, "utf-8"));
|
|
11315
12800
|
} catch {
|
|
11316
12801
|
return {};
|
|
11317
12802
|
}
|
|
@@ -11414,14 +12899,14 @@ function timeAgo(dateStr) {
|
|
|
11414
12899
|
|
|
11415
12900
|
// src/commands/connect.ts
|
|
11416
12901
|
var import_node_os7 = require("os");
|
|
11417
|
-
var
|
|
11418
|
-
var
|
|
12902
|
+
var import_node_fs15 = require("fs");
|
|
12903
|
+
var import_node_path12 = require("path");
|
|
11419
12904
|
var import_node_child_process3 = require("child_process");
|
|
11420
12905
|
var API_URL5 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
11421
|
-
var CONFIG_PATH3 = (0,
|
|
12906
|
+
var CONFIG_PATH3 = (0, import_node_path12.join)((0, import_node_os7.homedir)(), ".threatcrush", "config.json");
|
|
11422
12907
|
function readConfig3() {
|
|
11423
12908
|
try {
|
|
11424
|
-
return JSON.parse((0,
|
|
12909
|
+
return JSON.parse((0, import_node_fs15.readFileSync)(CONFIG_PATH3, "utf-8"));
|
|
11425
12910
|
} catch {
|
|
11426
12911
|
return {};
|
|
11427
12912
|
}
|
|
@@ -11576,19 +13061,19 @@ async function sshConnect(options) {
|
|
|
11576
13061
|
|
|
11577
13062
|
// src/commands/daemon.ts
|
|
11578
13063
|
var import_node_child_process7 = require("child_process");
|
|
11579
|
-
var
|
|
11580
|
-
var
|
|
11581
|
-
var
|
|
13064
|
+
var import_node_fs24 = require("fs");
|
|
13065
|
+
var import_node_path16 = require("path");
|
|
13066
|
+
var import_node_fs25 = require("fs");
|
|
11582
13067
|
|
|
11583
13068
|
// src/daemon/index.ts
|
|
11584
|
-
var
|
|
11585
|
-
var
|
|
13069
|
+
var import_node_fs23 = require("fs");
|
|
13070
|
+
var import_node_path15 = require("path");
|
|
11586
13071
|
init_paths();
|
|
11587
13072
|
init_pidfile();
|
|
11588
13073
|
|
|
11589
13074
|
// src/daemon/ipc-server.ts
|
|
11590
13075
|
var import_node_net2 = require("net");
|
|
11591
|
-
var
|
|
13076
|
+
var import_node_fs16 = require("fs");
|
|
11592
13077
|
init_paths();
|
|
11593
13078
|
|
|
11594
13079
|
// src/daemon/event-bus.ts
|
|
@@ -11635,13 +13120,13 @@ var IpcServer = class {
|
|
|
11635
13120
|
startedAt = /* @__PURE__ */ new Date();
|
|
11636
13121
|
counters = { events: 0, threats: 0, alerts: 0 };
|
|
11637
13122
|
async start() {
|
|
11638
|
-
if ((0,
|
|
13123
|
+
if ((0, import_node_fs16.existsSync)(PATHS.socket)) {
|
|
11639
13124
|
try {
|
|
11640
|
-
(0,
|
|
13125
|
+
(0, import_node_fs16.unlinkSync)(PATHS.socket);
|
|
11641
13126
|
} catch {
|
|
11642
13127
|
}
|
|
11643
13128
|
}
|
|
11644
|
-
return new Promise((
|
|
13129
|
+
return new Promise((resolve5, reject) => {
|
|
11645
13130
|
this.server = (0, import_node_net2.createServer)((sock) => this.handleClient(sock));
|
|
11646
13131
|
this.server.on("error", reject);
|
|
11647
13132
|
this.server.listen(PATHS.socket, () => {
|
|
@@ -11658,7 +13143,7 @@ var IpcServer = class {
|
|
|
11658
13143
|
} catch {
|
|
11659
13144
|
}
|
|
11660
13145
|
}
|
|
11661
|
-
|
|
13146
|
+
resolve5();
|
|
11662
13147
|
});
|
|
11663
13148
|
});
|
|
11664
13149
|
}
|
|
@@ -11670,20 +13155,20 @@ var IpcServer = class {
|
|
|
11670
13155
|
}
|
|
11671
13156
|
}
|
|
11672
13157
|
this.clients.clear();
|
|
11673
|
-
return new Promise((
|
|
13158
|
+
return new Promise((resolve5) => {
|
|
11674
13159
|
if (!this.server) {
|
|
11675
13160
|
try {
|
|
11676
|
-
if ((0,
|
|
13161
|
+
if ((0, import_node_fs16.existsSync)(PATHS.socket)) (0, import_node_fs16.unlinkSync)(PATHS.socket);
|
|
11677
13162
|
} catch {
|
|
11678
13163
|
}
|
|
11679
|
-
return
|
|
13164
|
+
return resolve5();
|
|
11680
13165
|
}
|
|
11681
13166
|
this.server.close(() => {
|
|
11682
13167
|
try {
|
|
11683
|
-
if ((0,
|
|
13168
|
+
if ((0, import_node_fs16.existsSync)(PATHS.socket)) (0, import_node_fs16.unlinkSync)(PATHS.socket);
|
|
11684
13169
|
} catch {
|
|
11685
13170
|
}
|
|
11686
|
-
|
|
13171
|
+
resolve5();
|
|
11687
13172
|
});
|
|
11688
13173
|
});
|
|
11689
13174
|
}
|
|
@@ -11786,14 +13271,14 @@ var IpcServer = class {
|
|
|
11786
13271
|
};
|
|
11787
13272
|
|
|
11788
13273
|
// src/daemon/module-host.ts
|
|
11789
|
-
var
|
|
11790
|
-
var
|
|
13274
|
+
var import_node_fs20 = require("fs");
|
|
13275
|
+
var import_node_path13 = require("path");
|
|
11791
13276
|
var import_node_url = require("url");
|
|
11792
13277
|
var import_toml4 = __toESM(require_toml());
|
|
11793
13278
|
init_paths();
|
|
11794
13279
|
|
|
11795
13280
|
// src/daemon/watchers/log-watcher.ts
|
|
11796
|
-
var
|
|
13281
|
+
var import_node_fs17 = require("fs");
|
|
11797
13282
|
var import_node_readline4 = require("readline");
|
|
11798
13283
|
init_state();
|
|
11799
13284
|
var DEFAULT_SOURCES = [
|
|
@@ -11815,9 +13300,9 @@ var LogWatcher = class {
|
|
|
11815
13300
|
start() {
|
|
11816
13301
|
const started = [];
|
|
11817
13302
|
for (const src of this.sources) {
|
|
11818
|
-
if (!(0,
|
|
13303
|
+
if (!(0, import_node_fs17.existsSync)(src.path)) continue;
|
|
11819
13304
|
try {
|
|
11820
|
-
(0,
|
|
13305
|
+
(0, import_node_fs17.accessSync)(src.path, import_node_fs17.constants.R_OK);
|
|
11821
13306
|
} catch {
|
|
11822
13307
|
continue;
|
|
11823
13308
|
}
|
|
@@ -11837,7 +13322,7 @@ var LogWatcher = class {
|
|
|
11837
13322
|
}
|
|
11838
13323
|
tail(src) {
|
|
11839
13324
|
try {
|
|
11840
|
-
this.positions.set(src.path, (0,
|
|
13325
|
+
this.positions.set(src.path, (0, import_node_fs17.statSync)(src.path).size);
|
|
11841
13326
|
} catch {
|
|
11842
13327
|
this.positions.set(src.path, 0);
|
|
11843
13328
|
}
|
|
@@ -11848,7 +13333,7 @@ var LogWatcher = class {
|
|
|
11848
13333
|
poll(src) {
|
|
11849
13334
|
let stat;
|
|
11850
13335
|
try {
|
|
11851
|
-
stat = (0,
|
|
13336
|
+
stat = (0, import_node_fs17.statSync)(src.path);
|
|
11852
13337
|
} catch {
|
|
11853
13338
|
return;
|
|
11854
13339
|
}
|
|
@@ -11858,7 +13343,7 @@ var LogWatcher = class {
|
|
|
11858
13343
|
return;
|
|
11859
13344
|
}
|
|
11860
13345
|
if (stat.size === prev) return;
|
|
11861
|
-
const stream = (0,
|
|
13346
|
+
const stream = (0, import_node_fs17.createReadStream)(src.path, { start: prev, encoding: "utf-8" });
|
|
11862
13347
|
stream.on("error", () => this.positions.set(src.path, stat.size));
|
|
11863
13348
|
const rl = (0, import_node_readline4.createInterface)({ input: stream });
|
|
11864
13349
|
rl.on("error", () => {
|
|
@@ -12051,7 +13536,7 @@ function realtimeToDate(rt) {
|
|
|
12051
13536
|
|
|
12052
13537
|
// src/modules/network-monitor/index.ts
|
|
12053
13538
|
var import_node_child_process5 = require("child_process");
|
|
12054
|
-
var
|
|
13539
|
+
var import_node_fs18 = require("fs");
|
|
12055
13540
|
init_state();
|
|
12056
13541
|
var NetworkMonitor = class {
|
|
12057
13542
|
constructor(bus2) {
|
|
@@ -12090,7 +13575,7 @@ var NetworkMonitor = class {
|
|
|
12090
13575
|
hasConntrackOrSs() {
|
|
12091
13576
|
const ss = (0, import_node_child_process5.spawnSync)("ss", ["--version"], { stdio: "pipe" });
|
|
12092
13577
|
if (ss.status === 0) return true;
|
|
12093
|
-
return (0,
|
|
13578
|
+
return (0, import_node_fs18.existsSync)("/proc/net/tcp");
|
|
12094
13579
|
}
|
|
12095
13580
|
poll() {
|
|
12096
13581
|
try {
|
|
@@ -12229,7 +13714,7 @@ var NetworkMonitor = class {
|
|
|
12229
13714
|
};
|
|
12230
13715
|
|
|
12231
13716
|
// src/modules/dns-monitor/index.ts
|
|
12232
|
-
var
|
|
13717
|
+
var import_node_fs19 = require("fs");
|
|
12233
13718
|
var import_node_readline5 = require("readline");
|
|
12234
13719
|
init_state();
|
|
12235
13720
|
var DNS_LOG_SOURCES = [
|
|
@@ -12264,9 +13749,9 @@ var DnsMonitor = class {
|
|
|
12264
13749
|
entropyThreshold = 3.5;
|
|
12265
13750
|
start() {
|
|
12266
13751
|
const sources = DNS_LOG_SOURCES.filter((p) => {
|
|
12267
|
-
if (!(0,
|
|
13752
|
+
if (!(0, import_node_fs19.existsSync)(p)) return false;
|
|
12268
13753
|
try {
|
|
12269
|
-
(0,
|
|
13754
|
+
(0, import_node_fs19.accessSync)(p, import_node_fs19.constants.R_OK);
|
|
12270
13755
|
return true;
|
|
12271
13756
|
} catch {
|
|
12272
13757
|
return false;
|
|
@@ -12290,7 +13775,7 @@ var DnsMonitor = class {
|
|
|
12290
13775
|
}
|
|
12291
13776
|
tailLog(path) {
|
|
12292
13777
|
try {
|
|
12293
|
-
this.positions.set(path, (0,
|
|
13778
|
+
this.positions.set(path, (0, import_node_fs19.statSync)(path).size);
|
|
12294
13779
|
} catch {
|
|
12295
13780
|
this.positions.set(path, 0);
|
|
12296
13781
|
}
|
|
@@ -12300,7 +13785,7 @@ var DnsMonitor = class {
|
|
|
12300
13785
|
pollLog(path) {
|
|
12301
13786
|
let stat;
|
|
12302
13787
|
try {
|
|
12303
|
-
stat = (0,
|
|
13788
|
+
stat = (0, import_node_fs19.statSync)(path);
|
|
12304
13789
|
} catch {
|
|
12305
13790
|
return;
|
|
12306
13791
|
}
|
|
@@ -12310,7 +13795,7 @@ var DnsMonitor = class {
|
|
|
12310
13795
|
return;
|
|
12311
13796
|
}
|
|
12312
13797
|
if (stat.size === prev) return;
|
|
12313
|
-
const stream = (0,
|
|
13798
|
+
const stream = (0, import_node_fs19.createReadStream)(path, { start: prev, encoding: "utf-8" });
|
|
12314
13799
|
stream.on("error", () => this.positions.set(path, stat.size));
|
|
12315
13800
|
const rl = (0, import_node_readline5.createInterface)({ input: stream });
|
|
12316
13801
|
rl.on("line", (line) => this.parseDnsLine(line));
|
|
@@ -12542,15 +14027,15 @@ var ModuleHost = class {
|
|
|
12542
14027
|
for (const m of builtins) this.modules.set(m.name, m);
|
|
12543
14028
|
}
|
|
12544
14029
|
async discoverAndStartInstalled() {
|
|
12545
|
-
if (!(0,
|
|
14030
|
+
if (!(0, import_node_fs20.existsSync)(PATHS.moduleDir)) return;
|
|
12546
14031
|
const configs = loadModuleConfigs(PATHS.confD);
|
|
12547
|
-
const entries = (0,
|
|
14032
|
+
const entries = (0, import_node_fs20.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
|
|
12548
14033
|
for (const entry of entries) {
|
|
12549
14034
|
if (!entry.isDirectory()) continue;
|
|
12550
|
-
const manifestPath = (0,
|
|
12551
|
-
if (!(0,
|
|
14035
|
+
const manifestPath = (0, import_node_path13.join)(PATHS.moduleDir, entry.name, "mod.toml");
|
|
14036
|
+
if (!(0, import_node_fs20.existsSync)(manifestPath)) continue;
|
|
12552
14037
|
try {
|
|
12553
|
-
const manifest = import_toml4.default.parse((0,
|
|
14038
|
+
const manifest = import_toml4.default.parse((0, import_node_fs20.readFileSync)(manifestPath, "utf-8"));
|
|
12554
14039
|
const name = manifest.module?.name || entry.name;
|
|
12555
14040
|
const defaults = manifest.module?.config?.defaults || {};
|
|
12556
14041
|
const config = {
|
|
@@ -12564,7 +14049,7 @@ var ModuleHost = class {
|
|
|
12564
14049
|
source: "installed",
|
|
12565
14050
|
status: config.enabled === false ? "disabled" : "loaded",
|
|
12566
14051
|
events: 0,
|
|
12567
|
-
path: (0,
|
|
14052
|
+
path: (0, import_node_path13.join)(PATHS.moduleDir, entry.name),
|
|
12568
14053
|
config
|
|
12569
14054
|
};
|
|
12570
14055
|
this.modules.set(name, hosted);
|
|
@@ -12579,7 +14064,7 @@ var ModuleHost = class {
|
|
|
12579
14064
|
status: "error",
|
|
12580
14065
|
events: 0,
|
|
12581
14066
|
detail: `manifest load failed: ${String(err.message || err)}`,
|
|
12582
|
-
path: (0,
|
|
14067
|
+
path: (0, import_node_path13.join)(PATHS.moduleDir, entry.name)
|
|
12583
14068
|
});
|
|
12584
14069
|
}
|
|
12585
14070
|
}
|
|
@@ -12611,17 +14096,17 @@ var ModuleHost = class {
|
|
|
12611
14096
|
}
|
|
12612
14097
|
}
|
|
12613
14098
|
installedEntrypoint(modulePath) {
|
|
12614
|
-
const packageJson = (0,
|
|
14099
|
+
const packageJson = (0, import_node_path13.join)(modulePath, "package.json");
|
|
12615
14100
|
const candidates = [];
|
|
12616
|
-
if ((0,
|
|
14101
|
+
if ((0, import_node_fs20.existsSync)(packageJson)) {
|
|
12617
14102
|
try {
|
|
12618
|
-
const pkg = JSON.parse((0,
|
|
12619
|
-
if (pkg.main) candidates.push((0,
|
|
14103
|
+
const pkg = JSON.parse((0, import_node_fs20.readFileSync)(packageJson, "utf-8"));
|
|
14104
|
+
if (pkg.main) candidates.push((0, import_node_path13.join)(modulePath, pkg.main));
|
|
12620
14105
|
} catch {
|
|
12621
14106
|
}
|
|
12622
14107
|
}
|
|
12623
|
-
candidates.push((0,
|
|
12624
|
-
return candidates.find((candidate) => (0,
|
|
14108
|
+
candidates.push((0, import_node_path13.join)(modulePath, "dist", "index.js"), (0, import_node_path13.join)(modulePath, "index.js"));
|
|
14109
|
+
return candidates.find((candidate) => (0, import_node_fs20.existsSync)(candidate)) || null;
|
|
12625
14110
|
}
|
|
12626
14111
|
isThreatCrushModule(value) {
|
|
12627
14112
|
return Boolean(
|
|
@@ -13142,8 +14627,8 @@ var RuleEngine = class {
|
|
|
13142
14627
|
};
|
|
13143
14628
|
|
|
13144
14629
|
// src/daemon/rules/loader.ts
|
|
13145
|
-
var
|
|
13146
|
-
var
|
|
14630
|
+
var import_node_fs21 = require("fs");
|
|
14631
|
+
var import_node_path14 = require("path");
|
|
13147
14632
|
|
|
13148
14633
|
// src/daemon/rules/default-rules.ts
|
|
13149
14634
|
var DEFAULT_RULES = [
|
|
@@ -13427,11 +14912,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
|
|
|
13427
14912
|
function loadAllRules(customDir) {
|
|
13428
14913
|
const rules = [...DEFAULT_RULES];
|
|
13429
14914
|
const dir = customDir || RULES_DIR;
|
|
13430
|
-
if ((0,
|
|
13431
|
-
const files = (0,
|
|
14915
|
+
if ((0, import_node_fs21.existsSync)(dir)) {
|
|
14916
|
+
const files = (0, import_node_fs21.readdirSync)(dir).filter((f) => f.endsWith(".json"));
|
|
13432
14917
|
for (const file of files) {
|
|
13433
14918
|
try {
|
|
13434
|
-
const raw = (0,
|
|
14919
|
+
const raw = (0, import_node_fs21.readFileSync)((0, import_node_path14.join)(dir, file), "utf-8");
|
|
13435
14920
|
const parsed = JSON.parse(raw);
|
|
13436
14921
|
const customRules = Array.isArray(parsed) ? parsed : [parsed];
|
|
13437
14922
|
for (const rule of customRules) {
|
|
@@ -13456,6 +14941,12 @@ function loadAllRules(customDir) {
|
|
|
13456
14941
|
|
|
13457
14942
|
// src/daemon/firewall/adapters.ts
|
|
13458
14943
|
var import_node_child_process6 = require("child_process");
|
|
14944
|
+
var import_node_net3 = require("net");
|
|
14945
|
+
function assertValidFirewallIp(ip) {
|
|
14946
|
+
if ((0, import_node_net3.isIP)(ip) !== 4) {
|
|
14947
|
+
throw new Error(`Invalid IPv4 address: ${ip}`);
|
|
14948
|
+
}
|
|
14949
|
+
}
|
|
13459
14950
|
var NftablesAdapter = class {
|
|
13460
14951
|
name = "nftables";
|
|
13461
14952
|
table = "threatcrush";
|
|
@@ -13475,16 +14966,19 @@ var NftablesAdapter = class {
|
|
|
13475
14966
|
}
|
|
13476
14967
|
}
|
|
13477
14968
|
async block(ip) {
|
|
14969
|
+
assertValidFirewallIp(ip);
|
|
13478
14970
|
this.ensureSetup();
|
|
13479
14971
|
(0, import_node_child_process6.execSync)(`nft add element inet ${this.table} ${this.set} '{ ${ip} }'`);
|
|
13480
14972
|
}
|
|
13481
14973
|
async unblock(ip) {
|
|
14974
|
+
assertValidFirewallIp(ip);
|
|
13482
14975
|
try {
|
|
13483
14976
|
(0, import_node_child_process6.execSync)(`nft delete element inet ${this.table} ${this.set} '{ ${ip} }'`);
|
|
13484
14977
|
} catch {
|
|
13485
14978
|
}
|
|
13486
14979
|
}
|
|
13487
14980
|
async isBlocked(ip) {
|
|
14981
|
+
assertValidFirewallIp(ip);
|
|
13488
14982
|
try {
|
|
13489
14983
|
const output = (0, import_node_child_process6.execSync)(`nft list set inet ${this.table} ${this.set}`, { encoding: "utf-8" });
|
|
13490
14984
|
return output.includes(ip);
|
|
@@ -13519,17 +15013,20 @@ var IptablesAdapter = class {
|
|
|
13519
15013
|
}
|
|
13520
15014
|
}
|
|
13521
15015
|
async block(ip) {
|
|
15016
|
+
assertValidFirewallIp(ip);
|
|
13522
15017
|
this.ensureChain();
|
|
13523
15018
|
if (await this.isBlocked(ip)) return;
|
|
13524
15019
|
(0, import_node_child_process6.execSync)(`iptables -A ${this.chain} -s ${ip} -j DROP`);
|
|
13525
15020
|
}
|
|
13526
15021
|
async unblock(ip) {
|
|
15022
|
+
assertValidFirewallIp(ip);
|
|
13527
15023
|
try {
|
|
13528
15024
|
(0, import_node_child_process6.execSync)(`iptables -D ${this.chain} -s ${ip} -j DROP`);
|
|
13529
15025
|
} catch {
|
|
13530
15026
|
}
|
|
13531
15027
|
}
|
|
13532
15028
|
async isBlocked(ip) {
|
|
15029
|
+
assertValidFirewallIp(ip);
|
|
13533
15030
|
try {
|
|
13534
15031
|
const output = (0, import_node_child_process6.execSync)(`iptables -n -L ${this.chain}`, { encoding: "utf-8" });
|
|
13535
15032
|
return output.includes(ip);
|
|
@@ -13558,12 +15055,15 @@ var DryRunAdapter = class {
|
|
|
13558
15055
|
return true;
|
|
13559
15056
|
}
|
|
13560
15057
|
async block(ip) {
|
|
15058
|
+
assertValidFirewallIp(ip);
|
|
13561
15059
|
this.blocked.add(ip);
|
|
13562
15060
|
}
|
|
13563
15061
|
async unblock(ip) {
|
|
15062
|
+
assertValidFirewallIp(ip);
|
|
13564
15063
|
this.blocked.delete(ip);
|
|
13565
15064
|
}
|
|
13566
15065
|
async isBlocked(ip) {
|
|
15066
|
+
assertValidFirewallIp(ip);
|
|
13567
15067
|
return this.blocked.has(ip);
|
|
13568
15068
|
}
|
|
13569
15069
|
async listBlocked() {
|
|
@@ -13579,7 +15079,7 @@ function detectFirewallAdapter() {
|
|
|
13579
15079
|
}
|
|
13580
15080
|
|
|
13581
15081
|
// src/daemon/firewall/remediation.ts
|
|
13582
|
-
var
|
|
15082
|
+
var import_node_fs22 = require("fs");
|
|
13583
15083
|
init_state();
|
|
13584
15084
|
init_paths();
|
|
13585
15085
|
var DEFAULT_CONFIG2 = {
|
|
@@ -13736,7 +15236,7 @@ var RemediationManager = class {
|
|
|
13736
15236
|
}
|
|
13737
15237
|
logLine(line) {
|
|
13738
15238
|
try {
|
|
13739
|
-
(0,
|
|
15239
|
+
(0, import_node_fs22.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
13740
15240
|
`);
|
|
13741
15241
|
} catch {
|
|
13742
15242
|
}
|
|
@@ -13793,9 +15293,9 @@ async function flushTelemetry(timeoutMs = 2e3) {
|
|
|
13793
15293
|
}
|
|
13794
15294
|
|
|
13795
15295
|
// src/daemon/index.ts
|
|
13796
|
-
function
|
|
15296
|
+
function readVersion2() {
|
|
13797
15297
|
try {
|
|
13798
|
-
const pkg = JSON.parse((0,
|
|
15298
|
+
const pkg = JSON.parse((0, import_node_fs23.readFileSync)((0, import_node_path15.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
13799
15299
|
return pkg.version || "0.0.0";
|
|
13800
15300
|
} catch {
|
|
13801
15301
|
return "0.0.0";
|
|
@@ -13803,7 +15303,7 @@ function readVersion() {
|
|
|
13803
15303
|
}
|
|
13804
15304
|
function logLine(line) {
|
|
13805
15305
|
try {
|
|
13806
|
-
(0,
|
|
15306
|
+
(0, import_node_fs23.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
13807
15307
|
`);
|
|
13808
15308
|
} catch {
|
|
13809
15309
|
}
|
|
@@ -13824,14 +15324,14 @@ async function runDaemon() {
|
|
|
13824
15324
|
logLine(`[daemon] unhandledRejection: ${String(reason)}`);
|
|
13825
15325
|
captureException(reason);
|
|
13826
15326
|
});
|
|
13827
|
-
const version =
|
|
15327
|
+
const version = readVersion2();
|
|
13828
15328
|
logLine(`[daemon] starting threatcrushd v${version} mode=${PATHS.mode}`);
|
|
13829
15329
|
try {
|
|
13830
15330
|
initStateDB(PATHS.stateDb);
|
|
13831
15331
|
} catch (err) {
|
|
13832
15332
|
logLine(`[daemon] state db unavailable: ${err.message}`);
|
|
13833
15333
|
}
|
|
13834
|
-
const config = loadConfig((0,
|
|
15334
|
+
const config = loadConfig((0, import_node_fs23.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
|
|
13835
15335
|
bus.on("event", (event) => {
|
|
13836
15336
|
logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
|
|
13837
15337
|
});
|
|
@@ -13923,7 +15423,7 @@ async function runDaemon() {
|
|
|
13923
15423
|
init_paths();
|
|
13924
15424
|
init_pidfile();
|
|
13925
15425
|
init_ipc_client();
|
|
13926
|
-
var DAEMON_ENTRY = (0,
|
|
15426
|
+
var DAEMON_ENTRY = (0, import_node_path16.join)(__dirname, "daemon.js");
|
|
13927
15427
|
async function daemonForeground() {
|
|
13928
15428
|
await runDaemon();
|
|
13929
15429
|
}
|
|
@@ -13934,7 +15434,7 @@ async function daemonStart() {
|
|
|
13934
15434
|
return;
|
|
13935
15435
|
}
|
|
13936
15436
|
ensureRuntimeDirs();
|
|
13937
|
-
if (!(0,
|
|
15437
|
+
if (!(0, import_node_fs24.existsSync)(DAEMON_ENTRY)) {
|
|
13938
15438
|
console.log(source_default.red(` Daemon entry not found at ${DAEMON_ENTRY}.`));
|
|
13939
15439
|
console.log(source_default.dim(" Reinstall with `threatcrush update` or `pnpm run build` in the cli package."));
|
|
13940
15440
|
return;
|
|
@@ -13942,8 +15442,8 @@ async function daemonStart() {
|
|
|
13942
15442
|
let out;
|
|
13943
15443
|
let err;
|
|
13944
15444
|
try {
|
|
13945
|
-
out = (0,
|
|
13946
|
-
err = (0,
|
|
15445
|
+
out = (0, import_node_fs25.openSync)(PATHS.logFile, "a");
|
|
15446
|
+
err = (0, import_node_fs25.openSync)(PATHS.logFile, "a");
|
|
13947
15447
|
} catch (e) {
|
|
13948
15448
|
const code = e.code;
|
|
13949
15449
|
console.log(source_default.red(` \u2717 Cannot write daemon log at ${PATHS.logFile} (${code ?? "error"}).`));
|
|
@@ -14022,19 +15522,19 @@ async function daemonStop() {
|
|
|
14022
15522
|
|
|
14023
15523
|
// src/commands/service.ts
|
|
14024
15524
|
var import_node_child_process8 = require("child_process");
|
|
14025
|
-
var
|
|
14026
|
-
var
|
|
15525
|
+
var import_node_fs26 = require("fs");
|
|
15526
|
+
var import_node_path17 = require("path");
|
|
14027
15527
|
var UNIT_PATH = "/etc/systemd/system/threatcrushd.service";
|
|
14028
15528
|
function resolveTemplate() {
|
|
14029
|
-
const templatePath = (0,
|
|
14030
|
-
if (!(0,
|
|
15529
|
+
const templatePath = (0, import_node_path17.join)(__dirname, "systemd", "threatcrushd.service");
|
|
15530
|
+
if (!(0, import_node_fs26.existsSync)(templatePath)) {
|
|
14031
15531
|
throw new Error(`systemd unit template not found at ${templatePath}`);
|
|
14032
15532
|
}
|
|
14033
|
-
return (0,
|
|
15533
|
+
return (0, import_node_fs26.readFileSync)(templatePath, "utf-8");
|
|
14034
15534
|
}
|
|
14035
15535
|
function resolveBinPath() {
|
|
14036
15536
|
const arg = process.argv[1];
|
|
14037
|
-
if (arg && (0,
|
|
15537
|
+
if (arg && (0, import_node_fs26.existsSync)(arg)) return arg;
|
|
14038
15538
|
try {
|
|
14039
15539
|
return (0, import_node_child_process8.execSync)("command -v threatcrush", { encoding: "utf-8" }).trim();
|
|
14040
15540
|
} catch {
|
|
@@ -14055,7 +15555,7 @@ async function installServiceCommand() {
|
|
|
14055
15555
|
return;
|
|
14056
15556
|
}
|
|
14057
15557
|
const unit = resolveTemplate().replace("{{BIN_PATH}}", resolveBinPath());
|
|
14058
|
-
(0,
|
|
15558
|
+
(0, import_node_fs26.writeFileSync)(UNIT_PATH, unit, { mode: 420 });
|
|
14059
15559
|
console.log(source_default.green(` \u2713 Installed unit file: ${UNIT_PATH}`));
|
|
14060
15560
|
ensureSystemDirs();
|
|
14061
15561
|
try {
|
|
@@ -14079,17 +15579,17 @@ function ensureSystemDirs() {
|
|
|
14079
15579
|
];
|
|
14080
15580
|
let admGid = null;
|
|
14081
15581
|
try {
|
|
14082
|
-
admGid = (0,
|
|
15582
|
+
admGid = (0, import_node_fs26.statSync)("/var/log/auth.log").gid;
|
|
14083
15583
|
} catch {
|
|
14084
15584
|
}
|
|
14085
15585
|
for (const { path, sticky } of dirs) {
|
|
14086
15586
|
try {
|
|
14087
|
-
(0,
|
|
15587
|
+
(0, import_node_fs26.mkdirSync)(path, { recursive: true });
|
|
14088
15588
|
} catch {
|
|
14089
15589
|
}
|
|
14090
15590
|
if (admGid !== null) {
|
|
14091
15591
|
try {
|
|
14092
|
-
(0,
|
|
15592
|
+
(0, import_node_fs26.chmodSync)(path, sticky ? 1533 : 509);
|
|
14093
15593
|
(0, import_node_child_process8.execSync)(`chgrp adm ${path}`, { stdio: "ignore" });
|
|
14094
15594
|
} catch {
|
|
14095
15595
|
}
|
|
@@ -14116,7 +15616,7 @@ async function uninstallServiceCommand() {
|
|
|
14116
15616
|
} catch {
|
|
14117
15617
|
}
|
|
14118
15618
|
try {
|
|
14119
|
-
if ((0,
|
|
15619
|
+
if ((0, import_node_fs26.existsSync)(UNIT_PATH)) {
|
|
14120
15620
|
(0, import_node_child_process8.execSync)(`rm -f ${UNIT_PATH}`);
|
|
14121
15621
|
console.log(source_default.green(` \u2713 Removed unit file: ${UNIT_PATH}`));
|
|
14122
15622
|
}
|
|
@@ -14215,16 +15715,16 @@ function welcomeCommand() {
|
|
|
14215
15715
|
}
|
|
14216
15716
|
|
|
14217
15717
|
// src/commands/properties.ts
|
|
14218
|
-
var
|
|
14219
|
-
var
|
|
15718
|
+
var import_node_fs27 = require("fs");
|
|
15719
|
+
var import_node_path18 = require("path");
|
|
14220
15720
|
var import_node_readline6 = __toESM(require("readline"));
|
|
14221
15721
|
var API_URL7 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
14222
15722
|
var KINDS = ["url", "api", "domain", "ip", "repo"];
|
|
14223
15723
|
function prompt2(question) {
|
|
14224
15724
|
const rl = import_node_readline6.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
14225
|
-
return new Promise((
|
|
15725
|
+
return new Promise((resolve5) => rl.question(question, (answer) => {
|
|
14226
15726
|
rl.close();
|
|
14227
|
-
|
|
15727
|
+
resolve5(answer.trim());
|
|
14228
15728
|
}));
|
|
14229
15729
|
}
|
|
14230
15730
|
async function requireAuth() {
|
|
@@ -14545,8 +16045,8 @@ async function propertiesRunsCommand(opts) {
|
|
|
14545
16045
|
}
|
|
14546
16046
|
}
|
|
14547
16047
|
function parseImportFile(path) {
|
|
14548
|
-
const ext = (0,
|
|
14549
|
-
const raw = (0,
|
|
16048
|
+
const ext = (0, import_node_path18.extname)(path).toLowerCase();
|
|
16049
|
+
const raw = (0, import_node_fs27.readFileSync)(path, "utf-8");
|
|
14550
16050
|
if (ext === ".json") {
|
|
14551
16051
|
const parsed = JSON.parse(raw);
|
|
14552
16052
|
if (!Array.isArray(parsed)) throw new Error("JSON must be an array of objects");
|
|
@@ -14736,7 +16236,7 @@ async function rulesCommand(opts) {
|
|
|
14736
16236
|
}
|
|
14737
16237
|
|
|
14738
16238
|
// src/commands/harden.ts
|
|
14739
|
-
var
|
|
16239
|
+
var import_node_fs28 = require("fs");
|
|
14740
16240
|
var import_node_child_process9 = require("child_process");
|
|
14741
16241
|
function tryExec(cmd) {
|
|
14742
16242
|
try {
|
|
@@ -14747,7 +16247,7 @@ function tryExec(cmd) {
|
|
|
14747
16247
|
}
|
|
14748
16248
|
function tryRead(path) {
|
|
14749
16249
|
try {
|
|
14750
|
-
return (0,
|
|
16250
|
+
return (0, import_node_fs28.readFileSync)(path, "utf-8");
|
|
14751
16251
|
} catch {
|
|
14752
16252
|
return null;
|
|
14753
16253
|
}
|
|
@@ -14851,8 +16351,8 @@ function checkSshWeakConfig() {
|
|
|
14851
16351
|
};
|
|
14852
16352
|
}
|
|
14853
16353
|
function checkAutoUpdates() {
|
|
14854
|
-
const unattended = (0,
|
|
14855
|
-
const dnfAuto = (0,
|
|
16354
|
+
const unattended = (0, import_node_fs28.existsSync)("/etc/apt/apt.conf.d/20auto-upgrades") || (0, import_node_fs28.existsSync)("/etc/apt/apt.conf.d/50unattended-upgrades");
|
|
16355
|
+
const dnfAuto = (0, import_node_fs28.existsSync)("/etc/dnf/automatic.conf");
|
|
14856
16356
|
if (unattended || dnfAuto) {
|
|
14857
16357
|
return {
|
|
14858
16358
|
key: "auto-updates",
|
|
@@ -14978,7 +16478,7 @@ function checkFail2ban() {
|
|
|
14978
16478
|
explanation: "fail2ban is installed and running."
|
|
14979
16479
|
};
|
|
14980
16480
|
}
|
|
14981
|
-
if ((0,
|
|
16481
|
+
if ((0, import_node_fs28.existsSync)("/etc/fail2ban/fail2ban.conf")) {
|
|
14982
16482
|
return {
|
|
14983
16483
|
key: checkKey,
|
|
14984
16484
|
status: "warn",
|
|
@@ -15207,10 +16707,10 @@ async function allowlistCommand(opts) {
|
|
|
15207
16707
|
|
|
15208
16708
|
// src/index.ts
|
|
15209
16709
|
init_paths();
|
|
15210
|
-
var
|
|
16710
|
+
var PKG_VERSION2 = "0.1.8";
|
|
15211
16711
|
try {
|
|
15212
|
-
const pkg = JSON.parse((0,
|
|
15213
|
-
|
|
16712
|
+
const pkg = JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path19.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
16713
|
+
PKG_VERSION2 = pkg.version;
|
|
15214
16714
|
} catch {
|
|
15215
16715
|
}
|
|
15216
16716
|
var LOGO2 = `
|
|
@@ -15225,7 +16725,7 @@ ${source_default.dim(" C R U S H")}
|
|
|
15225
16725
|
var API_URL8 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
15226
16726
|
var PKG_NAME = "@profullstack/threatcrush";
|
|
15227
16727
|
var DESKTOP_PKG_NAME = "@profullstack/threatcrush-desktop";
|
|
15228
|
-
var INSTALL_CONFIG_PATH = (0,
|
|
16728
|
+
var INSTALL_CONFIG_PATH = (0, import_node_path19.join)((0, import_node_os8.homedir)(), ".threatcrush", "install.json");
|
|
15229
16729
|
function detectPackageManager() {
|
|
15230
16730
|
try {
|
|
15231
16731
|
const npmGlobal = (0, import_node_child_process10.execSync)("npm ls -g --depth=0 --json 2>/dev/null", { encoding: "utf-8" });
|
|
@@ -15251,7 +16751,7 @@ function detectPackageManager() {
|
|
|
15251
16751
|
}
|
|
15252
16752
|
function readInstallConfig() {
|
|
15253
16753
|
try {
|
|
15254
|
-
return JSON.parse((0,
|
|
16754
|
+
return JSON.parse((0, import_node_fs29.readFileSync)(INSTALL_CONFIG_PATH, "utf-8"));
|
|
15255
16755
|
} catch {
|
|
15256
16756
|
return {};
|
|
15257
16757
|
}
|
|
@@ -15318,7 +16818,7 @@ ${source_default.dim("Examples:")}
|
|
|
15318
16818
|
${source_default.green("$")} threatcrush modules install ${source_default.dim("# Install a module")}
|
|
15319
16819
|
${source_default.green("$")} threatcrush update ${source_default.dim("# Update to latest")}
|
|
15320
16820
|
${source_default.green("$")} threatcrush remove ${source_default.dim("# Uninstall completely")}`
|
|
15321
|
-
).version(
|
|
16821
|
+
).version(PKG_VERSION2, "-v, --version", "Show version number").helpOption("-h, --help", "Show this help").addHelpText("after", `
|
|
15322
16822
|
${source_default.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
|
|
15323
16823
|
${source_default.dim("Modules:")}
|
|
15324
16824
|
ThreatCrush uses pluggable security modules. Core modules included:
|
|
@@ -15354,8 +16854,33 @@ program2.command("logout").description("Clear stored threatcrush.com credentials
|
|
|
15354
16854
|
program2.command("whoami").description("Show the currently logged-in threatcrush.com account").action(async () => {
|
|
15355
16855
|
await whoamiCommand();
|
|
15356
16856
|
});
|
|
15357
|
-
program2.command("scan").description("Scan codebase for vulnerabilities and secrets").argument("[path]", "Path to scan", ".").
|
|
15358
|
-
|
|
16857
|
+
program2.command("scan").description("Scan codebase for vulnerabilities and secrets").argument("[path]", "Path to scan", ".").option("-f, --format <format>", "output format: text, json, or sarif", "text").option("-o, --output <file>", "write json/sarif output to a file instead of stdout").option(
|
|
16858
|
+
"--fail-on <severities>",
|
|
16859
|
+
"exit 1 when a finding at or above any of these exists (comma-separated: critical,high,medium,low,info)"
|
|
16860
|
+
).option(
|
|
16861
|
+
"--path-prefix <prefix>",
|
|
16862
|
+
"prepend this to SARIF file URIs \u2014 use when the scan root is not the repository root"
|
|
16863
|
+
).option("--deps", "also query OSV.dev for advisories against lockfile versions (network)").option("-v, --verbose", "list the paths that could not be read").action(async (targetPath, opts) => {
|
|
16864
|
+
const format = (opts.format ?? "text").toLowerCase();
|
|
16865
|
+
if (!["text", "json", "sarif"].includes(format)) {
|
|
16866
|
+
console.error(source_default.red(`Unknown --format "${opts.format}" (expected text, json, or sarif)`));
|
|
16867
|
+
process.exit(2);
|
|
16868
|
+
}
|
|
16869
|
+
let failOn;
|
|
16870
|
+
try {
|
|
16871
|
+
failOn = parseFailOn(opts.failOn);
|
|
16872
|
+
} catch (err) {
|
|
16873
|
+
console.error(source_default.red(err.message));
|
|
16874
|
+
process.exit(2);
|
|
16875
|
+
}
|
|
16876
|
+
await scanCommand(targetPath, {
|
|
16877
|
+
format,
|
|
16878
|
+
output: opts.output,
|
|
16879
|
+
failOn,
|
|
16880
|
+
pathPrefix: opts.pathPrefix,
|
|
16881
|
+
dependencies: opts.deps,
|
|
16882
|
+
verbose: opts.verbose
|
|
16883
|
+
});
|
|
15359
16884
|
});
|
|
15360
16885
|
program2.command("pentest").description("Penetration test URLs and APIs").argument("<url>", "Target URL to pentest").action(async (url) => {
|
|
15361
16886
|
await pentestCommand(url);
|
|
@@ -15401,7 +16926,7 @@ program2.command("uninstall-service").description("Remove the threatcrushd syste
|
|
|
15401
16926
|
program2.command("logs").description("Tail daemon logs").action(async () => {
|
|
15402
16927
|
console.log(LOGO2);
|
|
15403
16928
|
const logPath = PATHS.logFile;
|
|
15404
|
-
if (!(0,
|
|
16929
|
+
if (!(0, import_node_fs29.existsSync)(logPath)) {
|
|
15405
16930
|
console.log(source_default.yellow(` No log file found at ${logPath}`));
|
|
15406
16931
|
console.log(source_default.dim(" Start the daemon first with `threatcrush start`\n"));
|
|
15407
16932
|
return;
|
|
@@ -15414,10 +16939,10 @@ program2.command("logs").description("Tail daemon logs").action(async () => {
|
|
|
15414
16939
|
program2.command("activate").description("Activate your license key").action(async () => {
|
|
15415
16940
|
console.log(LOGO2);
|
|
15416
16941
|
const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
15417
|
-
const key = await new Promise((
|
|
16942
|
+
const key = await new Promise((resolve5) => {
|
|
15418
16943
|
rl.question(source_default.green(" Enter your ThreatCrush license key: "), (answer) => {
|
|
15419
16944
|
rl.close();
|
|
15420
|
-
|
|
16945
|
+
resolve5(answer.trim());
|
|
15421
16946
|
});
|
|
15422
16947
|
});
|
|
15423
16948
|
if (!key) {
|
|
@@ -15504,10 +17029,10 @@ program2.command("update").description("Update ThreatCrush CLI and installed bun
|
|
|
15504
17029
|
program2.command("remove").description("Uninstall ThreatCrush and the installed bundle").alias("uninstall").action(async () => {
|
|
15505
17030
|
console.log(LOGO2);
|
|
15506
17031
|
const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
15507
|
-
const confirm = await new Promise((
|
|
17032
|
+
const confirm = await new Promise((resolve5) => {
|
|
15508
17033
|
rl.question(source_default.yellow(" Are you sure you want to uninstall ThreatCrush? (y/N): "), (answer) => {
|
|
15509
17034
|
rl.close();
|
|
15510
|
-
|
|
17035
|
+
resolve5(answer.trim().toLowerCase());
|
|
15511
17036
|
});
|
|
15512
17037
|
});
|
|
15513
17038
|
if (confirm !== "y" && confirm !== "yes") {
|
|
@@ -15606,19 +17131,19 @@ storeCmd.command("search <query>").description("Search for modules in the store"
|
|
|
15606
17131
|
});
|
|
15607
17132
|
storeCmd.command("publish <url>").description("Publish a module from a git URL or web URL").action(async (url) => {
|
|
15608
17133
|
console.log(LOGO2);
|
|
15609
|
-
const configPath = (0,
|
|
17134
|
+
const configPath = (0, import_node_path19.join)((0, import_node_os8.homedir)(), ".threatcrush", "config.json");
|
|
15610
17135
|
let email = "";
|
|
15611
17136
|
try {
|
|
15612
|
-
const config = JSON.parse((0,
|
|
17137
|
+
const config = JSON.parse((0, import_node_fs29.readFileSync)(configPath, "utf-8"));
|
|
15613
17138
|
email = config.email || "";
|
|
15614
17139
|
} catch {
|
|
15615
17140
|
}
|
|
15616
17141
|
if (!email) {
|
|
15617
17142
|
const rl2 = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
15618
|
-
email = await new Promise((
|
|
17143
|
+
email = await new Promise((resolve5) => {
|
|
15619
17144
|
rl2.question(source_default.green(" Enter your email: "), (answer) => {
|
|
15620
17145
|
rl2.close();
|
|
15621
|
-
|
|
17146
|
+
resolve5(answer.trim());
|
|
15622
17147
|
});
|
|
15623
17148
|
});
|
|
15624
17149
|
if (!email || !email.includes("@")) {
|
|
@@ -15626,9 +17151,9 @@ storeCmd.command("publish <url>").description("Publish a module from a git URL o
|
|
|
15626
17151
|
return;
|
|
15627
17152
|
}
|
|
15628
17153
|
try {
|
|
15629
|
-
const dir = (0,
|
|
15630
|
-
if (!(0,
|
|
15631
|
-
(0,
|
|
17154
|
+
const dir = (0, import_node_path19.join)((0, import_node_os8.homedir)(), ".threatcrush");
|
|
17155
|
+
if (!(0, import_node_fs29.existsSync)(dir)) (0, import_node_fs29.mkdirSync)(dir, { recursive: true });
|
|
17156
|
+
(0, import_node_fs29.writeFileSync)(configPath, JSON.stringify({ email }, null, 2));
|
|
15632
17157
|
console.log(source_default.dim(` Saved email to ${configPath}`));
|
|
15633
17158
|
} catch {
|
|
15634
17159
|
}
|
|
@@ -15672,10 +17197,10 @@ storeCmd.command("publish <url>").description("Publish a module from a git URL o
|
|
|
15672
17197
|
}
|
|
15673
17198
|
console.log();
|
|
15674
17199
|
const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
15675
|
-
const confirm = await new Promise((
|
|
17200
|
+
const confirm = await new Promise((resolve5) => {
|
|
15676
17201
|
rl.question(source_default.yellow(" Publish this module? (y/N): "), (answer) => {
|
|
15677
17202
|
rl.close();
|
|
15678
|
-
|
|
17203
|
+
resolve5(answer.trim().toLowerCase());
|
|
15679
17204
|
});
|
|
15680
17205
|
});
|
|
15681
17206
|
if (confirm !== "y" && confirm !== "yes") {
|