@profullstack/threatcrush 0.11.1 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon.js +727 -137
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1010 -272
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3891,14 +3891,52 @@ var init_paths = __esm({
|
|
|
3891
3891
|
}
|
|
3892
3892
|
});
|
|
3893
3893
|
|
|
3894
|
+
// src/daemon/control-token.ts
|
|
3895
|
+
function issueControlToken() {
|
|
3896
|
+
const token = (0, import_node_crypto.randomBytes)(32).toString("hex");
|
|
3897
|
+
(0, import_node_fs2.writeFileSync)(CONTROL_TOKEN_FILE, token, { mode: 384 });
|
|
3898
|
+
try {
|
|
3899
|
+
(0, import_node_fs2.chmodSync)(CONTROL_TOKEN_FILE, 384);
|
|
3900
|
+
} catch {
|
|
3901
|
+
}
|
|
3902
|
+
return token;
|
|
3903
|
+
}
|
|
3904
|
+
function readControlToken() {
|
|
3905
|
+
if (!(0, import_node_fs2.existsSync)(CONTROL_TOKEN_FILE)) return null;
|
|
3906
|
+
try {
|
|
3907
|
+
const token = (0, import_node_fs2.readFileSync)(CONTROL_TOKEN_FILE, "utf-8").trim();
|
|
3908
|
+
return token || null;
|
|
3909
|
+
} catch {
|
|
3910
|
+
return null;
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
function tokensMatch(expected, provided) {
|
|
3914
|
+
if (typeof provided !== "string" || !provided) return false;
|
|
3915
|
+
const a = Buffer.from(expected);
|
|
3916
|
+
const b = Buffer.from(provided);
|
|
3917
|
+
return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
|
|
3918
|
+
}
|
|
3919
|
+
var import_node_crypto, import_node_fs2, import_node_path2, CONTROL_TOKEN_FILE;
|
|
3920
|
+
var init_control_token = __esm({
|
|
3921
|
+
"src/daemon/control-token.ts"() {
|
|
3922
|
+
"use strict";
|
|
3923
|
+
import_node_crypto = require("crypto");
|
|
3924
|
+
import_node_fs2 = require("fs");
|
|
3925
|
+
import_node_path2 = require("path");
|
|
3926
|
+
init_paths();
|
|
3927
|
+
CONTROL_TOKEN_FILE = (0, import_node_path2.join)(PATHS.runDir, "control.token");
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3930
|
+
|
|
3894
3931
|
// src/core/ipc-client.ts
|
|
3895
|
-
var import_node_net,
|
|
3932
|
+
var import_node_net, import_node_fs3, IpcClient;
|
|
3896
3933
|
var init_ipc_client = __esm({
|
|
3897
3934
|
"src/core/ipc-client.ts"() {
|
|
3898
3935
|
"use strict";
|
|
3899
3936
|
import_node_net = require("net");
|
|
3900
|
-
|
|
3937
|
+
import_node_fs3 = require("fs");
|
|
3901
3938
|
init_paths();
|
|
3939
|
+
init_control_token();
|
|
3902
3940
|
IpcClient = class {
|
|
3903
3941
|
constructor(opts = {}) {
|
|
3904
3942
|
this.opts = opts;
|
|
@@ -3911,10 +3949,10 @@ var init_ipc_client = __esm({
|
|
|
3911
3949
|
pending = /* @__PURE__ */ new Map();
|
|
3912
3950
|
socketPath;
|
|
3913
3951
|
static isDaemonRunning(socketPath = resolveClientSocket()) {
|
|
3914
|
-
return (0,
|
|
3952
|
+
return (0, import_node_fs3.existsSync)(socketPath);
|
|
3915
3953
|
}
|
|
3916
3954
|
async connect(timeoutMs = 2e3) {
|
|
3917
|
-
if (!(0,
|
|
3955
|
+
if (!(0, import_node_fs3.existsSync)(this.socketPath)) {
|
|
3918
3956
|
throw new Error(`threatcrushd socket not found at ${this.socketPath}`);
|
|
3919
3957
|
}
|
|
3920
3958
|
return new Promise((resolve5, reject) => {
|
|
@@ -3975,7 +4013,7 @@ var init_ipc_client = __esm({
|
|
|
3975
4013
|
await this.request("subscribe", { channels });
|
|
3976
4014
|
}
|
|
3977
4015
|
async shutdown() {
|
|
3978
|
-
await this.request("shutdown");
|
|
4016
|
+
await this.request("shutdown", { token: readControlToken() });
|
|
3979
4017
|
}
|
|
3980
4018
|
onData(chunk) {
|
|
3981
4019
|
this.buffer += chunk;
|
|
@@ -4018,17 +4056,17 @@ var init_ipc_client = __esm({
|
|
|
4018
4056
|
// src/daemon/pidfile.ts
|
|
4019
4057
|
function writePidFile() {
|
|
4020
4058
|
ensureRuntimeDirs();
|
|
4021
|
-
(0,
|
|
4059
|
+
(0, import_node_fs4.writeFileSync)(PATHS.pidFile, String(process.pid), "utf-8");
|
|
4022
4060
|
}
|
|
4023
4061
|
function readPidFile() {
|
|
4024
|
-
if (!(0,
|
|
4025
|
-
const raw = (0,
|
|
4062
|
+
if (!(0, import_node_fs4.existsSync)(PATHS.pidFile)) return null;
|
|
4063
|
+
const raw = (0, import_node_fs4.readFileSync)(PATHS.pidFile, "utf-8").trim();
|
|
4026
4064
|
const pid = parseInt(raw, 10);
|
|
4027
4065
|
return Number.isFinite(pid) ? pid : null;
|
|
4028
4066
|
}
|
|
4029
4067
|
function removePidFile() {
|
|
4030
4068
|
try {
|
|
4031
|
-
if ((0,
|
|
4069
|
+
if ((0, import_node_fs4.existsSync)(PATHS.pidFile)) (0, import_node_fs4.unlinkSync)(PATHS.pidFile);
|
|
4032
4070
|
} catch {
|
|
4033
4071
|
}
|
|
4034
4072
|
}
|
|
@@ -4047,11 +4085,11 @@ function findRunningDaemon() {
|
|
|
4047
4085
|
if (pid) removePidFile();
|
|
4048
4086
|
return null;
|
|
4049
4087
|
}
|
|
4050
|
-
var
|
|
4088
|
+
var import_node_fs4;
|
|
4051
4089
|
var init_pidfile = __esm({
|
|
4052
4090
|
"src/daemon/pidfile.ts"() {
|
|
4053
4091
|
"use strict";
|
|
4054
|
-
|
|
4092
|
+
import_node_fs4 = require("fs");
|
|
4055
4093
|
init_paths();
|
|
4056
4094
|
}
|
|
4057
4095
|
});
|
|
@@ -8260,12 +8298,12 @@ var source_default = chalk;
|
|
|
8260
8298
|
// src/index.ts
|
|
8261
8299
|
var import_readline = __toESM(require("readline"));
|
|
8262
8300
|
var import_node_child_process10 = require("child_process");
|
|
8263
|
-
var
|
|
8264
|
-
var
|
|
8301
|
+
var import_node_fs31 = require("fs");
|
|
8302
|
+
var import_node_path21 = require("path");
|
|
8265
8303
|
var import_node_os8 = require("os");
|
|
8266
8304
|
|
|
8267
8305
|
// src/commands/monitor.ts
|
|
8268
|
-
var
|
|
8306
|
+
var import_node_fs5 = require("fs");
|
|
8269
8307
|
var import_node_readline = require("readline");
|
|
8270
8308
|
|
|
8271
8309
|
// src/core/logger.ts
|
|
@@ -8473,9 +8511,9 @@ async function monitorCommand(options) {
|
|
|
8473
8511
|
const unreadable = [];
|
|
8474
8512
|
for (const s of LOG_SOURCES) {
|
|
8475
8513
|
if (moduleFilter && !moduleFilter.includes(s.name)) continue;
|
|
8476
|
-
if (!(0,
|
|
8514
|
+
if (!(0, import_node_fs5.existsSync)(s.path)) continue;
|
|
8477
8515
|
try {
|
|
8478
|
-
(0,
|
|
8516
|
+
(0, import_node_fs5.accessSync)(s.path, import_node_fs5.constants.R_OK);
|
|
8479
8517
|
availableSources.push(s);
|
|
8480
8518
|
} catch {
|
|
8481
8519
|
unreadable.push(s);
|
|
@@ -8492,11 +8530,11 @@ async function monitorCommand(options) {
|
|
|
8492
8530
|
logger.warn("No readable log files found to monitor.");
|
|
8493
8531
|
logger.info("Available log paths checked:");
|
|
8494
8532
|
for (const s of LOG_SOURCES) {
|
|
8495
|
-
const exists = (0,
|
|
8533
|
+
const exists = (0, import_node_fs5.existsSync)(s.path);
|
|
8496
8534
|
let readable = false;
|
|
8497
8535
|
if (exists) {
|
|
8498
8536
|
try {
|
|
8499
|
-
(0,
|
|
8537
|
+
(0, import_node_fs5.accessSync)(s.path, import_node_fs5.constants.R_OK);
|
|
8500
8538
|
readable = true;
|
|
8501
8539
|
} catch {
|
|
8502
8540
|
readable = false;
|
|
@@ -8528,16 +8566,16 @@ async function monitorCommand(options) {
|
|
|
8528
8566
|
}
|
|
8529
8567
|
function tailLog(source) {
|
|
8530
8568
|
const { path, name, category } = source;
|
|
8531
|
-
const stat = (0,
|
|
8569
|
+
const stat = (0, import_node_fs5.statSync)(path);
|
|
8532
8570
|
let position = stat.size;
|
|
8533
8571
|
const checkForNewData = () => {
|
|
8534
8572
|
try {
|
|
8535
|
-
const currentStat = (0,
|
|
8573
|
+
const currentStat = (0, import_node_fs5.statSync)(path);
|
|
8536
8574
|
if (currentStat.size <= position) {
|
|
8537
8575
|
if (currentStat.size < position) position = 0;
|
|
8538
8576
|
return;
|
|
8539
8577
|
}
|
|
8540
|
-
const stream = (0,
|
|
8578
|
+
const stream = (0, import_node_fs5.createReadStream)(path, { start: position, encoding: "utf-8" });
|
|
8541
8579
|
stream.on("error", (err) => {
|
|
8542
8580
|
logger.warn(`stopped tailing ${path}: ${err.code || err.message}`);
|
|
8543
8581
|
position = currentStat.size;
|
|
@@ -8639,8 +8677,8 @@ async function runDemoMode() {
|
|
|
8639
8677
|
}
|
|
8640
8678
|
|
|
8641
8679
|
// src/commands/scan.ts
|
|
8642
|
-
var
|
|
8643
|
-
var
|
|
8680
|
+
var import_node_fs8 = require("fs");
|
|
8681
|
+
var import_node_path6 = require("path");
|
|
8644
8682
|
|
|
8645
8683
|
// ../../node_modules/.pnpm/ora@8.2.0/node_modules/ora/index.js
|
|
8646
8684
|
var import_node_process7 = __toESM(require("process"), 1);
|
|
@@ -9797,7 +9835,17 @@ var NODE_RULES = [
|
|
|
9797
9835
|
// arrangement of nearby lines that makes an echoed origin safe — except a
|
|
9798
9836
|
// membership test on the value, which is what the guard looks for.
|
|
9799
9837
|
inherent: true,
|
|
9800
|
-
guard: ORIGIN_ALLOWLIST_GUARD
|
|
9838
|
+
guard: ORIGIN_ALLOWLIST_GUARD,
|
|
9839
|
+
/**
|
|
9840
|
+
* Logging the origin is not reflecting it.
|
|
9841
|
+
*
|
|
9842
|
+
* `origin: request.headers.origin` is the CORS mistake *and* the ordinary
|
|
9843
|
+
* way to record who called — the two are character-for-character
|
|
9844
|
+
* identical, and only what encloses them differs. A response header goes
|
|
9845
|
+
* out to the browser; a log line goes to stdout, where it grants nobody
|
|
9846
|
+
* anything.
|
|
9847
|
+
*/
|
|
9848
|
+
enclosingCallGuard: /(?:^|\.)(?:log|debug|info|warn|error|trace|verbose|fatal)$/
|
|
9801
9849
|
},
|
|
9802
9850
|
// ── Server-side request forgery ──────────────────────────────────────────
|
|
9803
9851
|
{
|
|
@@ -9891,7 +9939,11 @@ var NODE_RULES = [
|
|
|
9891
9939
|
severity: "medium",
|
|
9892
9940
|
languages: ["javascript", "typescript"],
|
|
9893
9941
|
pattern: /\bBuffer\s*\.\s*allocUnsafe(?:Slow)?\s*\(|\bnew\s+Buffer\s*\(\s*(?![`'"])[a-zA-Z_$0-9]/,
|
|
9894
|
-
inherent: true
|
|
9942
|
+
inherent: true,
|
|
9943
|
+
// Filling the buffer yourself is the whole reason to call `allocUnsafe`,
|
|
9944
|
+
// so reporting every call reports correct code. What is left reported is
|
|
9945
|
+
// an allocation whose bytes are never written before it escapes.
|
|
9946
|
+
filledBeforeUseGuard: true
|
|
9895
9947
|
},
|
|
9896
9948
|
{
|
|
9897
9949
|
id: "js-oversized-request-body-limit",
|
|
@@ -10001,8 +10053,19 @@ var CODE_RULES = [
|
|
|
10001
10053
|
// The tail is what distinguishes assembly from parameterisation. A bound
|
|
10002
10054
|
// query leaves a comma after the closing quote (`"… = $1", [id]`) and
|
|
10003
10055
|
// matches none of these.
|
|
10056
|
+
//
|
|
10057
|
+
// The clause alternative anchors the keyword to the *start* of the
|
|
10058
|
+
// concatenated fragment, because that is where a clause being appended
|
|
10059
|
+
// actually sits: `sql + "WHERE id = " + id`, `sql + " ORDER BY " + col`.
|
|
10060
|
+
//
|
|
10061
|
+
// Allowing it anywhere in the fragment made the rule read English. `WHERE`
|
|
10062
|
+
// and `SET` are ordinary words, and without a leading `\b` they were not
|
|
10063
|
+
// even required to be whole ones — "any`where`" and "sub`set`" both
|
|
10064
|
+
// matched. A help string reading "lists them anywhere" was reported as
|
|
10065
|
+
// critical SQL injection, which is the kind of finding that teaches a team
|
|
10066
|
+
// the scanner is not worth reading.
|
|
10004
10067
|
pattern: new RegExp(
|
|
10005
|
-
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"
|
|
10068
|
+
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"|')\\s*\\b(?:WHERE|ORDER\\s+BY|VALUES|SET)\\b`,
|
|
10006
10069
|
"i"
|
|
10007
10070
|
)
|
|
10008
10071
|
},
|
|
@@ -10015,7 +10078,26 @@ var CODE_RULES = [
|
|
|
10015
10078
|
pattern: new RegExp(
|
|
10016
10079
|
`\`[^\`\\n]*(?:${SQL_KEYWORDS})\\b[^\`\\n]*\\$\\{|\\bf"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*\\{|\\bf'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*\\{`,
|
|
10017
10080
|
"i"
|
|
10018
|
-
)
|
|
10081
|
+
),
|
|
10082
|
+
/**
|
|
10083
|
+
* Interpolating a column list is not interpolating a value.
|
|
10084
|
+
*
|
|
10085
|
+
* const COLS = `tld, user_id, owner_email, price_usd`;
|
|
10086
|
+
* get(`SELECT ${COLS} FROM moshpit_tlds WHERE tld = ?`, [tld]);
|
|
10087
|
+
*
|
|
10088
|
+
* That query *is* parameterised: every value the caller supplies rides a
|
|
10089
|
+
* `?`, and the only thing spliced into the text is a constant written a
|
|
10090
|
+
* few lines up. Naming the column list once instead of repeating it in
|
|
10091
|
+
* fourteen queries is ordinary hygiene, and it is the shape this rule met
|
|
10092
|
+
* most often in practice — one repository produced eighteen findings this
|
|
10093
|
+
* way and not one of them could be injected into.
|
|
10094
|
+
*
|
|
10095
|
+
* The guard resolves the interpolations rather than trusting the shape, so
|
|
10096
|
+
* the moment a query mixes a constant with anything else —
|
|
10097
|
+
* `` `SELECT ${COLS} FROM t WHERE id = ${req.query.id}` `` — it is reported
|
|
10098
|
+
* again. `interpolationsAreConstant` requires *every* `${…}` to resolve.
|
|
10099
|
+
*/
|
|
10100
|
+
constantInterpolationGuard: true
|
|
10019
10101
|
},
|
|
10020
10102
|
{
|
|
10021
10103
|
id: "sql-format-call",
|
|
@@ -10046,7 +10128,8 @@ var CODE_RULES = [
|
|
|
10046
10128
|
cwe: "CWE-78",
|
|
10047
10129
|
severity: "critical",
|
|
10048
10130
|
languages: ["javascript", "typescript"],
|
|
10049
|
-
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)
|
|
10131
|
+
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/,
|
|
10132
|
+
constantInterpolationGuard: true
|
|
10050
10133
|
},
|
|
10051
10134
|
{
|
|
10052
10135
|
id: "py-shell-command-string",
|
|
@@ -10064,7 +10147,22 @@ var CODE_RULES = [
|
|
|
10064
10147
|
cwe: "CWE-78",
|
|
10065
10148
|
severity: "critical",
|
|
10066
10149
|
languages: ["go"],
|
|
10067
|
-
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"
|
|
10150
|
+
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"/,
|
|
10151
|
+
// A call whose whole argv is string literals cannot be injected into.
|
|
10152
|
+
// `exec.Command("cmd", "/c", "ver")` reads the Windows version; there is no
|
|
10153
|
+
// value in it for an attacker to reach, and the consequence above — that a
|
|
10154
|
+
// shell will interpret metacharacters — describes metacharacters nobody can
|
|
10155
|
+
// supply. gosec's G204 draws the line in the same place, and this was the
|
|
10156
|
+
// single finding a Go project got out of a whole scan before declining the
|
|
10157
|
+
// offer, which is an expensive way to report nothing.
|
|
10158
|
+
//
|
|
10159
|
+
// The guard has to end at the closing paren, so a literal followed by
|
|
10160
|
+
// anything else still reports: `"ls " + dir` leaves a `+` before the `)`,
|
|
10161
|
+
// `fmt.Sprintf(…)` leaves an identifier, and a bare variable leaves a name.
|
|
10162
|
+
// `(?:[^"\\]|\\.)*` rather than `[^"]*` so an escaped quote inside a
|
|
10163
|
+
// literal — `"echo \"hi\""` — does not end the literal early and drop the
|
|
10164
|
+
// guard on a line it should have covered.
|
|
10165
|
+
lineGuard: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"\s*(?:,\s*"(?:[^"\\]|\\.)*")*\s*,?\s*\)/
|
|
10068
10166
|
},
|
|
10069
10167
|
{
|
|
10070
10168
|
id: "rb-backtick-interpolation",
|
|
@@ -10133,17 +10231,43 @@ var CODE_RULES = [
|
|
|
10133
10231
|
cwe: "CWE-79",
|
|
10134
10232
|
severity: "high",
|
|
10135
10233
|
languages: ["javascript", "typescript"],
|
|
10136
|
-
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=\s*(?!\s*['"`]\s*['"`]\s*;?\s*$)|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/,
|
|
10137
10234
|
/**
|
|
10235
|
+
* The assignment alternative carries its own exemption, as a lookahead, so
|
|
10236
|
+
* that it is decided per assignment rather than per line.
|
|
10237
|
+
*
|
|
10138
10238
|
* A whole-statement assignment of a string with no interpolation and no
|
|
10139
|
-
* concatenation carries no data, so it cannot carry attacker data. This
|
|
10140
|
-
*
|
|
10141
|
-
*
|
|
10142
|
-
*
|
|
10239
|
+
* concatenation carries no data, so it cannot carry attacker data. This was
|
|
10240
|
+
* the single largest source of noise: a codebase that builds its UI with
|
|
10241
|
+
* innerHTML reports every static heading and spinner as XSS, and a rule
|
|
10242
|
+
* that flags 40 safe lines to catch one real one gets switched off.
|
|
10143
10243
|
*
|
|
10144
|
-
*
|
|
10244
|
+
* Two things this has to get right, and a `lineGuard` could get neither:
|
|
10245
|
+
*
|
|
10246
|
+
* A statement ends at its semicolon, not at the newline. Anchoring to `$`
|
|
10247
|
+
* held the exemption for `el.innerHTML = '';` alone on a line and dropped
|
|
10248
|
+
* it the moment anything followed:
|
|
10249
|
+
*
|
|
10250
|
+
* function hide() { out.hidden = true; out.innerHTML = ''; rendered = null; }
|
|
10251
|
+
*
|
|
10252
|
+
* — the same assignment, clearing a node, reported as high-severity XSS
|
|
10253
|
+
* because two neighbours shared its line.
|
|
10254
|
+
*
|
|
10255
|
+
* And an exemption must not become a line-wide amnesty. A guard is tested
|
|
10256
|
+
* against the whole line, so one safe clear would exonerate a real sink
|
|
10257
|
+
* beside it:
|
|
10258
|
+
*
|
|
10259
|
+
* a.innerHTML = ''; b.innerHTML = userInput;
|
|
10260
|
+
*
|
|
10261
|
+
* As a lookahead the regex decides at each `=` it reaches, so the first
|
|
10262
|
+
* assignment is exempt and the second is still reported.
|
|
10263
|
+
*
|
|
10264
|
+
* The whitespace after `=` is matched *inside* the lookahead rather than
|
|
10265
|
+
* before it. Left outside, `\s*` backtracks to zero width, the lookahead
|
|
10266
|
+
* then starts on the space instead of the quote, fails to see a literal,
|
|
10267
|
+
* and the negative lookahead succeeds — reinstating every finding the
|
|
10268
|
+
* exemption was written to remove.
|
|
10145
10269
|
*/
|
|
10146
|
-
|
|
10270
|
+
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=(?!\s*(?:'[^'\\\n]*'|"[^"\\\n]*"|`[^`$\\\n]*`)\s*(?:;|$))|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/
|
|
10147
10271
|
},
|
|
10148
10272
|
{
|
|
10149
10273
|
id: "java-html-writer-concatenation",
|
|
@@ -10945,7 +11069,26 @@ var CODE_RULES = [
|
|
|
10945
11069
|
cwe: "CWE-346",
|
|
10946
11070
|
severity: "high",
|
|
10947
11071
|
languages: ["javascript", "typescript"],
|
|
10948
|
-
pattern: /\$\{[^}\n]*\breq(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname|host)\b|['"`]\s*\+\s*req(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname)\b
|
|
11072
|
+
pattern: /\$\{[^}\n]*\breq(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname|host)\b|['"`]\s*\+\s*req(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname)\b/,
|
|
11073
|
+
/**
|
|
11074
|
+
* Parsing the request's own URL is not building a link from the Host
|
|
11075
|
+
* header, even though it is spelled with one.
|
|
11076
|
+
*
|
|
11077
|
+
* const url = new URL(request.url, `http://${request.headers.host}`);
|
|
11078
|
+
* const token = url.searchParams.get('token');
|
|
11079
|
+
*
|
|
11080
|
+
* `request.url` on a Node server is a path — `/ws?token=…` — and `new URL`
|
|
11081
|
+
* refuses a relative input without a base. The base exists to satisfy the
|
|
11082
|
+
* parser and is thrown away; only the path and query are ever read. Every
|
|
11083
|
+
* Node HTTP handler that wants a query parameter is written this way, so
|
|
11084
|
+
* the rule fired on the framework idiom rather than on the defect.
|
|
11085
|
+
*
|
|
11086
|
+
* Narrow on purpose: the first argument must be `req.url` itself. The
|
|
11087
|
+
* dangerous shape passes a *path* the application chose —
|
|
11088
|
+
* `new URL('/reset?t=…', `https://${req.headers.host}`)` — and that is what
|
|
11089
|
+
* produces an attacker-controlled link. It does not match this guard.
|
|
11090
|
+
*/
|
|
11091
|
+
lineGuard: /\bnew\s+URL\s*\(\s*(?:req|request|ctx)(?:uest)?\s*\.\s*url\b\s*,/
|
|
10949
11092
|
},
|
|
10950
11093
|
// A recursive-merge prototype-pollution rule (`target[key] = source[key]`
|
|
10951
11094
|
// with no `__proto__` guard) was built and dropped. The bare copy-by-key is
|
|
@@ -11013,6 +11156,94 @@ function fileTextOf(lines) {
|
|
|
11013
11156
|
function withoutSingleQuoted(text) {
|
|
11014
11157
|
return text.replace(/'[^'\n]*'/g, "''");
|
|
11015
11158
|
}
|
|
11159
|
+
function calleeEndingAt(text, open) {
|
|
11160
|
+
let end = open;
|
|
11161
|
+
while (end > 0 && (text[end - 1] === " " || text[end - 1] === " ")) end -= 1;
|
|
11162
|
+
let start = end;
|
|
11163
|
+
while (start > 0 && /[\w$.]/.test(text[start - 1])) start -= 1;
|
|
11164
|
+
return text.slice(start, end);
|
|
11165
|
+
}
|
|
11166
|
+
function enclosingCallees(lines, index, back) {
|
|
11167
|
+
const before = lines.slice(Math.max(0, index - back), index).join("\n");
|
|
11168
|
+
const stack = [];
|
|
11169
|
+
let quote = null;
|
|
11170
|
+
for (let i = 0; i < before.length; i += 1) {
|
|
11171
|
+
const ch = before[i];
|
|
11172
|
+
if (quote) {
|
|
11173
|
+
if (ch === "\\") i += 1;
|
|
11174
|
+
else if (ch === quote) quote = null;
|
|
11175
|
+
continue;
|
|
11176
|
+
}
|
|
11177
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
11178
|
+
else if (ch === "(") stack.push(calleeEndingAt(before, i));
|
|
11179
|
+
else if (ch === ")") stack.pop();
|
|
11180
|
+
}
|
|
11181
|
+
return stack;
|
|
11182
|
+
}
|
|
11183
|
+
function interpolations(line) {
|
|
11184
|
+
const found = [];
|
|
11185
|
+
for (let i = 0; ; ) {
|
|
11186
|
+
const start = line.indexOf("${", i);
|
|
11187
|
+
if (start === -1) break;
|
|
11188
|
+
const end = line.indexOf("}", start + 2);
|
|
11189
|
+
if (end === -1) return null;
|
|
11190
|
+
found.push(line.slice(start + 2, end).trim());
|
|
11191
|
+
i = end + 1;
|
|
11192
|
+
}
|
|
11193
|
+
return found.length > 0 ? found : null;
|
|
11194
|
+
}
|
|
11195
|
+
function isConstantString(name, fileText) {
|
|
11196
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11197
|
+
return new RegExp(
|
|
11198
|
+
`\\bconst\\s+${escaped}\\s*(?::[^=\\n]+)?=\\s*(?:'[^'\\n]*'|"[^"\\n]*"|\`[^\`$\\n]*\`)`
|
|
11199
|
+
).test(fileText);
|
|
11200
|
+
}
|
|
11201
|
+
function interpolationsAreConstant(line, fileText) {
|
|
11202
|
+
const found = interpolations(line);
|
|
11203
|
+
if (!found) return false;
|
|
11204
|
+
return found.every(
|
|
11205
|
+
(expr) => /^[A-Za-z_$][\w$]*$/.test(expr) && isConstantString(expr, fileText)
|
|
11206
|
+
);
|
|
11207
|
+
}
|
|
11208
|
+
var FILL_LOOKAHEAD = 16;
|
|
11209
|
+
var NAME = String.raw`(?<![\w$])([A-Za-z_$][\w$]*)`;
|
|
11210
|
+
var ALLOCATION_BINDING = new RegExp(
|
|
11211
|
+
`${NAME}\\s*=\\s*(?:new\\s+Buffer\\s*\\(|Buffer\\s*\\.\\s*allocUnsafe(?:Slow)?\\s*\\()`
|
|
11212
|
+
);
|
|
11213
|
+
function allocationBinding(line) {
|
|
11214
|
+
return ALLOCATION_BINDING.exec(line)?.[1] ?? null;
|
|
11215
|
+
}
|
|
11216
|
+
var WRITE_SHAPES = [
|
|
11217
|
+
// `src.copy(name, …)` — name is the destination.
|
|
11218
|
+
/\.\s*copy\s*\(\s*([A-Za-z_$][\w$]*)\s*[,)]/g,
|
|
11219
|
+
// `name.fill(…)`, `name.write*(…)`, `name.set(…)`.
|
|
11220
|
+
new RegExp(`${NAME}\\s*\\.\\s*(?:fill|set|write[A-Za-z0-9]*)\\s*\\(`, "g"),
|
|
11221
|
+
// `name[i] = …`, but not `name[i] === …`.
|
|
11222
|
+
new RegExp(`${NAME}\\s*\\[[^\\]\\n]*\\]\\s*=(?!=)`, "g")
|
|
11223
|
+
];
|
|
11224
|
+
function writesInto(text, name) {
|
|
11225
|
+
for (const shape of WRITE_SHAPES) {
|
|
11226
|
+
shape.lastIndex = 0;
|
|
11227
|
+
for (let found = shape.exec(text); found !== null; found = shape.exec(text)) {
|
|
11228
|
+
if (found[1] === name) return true;
|
|
11229
|
+
}
|
|
11230
|
+
}
|
|
11231
|
+
return false;
|
|
11232
|
+
}
|
|
11233
|
+
function bufferFilledBeforeUse(ctx) {
|
|
11234
|
+
const line = ctx.lines[ctx.index] ?? "";
|
|
11235
|
+
if (/\ballocUnsafe(?:Slow)?\s*\([^)\n]*\)\s*\.\s*fill\s*\(/.test(line)) return true;
|
|
11236
|
+
const name = allocationBinding(line);
|
|
11237
|
+
if (!name) return false;
|
|
11238
|
+
if (writesInto(line.slice(line.indexOf("=") + 1), name)) return true;
|
|
11239
|
+
const last = Math.min(ctx.lines.length - 1, ctx.index + FILL_LOOKAHEAD);
|
|
11240
|
+
for (let i = ctx.index + 1; i <= last; i += 1) {
|
|
11241
|
+
const next = ctx.lines[i] ?? "";
|
|
11242
|
+
if (skippable(next, i, ctx.prose)) continue;
|
|
11243
|
+
if (writesInto(next, name)) return true;
|
|
11244
|
+
}
|
|
11245
|
+
return false;
|
|
11246
|
+
}
|
|
11016
11247
|
function evaluateRule(rule, ctx) {
|
|
11017
11248
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
11018
11249
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -11024,6 +11255,14 @@ function evaluateRule(rule, ctx) {
|
|
|
11024
11255
|
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
11025
11256
|
if (rule.requires && !rule.requires.test(context)) return null;
|
|
11026
11257
|
if (rule.lineGuard?.test(line)) return null;
|
|
11258
|
+
if (rule.enclosingCallGuard) {
|
|
11259
|
+
const callees = enclosingCallees(ctx.lines, ctx.index, back);
|
|
11260
|
+
if (callees.some((callee) => rule.enclosingCallGuard.test(callee))) return null;
|
|
11261
|
+
}
|
|
11262
|
+
if (rule.constantInterpolationGuard && interpolationsAreConstant(line, fileTextOf(ctx.lines))) {
|
|
11263
|
+
return null;
|
|
11264
|
+
}
|
|
11265
|
+
if (rule.filledBeforeUseGuard && bufferFilledBeforeUse(ctx)) return null;
|
|
11027
11266
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
11028
11267
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
11029
11268
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
@@ -11606,7 +11845,8 @@ var SECRET_RULES = [
|
|
|
11606
11845
|
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
|
|
11607
11846
|
severity: "medium",
|
|
11608
11847
|
cwe: "CWE-798",
|
|
11609
|
-
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived."
|
|
11848
|
+
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived.",
|
|
11849
|
+
keywordShaped: true
|
|
11610
11850
|
},
|
|
11611
11851
|
{
|
|
11612
11852
|
id: "secret-generic-api-key",
|
|
@@ -11617,7 +11857,8 @@ var SECRET_RULES = [
|
|
|
11617
11857
|
pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
|
|
11618
11858
|
severity: "high",
|
|
11619
11859
|
cwe: "CWE-798",
|
|
11620
|
-
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid."
|
|
11860
|
+
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid.",
|
|
11861
|
+
keywordShaped: true
|
|
11621
11862
|
},
|
|
11622
11863
|
{
|
|
11623
11864
|
id: "secret-generic-credential",
|
|
@@ -11625,7 +11866,8 @@ var SECRET_RULES = [
|
|
|
11625
11866
|
pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
|
|
11626
11867
|
severity: "high",
|
|
11627
11868
|
cwe: "CWE-798",
|
|
11628
|
-
consequence: "A password in source is a password in every clone, fork and CI cache of that source."
|
|
11869
|
+
consequence: "A password in source is a password in every clone, fork and CI cache of that source.",
|
|
11870
|
+
keywordShaped: true
|
|
11629
11871
|
},
|
|
11630
11872
|
{
|
|
11631
11873
|
id: "secret-hex-token",
|
|
@@ -11633,7 +11875,8 @@ var SECRET_RULES = [
|
|
|
11633
11875
|
pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
|
|
11634
11876
|
severity: "medium",
|
|
11635
11877
|
cwe: "CWE-798",
|
|
11636
|
-
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign."
|
|
11878
|
+
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign.",
|
|
11879
|
+
keywordShaped: true
|
|
11637
11880
|
}
|
|
11638
11881
|
];
|
|
11639
11882
|
var KNOWN_PLACEHOLDERS = [
|
|
@@ -11664,6 +11907,62 @@ var KNOWN_PLACEHOLDERS = [
|
|
|
11664
11907
|
function isKnownPlaceholder(text) {
|
|
11665
11908
|
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
11666
11909
|
}
|
|
11910
|
+
function isPlaceholderAttribute(line, value) {
|
|
11911
|
+
const at = line.lastIndexOf(value);
|
|
11912
|
+
if (at === -1) return false;
|
|
11913
|
+
return /(?:^|\s)(?:aria-)?placeholder\s*=\s*[{("'`]*$/i.test(line.slice(0, at));
|
|
11914
|
+
}
|
|
11915
|
+
function isVariableReference(value) {
|
|
11916
|
+
const trimmed = value.trim();
|
|
11917
|
+
const braced = /^\$\{\s*([A-Za-z_][\w.]*)\s*(?::?[-=+?]([\s\S]*))?\}$/.exec(trimmed);
|
|
11918
|
+
if (braced) {
|
|
11919
|
+
const fallback2 = braced[2];
|
|
11920
|
+
if (fallback2 === void 0 || fallback2.trim() === "") return true;
|
|
11921
|
+
return /^\$\{?[A-Za-z_][\w.]*\}?$/.test(fallback2.trim());
|
|
11922
|
+
}
|
|
11923
|
+
return /^\$[A-Za-z_]\w*$/.test(trimmed) || // $VAR
|
|
11924
|
+
/^\$\([\s\S]*\)$/.test(trimmed) || // $(command substitution)
|
|
11925
|
+
/^%[A-Za-z_]\w*%$/.test(trimmed) || // %VAR% on Windows
|
|
11926
|
+
/^\{\{[\s\S]*\}\}$/.test(trimmed) || // {{ template }}
|
|
11927
|
+
/^#\{[\s\S]*\}$/.test(trimmed) || // #{ruby}
|
|
11928
|
+
/^<%=?[\s\S]*%>$/.test(trimmed);
|
|
11929
|
+
}
|
|
11930
|
+
var FIXTURE_STEMS = [
|
|
11931
|
+
"test",
|
|
11932
|
+
"mock",
|
|
11933
|
+
"fake",
|
|
11934
|
+
"dummy",
|
|
11935
|
+
"stub",
|
|
11936
|
+
"sample",
|
|
11937
|
+
"example",
|
|
11938
|
+
"placeholder",
|
|
11939
|
+
"fixture",
|
|
11940
|
+
"invalid",
|
|
11941
|
+
"expired",
|
|
11942
|
+
"forged",
|
|
11943
|
+
"bogus",
|
|
11944
|
+
"notreal",
|
|
11945
|
+
"nonexistent",
|
|
11946
|
+
"changeme",
|
|
11947
|
+
"foobar",
|
|
11948
|
+
"lorem"
|
|
11949
|
+
];
|
|
11950
|
+
var KEY_NOISE = /* @__PURE__ */ new Set(["const", "this", "return", "await", "async", "expect", "value"]);
|
|
11951
|
+
function words(text) {
|
|
11952
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^A-Za-z]+/g, " ").toLowerCase().split(" ").filter(Boolean);
|
|
11953
|
+
}
|
|
11954
|
+
function isTestFixtureValue(line, value) {
|
|
11955
|
+
const valueWords = words(value);
|
|
11956
|
+
if (valueWords.some((word) => FIXTURE_STEMS.some((stem) => word.startsWith(stem)))) return true;
|
|
11957
|
+
return describesItsOwnKey(line, value);
|
|
11958
|
+
}
|
|
11959
|
+
function describesItsOwnKey(line, value) {
|
|
11960
|
+
if (value.length > 48) return false;
|
|
11961
|
+
const valueAt = line.lastIndexOf(value);
|
|
11962
|
+
const key = valueAt === -1 ? line : line.slice(0, valueAt);
|
|
11963
|
+
const flattened = words(value).join("");
|
|
11964
|
+
return words(key).filter((word) => word.length >= 4 && !KEY_NOISE.has(word)).some((word) => flattened.includes(word));
|
|
11965
|
+
}
|
|
11667
11966
|
function redactSecret(line) {
|
|
11668
11967
|
return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
|
|
11669
11968
|
if (match.length <= 12) return match;
|
|
@@ -11841,9 +12140,9 @@ function languageOfShebang(firstLine) {
|
|
|
11841
12140
|
}
|
|
11842
12141
|
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
11843
12142
|
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
11844
|
-
var
|
|
11845
|
-
function
|
|
11846
|
-
return
|
|
12143
|
+
var FOREIGN_SECURITY = /\b(?:nolint:[\w,]*gosec|nosec)\b/;
|
|
12144
|
+
function foreignSecurityMark(line) {
|
|
12145
|
+
return FOREIGN_SECURITY.test(line);
|
|
11847
12146
|
}
|
|
11848
12147
|
function collectSuppressions(lines) {
|
|
11849
12148
|
const byLine = /* @__PURE__ */ new Map();
|
|
@@ -11869,31 +12168,68 @@ function isSuppressed(suppressions, index, ruleId) {
|
|
|
11869
12168
|
}
|
|
11870
12169
|
function isTestPath(relativePath) {
|
|
11871
12170
|
const p = relativePath.replace(/\\/g, "/");
|
|
11872
|
-
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);
|
|
12171
|
+
return /(?:^|\/)(?:tests?|__tests__|__mocks__|spec|specs|fixtures?|mocks?|e2e|testdata|testutils?|harness)\//i.test(p) || /(?:^|\/)(?:test|conftest)_[^/]+$/i.test(p) || /[._-](?:test|spec)\.[a-z]+$/i.test(p) || /_test\.[a-z]+$/i.test(p);
|
|
12172
|
+
}
|
|
12173
|
+
function isDocPath(relativePath) {
|
|
12174
|
+
const p = relativePath.replace(/\\/g, "/");
|
|
12175
|
+
return /(?:^|\/)(?:docs?|examples?|samples?)\//i.test(p) || /\.(?:md|mdx|markdown|rst|adoc)$/i.test(p);
|
|
11873
12176
|
}
|
|
12177
|
+
var SOFTENING_ORDER = [
|
|
12178
|
+
"suppressed",
|
|
12179
|
+
"placeholder",
|
|
12180
|
+
"self-describing",
|
|
12181
|
+
"test",
|
|
12182
|
+
"docs"
|
|
12183
|
+
];
|
|
12184
|
+
function softening(reasons) {
|
|
12185
|
+
return SOFTENING_ORDER.find((reason) => reasons[reason]) ?? null;
|
|
12186
|
+
}
|
|
12187
|
+
var SECRET_SOFTENING = {
|
|
12188
|
+
test: "in a test file \u2014 usually a fixture, still worth confirming it is not a live credential",
|
|
12189
|
+
docs: "in documentation \u2014 usually an illustrative example, still worth confirming it is not a live credential",
|
|
12190
|
+
suppressed: "on a line already marked as a false positive for another linter's security rule",
|
|
12191
|
+
placeholder: "in example text an empty input field shows, not in data",
|
|
12192
|
+
"self-describing": "in a value that repeats the name of the field holding it \u2014 usually a description of a credential rather than one"
|
|
12193
|
+
};
|
|
12194
|
+
var CODE_SOFTENING = {
|
|
12195
|
+
test: "in a test file, where the construct is ordinary",
|
|
12196
|
+
docs: "in documentation or example code, which nothing runs",
|
|
12197
|
+
suppressed: "on a line another linter's security suppression already covers",
|
|
12198
|
+
placeholder: "in example text rather than in data",
|
|
12199
|
+
"self-describing": "in a value that describes itself"
|
|
12200
|
+
};
|
|
11874
12201
|
function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
11875
12202
|
const findings = [];
|
|
11876
12203
|
const lines = text.split("\n");
|
|
11877
12204
|
const suppressions = collectSuppressions(lines);
|
|
11878
12205
|
const inTests = isTestPath(relativePath);
|
|
12206
|
+
const inDocs = isDocPath(relativePath);
|
|
11879
12207
|
lines.forEach((line, index) => {
|
|
11880
12208
|
for (const rule of SECRET_RULES) {
|
|
11881
12209
|
const match = rule.pattern.exec(line);
|
|
11882
12210
|
if (!match) continue;
|
|
11883
12211
|
if (isKnownPlaceholder(match[0])) continue;
|
|
11884
12212
|
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
11885
|
-
const
|
|
12213
|
+
const value = match[1] ?? match[0];
|
|
12214
|
+
if (isVariableReference(value)) continue;
|
|
12215
|
+
if (inTests && rule.keywordShaped && isTestFixtureValue(line, value)) continue;
|
|
12216
|
+
const soft = softening({
|
|
12217
|
+
test: inTests,
|
|
12218
|
+
suppressed: foreignSecurityMark(line),
|
|
12219
|
+
placeholder: isPlaceholderAttribute(line, value),
|
|
12220
|
+
"self-describing": rule.keywordShaped === true && describesItsOwnKey(line, value)
|
|
12221
|
+
});
|
|
11886
12222
|
findings.push({
|
|
11887
12223
|
ruleId: rule.id,
|
|
11888
12224
|
title: rule.name,
|
|
11889
12225
|
file: relativePath,
|
|
11890
12226
|
line: index + 1,
|
|
11891
|
-
// Reported but not blocking
|
|
11892
|
-
//
|
|
11893
|
-
severity:
|
|
12227
|
+
// Reported but not blocking wherever context weakens the claim — see
|
|
12228
|
+
// `softening`. Never dropped: the count is the same either way.
|
|
12229
|
+
severity: soft ? "low" : rule.severity,
|
|
11894
12230
|
// A matched credential format is the finding, not a proxy for one.
|
|
11895
12231
|
confidence: "evidence",
|
|
11896
|
-
message:
|
|
12232
|
+
message: soft ? `Possible ${rule.name} detected ${SECRET_SOFTENING[soft]}` : `Possible ${rule.name} detected`,
|
|
11897
12233
|
consequence: rule.consequence,
|
|
11898
12234
|
cwe: rule.cwe,
|
|
11899
12235
|
excerpt: redactSecret(line.trim()).slice(0, 200),
|
|
@@ -11908,14 +12244,19 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
11908
12244
|
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
11909
12245
|
const match = evaluateRule(rule, { lines, index, language, prose });
|
|
11910
12246
|
if (!match) continue;
|
|
12247
|
+
const soft = softening({
|
|
12248
|
+
test: inTests,
|
|
12249
|
+
docs: inDocs,
|
|
12250
|
+
suppressed: foreignSecurityMark(lines[index] ?? "")
|
|
12251
|
+
});
|
|
11911
12252
|
findings.push({
|
|
11912
12253
|
ruleId: rule.id,
|
|
11913
12254
|
title: rule.title,
|
|
11914
12255
|
file: relativePath,
|
|
11915
12256
|
line: index + 1,
|
|
11916
|
-
severity: match.severity,
|
|
12257
|
+
severity: soft ? "low" : match.severity,
|
|
11917
12258
|
confidence: match.confidence,
|
|
11918
|
-
message: `${rule.title} (${rule.cwe})`,
|
|
12259
|
+
message: soft ? `${rule.title} (${rule.cwe}) \u2014 ${CODE_SOFTENING[soft]}` : `${rule.title} (${rule.cwe})`,
|
|
11919
12260
|
consequence: rule.consequence,
|
|
11920
12261
|
cwe: rule.cwe,
|
|
11921
12262
|
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
@@ -11926,14 +12267,19 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
11926
12267
|
for (const match of evaluateTemplateRules(extensionOf(relativePath), lines)) {
|
|
11927
12268
|
const index = match.line - 1;
|
|
11928
12269
|
if (isSuppressed(suppressions, index, match.rule.id)) continue;
|
|
12270
|
+
const soft = softening({
|
|
12271
|
+
test: inTests,
|
|
12272
|
+
docs: inDocs,
|
|
12273
|
+
suppressed: foreignSecurityMark(lines[index] ?? "")
|
|
12274
|
+
});
|
|
11929
12275
|
findings.push({
|
|
11930
12276
|
ruleId: match.rule.id,
|
|
11931
12277
|
title: match.rule.title,
|
|
11932
12278
|
file: relativePath,
|
|
11933
12279
|
line: match.line,
|
|
11934
|
-
severity: match.severity,
|
|
12280
|
+
severity: soft ? "low" : match.severity,
|
|
11935
12281
|
confidence: "pattern",
|
|
11936
|
-
message: `${match.rule.title} (${match.rule.cwe})`,
|
|
12282
|
+
message: soft ? `${match.rule.title} (${match.rule.cwe}) \u2014 ${CODE_SOFTENING[soft]}` : `${match.rule.title} (${match.rule.cwe})`,
|
|
11937
12283
|
consequence: match.rule.consequence,
|
|
11938
12284
|
cwe: match.rule.cwe,
|
|
11939
12285
|
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
@@ -11965,8 +12311,8 @@ function meetsFailThreshold(findings, threshold) {
|
|
|
11965
12311
|
}
|
|
11966
12312
|
|
|
11967
12313
|
// ../../packages/scan/src/node/walk.ts
|
|
11968
|
-
var
|
|
11969
|
-
var
|
|
12314
|
+
var import_node_fs6 = require("fs");
|
|
12315
|
+
var import_node_path3 = require("path");
|
|
11970
12316
|
function compileExcludes(patterns) {
|
|
11971
12317
|
const matchers = patterns.map((p) => p.trim()).filter((p) => p.length > 0 && !p.startsWith("#")).map(compilePattern);
|
|
11972
12318
|
if (matchers.length === 0) return () => false;
|
|
@@ -12037,7 +12383,7 @@ function matchPrefix(parts, segs) {
|
|
|
12037
12383
|
}
|
|
12038
12384
|
function readIgnoreFile(root) {
|
|
12039
12385
|
try {
|
|
12040
|
-
return (0,
|
|
12386
|
+
return (0, import_node_fs6.readFileSync)((0, import_node_path3.join)(root, ".threatcrushignore"), "utf-8").split("\n");
|
|
12041
12387
|
} catch {
|
|
12042
12388
|
return [];
|
|
12043
12389
|
}
|
|
@@ -12053,16 +12399,16 @@ function scanPath(targetPath, options = {}) {
|
|
|
12053
12399
|
let excluded = 0;
|
|
12054
12400
|
const rootIsDirectory = (() => {
|
|
12055
12401
|
try {
|
|
12056
|
-
return (0,
|
|
12402
|
+
return (0, import_node_fs6.statSync)(targetPath).isDirectory();
|
|
12057
12403
|
} catch {
|
|
12058
12404
|
return true;
|
|
12059
12405
|
}
|
|
12060
12406
|
})();
|
|
12061
|
-
const walkRoot = rootIsDirectory ? targetPath : (0,
|
|
12407
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path3.dirname)(targetPath);
|
|
12062
12408
|
const isExcluded = compileExcludes([...options.exclude ?? [], ...readIgnoreFile(walkRoot)]);
|
|
12063
12409
|
const scanFile = (fullPath, filename) => {
|
|
12064
12410
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
12065
|
-
const extension = (0,
|
|
12411
|
+
const extension = (0, import_node_path3.extname)(filename).toLowerCase();
|
|
12066
12412
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
12067
12413
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
12068
12414
|
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
@@ -12074,26 +12420,26 @@ function scanPath(targetPath, options = {}) {
|
|
|
12074
12420
|
let handle;
|
|
12075
12421
|
let declared = null;
|
|
12076
12422
|
try {
|
|
12077
|
-
handle = (0,
|
|
12423
|
+
handle = (0, import_node_fs6.openSync)(fullPath, "r");
|
|
12078
12424
|
} catch {
|
|
12079
12425
|
unreadable.push(relativePath);
|
|
12080
12426
|
return;
|
|
12081
12427
|
}
|
|
12082
12428
|
try {
|
|
12083
|
-
if ((0,
|
|
12429
|
+
if ((0, import_node_fs6.fstatSync)(handle).size > maxFileBytes) return;
|
|
12084
12430
|
if (mayDeclareInterpreter) {
|
|
12085
12431
|
const prefix = Buffer.alloc(128);
|
|
12086
|
-
const read = (0,
|
|
12432
|
+
const read = (0, import_node_fs6.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
12087
12433
|
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
12088
12434
|
if (!declared) return;
|
|
12089
12435
|
}
|
|
12090
|
-
text = (0,
|
|
12436
|
+
text = (0, import_node_fs6.readFileSync)(handle, "utf-8");
|
|
12091
12437
|
} catch {
|
|
12092
12438
|
unreadable.push(relativePath);
|
|
12093
12439
|
return;
|
|
12094
12440
|
} finally {
|
|
12095
12441
|
try {
|
|
12096
|
-
(0,
|
|
12442
|
+
(0, import_node_fs6.closeSync)(handle);
|
|
12097
12443
|
} catch {
|
|
12098
12444
|
}
|
|
12099
12445
|
}
|
|
@@ -12111,13 +12457,13 @@ function scanPath(targetPath, options = {}) {
|
|
|
12111
12457
|
const walk = (currentPath) => {
|
|
12112
12458
|
let entries;
|
|
12113
12459
|
try {
|
|
12114
|
-
entries = (0,
|
|
12460
|
+
entries = (0, import_node_fs6.readdirSync)(currentPath, { withFileTypes: true });
|
|
12115
12461
|
} catch {
|
|
12116
12462
|
unreadable.push(toRelative(walkRoot, currentPath));
|
|
12117
12463
|
return;
|
|
12118
12464
|
}
|
|
12119
12465
|
for (const entry of entries) {
|
|
12120
|
-
const fullPath = (0,
|
|
12466
|
+
const fullPath = (0, import_node_path3.join)(currentPath, entry.name);
|
|
12121
12467
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
12122
12468
|
if (entry.isDirectory()) {
|
|
12123
12469
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
@@ -12141,7 +12487,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
12141
12487
|
} else if (isExcluded(toRelative(walkRoot, targetPath))) {
|
|
12142
12488
|
excluded += 1;
|
|
12143
12489
|
} else {
|
|
12144
|
-
scanFile(targetPath, (0,
|
|
12490
|
+
scanFile(targetPath, (0, import_node_path3.basename)(targetPath));
|
|
12145
12491
|
}
|
|
12146
12492
|
if (options.missingControls) findings.push(...controls.findings());
|
|
12147
12493
|
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
@@ -12173,32 +12519,37 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
|
12173
12519
|
}
|
|
12174
12520
|
}
|
|
12175
12521
|
function toRelative(base, target) {
|
|
12176
|
-
const rel = (0,
|
|
12177
|
-
return (rel === "" ? target : rel).split(
|
|
12522
|
+
const rel = (0, import_node_path3.relative)(base, target);
|
|
12523
|
+
return (rel === "" ? target : rel).split(import_node_path3.sep).join("/");
|
|
12178
12524
|
}
|
|
12179
12525
|
|
|
12180
12526
|
// ../../packages/scan/src/node/dependencies.ts
|
|
12181
|
-
var
|
|
12182
|
-
var
|
|
12183
|
-
var LOCKFILES = [
|
|
12184
|
-
{ file: "package-lock.json", ecosystem: "npm" },
|
|
12185
|
-
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
12186
|
-
{ file: "yarn.lock", ecosystem: "npm" },
|
|
12187
|
-
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
12188
|
-
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
12189
|
-
];
|
|
12527
|
+
var import_node_fs7 = require("fs");
|
|
12528
|
+
var import_node_path4 = require("path");
|
|
12190
12529
|
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
12191
12530
|
async function scanDependencies(targetPath) {
|
|
12192
12531
|
const findings = [];
|
|
12193
|
-
for (const { file, ecosystem } of LOCKFILES) {
|
|
12194
|
-
const lockPath = (0,
|
|
12195
|
-
if (!(0,
|
|
12532
|
+
for (const { file, ecosystem, parse } of LOCKFILES) {
|
|
12533
|
+
const lockPath = (0, import_node_path4.join)(targetPath, file);
|
|
12534
|
+
if (!(0, import_node_fs7.existsSync)(lockPath)) continue;
|
|
12196
12535
|
let deps;
|
|
12197
12536
|
try {
|
|
12198
|
-
deps =
|
|
12537
|
+
deps = dedupe(parse((0, import_node_fs7.readFileSync)(lockPath, "utf-8")));
|
|
12199
12538
|
} catch {
|
|
12200
12539
|
continue;
|
|
12201
12540
|
}
|
|
12541
|
+
if (deps.length === 0) {
|
|
12542
|
+
findings.push(incompleteFinding(file, "No dependencies could be read from this lockfile."));
|
|
12543
|
+
continue;
|
|
12544
|
+
}
|
|
12545
|
+
if (deps.length > MAX_DEPS_PER_LOCKFILE) {
|
|
12546
|
+
findings.push(
|
|
12547
|
+
incompleteFinding(
|
|
12548
|
+
file,
|
|
12549
|
+
`Only the first ${MAX_DEPS_PER_LOCKFILE} of ${deps.length} locked packages were checked against OSV.`
|
|
12550
|
+
)
|
|
12551
|
+
);
|
|
12552
|
+
}
|
|
12202
12553
|
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
12203
12554
|
let vulns;
|
|
12204
12555
|
try {
|
|
@@ -12225,6 +12576,20 @@ async function scanDependencies(targetPath) {
|
|
|
12225
12576
|
}
|
|
12226
12577
|
return findings;
|
|
12227
12578
|
}
|
|
12579
|
+
function incompleteFinding(file, message) {
|
|
12580
|
+
return {
|
|
12581
|
+
ruleId: "dependency-scan-incomplete",
|
|
12582
|
+
title: "Dependency scan incomplete",
|
|
12583
|
+
file,
|
|
12584
|
+
line: 1,
|
|
12585
|
+
severity: "low",
|
|
12586
|
+
confidence: "evidence",
|
|
12587
|
+
message,
|
|
12588
|
+
consequence: "Advisories affecting the unchecked packages would not appear in this report.",
|
|
12589
|
+
excerpt: file,
|
|
12590
|
+
category: "dependency"
|
|
12591
|
+
};
|
|
12592
|
+
}
|
|
12228
12593
|
function severityFromCvss(score) {
|
|
12229
12594
|
if (!score) return "medium";
|
|
12230
12595
|
const value = Number.parseFloat(score);
|
|
@@ -12234,26 +12599,131 @@ function severityFromCvss(score) {
|
|
|
12234
12599
|
if (value >= 4) return "medium";
|
|
12235
12600
|
return "low";
|
|
12236
12601
|
}
|
|
12237
|
-
function
|
|
12602
|
+
function dedupe(deps) {
|
|
12603
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12604
|
+
const unique = [];
|
|
12605
|
+
for (const dep of deps) {
|
|
12606
|
+
const key = `${dep.name}@${dep.version}`;
|
|
12607
|
+
if (seen.has(key)) continue;
|
|
12608
|
+
seen.add(key);
|
|
12609
|
+
unique.push(dep);
|
|
12610
|
+
}
|
|
12611
|
+
return unique;
|
|
12612
|
+
}
|
|
12613
|
+
function splitNameVersion(spec) {
|
|
12614
|
+
const at = spec.lastIndexOf("@");
|
|
12615
|
+
if (at <= 0) return null;
|
|
12616
|
+
const name = spec.slice(0, at);
|
|
12617
|
+
const version = spec.slice(at + 1);
|
|
12618
|
+
if (!name || !version) return null;
|
|
12619
|
+
return { name, version };
|
|
12620
|
+
}
|
|
12621
|
+
function exactVersion(raw) {
|
|
12622
|
+
const version = raw.trim().replace(/^[=v]+/, "");
|
|
12623
|
+
return /^[0-9][0-9a-zA-Z.+-]*$/.test(version) ? version : null;
|
|
12624
|
+
}
|
|
12625
|
+
function parsePackageLock(content) {
|
|
12626
|
+
const lock = JSON.parse(content);
|
|
12627
|
+
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
12628
|
+
const deps = [];
|
|
12629
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
12630
|
+
const name = key.replace(/^.*node_modules\//, "");
|
|
12631
|
+
const version = value?.version;
|
|
12632
|
+
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
12633
|
+
}
|
|
12634
|
+
return deps;
|
|
12635
|
+
}
|
|
12636
|
+
function parsePnpmLock(content) {
|
|
12637
|
+
const deps = [];
|
|
12638
|
+
let inPackages = false;
|
|
12639
|
+
for (const line of content.split("\n")) {
|
|
12640
|
+
if (/^[a-zA-Z]/.test(line)) {
|
|
12641
|
+
inPackages = line.startsWith("packages:");
|
|
12642
|
+
continue;
|
|
12643
|
+
}
|
|
12644
|
+
if (!inPackages) continue;
|
|
12645
|
+
const match = /^ {2}(?! )(.+):\s*$/.exec(line);
|
|
12646
|
+
if (!match?.[1]) continue;
|
|
12647
|
+
let key = match[1].trim().replace(/^['"]|['"]$/g, "");
|
|
12648
|
+
key = key.replace(/^\//, "");
|
|
12649
|
+
key = key.replace(/\(.*$/, "");
|
|
12650
|
+
let name;
|
|
12651
|
+
let rawVersion;
|
|
12652
|
+
const slashed = /^(@?[^@]+)\/([0-9][^/]*)$/.exec(key);
|
|
12653
|
+
if (slashed?.[1] && slashed[2]) {
|
|
12654
|
+
name = slashed[1];
|
|
12655
|
+
rawVersion = slashed[2];
|
|
12656
|
+
} else {
|
|
12657
|
+
const dep = splitNameVersion(key);
|
|
12658
|
+
if (!dep) continue;
|
|
12659
|
+
name = dep.name;
|
|
12660
|
+
rawVersion = dep.version;
|
|
12661
|
+
}
|
|
12662
|
+
const version = exactVersion(rawVersion.replace(/_.*$/, ""));
|
|
12663
|
+
if (version) deps.push({ name, version });
|
|
12664
|
+
}
|
|
12665
|
+
return deps;
|
|
12666
|
+
}
|
|
12667
|
+
function parseYarnLock(content) {
|
|
12238
12668
|
const deps = [];
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
const
|
|
12245
|
-
|
|
12669
|
+
let pendingName = null;
|
|
12670
|
+
for (const line of content.split("\n")) {
|
|
12671
|
+
if (line.startsWith("#") || line.trim() === "") continue;
|
|
12672
|
+
if (!/^\s/.test(line)) {
|
|
12673
|
+
pendingName = null;
|
|
12674
|
+
const header = line.replace(/:\s*$/, "");
|
|
12675
|
+
const first = header.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
|
|
12676
|
+
if (!first) continue;
|
|
12677
|
+
if (!first.includes("@") || first === "__metadata") continue;
|
|
12678
|
+
if (/@(?:workspace|file|link|portal|exec|patch):/.test(first)) continue;
|
|
12679
|
+
const dep = splitNameVersion(first.replace(/@npm:/, "@"));
|
|
12680
|
+
if (dep) pendingName = dep.name;
|
|
12681
|
+
continue;
|
|
12246
12682
|
}
|
|
12247
|
-
|
|
12683
|
+
if (!pendingName) continue;
|
|
12684
|
+
const version = /^\s+version:?\s+["']?([^"'\s]+)["']?\s*$/.exec(line);
|
|
12685
|
+
if (!version?.[1]) continue;
|
|
12686
|
+
const exact = exactVersion(version[1]);
|
|
12687
|
+
if (exact) deps.push({ name: pendingName, version: exact });
|
|
12688
|
+
pendingName = null;
|
|
12248
12689
|
}
|
|
12249
|
-
|
|
12250
|
-
|
|
12251
|
-
|
|
12252
|
-
|
|
12690
|
+
return deps;
|
|
12691
|
+
}
|
|
12692
|
+
function parsePipfileLock(content) {
|
|
12693
|
+
const lock = JSON.parse(content);
|
|
12694
|
+
const deps = [];
|
|
12695
|
+
for (const section of ["default", "develop"]) {
|
|
12696
|
+
const packages = lock[section];
|
|
12697
|
+
if (!packages || typeof packages !== "object") continue;
|
|
12698
|
+
for (const [name, value] of Object.entries(packages)) {
|
|
12699
|
+
const version = exactVersion(String(value?.version ?? "").replace(/^==/, ""));
|
|
12700
|
+
if (name && version) deps.push({ name, version });
|
|
12253
12701
|
}
|
|
12254
12702
|
}
|
|
12255
12703
|
return deps;
|
|
12256
12704
|
}
|
|
12705
|
+
function parseRequirementsTxt(content) {
|
|
12706
|
+
const deps = [];
|
|
12707
|
+
for (const raw of content.split("\n")) {
|
|
12708
|
+
const line = raw.split("#")[0]?.split(";")[0]?.trim();
|
|
12709
|
+
if (!line || line.startsWith("-")) continue;
|
|
12710
|
+
const match = /^([a-zA-Z0-9._-]+)\s*(?:\[[^\]]*\])?\s*==\s*([^\s,]+)/.exec(line);
|
|
12711
|
+
if (!match?.[1] || !match[2]) continue;
|
|
12712
|
+
const version = exactVersion(match[2]);
|
|
12713
|
+
if (version) deps.push({ name: match[1], version });
|
|
12714
|
+
}
|
|
12715
|
+
return deps;
|
|
12716
|
+
}
|
|
12717
|
+
var LOCKFILES = [
|
|
12718
|
+
{ file: "package-lock.json", ecosystem: "npm", parse: parsePackageLock },
|
|
12719
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm", parse: parsePnpmLock },
|
|
12720
|
+
{ file: "yarn.lock", ecosystem: "npm", parse: parseYarnLock },
|
|
12721
|
+
{ file: "requirements.txt", ecosystem: "PyPI", parse: parseRequirementsTxt },
|
|
12722
|
+
{ file: "Pipfile.lock", ecosystem: "PyPI", parse: parsePipfileLock }
|
|
12723
|
+
];
|
|
12724
|
+
var LOCKFILE_PARSERS = Object.fromEntries(
|
|
12725
|
+
LOCKFILES.map((entry) => [entry.file, entry.parse])
|
|
12726
|
+
);
|
|
12257
12727
|
function isValidPackageName(name) {
|
|
12258
12728
|
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
12259
12729
|
}
|
|
@@ -12278,12 +12748,12 @@ async function queryOsv(name, version, ecosystem) {
|
|
|
12278
12748
|
}
|
|
12279
12749
|
|
|
12280
12750
|
// ../../packages/scan/src/node/sarif.ts
|
|
12281
|
-
var
|
|
12282
|
-
var
|
|
12751
|
+
var import_node_crypto2 = require("crypto");
|
|
12752
|
+
var import_node_path5 = require("path");
|
|
12283
12753
|
var FINGERPRINT_KEY = "threatcrush/contentHash/v1";
|
|
12284
12754
|
function fingerprintOf(finding) {
|
|
12285
12755
|
const content = finding.excerpt.replace(/\s+/g, " ").trim();
|
|
12286
|
-
return (0,
|
|
12756
|
+
return (0, import_node_crypto2.createHash)("sha256").update(`${finding.ruleId}
|
|
12287
12757
|
${finding.file}
|
|
12288
12758
|
${content}`).digest("hex").slice(0, 32);
|
|
12289
12759
|
}
|
|
@@ -12317,11 +12787,11 @@ function securitySeverity(severity) {
|
|
|
12317
12787
|
}
|
|
12318
12788
|
}
|
|
12319
12789
|
function toArtifactUri(filePath, base, prefix = "", root = base) {
|
|
12320
|
-
const absolute = (0,
|
|
12321
|
-
const relativePath = (0,
|
|
12790
|
+
const absolute = (0, import_node_path5.isAbsolute)(filePath) ? filePath : (0, import_node_path5.resolve)(root, filePath);
|
|
12791
|
+
const relativePath = (0, import_node_path5.relative)(base, absolute);
|
|
12322
12792
|
const escapedOut = relativePath.startsWith("..") || relativePath === "";
|
|
12323
12793
|
const chosen = escapedOut ? absolute : relativePath;
|
|
12324
|
-
const posix = chosen.split(
|
|
12794
|
+
const posix = chosen.split(import_node_path5.sep).join("/").replace(/^\.\//, "");
|
|
12325
12795
|
if (!prefix || escapedOut) return posix;
|
|
12326
12796
|
const trimmed = prefix.replace(/^\/+|\/+$/g, "");
|
|
12327
12797
|
return trimmed ? `${trimmed}/${posix}` : posix;
|
|
@@ -12411,11 +12881,11 @@ ${finding.consequence}` : `**${finding.title}**`
|
|
|
12411
12881
|
// src/commands/scan.ts
|
|
12412
12882
|
function readVersion() {
|
|
12413
12883
|
for (const candidate of [
|
|
12414
|
-
(0,
|
|
12415
|
-
(0,
|
|
12884
|
+
(0, import_node_path6.join)(__dirname, "..", "package.json"),
|
|
12885
|
+
(0, import_node_path6.join)(__dirname, "..", "..", "package.json")
|
|
12416
12886
|
]) {
|
|
12417
12887
|
try {
|
|
12418
|
-
return JSON.parse((0,
|
|
12888
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
|
|
12419
12889
|
} catch {
|
|
12420
12890
|
}
|
|
12421
12891
|
}
|
|
@@ -12482,7 +12952,7 @@ async function scanCommand(targetPath, options = {}) {
|
|
|
12482
12952
|
const say = machineReadable ? (line) => process.stderr.write(`${line}
|
|
12483
12953
|
`) : (line) => process.stdout.write(`${line}
|
|
12484
12954
|
`);
|
|
12485
|
-
if (!(0,
|
|
12955
|
+
if (!(0, import_node_fs8.existsSync)(targetPath)) {
|
|
12486
12956
|
say(source_default.red(`Scan target does not exist: ${targetPath}`));
|
|
12487
12957
|
process.exitCode = 2;
|
|
12488
12958
|
return failedResult(targetPath, `no such path: ${targetPath}`);
|
|
@@ -12578,7 +13048,7 @@ function emitMachineReadable(format, outcome, targetPath, options, say) {
|
|
|
12578
13048
|
// and it fails silently. `--path-prefix` covers the remaining case:
|
|
12579
13049
|
// a scan run from inside the subdirectory it is scanning.
|
|
12580
13050
|
base: process.cwd(),
|
|
12581
|
-
root: (0,
|
|
13051
|
+
root: (0, import_node_path6.resolve)(outcome.root)
|
|
12582
13052
|
}) : {
|
|
12583
13053
|
tool: "threatcrush",
|
|
12584
13054
|
version: PKG_VERSION,
|
|
@@ -12592,8 +13062,8 @@ function emitMachineReadable(format, outcome, targetPath, options, say) {
|
|
|
12592
13062
|
const serialized = `${JSON.stringify(payload, null, 2)}
|
|
12593
13063
|
`;
|
|
12594
13064
|
if (options.output) {
|
|
12595
|
-
(0,
|
|
12596
|
-
(0,
|
|
13065
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path6.dirname)((0, import_node_path6.resolve)(options.output)), { recursive: true });
|
|
13066
|
+
(0, import_node_fs8.writeFileSync)(options.output, serialized, "utf-8");
|
|
12597
13067
|
say(
|
|
12598
13068
|
source_default.gray(
|
|
12599
13069
|
` ${format.toUpperCase()} written to ${options.output} (${outcome.findings.length} finding(s))`
|
|
@@ -12645,13 +13115,13 @@ function printHuman(outcome) {
|
|
|
12645
13115
|
}
|
|
12646
13116
|
|
|
12647
13117
|
// src/commands/init.ts
|
|
12648
|
-
var
|
|
13118
|
+
var import_node_fs11 = require("fs");
|
|
12649
13119
|
var import_node_child_process = require("child_process");
|
|
12650
13120
|
var import_node_readline3 = __toESM(require("readline"));
|
|
12651
13121
|
|
|
12652
13122
|
// src/core/config.ts
|
|
12653
|
-
var
|
|
12654
|
-
var
|
|
13123
|
+
var import_node_fs9 = require("fs");
|
|
13124
|
+
var import_node_path7 = require("path");
|
|
12655
13125
|
var import_toml = __toESM(require_toml());
|
|
12656
13126
|
var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
|
|
12657
13127
|
var DEFAULT_CONFDIR = "/etc/threatcrush/threatcrushd.conf.d";
|
|
@@ -12677,11 +13147,11 @@ var DEFAULT_CONFIG = {
|
|
|
12677
13147
|
};
|
|
12678
13148
|
function loadConfig(configPath) {
|
|
12679
13149
|
const path = configPath || DEFAULT_CONFIG_PATH;
|
|
12680
|
-
if (!(0,
|
|
13150
|
+
if (!(0, import_node_fs9.existsSync)(path)) {
|
|
12681
13151
|
return { ...DEFAULT_CONFIG };
|
|
12682
13152
|
}
|
|
12683
13153
|
try {
|
|
12684
|
-
const raw = (0,
|
|
13154
|
+
const raw = (0, import_node_fs9.readFileSync)(path, "utf-8");
|
|
12685
13155
|
const parsed = import_toml.default.parse(raw);
|
|
12686
13156
|
return {
|
|
12687
13157
|
daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
|
|
@@ -12697,13 +13167,13 @@ function loadConfig(configPath) {
|
|
|
12697
13167
|
function loadModuleConfigs(confDir) {
|
|
12698
13168
|
const dir = confDir || DEFAULT_CONFDIR;
|
|
12699
13169
|
const configs = /* @__PURE__ */ new Map();
|
|
12700
|
-
if (!(0,
|
|
13170
|
+
if (!(0, import_node_fs9.existsSync)(dir)) {
|
|
12701
13171
|
return configs;
|
|
12702
13172
|
}
|
|
12703
|
-
const files = (0,
|
|
13173
|
+
const files = (0, import_node_fs9.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
|
|
12704
13174
|
for (const file of files) {
|
|
12705
13175
|
try {
|
|
12706
|
-
const raw = (0,
|
|
13176
|
+
const raw = (0, import_node_fs9.readFileSync)((0, import_node_path7.join)(dir, file), "utf-8");
|
|
12707
13177
|
const parsed = import_toml.default.parse(raw);
|
|
12708
13178
|
for (const [name, config] of Object.entries(parsed)) {
|
|
12709
13179
|
configs.set(name, config);
|
|
@@ -12732,23 +13202,23 @@ function generateModuleConfig(moduleName, defaults = {}) {
|
|
|
12732
13202
|
}
|
|
12733
13203
|
|
|
12734
13204
|
// src/core/cli-config.ts
|
|
12735
|
-
var
|
|
12736
|
-
var
|
|
13205
|
+
var import_node_fs10 = require("fs");
|
|
13206
|
+
var import_node_path8 = require("path");
|
|
12737
13207
|
var import_node_os4 = require("os");
|
|
12738
|
-
var CLI_CONFIG_DIR = (0,
|
|
12739
|
-
var CLI_CONFIG_PATH = (0,
|
|
13208
|
+
var CLI_CONFIG_DIR = (0, import_node_path8.join)((0, import_node_os4.homedir)(), ".threatcrush");
|
|
13209
|
+
var CLI_CONFIG_PATH = (0, import_node_path8.join)(CLI_CONFIG_DIR, "config.json");
|
|
12740
13210
|
function readCliConfig() {
|
|
12741
13211
|
try {
|
|
12742
|
-
return JSON.parse((0,
|
|
13212
|
+
return JSON.parse((0, import_node_fs10.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
|
|
12743
13213
|
} catch {
|
|
12744
13214
|
return {};
|
|
12745
13215
|
}
|
|
12746
13216
|
}
|
|
12747
13217
|
function writeCliConfig(config) {
|
|
12748
|
-
if (!(0,
|
|
12749
|
-
(0,
|
|
13218
|
+
if (!(0, import_node_fs10.existsSync)(CLI_CONFIG_DIR)) (0, import_node_fs10.mkdirSync)(CLI_CONFIG_DIR, { recursive: true });
|
|
13219
|
+
(0, import_node_fs10.writeFileSync)(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
12750
13220
|
try {
|
|
12751
|
-
(0,
|
|
13221
|
+
(0, import_node_fs10.chmodSync)(CLI_CONFIG_PATH, 384);
|
|
12752
13222
|
} catch {
|
|
12753
13223
|
}
|
|
12754
13224
|
}
|
|
@@ -12993,7 +13463,7 @@ function binaryExists(name) {
|
|
|
12993
13463
|
}
|
|
12994
13464
|
}
|
|
12995
13465
|
function findLogPath(paths) {
|
|
12996
|
-
return paths.find((p) => (0,
|
|
13466
|
+
return paths.find((p) => (0, import_node_fs11.existsSync)(p));
|
|
12997
13467
|
}
|
|
12998
13468
|
async function promptYesNo(question, fallback2) {
|
|
12999
13469
|
const rl = import_node_readline3.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -13109,11 +13579,11 @@ async function initCommand() {
|
|
|
13109
13579
|
}
|
|
13110
13580
|
} else {
|
|
13111
13581
|
const spinner2 = ora({ text: "Writing configuration files...", color: "green" }).start();
|
|
13112
|
-
(0,
|
|
13113
|
-
(0,
|
|
13114
|
-
(0,
|
|
13582
|
+
(0, import_node_fs11.mkdirSync)(confDDir, { recursive: true });
|
|
13583
|
+
(0, import_node_fs11.mkdirSync)("/var/log/threatcrush", { recursive: true });
|
|
13584
|
+
(0, import_node_fs11.mkdirSync)("/var/lib/threatcrush", { recursive: true });
|
|
13115
13585
|
const mainConfig = generateDefaultConfig(detected.map((d) => d.name));
|
|
13116
|
-
(0,
|
|
13586
|
+
(0, import_node_fs11.writeFileSync)(`${configDir}/threatcrushd.conf`, mainConfig);
|
|
13117
13587
|
for (const svc of detected) {
|
|
13118
13588
|
const svcDef = SERVICES_TO_DETECT.find((s) => s.name === svc.name);
|
|
13119
13589
|
if (!svcDef) continue;
|
|
@@ -13122,7 +13592,7 @@ async function initCommand() {
|
|
|
13122
13592
|
...svcDef.moduleConfig,
|
|
13123
13593
|
log_path: svc.logPath || svcDef.logPaths[0]
|
|
13124
13594
|
});
|
|
13125
|
-
(0,
|
|
13595
|
+
(0, import_node_fs11.writeFileSync)(`${confDDir}/${modName}.conf`, modConfig);
|
|
13126
13596
|
}
|
|
13127
13597
|
spinner2.succeed("Configuration written successfully");
|
|
13128
13598
|
console.log();
|
|
@@ -13140,8 +13610,8 @@ async function initCommand() {
|
|
|
13140
13610
|
}
|
|
13141
13611
|
function checkWriteAccess(dir) {
|
|
13142
13612
|
try {
|
|
13143
|
-
if (!(0,
|
|
13144
|
-
(0,
|
|
13613
|
+
if (!(0, import_node_fs11.existsSync)(dir)) {
|
|
13614
|
+
(0, import_node_fs11.mkdirSync)(dir, { recursive: true });
|
|
13145
13615
|
}
|
|
13146
13616
|
return true;
|
|
13147
13617
|
} catch {
|
|
@@ -13150,8 +13620,8 @@ function checkWriteAccess(dir) {
|
|
|
13150
13620
|
}
|
|
13151
13621
|
|
|
13152
13622
|
// src/core/module-loader.ts
|
|
13153
|
-
var
|
|
13154
|
-
var
|
|
13623
|
+
var import_node_fs12 = require("fs");
|
|
13624
|
+
var import_node_path9 = require("path");
|
|
13155
13625
|
var import_toml2 = __toESM(require_toml());
|
|
13156
13626
|
init_paths();
|
|
13157
13627
|
function discoverModules(moduleDir, confDir) {
|
|
@@ -13159,22 +13629,22 @@ function discoverModules(moduleDir, confDir) {
|
|
|
13159
13629
|
const configs = loadModuleConfigs(confDir || PATHS.confD);
|
|
13160
13630
|
const searchPaths = [
|
|
13161
13631
|
moduleDir || PATHS.moduleDir,
|
|
13162
|
-
(0,
|
|
13632
|
+
(0, import_node_path9.resolve)(process.cwd(), "modules")
|
|
13163
13633
|
];
|
|
13164
|
-
const builtinDir = (0,
|
|
13165
|
-
if ((0,
|
|
13634
|
+
const builtinDir = (0, import_node_path9.resolve)(__dirname || ".", "..", "modules");
|
|
13635
|
+
if ((0, import_node_fs12.existsSync)(builtinDir)) {
|
|
13166
13636
|
searchPaths.push(builtinDir);
|
|
13167
13637
|
}
|
|
13168
13638
|
for (const basePath of searchPaths) {
|
|
13169
|
-
if (!(0,
|
|
13170
|
-
const entries = (0,
|
|
13639
|
+
if (!(0, import_node_fs12.existsSync)(basePath)) continue;
|
|
13640
|
+
const entries = (0, import_node_fs12.readdirSync)(basePath, { withFileTypes: true });
|
|
13171
13641
|
for (const entry of entries) {
|
|
13172
13642
|
if (!entry.isDirectory()) continue;
|
|
13173
|
-
const modPath = (0,
|
|
13174
|
-
const manifestPath = (0,
|
|
13175
|
-
if (!(0,
|
|
13643
|
+
const modPath = (0, import_node_path9.join)(basePath, entry.name);
|
|
13644
|
+
const manifestPath = (0, import_node_path9.join)(modPath, "mod.toml");
|
|
13645
|
+
if (!(0, import_node_fs12.existsSync)(manifestPath)) continue;
|
|
13176
13646
|
try {
|
|
13177
|
-
const raw = (0,
|
|
13647
|
+
const raw = (0, import_node_fs12.readFileSync)(manifestPath, "utf-8");
|
|
13178
13648
|
const manifest = import_toml2.default.parse(raw);
|
|
13179
13649
|
const config = configs.get(manifest.module.name) || { enabled: true };
|
|
13180
13650
|
modules.push({
|
|
@@ -13293,11 +13763,163 @@ function formatUptime(seconds) {
|
|
|
13293
13763
|
|
|
13294
13764
|
// src/commands/modules.ts
|
|
13295
13765
|
var import_node_child_process2 = require("child_process");
|
|
13296
|
-
var
|
|
13297
|
-
var
|
|
13766
|
+
var import_node_fs14 = require("fs");
|
|
13767
|
+
var import_node_path11 = require("path");
|
|
13298
13768
|
var import_toml3 = __toESM(require_toml());
|
|
13299
13769
|
init_paths();
|
|
13300
13770
|
init_pidfile();
|
|
13771
|
+
|
|
13772
|
+
// src/daemon/module-trust.ts
|
|
13773
|
+
var import_node_crypto3 = require("crypto");
|
|
13774
|
+
var import_node_fs13 = require("fs");
|
|
13775
|
+
var import_node_path10 = require("path");
|
|
13776
|
+
init_paths();
|
|
13777
|
+
var TRUST_FILE = (0, import_node_path10.join)(PATHS.configDir, "trusted-modules.json");
|
|
13778
|
+
var PUBLISHER_KEYS_FILE = (0, import_node_path10.join)(PATHS.configDir, "publisher-keys.json");
|
|
13779
|
+
var DIGEST_EXCLUDED_DIRS = /* @__PURE__ */ new Set([".git"]);
|
|
13780
|
+
function collectFiles(root, dir = root, out = []) {
|
|
13781
|
+
for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true }).sort(
|
|
13782
|
+
(a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
13783
|
+
)) {
|
|
13784
|
+
const full = (0, import_node_path10.join)(dir, entry.name);
|
|
13785
|
+
if (entry.isSymbolicLink()) {
|
|
13786
|
+
out.push({ path: full, isSymlink: true });
|
|
13787
|
+
} else if (entry.isDirectory()) {
|
|
13788
|
+
if (DIGEST_EXCLUDED_DIRS.has(entry.name)) continue;
|
|
13789
|
+
collectFiles(root, full, out);
|
|
13790
|
+
} else if (entry.isFile()) {
|
|
13791
|
+
out.push({ path: full, isSymlink: false });
|
|
13792
|
+
}
|
|
13793
|
+
}
|
|
13794
|
+
return out;
|
|
13795
|
+
}
|
|
13796
|
+
function computeModuleDigest(modulePath) {
|
|
13797
|
+
const hash = (0, import_node_crypto3.createHash)("sha256");
|
|
13798
|
+
for (const entry of collectFiles(modulePath)) {
|
|
13799
|
+
const relPath = (0, import_node_path10.relative)(modulePath, entry.path).split(import_node_path10.sep).join("/");
|
|
13800
|
+
hash.update(relPath);
|
|
13801
|
+
hash.update("\0");
|
|
13802
|
+
if (entry.isSymlink) {
|
|
13803
|
+
hash.update("symlink:");
|
|
13804
|
+
hash.update((0, import_node_fs13.readlinkSync)(entry.path));
|
|
13805
|
+
} else {
|
|
13806
|
+
hash.update("file:");
|
|
13807
|
+
hash.update((0, import_node_fs13.readFileSync)(entry.path));
|
|
13808
|
+
}
|
|
13809
|
+
hash.update("\0");
|
|
13810
|
+
}
|
|
13811
|
+
return hash.digest("hex");
|
|
13812
|
+
}
|
|
13813
|
+
function readTrustFile() {
|
|
13814
|
+
if (!(0, import_node_fs13.existsSync)(TRUST_FILE)) return { version: 1, modules: {} };
|
|
13815
|
+
try {
|
|
13816
|
+
const parsed = JSON.parse((0, import_node_fs13.readFileSync)(TRUST_FILE, "utf-8"));
|
|
13817
|
+
if (parsed.version !== 1 || typeof parsed.modules !== "object" || !parsed.modules) {
|
|
13818
|
+
return { version: 1, modules: {} };
|
|
13819
|
+
}
|
|
13820
|
+
return { version: 1, modules: parsed.modules };
|
|
13821
|
+
} catch {
|
|
13822
|
+
return { version: 1, modules: {} };
|
|
13823
|
+
}
|
|
13824
|
+
}
|
|
13825
|
+
function writeTrustFile(file) {
|
|
13826
|
+
(0, import_node_fs13.writeFileSync)(TRUST_FILE, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
|
|
13827
|
+
try {
|
|
13828
|
+
(0, import_node_fs13.chmodSync)(TRUST_FILE, 384);
|
|
13829
|
+
} catch {
|
|
13830
|
+
}
|
|
13831
|
+
}
|
|
13832
|
+
function trustModule(name, modulePath, source) {
|
|
13833
|
+
const file = readTrustFile();
|
|
13834
|
+
const record = {
|
|
13835
|
+
digest: computeModuleDigest(modulePath),
|
|
13836
|
+
trustedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13837
|
+
source
|
|
13838
|
+
};
|
|
13839
|
+
file.modules[name] = record;
|
|
13840
|
+
writeTrustFile(file);
|
|
13841
|
+
return record;
|
|
13842
|
+
}
|
|
13843
|
+
function untrustModule(name) {
|
|
13844
|
+
const file = readTrustFile();
|
|
13845
|
+
if (!file.modules[name]) return false;
|
|
13846
|
+
delete file.modules[name];
|
|
13847
|
+
writeTrustFile(file);
|
|
13848
|
+
return true;
|
|
13849
|
+
}
|
|
13850
|
+
function listTrustedModules() {
|
|
13851
|
+
return readTrustFile().modules;
|
|
13852
|
+
}
|
|
13853
|
+
function readPublisherKeys() {
|
|
13854
|
+
if (!(0, import_node_fs13.existsSync)(PUBLISHER_KEYS_FILE)) return {};
|
|
13855
|
+
try {
|
|
13856
|
+
const parsed = JSON.parse((0, import_node_fs13.readFileSync)(PUBLISHER_KEYS_FILE, "utf-8"));
|
|
13857
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
13858
|
+
} catch {
|
|
13859
|
+
return {};
|
|
13860
|
+
}
|
|
13861
|
+
}
|
|
13862
|
+
function verifyModuleSignature(modulePath, digest) {
|
|
13863
|
+
const keys = readPublisherKeys();
|
|
13864
|
+
const pinnedKeyIds = Object.keys(keys);
|
|
13865
|
+
const sigPath = (0, import_node_path10.join)(modulePath, "mod.sig");
|
|
13866
|
+
if (pinnedKeyIds.length === 0) {
|
|
13867
|
+
return { ok: true };
|
|
13868
|
+
}
|
|
13869
|
+
if (!(0, import_node_fs13.existsSync)(sigPath)) {
|
|
13870
|
+
return { ok: false, reason: "publisher keys are pinned but the module ships no mod.sig" };
|
|
13871
|
+
}
|
|
13872
|
+
let parsed;
|
|
13873
|
+
try {
|
|
13874
|
+
parsed = JSON.parse((0, import_node_fs13.readFileSync)(sigPath, "utf-8"));
|
|
13875
|
+
} catch {
|
|
13876
|
+
return { ok: false, reason: "mod.sig is not valid JSON" };
|
|
13877
|
+
}
|
|
13878
|
+
if (!parsed.keyId || !parsed.signature) {
|
|
13879
|
+
return { ok: false, reason: "mod.sig is missing keyId or signature" };
|
|
13880
|
+
}
|
|
13881
|
+
const publicKey = keys[parsed.keyId];
|
|
13882
|
+
if (!publicKey) {
|
|
13883
|
+
return { ok: false, reason: `mod.sig references unpinned key "${parsed.keyId}"` };
|
|
13884
|
+
}
|
|
13885
|
+
try {
|
|
13886
|
+
const valid = (0, import_node_crypto3.verify)(
|
|
13887
|
+
null,
|
|
13888
|
+
Buffer.from(digest, "hex"),
|
|
13889
|
+
publicKey,
|
|
13890
|
+
Buffer.from(parsed.signature, "base64")
|
|
13891
|
+
);
|
|
13892
|
+
return valid ? { ok: true } : { ok: false, reason: "mod.sig signature does not match" };
|
|
13893
|
+
} catch (err) {
|
|
13894
|
+
return { ok: false, reason: `signature check failed: ${String(err.message || err)}` };
|
|
13895
|
+
}
|
|
13896
|
+
}
|
|
13897
|
+
function verifyModuleTrust(name, modulePath) {
|
|
13898
|
+
let digest;
|
|
13899
|
+
try {
|
|
13900
|
+
digest = computeModuleDigest(modulePath);
|
|
13901
|
+
} catch (err) {
|
|
13902
|
+
return { ok: false, reason: `could not hash module: ${String(err.message || err)}` };
|
|
13903
|
+
}
|
|
13904
|
+
const signature = verifyModuleSignature(modulePath, digest);
|
|
13905
|
+
if (!signature.ok) return signature;
|
|
13906
|
+
const record = readTrustFile().modules[name];
|
|
13907
|
+
if (!record) {
|
|
13908
|
+
return {
|
|
13909
|
+
ok: false,
|
|
13910
|
+
reason: `not trusted \u2014 review it, then run: threatcrush modules trust ${name}`
|
|
13911
|
+
};
|
|
13912
|
+
}
|
|
13913
|
+
if (record.digest !== digest) {
|
|
13914
|
+
return {
|
|
13915
|
+
ok: false,
|
|
13916
|
+
reason: `contents changed since it was trusted on ${record.trustedAt} \u2014 re-review it, then run: threatcrush modules trust ${name}`
|
|
13917
|
+
};
|
|
13918
|
+
}
|
|
13919
|
+
return { ok: true };
|
|
13920
|
+
}
|
|
13921
|
+
|
|
13922
|
+
// src/commands/modules.ts
|
|
13301
13923
|
var API_URL2 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
13302
13924
|
function modulesDir() {
|
|
13303
13925
|
ensureRuntimeDirs();
|
|
@@ -13310,7 +13932,7 @@ function safeModuleDirName(name, label = "module name") {
|
|
|
13310
13932
|
return name;
|
|
13311
13933
|
}
|
|
13312
13934
|
function moduleDestination(dir, name, label) {
|
|
13313
|
-
return (0,
|
|
13935
|
+
return (0, import_node_path11.join)(dir, safeModuleDirName(name, label));
|
|
13314
13936
|
}
|
|
13315
13937
|
function assertSafeTarballEntries(tarPath) {
|
|
13316
13938
|
const listing = (0, import_node_child_process2.execFileSync)("tar", ["-tzf", tarPath], { encoding: "utf-8" });
|
|
@@ -13323,12 +13945,12 @@ function assertSafeTarballEntries(tarPath) {
|
|
|
13323
13945
|
}
|
|
13324
13946
|
}
|
|
13325
13947
|
function validateManifest(modPath) {
|
|
13326
|
-
const manifestPath = (0,
|
|
13327
|
-
if (!(0,
|
|
13948
|
+
const manifestPath = (0, import_node_path11.join)(modPath, "mod.toml");
|
|
13949
|
+
if (!(0, import_node_fs14.existsSync)(manifestPath)) {
|
|
13328
13950
|
return { ok: false, error: `mod.toml not found at ${manifestPath}` };
|
|
13329
13951
|
}
|
|
13330
13952
|
try {
|
|
13331
|
-
const raw = (0,
|
|
13953
|
+
const raw = (0, import_node_fs14.readFileSync)(manifestPath, "utf-8");
|
|
13332
13954
|
const parsed = import_toml3.default.parse(raw);
|
|
13333
13955
|
const name = parsed.module?.name;
|
|
13334
13956
|
const version = parsed.module?.version;
|
|
@@ -13339,6 +13961,12 @@ function validateManifest(modPath) {
|
|
|
13339
13961
|
return { ok: false, error: `Invalid mod.toml: ${err.message}` };
|
|
13340
13962
|
}
|
|
13341
13963
|
}
|
|
13964
|
+
function printTrustRequired(name) {
|
|
13965
|
+
console.log();
|
|
13966
|
+
console.log(source_default.yellow(` ! ${name} is installed but NOT trusted, so threatcrushd will not load it.`));
|
|
13967
|
+
console.log(source_default.dim(" Review the module source, then run:"));
|
|
13968
|
+
console.log(source_default.dim(` ${source_default.white(`threatcrush modules trust ${name}`)}`));
|
|
13969
|
+
}
|
|
13342
13970
|
function notifyDaemonIfRunning() {
|
|
13343
13971
|
if (!findRunningDaemon()) return;
|
|
13344
13972
|
console.log(source_default.dim(` \u2139 threatcrushd is running \u2014 restart it to load/unload modules:`));
|
|
@@ -13382,8 +14010,8 @@ async function modulesInstallCommand(source) {
|
|
|
13382
14010
|
console.log();
|
|
13383
14011
|
const dir = modulesDir();
|
|
13384
14012
|
if (source.startsWith("./") || source.startsWith("/") || source.startsWith("~")) {
|
|
13385
|
-
const absPath = (0,
|
|
13386
|
-
if (!(0,
|
|
14013
|
+
const absPath = (0, import_node_path11.resolve)(source.startsWith("~") ? source.replace("~", process.env.HOME || "") : source);
|
|
14014
|
+
if (!(0, import_node_fs14.existsSync)(absPath)) {
|
|
13387
14015
|
console.log(source_default.red(` \u2717 Path not found: ${absPath}
|
|
13388
14016
|
`));
|
|
13389
14017
|
return;
|
|
@@ -13402,7 +14030,7 @@ async function modulesInstallCommand(source) {
|
|
|
13402
14030
|
`));
|
|
13403
14031
|
return;
|
|
13404
14032
|
}
|
|
13405
|
-
if ((0,
|
|
14033
|
+
if ((0, import_node_fs14.existsSync)(dest2)) {
|
|
13406
14034
|
console.log(source_default.yellow(` ! ${check.name} is already installed at ${dest2}`));
|
|
13407
14035
|
console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${check.name}`)} first.
|
|
13408
14036
|
`));
|
|
@@ -13410,12 +14038,13 @@ async function modulesInstallCommand(source) {
|
|
|
13410
14038
|
}
|
|
13411
14039
|
const spinner2 = ora({ text: `Copying module files...`, color: "green" }).start();
|
|
13412
14040
|
try {
|
|
13413
|
-
(0,
|
|
14041
|
+
(0, import_node_fs14.cpSync)(absPath, dest2, { recursive: true, dereference: true });
|
|
13414
14042
|
spinner2.succeed(`Installed ${source_default.white(check.name)} v${check.version} \u2192 ${dest2}`);
|
|
13415
14043
|
} catch (err) {
|
|
13416
14044
|
spinner2.fail(`Copy failed: ${err.message}`);
|
|
13417
14045
|
return;
|
|
13418
14046
|
}
|
|
14047
|
+
printTrustRequired(check.name);
|
|
13419
14048
|
notifyDaemonIfRunning();
|
|
13420
14049
|
console.log();
|
|
13421
14050
|
return;
|
|
@@ -13425,14 +14054,14 @@ async function modulesInstallCommand(source) {
|
|
|
13425
14054
|
let name;
|
|
13426
14055
|
let dest2;
|
|
13427
14056
|
try {
|
|
13428
|
-
name = safeModuleDirName((0,
|
|
14057
|
+
name = safeModuleDirName((0, import_node_path11.basename)(gitUrl.replace(/\.git$/, "")), "repository name");
|
|
13429
14058
|
dest2 = moduleDestination(dir, name);
|
|
13430
14059
|
} catch (err) {
|
|
13431
14060
|
console.log(source_default.red(` x ${err.message}
|
|
13432
14061
|
`));
|
|
13433
14062
|
return;
|
|
13434
14063
|
}
|
|
13435
|
-
if ((0,
|
|
14064
|
+
if ((0, import_node_fs14.existsSync)(dest2)) {
|
|
13436
14065
|
console.log(source_default.yellow(` ! ${name} is already installed at ${dest2}`));
|
|
13437
14066
|
console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${name}`)} first.
|
|
13438
14067
|
`));
|
|
@@ -13449,12 +14078,13 @@ async function modulesInstallCommand(source) {
|
|
|
13449
14078
|
if (!check.ok) {
|
|
13450
14079
|
spinner2.fail(check.error);
|
|
13451
14080
|
try {
|
|
13452
|
-
(0,
|
|
14081
|
+
(0, import_node_fs14.rmSync)(dest2, { recursive: true, force: true });
|
|
13453
14082
|
} catch {
|
|
13454
14083
|
}
|
|
13455
14084
|
return;
|
|
13456
14085
|
}
|
|
13457
14086
|
spinner2.succeed(`Installed ${source_default.white(check.name)} v${check.version} \u2192 ${dest2}`);
|
|
14087
|
+
printTrustRequired(check.name);
|
|
13458
14088
|
notifyDaemonIfRunning();
|
|
13459
14089
|
console.log();
|
|
13460
14090
|
return;
|
|
@@ -13494,7 +14124,7 @@ async function modulesInstallCommand(source) {
|
|
|
13494
14124
|
`));
|
|
13495
14125
|
return;
|
|
13496
14126
|
}
|
|
13497
|
-
if ((0,
|
|
14127
|
+
if ((0, import_node_fs14.existsSync)(dest)) {
|
|
13498
14128
|
console.log(source_default.yellow(` ! ${mod.slug} is already installed at ${dest}
|
|
13499
14129
|
`));
|
|
13500
14130
|
return;
|
|
@@ -13520,7 +14150,7 @@ async function modulesInstallCommand(source) {
|
|
|
13520
14150
|
if (!check.ok) {
|
|
13521
14151
|
cloneSpinner.fail(check.error);
|
|
13522
14152
|
try {
|
|
13523
|
-
(0,
|
|
14153
|
+
(0, import_node_fs14.rmSync)(dest, { recursive: true, force: true });
|
|
13524
14154
|
} catch {
|
|
13525
14155
|
}
|
|
13526
14156
|
return;
|
|
@@ -13534,11 +14164,11 @@ async function modulesInstallCommand(source) {
|
|
|
13534
14164
|
dlSpinner.fail(`HTTP ${res.status}`);
|
|
13535
14165
|
return;
|
|
13536
14166
|
}
|
|
13537
|
-
const tar = (0,
|
|
13538
|
-
(0,
|
|
14167
|
+
const tar = (0, import_node_path11.join)(dir, `${mod.slug}.tar.gz`);
|
|
14168
|
+
(0, import_node_fs14.writeFileSync)(tar, Buffer.from(await res.arrayBuffer()));
|
|
13539
14169
|
assertSafeTarballEntries(tar);
|
|
13540
14170
|
(0, import_node_child_process2.execFileSync)("tar", ["-xzf", tar, "-C", dir], { stdio: "pipe" });
|
|
13541
|
-
(0,
|
|
14171
|
+
(0, import_node_fs14.rmSync)(tar, { force: true });
|
|
13542
14172
|
const check = validateManifest(dest);
|
|
13543
14173
|
if (!check.ok) {
|
|
13544
14174
|
dlSpinner.fail(check.error);
|
|
@@ -13553,6 +14183,7 @@ async function modulesInstallCommand(source) {
|
|
|
13553
14183
|
logger.info("No installable artifact provided for this module.");
|
|
13554
14184
|
return;
|
|
13555
14185
|
}
|
|
14186
|
+
printTrustRequired(mod.slug);
|
|
13556
14187
|
notifyDaemonIfRunning();
|
|
13557
14188
|
console.log();
|
|
13558
14189
|
}
|
|
@@ -13569,7 +14200,7 @@ async function modulesRemoveCommand(name) {
|
|
|
13569
14200
|
`));
|
|
13570
14201
|
return;
|
|
13571
14202
|
}
|
|
13572
|
-
if (!(0,
|
|
14203
|
+
if (!(0, import_node_fs14.existsSync)(target)) {
|
|
13573
14204
|
console.log(source_default.yellow(` ! No module named "${name}" in ${dir}
|
|
13574
14205
|
`));
|
|
13575
14206
|
return;
|
|
@@ -13579,7 +14210,8 @@ async function modulesRemoveCommand(name) {
|
|
|
13579
14210
|
console.log(source_default.yellow(` ! Directory name "${name}" does not match manifest name "${check.name}"`));
|
|
13580
14211
|
}
|
|
13581
14212
|
try {
|
|
13582
|
-
(0,
|
|
14213
|
+
(0, import_node_fs14.rmSync)(target, { recursive: true, force: true });
|
|
14214
|
+
untrustModule(name);
|
|
13583
14215
|
console.log(source_default.green(` \u2713 Removed ${name} from ${dir}
|
|
13584
14216
|
`));
|
|
13585
14217
|
} catch (err) {
|
|
@@ -13589,6 +14221,72 @@ async function modulesRemoveCommand(name) {
|
|
|
13589
14221
|
}
|
|
13590
14222
|
notifyDaemonIfRunning();
|
|
13591
14223
|
}
|
|
14224
|
+
async function modulesTrustCommand(name) {
|
|
14225
|
+
banner();
|
|
14226
|
+
console.log(source_default.green.bold(" Trust Module"));
|
|
14227
|
+
console.log(source_default.gray(" " + "\u2500".repeat(50)));
|
|
14228
|
+
console.log();
|
|
14229
|
+
const dir = modulesDir();
|
|
14230
|
+
let target;
|
|
14231
|
+
try {
|
|
14232
|
+
target = moduleDestination(dir, name);
|
|
14233
|
+
} catch (err) {
|
|
14234
|
+
console.log(source_default.red(` x ${err.message}
|
|
14235
|
+
`));
|
|
14236
|
+
return;
|
|
14237
|
+
}
|
|
14238
|
+
if (!(0, import_node_fs14.existsSync)(target)) {
|
|
14239
|
+
console.log(source_default.yellow(` ! No module named "${name}" in ${dir}
|
|
14240
|
+
`));
|
|
14241
|
+
return;
|
|
14242
|
+
}
|
|
14243
|
+
const check = validateManifest(target);
|
|
14244
|
+
if (!check.ok) {
|
|
14245
|
+
console.log(source_default.red(` \u2717 ${check.error}
|
|
14246
|
+
`));
|
|
14247
|
+
return;
|
|
14248
|
+
}
|
|
14249
|
+
const record = trustModule(name, target, target);
|
|
14250
|
+
console.log(source_default.green(` \u2713 Trusted ${source_default.white(name)} v${check.version}`));
|
|
14251
|
+
console.log(source_default.dim(` digest ${record.digest.slice(0, 16)}\u2026`));
|
|
14252
|
+
console.log(
|
|
14253
|
+
source_default.dim(" threatcrushd will refuse to load it again if its contents change.\n")
|
|
14254
|
+
);
|
|
14255
|
+
notifyDaemonIfRunning();
|
|
14256
|
+
}
|
|
14257
|
+
async function modulesUntrustCommand(name) {
|
|
14258
|
+
banner();
|
|
14259
|
+
console.log(source_default.green.bold(" Revoke Module Trust"));
|
|
14260
|
+
console.log(source_default.gray(" " + "\u2500".repeat(50)));
|
|
14261
|
+
console.log();
|
|
14262
|
+
if (untrustModule(name)) {
|
|
14263
|
+
console.log(source_default.green(` \u2713 Revoked trust for ${source_default.white(name)}
|
|
14264
|
+
`));
|
|
14265
|
+
notifyDaemonIfRunning();
|
|
14266
|
+
} else {
|
|
14267
|
+
console.log(source_default.yellow(` ! ${name} was not trusted
|
|
14268
|
+
`));
|
|
14269
|
+
}
|
|
14270
|
+
}
|
|
14271
|
+
async function modulesTrustedCommand() {
|
|
14272
|
+
banner();
|
|
14273
|
+
console.log(source_default.green.bold(" Trusted Modules"));
|
|
14274
|
+
console.log(source_default.gray(" " + "\u2500".repeat(50)));
|
|
14275
|
+
console.log();
|
|
14276
|
+
const trusted = Object.entries(listTrustedModules());
|
|
14277
|
+
if (trusted.length === 0) {
|
|
14278
|
+
console.log(source_default.yellow(" No modules are trusted."));
|
|
14279
|
+
console.log(source_default.dim(" Installed modules stay dormant until you run:"));
|
|
14280
|
+
console.log(source_default.dim(` ${source_default.white("threatcrush modules trust <name>")}
|
|
14281
|
+
`));
|
|
14282
|
+
return;
|
|
14283
|
+
}
|
|
14284
|
+
for (const [name, record] of trusted) {
|
|
14285
|
+
console.log(` ${source_default.white(name)} ${source_default.dim(record.digest.slice(0, 16) + "\u2026")}`);
|
|
14286
|
+
console.log(source_default.dim(` trusted ${record.trustedAt}`));
|
|
14287
|
+
}
|
|
14288
|
+
console.log();
|
|
14289
|
+
}
|
|
13592
14290
|
async function modulesCommand(opts) {
|
|
13593
14291
|
const action = opts.action || "list";
|
|
13594
14292
|
switch (action) {
|
|
@@ -13622,10 +14320,31 @@ async function modulesCommand(opts) {
|
|
|
13622
14320
|
}
|
|
13623
14321
|
await modulesRemoveCommand(opts.name);
|
|
13624
14322
|
break;
|
|
14323
|
+
case "trust":
|
|
14324
|
+
if (!opts.name) {
|
|
14325
|
+
banner();
|
|
14326
|
+
console.log(source_default.red(" Module name required."));
|
|
14327
|
+
console.log(source_default.gray(" Usage: threatcrush modules trust <name>\n"));
|
|
14328
|
+
return;
|
|
14329
|
+
}
|
|
14330
|
+
await modulesTrustCommand(opts.name);
|
|
14331
|
+
break;
|
|
14332
|
+
case "untrust":
|
|
14333
|
+
if (!opts.name) {
|
|
14334
|
+
banner();
|
|
14335
|
+
console.log(source_default.red(" Module name required."));
|
|
14336
|
+
console.log(source_default.gray(" Usage: threatcrush modules untrust <name>\n"));
|
|
14337
|
+
return;
|
|
14338
|
+
}
|
|
14339
|
+
await modulesUntrustCommand(opts.name);
|
|
14340
|
+
break;
|
|
14341
|
+
case "trusted":
|
|
14342
|
+
await modulesTrustedCommand();
|
|
14343
|
+
break;
|
|
13625
14344
|
default:
|
|
13626
14345
|
banner();
|
|
13627
14346
|
console.log(source_default.yellow(` Unknown action: ${action}`));
|
|
13628
|
-
console.log(source_default.gray(" Available actions: list, install, remove\n"));
|
|
14347
|
+
console.log(source_default.gray(" Available actions: list, install, remove, trust, untrust, trusted\n"));
|
|
13629
14348
|
await modulesListCommand();
|
|
13630
14349
|
break;
|
|
13631
14350
|
}
|
|
@@ -13952,21 +14671,21 @@ async function pentestCommand(targetUrl) {
|
|
|
13952
14671
|
|
|
13953
14672
|
// src/commands/orgs.ts
|
|
13954
14673
|
var import_node_os5 = require("os");
|
|
13955
|
-
var
|
|
13956
|
-
var
|
|
14674
|
+
var import_node_fs15 = require("fs");
|
|
14675
|
+
var import_node_path12 = require("path");
|
|
13957
14676
|
var API_URL3 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
13958
|
-
var CONFIG_PATH = (0,
|
|
14677
|
+
var CONFIG_PATH = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".threatcrush", "config.json");
|
|
13959
14678
|
function readConfig() {
|
|
13960
14679
|
try {
|
|
13961
|
-
return JSON.parse((0,
|
|
14680
|
+
return JSON.parse((0, import_node_fs15.readFileSync)(CONFIG_PATH, "utf-8"));
|
|
13962
14681
|
} catch {
|
|
13963
14682
|
return {};
|
|
13964
14683
|
}
|
|
13965
14684
|
}
|
|
13966
14685
|
function writeConfig(config) {
|
|
13967
|
-
const dir = (0,
|
|
13968
|
-
if (!(0,
|
|
13969
|
-
(0,
|
|
14686
|
+
const dir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".threatcrush");
|
|
14687
|
+
if (!(0, import_node_fs15.existsSync)(dir)) (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
14688
|
+
(0, import_node_fs15.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
13970
14689
|
}
|
|
13971
14690
|
function getAuthHeaders() {
|
|
13972
14691
|
const config = readConfig();
|
|
@@ -14102,13 +14821,13 @@ async function useOrganization(slug) {
|
|
|
14102
14821
|
|
|
14103
14822
|
// src/commands/servers.ts
|
|
14104
14823
|
var import_node_os6 = require("os");
|
|
14105
|
-
var
|
|
14106
|
-
var
|
|
14824
|
+
var import_node_fs16 = require("fs");
|
|
14825
|
+
var import_node_path13 = require("path");
|
|
14107
14826
|
var API_URL4 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
14108
|
-
var CONFIG_PATH2 = (0,
|
|
14827
|
+
var CONFIG_PATH2 = (0, import_node_path13.join)((0, import_node_os6.homedir)(), ".threatcrush", "config.json");
|
|
14109
14828
|
function readConfig2() {
|
|
14110
14829
|
try {
|
|
14111
|
-
return JSON.parse((0,
|
|
14830
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(CONFIG_PATH2, "utf-8"));
|
|
14112
14831
|
} catch {
|
|
14113
14832
|
return {};
|
|
14114
14833
|
}
|
|
@@ -14211,14 +14930,14 @@ function timeAgo(dateStr) {
|
|
|
14211
14930
|
|
|
14212
14931
|
// src/commands/connect.ts
|
|
14213
14932
|
var import_node_os7 = require("os");
|
|
14214
|
-
var
|
|
14215
|
-
var
|
|
14933
|
+
var import_node_fs17 = require("fs");
|
|
14934
|
+
var import_node_path14 = require("path");
|
|
14216
14935
|
var import_node_child_process3 = require("child_process");
|
|
14217
14936
|
var API_URL5 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
14218
|
-
var CONFIG_PATH3 = (0,
|
|
14937
|
+
var CONFIG_PATH3 = (0, import_node_path14.join)((0, import_node_os7.homedir)(), ".threatcrush", "config.json");
|
|
14219
14938
|
function readConfig3() {
|
|
14220
14939
|
try {
|
|
14221
|
-
return JSON.parse((0,
|
|
14940
|
+
return JSON.parse((0, import_node_fs17.readFileSync)(CONFIG_PATH3, "utf-8"));
|
|
14222
14941
|
} catch {
|
|
14223
14942
|
return {};
|
|
14224
14943
|
}
|
|
@@ -14373,20 +15092,21 @@ async function sshConnect(options) {
|
|
|
14373
15092
|
|
|
14374
15093
|
// src/commands/daemon.ts
|
|
14375
15094
|
var import_node_child_process7 = require("child_process");
|
|
14376
|
-
var
|
|
14377
|
-
var
|
|
14378
|
-
var
|
|
15095
|
+
var import_node_fs26 = require("fs");
|
|
15096
|
+
var import_node_path18 = require("path");
|
|
15097
|
+
var import_node_fs27 = require("fs");
|
|
14379
15098
|
|
|
14380
15099
|
// src/daemon/index.ts
|
|
14381
|
-
var
|
|
14382
|
-
var
|
|
15100
|
+
var import_node_fs25 = require("fs");
|
|
15101
|
+
var import_node_path17 = require("path");
|
|
14383
15102
|
init_paths();
|
|
14384
15103
|
init_pidfile();
|
|
14385
15104
|
|
|
14386
15105
|
// src/daemon/ipc-server.ts
|
|
14387
15106
|
var import_node_net2 = require("net");
|
|
14388
|
-
var
|
|
15107
|
+
var import_node_fs18 = require("fs");
|
|
14389
15108
|
init_paths();
|
|
15109
|
+
init_control_token();
|
|
14390
15110
|
|
|
14391
15111
|
// src/daemon/event-bus.ts
|
|
14392
15112
|
var import_node_events = require("events");
|
|
@@ -14431,10 +15151,12 @@ var IpcServer = class {
|
|
|
14431
15151
|
nextClientId = 1;
|
|
14432
15152
|
startedAt = /* @__PURE__ */ new Date();
|
|
14433
15153
|
counters = { events: 0, threats: 0, alerts: 0 };
|
|
15154
|
+
controlToken = "";
|
|
14434
15155
|
async start() {
|
|
14435
|
-
|
|
15156
|
+
this.controlToken = issueControlToken();
|
|
15157
|
+
if ((0, import_node_fs18.existsSync)(PATHS.socket)) {
|
|
14436
15158
|
try {
|
|
14437
|
-
(0,
|
|
15159
|
+
(0, import_node_fs18.unlinkSync)(PATHS.socket);
|
|
14438
15160
|
} catch {
|
|
14439
15161
|
}
|
|
14440
15162
|
}
|
|
@@ -14470,14 +15192,14 @@ var IpcServer = class {
|
|
|
14470
15192
|
return new Promise((resolve5) => {
|
|
14471
15193
|
if (!this.server) {
|
|
14472
15194
|
try {
|
|
14473
|
-
if ((0,
|
|
15195
|
+
if ((0, import_node_fs18.existsSync)(PATHS.socket)) (0, import_node_fs18.unlinkSync)(PATHS.socket);
|
|
14474
15196
|
} catch {
|
|
14475
15197
|
}
|
|
14476
15198
|
return resolve5();
|
|
14477
15199
|
}
|
|
14478
15200
|
this.server.close(() => {
|
|
14479
15201
|
try {
|
|
14480
|
-
if ((0,
|
|
15202
|
+
if ((0, import_node_fs18.existsSync)(PATHS.socket)) (0, import_node_fs18.unlinkSync)(PATHS.socket);
|
|
14481
15203
|
} catch {
|
|
14482
15204
|
}
|
|
14483
15205
|
resolve5();
|
|
@@ -14563,6 +15285,13 @@ var IpcServer = class {
|
|
|
14563
15285
|
for (const ch of req.params.channels) client.subscriptions.add(ch);
|
|
14564
15286
|
return this.send(client, { id: req.id, ok: true, result: { subscribed: [...client.subscriptions] } });
|
|
14565
15287
|
case "shutdown":
|
|
15288
|
+
if (!tokensMatch(this.controlToken, req.params?.token)) {
|
|
15289
|
+
return this.send(client, {
|
|
15290
|
+
id: req.id,
|
|
15291
|
+
ok: false,
|
|
15292
|
+
error: "shutdown requires the daemon control token (run as root, or use systemctl)"
|
|
15293
|
+
});
|
|
15294
|
+
}
|
|
14566
15295
|
this.send(client, { id: req.id, ok: true, result: "shutting down" });
|
|
14567
15296
|
setTimeout(() => process.emit("SIGTERM"), 50);
|
|
14568
15297
|
return;
|
|
@@ -14583,14 +15312,14 @@ var IpcServer = class {
|
|
|
14583
15312
|
};
|
|
14584
15313
|
|
|
14585
15314
|
// src/daemon/module-host.ts
|
|
14586
|
-
var
|
|
14587
|
-
var
|
|
15315
|
+
var import_node_fs22 = require("fs");
|
|
15316
|
+
var import_node_path15 = require("path");
|
|
14588
15317
|
var import_node_url = require("url");
|
|
14589
15318
|
var import_toml4 = __toESM(require_toml());
|
|
14590
15319
|
init_paths();
|
|
14591
15320
|
|
|
14592
15321
|
// src/daemon/watchers/log-watcher.ts
|
|
14593
|
-
var
|
|
15322
|
+
var import_node_fs19 = require("fs");
|
|
14594
15323
|
var import_node_readline4 = require("readline");
|
|
14595
15324
|
init_state();
|
|
14596
15325
|
var DEFAULT_SOURCES = [
|
|
@@ -14612,9 +15341,9 @@ var LogWatcher = class {
|
|
|
14612
15341
|
start() {
|
|
14613
15342
|
const started = [];
|
|
14614
15343
|
for (const src of this.sources) {
|
|
14615
|
-
if (!(0,
|
|
15344
|
+
if (!(0, import_node_fs19.existsSync)(src.path)) continue;
|
|
14616
15345
|
try {
|
|
14617
|
-
(0,
|
|
15346
|
+
(0, import_node_fs19.accessSync)(src.path, import_node_fs19.constants.R_OK);
|
|
14618
15347
|
} catch {
|
|
14619
15348
|
continue;
|
|
14620
15349
|
}
|
|
@@ -14634,7 +15363,7 @@ var LogWatcher = class {
|
|
|
14634
15363
|
}
|
|
14635
15364
|
tail(src) {
|
|
14636
15365
|
try {
|
|
14637
|
-
this.positions.set(src.path, (0,
|
|
15366
|
+
this.positions.set(src.path, (0, import_node_fs19.statSync)(src.path).size);
|
|
14638
15367
|
} catch {
|
|
14639
15368
|
this.positions.set(src.path, 0);
|
|
14640
15369
|
}
|
|
@@ -14645,7 +15374,7 @@ var LogWatcher = class {
|
|
|
14645
15374
|
poll(src) {
|
|
14646
15375
|
let stat;
|
|
14647
15376
|
try {
|
|
14648
|
-
stat = (0,
|
|
15377
|
+
stat = (0, import_node_fs19.statSync)(src.path);
|
|
14649
15378
|
} catch {
|
|
14650
15379
|
return;
|
|
14651
15380
|
}
|
|
@@ -14655,7 +15384,7 @@ var LogWatcher = class {
|
|
|
14655
15384
|
return;
|
|
14656
15385
|
}
|
|
14657
15386
|
if (stat.size === prev) return;
|
|
14658
|
-
const stream = (0,
|
|
15387
|
+
const stream = (0, import_node_fs19.createReadStream)(src.path, { start: prev, encoding: "utf-8" });
|
|
14659
15388
|
stream.on("error", () => this.positions.set(src.path, stat.size));
|
|
14660
15389
|
const rl = (0, import_node_readline4.createInterface)({ input: stream });
|
|
14661
15390
|
rl.on("error", () => {
|
|
@@ -14848,7 +15577,7 @@ function realtimeToDate(rt) {
|
|
|
14848
15577
|
|
|
14849
15578
|
// src/modules/network-monitor/index.ts
|
|
14850
15579
|
var import_node_child_process5 = require("child_process");
|
|
14851
|
-
var
|
|
15580
|
+
var import_node_fs20 = require("fs");
|
|
14852
15581
|
init_state();
|
|
14853
15582
|
var NetworkMonitor = class {
|
|
14854
15583
|
constructor(bus2) {
|
|
@@ -14887,7 +15616,7 @@ var NetworkMonitor = class {
|
|
|
14887
15616
|
hasConntrackOrSs() {
|
|
14888
15617
|
const ss = (0, import_node_child_process5.spawnSync)("ss", ["--version"], { stdio: "pipe" });
|
|
14889
15618
|
if (ss.status === 0) return true;
|
|
14890
|
-
return (0,
|
|
15619
|
+
return (0, import_node_fs20.existsSync)("/proc/net/tcp");
|
|
14891
15620
|
}
|
|
14892
15621
|
poll() {
|
|
14893
15622
|
try {
|
|
@@ -15026,7 +15755,7 @@ var NetworkMonitor = class {
|
|
|
15026
15755
|
};
|
|
15027
15756
|
|
|
15028
15757
|
// src/modules/dns-monitor/index.ts
|
|
15029
|
-
var
|
|
15758
|
+
var import_node_fs21 = require("fs");
|
|
15030
15759
|
var import_node_readline5 = require("readline");
|
|
15031
15760
|
init_state();
|
|
15032
15761
|
var DNS_LOG_SOURCES = [
|
|
@@ -15061,9 +15790,9 @@ var DnsMonitor = class {
|
|
|
15061
15790
|
entropyThreshold = 3.5;
|
|
15062
15791
|
start() {
|
|
15063
15792
|
const sources = DNS_LOG_SOURCES.filter((p) => {
|
|
15064
|
-
if (!(0,
|
|
15793
|
+
if (!(0, import_node_fs21.existsSync)(p)) return false;
|
|
15065
15794
|
try {
|
|
15066
|
-
(0,
|
|
15795
|
+
(0, import_node_fs21.accessSync)(p, import_node_fs21.constants.R_OK);
|
|
15067
15796
|
return true;
|
|
15068
15797
|
} catch {
|
|
15069
15798
|
return false;
|
|
@@ -15087,7 +15816,7 @@ var DnsMonitor = class {
|
|
|
15087
15816
|
}
|
|
15088
15817
|
tailLog(path) {
|
|
15089
15818
|
try {
|
|
15090
|
-
this.positions.set(path, (0,
|
|
15819
|
+
this.positions.set(path, (0, import_node_fs21.statSync)(path).size);
|
|
15091
15820
|
} catch {
|
|
15092
15821
|
this.positions.set(path, 0);
|
|
15093
15822
|
}
|
|
@@ -15097,7 +15826,7 @@ var DnsMonitor = class {
|
|
|
15097
15826
|
pollLog(path) {
|
|
15098
15827
|
let stat;
|
|
15099
15828
|
try {
|
|
15100
|
-
stat = (0,
|
|
15829
|
+
stat = (0, import_node_fs21.statSync)(path);
|
|
15101
15830
|
} catch {
|
|
15102
15831
|
return;
|
|
15103
15832
|
}
|
|
@@ -15107,7 +15836,7 @@ var DnsMonitor = class {
|
|
|
15107
15836
|
return;
|
|
15108
15837
|
}
|
|
15109
15838
|
if (stat.size === prev) return;
|
|
15110
|
-
const stream = (0,
|
|
15839
|
+
const stream = (0, import_node_fs21.createReadStream)(path, { start: prev, encoding: "utf-8" });
|
|
15111
15840
|
stream.on("error", () => this.positions.set(path, stat.size));
|
|
15112
15841
|
const rl = (0, import_node_readline5.createInterface)({ input: stream });
|
|
15113
15842
|
rl.on("line", (line) => this.parseDnsLine(line));
|
|
@@ -15339,15 +16068,15 @@ var ModuleHost = class {
|
|
|
15339
16068
|
for (const m of builtins) this.modules.set(m.name, m);
|
|
15340
16069
|
}
|
|
15341
16070
|
async discoverAndStartInstalled() {
|
|
15342
|
-
if (!(0,
|
|
16071
|
+
if (!(0, import_node_fs22.existsSync)(PATHS.moduleDir)) return;
|
|
15343
16072
|
const configs = loadModuleConfigs(PATHS.confD);
|
|
15344
|
-
const entries = (0,
|
|
16073
|
+
const entries = (0, import_node_fs22.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
|
|
15345
16074
|
for (const entry of entries) {
|
|
15346
16075
|
if (!entry.isDirectory()) continue;
|
|
15347
|
-
const manifestPath = (0,
|
|
15348
|
-
if (!(0,
|
|
16076
|
+
const manifestPath = (0, import_node_path15.join)(PATHS.moduleDir, entry.name, "mod.toml");
|
|
16077
|
+
if (!(0, import_node_fs22.existsSync)(manifestPath)) continue;
|
|
15349
16078
|
try {
|
|
15350
|
-
const manifest = import_toml4.default.parse((0,
|
|
16079
|
+
const manifest = import_toml4.default.parse((0, import_node_fs22.readFileSync)(manifestPath, "utf-8"));
|
|
15351
16080
|
const name = manifest.module?.name || entry.name;
|
|
15352
16081
|
const defaults = manifest.module?.config?.defaults || {};
|
|
15353
16082
|
const config = {
|
|
@@ -15361,7 +16090,7 @@ var ModuleHost = class {
|
|
|
15361
16090
|
source: "installed",
|
|
15362
16091
|
status: config.enabled === false ? "disabled" : "loaded",
|
|
15363
16092
|
events: 0,
|
|
15364
|
-
path: (0,
|
|
16093
|
+
path: (0, import_node_path15.join)(PATHS.moduleDir, entry.name),
|
|
15365
16094
|
config
|
|
15366
16095
|
};
|
|
15367
16096
|
this.modules.set(name, hosted);
|
|
@@ -15376,7 +16105,7 @@ var ModuleHost = class {
|
|
|
15376
16105
|
status: "error",
|
|
15377
16106
|
events: 0,
|
|
15378
16107
|
detail: `manifest load failed: ${String(err.message || err)}`,
|
|
15379
|
-
path: (0,
|
|
16108
|
+
path: (0, import_node_path15.join)(PATHS.moduleDir, entry.name)
|
|
15380
16109
|
});
|
|
15381
16110
|
}
|
|
15382
16111
|
}
|
|
@@ -15388,6 +16117,13 @@ var ModuleHost = class {
|
|
|
15388
16117
|
hosted.detail = "no built entrypoint found; run npm install && npm run build in the module directory";
|
|
15389
16118
|
return;
|
|
15390
16119
|
}
|
|
16120
|
+
const trust = verifyModuleTrust(hosted.name, hosted.path);
|
|
16121
|
+
if (!trust.ok) {
|
|
16122
|
+
hosted.status = "error";
|
|
16123
|
+
hosted.detail = `refusing to load: ${trust.reason}`;
|
|
16124
|
+
this.bus.announceModule(hosted.name, "error", hosted.detail);
|
|
16125
|
+
return;
|
|
16126
|
+
}
|
|
15391
16127
|
try {
|
|
15392
16128
|
const imported = await import((0, import_node_url.pathToFileURL)(entrypoint).href);
|
|
15393
16129
|
const exported = imported.default || imported.module || imported;
|
|
@@ -15408,17 +16144,17 @@ var ModuleHost = class {
|
|
|
15408
16144
|
}
|
|
15409
16145
|
}
|
|
15410
16146
|
installedEntrypoint(modulePath) {
|
|
15411
|
-
const packageJson = (0,
|
|
16147
|
+
const packageJson = (0, import_node_path15.join)(modulePath, "package.json");
|
|
15412
16148
|
const candidates = [];
|
|
15413
|
-
if ((0,
|
|
16149
|
+
if ((0, import_node_fs22.existsSync)(packageJson)) {
|
|
15414
16150
|
try {
|
|
15415
|
-
const pkg = JSON.parse((0,
|
|
15416
|
-
if (pkg.main) candidates.push((0,
|
|
16151
|
+
const pkg = JSON.parse((0, import_node_fs22.readFileSync)(packageJson, "utf-8"));
|
|
16152
|
+
if (pkg.main) candidates.push((0, import_node_path15.join)(modulePath, pkg.main));
|
|
15417
16153
|
} catch {
|
|
15418
16154
|
}
|
|
15419
16155
|
}
|
|
15420
|
-
candidates.push((0,
|
|
15421
|
-
return candidates.find((candidate) => (0,
|
|
16156
|
+
candidates.push((0, import_node_path15.join)(modulePath, "dist", "index.js"), (0, import_node_path15.join)(modulePath, "index.js"));
|
|
16157
|
+
return candidates.find((candidate) => (0, import_node_fs22.existsSync)(candidate)) || null;
|
|
15422
16158
|
}
|
|
15423
16159
|
isThreatCrushModule(value) {
|
|
15424
16160
|
return Boolean(
|
|
@@ -15939,8 +16675,8 @@ var RuleEngine = class {
|
|
|
15939
16675
|
};
|
|
15940
16676
|
|
|
15941
16677
|
// src/daemon/rules/loader.ts
|
|
15942
|
-
var
|
|
15943
|
-
var
|
|
16678
|
+
var import_node_fs23 = require("fs");
|
|
16679
|
+
var import_node_path16 = require("path");
|
|
15944
16680
|
|
|
15945
16681
|
// src/daemon/rules/default-rules.ts
|
|
15946
16682
|
var DEFAULT_RULES = [
|
|
@@ -16224,11 +16960,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
|
|
|
16224
16960
|
function loadAllRules(customDir) {
|
|
16225
16961
|
const rules = [...DEFAULT_RULES];
|
|
16226
16962
|
const dir = customDir || RULES_DIR;
|
|
16227
|
-
if ((0,
|
|
16228
|
-
const files = (0,
|
|
16963
|
+
if ((0, import_node_fs23.existsSync)(dir)) {
|
|
16964
|
+
const files = (0, import_node_fs23.readdirSync)(dir).filter((f) => f.endsWith(".json"));
|
|
16229
16965
|
for (const file of files) {
|
|
16230
16966
|
try {
|
|
16231
|
-
const raw = (0,
|
|
16967
|
+
const raw = (0, import_node_fs23.readFileSync)((0, import_node_path16.join)(dir, file), "utf-8");
|
|
16232
16968
|
const parsed = JSON.parse(raw);
|
|
16233
16969
|
const customRules = Array.isArray(parsed) ? parsed : [parsed];
|
|
16234
16970
|
for (const rule of customRules) {
|
|
@@ -16391,7 +17127,7 @@ function detectFirewallAdapter() {
|
|
|
16391
17127
|
}
|
|
16392
17128
|
|
|
16393
17129
|
// src/daemon/firewall/remediation.ts
|
|
16394
|
-
var
|
|
17130
|
+
var import_node_fs24 = require("fs");
|
|
16395
17131
|
init_state();
|
|
16396
17132
|
init_paths();
|
|
16397
17133
|
var DEFAULT_CONFIG2 = {
|
|
@@ -16548,7 +17284,7 @@ var RemediationManager = class {
|
|
|
16548
17284
|
}
|
|
16549
17285
|
logLine(line) {
|
|
16550
17286
|
try {
|
|
16551
|
-
(0,
|
|
17287
|
+
(0, import_node_fs24.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
16552
17288
|
`);
|
|
16553
17289
|
} catch {
|
|
16554
17290
|
}
|
|
@@ -16607,7 +17343,7 @@ async function flushTelemetry(timeoutMs = 2e3) {
|
|
|
16607
17343
|
// src/daemon/index.ts
|
|
16608
17344
|
function readVersion2() {
|
|
16609
17345
|
try {
|
|
16610
|
-
const pkg = JSON.parse((0,
|
|
17346
|
+
const pkg = JSON.parse((0, import_node_fs25.readFileSync)((0, import_node_path17.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
16611
17347
|
return pkg.version || "0.0.0";
|
|
16612
17348
|
} catch {
|
|
16613
17349
|
return "0.0.0";
|
|
@@ -16615,7 +17351,7 @@ function readVersion2() {
|
|
|
16615
17351
|
}
|
|
16616
17352
|
function logLine(line) {
|
|
16617
17353
|
try {
|
|
16618
|
-
(0,
|
|
17354
|
+
(0, import_node_fs25.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
16619
17355
|
`);
|
|
16620
17356
|
} catch {
|
|
16621
17357
|
}
|
|
@@ -16643,7 +17379,7 @@ async function runDaemon() {
|
|
|
16643
17379
|
} catch (err) {
|
|
16644
17380
|
logLine(`[daemon] state db unavailable: ${err.message}`);
|
|
16645
17381
|
}
|
|
16646
|
-
const config = loadConfig((0,
|
|
17382
|
+
const config = loadConfig((0, import_node_fs25.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
|
|
16647
17383
|
bus.on("event", (event) => {
|
|
16648
17384
|
logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
|
|
16649
17385
|
});
|
|
@@ -16735,7 +17471,7 @@ async function runDaemon() {
|
|
|
16735
17471
|
init_paths();
|
|
16736
17472
|
init_pidfile();
|
|
16737
17473
|
init_ipc_client();
|
|
16738
|
-
var DAEMON_ENTRY = (0,
|
|
17474
|
+
var DAEMON_ENTRY = (0, import_node_path18.join)(__dirname, "daemon.js");
|
|
16739
17475
|
async function daemonForeground() {
|
|
16740
17476
|
await runDaemon();
|
|
16741
17477
|
}
|
|
@@ -16746,7 +17482,7 @@ async function daemonStart() {
|
|
|
16746
17482
|
return;
|
|
16747
17483
|
}
|
|
16748
17484
|
ensureRuntimeDirs();
|
|
16749
|
-
if (!(0,
|
|
17485
|
+
if (!(0, import_node_fs26.existsSync)(DAEMON_ENTRY)) {
|
|
16750
17486
|
console.log(source_default.red(` Daemon entry not found at ${DAEMON_ENTRY}.`));
|
|
16751
17487
|
console.log(source_default.dim(" Reinstall with `threatcrush update` or `pnpm run build` in the cli package."));
|
|
16752
17488
|
return;
|
|
@@ -16754,8 +17490,8 @@ async function daemonStart() {
|
|
|
16754
17490
|
let out;
|
|
16755
17491
|
let err;
|
|
16756
17492
|
try {
|
|
16757
|
-
out = (0,
|
|
16758
|
-
err = (0,
|
|
17493
|
+
out = (0, import_node_fs27.openSync)(PATHS.logFile, "a");
|
|
17494
|
+
err = (0, import_node_fs27.openSync)(PATHS.logFile, "a");
|
|
16759
17495
|
} catch (e) {
|
|
16760
17496
|
const code = e.code;
|
|
16761
17497
|
console.log(source_default.red(` \u2717 Cannot write daemon log at ${PATHS.logFile} (${code ?? "error"}).`));
|
|
@@ -16834,19 +17570,19 @@ async function daemonStop() {
|
|
|
16834
17570
|
|
|
16835
17571
|
// src/commands/service.ts
|
|
16836
17572
|
var import_node_child_process8 = require("child_process");
|
|
16837
|
-
var
|
|
16838
|
-
var
|
|
17573
|
+
var import_node_fs28 = require("fs");
|
|
17574
|
+
var import_node_path19 = require("path");
|
|
16839
17575
|
var UNIT_PATH = "/etc/systemd/system/threatcrushd.service";
|
|
16840
17576
|
function resolveTemplate() {
|
|
16841
|
-
const templatePath = (0,
|
|
16842
|
-
if (!(0,
|
|
17577
|
+
const templatePath = (0, import_node_path19.join)(__dirname, "systemd", "threatcrushd.service");
|
|
17578
|
+
if (!(0, import_node_fs28.existsSync)(templatePath)) {
|
|
16843
17579
|
throw new Error(`systemd unit template not found at ${templatePath}`);
|
|
16844
17580
|
}
|
|
16845
|
-
return (0,
|
|
17581
|
+
return (0, import_node_fs28.readFileSync)(templatePath, "utf-8");
|
|
16846
17582
|
}
|
|
16847
17583
|
function resolveBinPath() {
|
|
16848
17584
|
const arg = process.argv[1];
|
|
16849
|
-
if (arg && (0,
|
|
17585
|
+
if (arg && (0, import_node_fs28.existsSync)(arg)) return arg;
|
|
16850
17586
|
try {
|
|
16851
17587
|
return (0, import_node_child_process8.execSync)("command -v threatcrush", { encoding: "utf-8" }).trim();
|
|
16852
17588
|
} catch {
|
|
@@ -16867,7 +17603,7 @@ async function installServiceCommand() {
|
|
|
16867
17603
|
return;
|
|
16868
17604
|
}
|
|
16869
17605
|
const unit = resolveTemplate().replace("{{BIN_PATH}}", resolveBinPath());
|
|
16870
|
-
(0,
|
|
17606
|
+
(0, import_node_fs28.writeFileSync)(UNIT_PATH, unit, { mode: 420 });
|
|
16871
17607
|
console.log(source_default.green(` \u2713 Installed unit file: ${UNIT_PATH}`));
|
|
16872
17608
|
ensureSystemDirs();
|
|
16873
17609
|
try {
|
|
@@ -16882,29 +17618,31 @@ async function installServiceCommand() {
|
|
|
16882
17618
|
}
|
|
16883
17619
|
function ensureSystemDirs() {
|
|
16884
17620
|
const dirs = [
|
|
16885
|
-
{ path: "/etc/threatcrush" },
|
|
16886
|
-
{ path: "/etc/threatcrush/modules", sticky: true },
|
|
16887
|
-
{ path: "/etc/threatcrush/threatcrushd.conf.d" },
|
|
16888
|
-
{ path: "/var/log/threatcrush" },
|
|
16889
|
-
{ path: "/var/lib/threatcrush" },
|
|
16890
|
-
{ path: "/var/run/threatcrush" }
|
|
17621
|
+
{ path: "/etc/threatcrush", groupWritable: true },
|
|
17622
|
+
{ path: "/etc/threatcrush/modules", groupWritable: true, sticky: true },
|
|
17623
|
+
{ path: "/etc/threatcrush/threatcrushd.conf.d", groupWritable: true },
|
|
17624
|
+
{ path: "/var/log/threatcrush", groupWritable: false },
|
|
17625
|
+
{ path: "/var/lib/threatcrush", groupWritable: false },
|
|
17626
|
+
{ path: "/var/run/threatcrush", groupWritable: false }
|
|
16891
17627
|
];
|
|
16892
17628
|
let admGid = null;
|
|
16893
17629
|
try {
|
|
16894
|
-
admGid = (0,
|
|
17630
|
+
admGid = (0, import_node_fs28.statSync)("/var/log/auth.log").gid;
|
|
16895
17631
|
} catch {
|
|
16896
17632
|
}
|
|
16897
|
-
for (const { path, sticky } of dirs) {
|
|
17633
|
+
for (const { path, groupWritable, sticky } of dirs) {
|
|
16898
17634
|
try {
|
|
16899
|
-
(0,
|
|
17635
|
+
(0, import_node_fs28.mkdirSync)(path, { recursive: true });
|
|
16900
17636
|
} catch {
|
|
16901
17637
|
}
|
|
16902
|
-
|
|
16903
|
-
|
|
16904
|
-
(0,
|
|
17638
|
+
try {
|
|
17639
|
+
if (groupWritable && admGid !== null) {
|
|
17640
|
+
(0, import_node_fs28.chmodSync)(path, sticky ? 1533 : 509);
|
|
16905
17641
|
(0, import_node_child_process8.execSync)(`chgrp adm ${path}`, { stdio: "ignore" });
|
|
16906
|
-
}
|
|
17642
|
+
} else {
|
|
17643
|
+
(0, import_node_fs28.chmodSync)(path, 493);
|
|
16907
17644
|
}
|
|
17645
|
+
} catch {
|
|
16908
17646
|
}
|
|
16909
17647
|
}
|
|
16910
17648
|
console.log(source_default.green(" \u2713 Runtime dirs prepared (group `adm` may install modules / edit config without sudo)."));
|
|
@@ -16928,7 +17666,7 @@ async function uninstallServiceCommand() {
|
|
|
16928
17666
|
} catch {
|
|
16929
17667
|
}
|
|
16930
17668
|
try {
|
|
16931
|
-
if ((0,
|
|
17669
|
+
if ((0, import_node_fs28.existsSync)(UNIT_PATH)) {
|
|
16932
17670
|
(0, import_node_child_process8.execSync)(`rm -f ${UNIT_PATH}`);
|
|
16933
17671
|
console.log(source_default.green(` \u2713 Removed unit file: ${UNIT_PATH}`));
|
|
16934
17672
|
}
|
|
@@ -17027,8 +17765,8 @@ function welcomeCommand() {
|
|
|
17027
17765
|
}
|
|
17028
17766
|
|
|
17029
17767
|
// src/commands/properties.ts
|
|
17030
|
-
var
|
|
17031
|
-
var
|
|
17768
|
+
var import_node_fs29 = require("fs");
|
|
17769
|
+
var import_node_path20 = require("path");
|
|
17032
17770
|
var import_node_readline6 = __toESM(require("readline"));
|
|
17033
17771
|
var API_URL7 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
17034
17772
|
var KINDS = ["url", "api", "domain", "ip", "repo"];
|
|
@@ -17357,8 +18095,8 @@ async function propertiesRunsCommand(opts) {
|
|
|
17357
18095
|
}
|
|
17358
18096
|
}
|
|
17359
18097
|
function parseImportFile(path) {
|
|
17360
|
-
const ext = (0,
|
|
17361
|
-
const raw = (0,
|
|
18098
|
+
const ext = (0, import_node_path20.extname)(path).toLowerCase();
|
|
18099
|
+
const raw = (0, import_node_fs29.readFileSync)(path, "utf-8");
|
|
17362
18100
|
if (ext === ".json") {
|
|
17363
18101
|
const parsed = JSON.parse(raw);
|
|
17364
18102
|
if (!Array.isArray(parsed)) throw new Error("JSON must be an array of objects");
|
|
@@ -17548,7 +18286,7 @@ async function rulesCommand(opts) {
|
|
|
17548
18286
|
}
|
|
17549
18287
|
|
|
17550
18288
|
// src/commands/harden.ts
|
|
17551
|
-
var
|
|
18289
|
+
var import_node_fs30 = require("fs");
|
|
17552
18290
|
var import_node_child_process9 = require("child_process");
|
|
17553
18291
|
function tryExec(cmd) {
|
|
17554
18292
|
try {
|
|
@@ -17559,7 +18297,7 @@ function tryExec(cmd) {
|
|
|
17559
18297
|
}
|
|
17560
18298
|
function tryRead(path) {
|
|
17561
18299
|
try {
|
|
17562
|
-
return (0,
|
|
18300
|
+
return (0, import_node_fs30.readFileSync)(path, "utf-8");
|
|
17563
18301
|
} catch {
|
|
17564
18302
|
return null;
|
|
17565
18303
|
}
|
|
@@ -17663,8 +18401,8 @@ function checkSshWeakConfig() {
|
|
|
17663
18401
|
};
|
|
17664
18402
|
}
|
|
17665
18403
|
function checkAutoUpdates() {
|
|
17666
|
-
const unattended = (0,
|
|
17667
|
-
const dnfAuto = (0,
|
|
18404
|
+
const unattended = (0, import_node_fs30.existsSync)("/etc/apt/apt.conf.d/20auto-upgrades") || (0, import_node_fs30.existsSync)("/etc/apt/apt.conf.d/50unattended-upgrades");
|
|
18405
|
+
const dnfAuto = (0, import_node_fs30.existsSync)("/etc/dnf/automatic.conf");
|
|
17668
18406
|
if (unattended || dnfAuto) {
|
|
17669
18407
|
return {
|
|
17670
18408
|
key: "auto-updates",
|
|
@@ -17790,7 +18528,7 @@ function checkFail2ban() {
|
|
|
17790
18528
|
explanation: "fail2ban is installed and running."
|
|
17791
18529
|
};
|
|
17792
18530
|
}
|
|
17793
|
-
if ((0,
|
|
18531
|
+
if ((0, import_node_fs30.existsSync)("/etc/fail2ban/fail2ban.conf")) {
|
|
17794
18532
|
return {
|
|
17795
18533
|
key: checkKey,
|
|
17796
18534
|
status: "warn",
|
|
@@ -18021,7 +18759,7 @@ async function allowlistCommand(opts) {
|
|
|
18021
18759
|
init_paths();
|
|
18022
18760
|
var PKG_VERSION2 = "0.1.8";
|
|
18023
18761
|
try {
|
|
18024
|
-
const pkg = JSON.parse((0,
|
|
18762
|
+
const pkg = JSON.parse((0, import_node_fs31.readFileSync)((0, import_node_path21.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
18025
18763
|
PKG_VERSION2 = pkg.version;
|
|
18026
18764
|
} catch {
|
|
18027
18765
|
}
|
|
@@ -18037,7 +18775,7 @@ ${source_default.dim(" C R U S H")}
|
|
|
18037
18775
|
var API_URL8 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
18038
18776
|
var PKG_NAME = "@profullstack/threatcrush";
|
|
18039
18777
|
var DESKTOP_PKG_NAME = "@profullstack/threatcrush-desktop";
|
|
18040
|
-
var INSTALL_CONFIG_PATH = (0,
|
|
18778
|
+
var INSTALL_CONFIG_PATH = (0, import_node_path21.join)((0, import_node_os8.homedir)(), ".threatcrush", "install.json");
|
|
18041
18779
|
function detectPackageManager() {
|
|
18042
18780
|
try {
|
|
18043
18781
|
const npmGlobal = (0, import_node_child_process10.execSync)("npm ls -g --depth=0 --json 2>/dev/null", { encoding: "utf-8" });
|
|
@@ -18063,7 +18801,7 @@ function detectPackageManager() {
|
|
|
18063
18801
|
}
|
|
18064
18802
|
function readInstallConfig() {
|
|
18065
18803
|
try {
|
|
18066
|
-
return JSON.parse((0,
|
|
18804
|
+
return JSON.parse((0, import_node_fs31.readFileSync)(INSTALL_CONFIG_PATH, "utf-8"));
|
|
18067
18805
|
} catch {
|
|
18068
18806
|
return {};
|
|
18069
18807
|
}
|
|
@@ -18248,7 +18986,7 @@ program2.command("uninstall-service").description("Remove the threatcrushd syste
|
|
|
18248
18986
|
program2.command("logs").description("Tail daemon logs").action(async () => {
|
|
18249
18987
|
console.log(LOGO2);
|
|
18250
18988
|
const logPath = PATHS.logFile;
|
|
18251
|
-
if (!(0,
|
|
18989
|
+
if (!(0, import_node_fs31.existsSync)(logPath)) {
|
|
18252
18990
|
console.log(source_default.yellow(` No log file found at ${logPath}`));
|
|
18253
18991
|
console.log(source_default.dim(" Start the daemon first with `threatcrush start`\n"));
|
|
18254
18992
|
return;
|
|
@@ -18453,10 +19191,10 @@ storeCmd.command("search <query>").description("Search for modules in the store"
|
|
|
18453
19191
|
});
|
|
18454
19192
|
storeCmd.command("publish <url>").description("Publish a module from a git URL or web URL").action(async (url) => {
|
|
18455
19193
|
console.log(LOGO2);
|
|
18456
|
-
const configPath = (0,
|
|
19194
|
+
const configPath = (0, import_node_path21.join)((0, import_node_os8.homedir)(), ".threatcrush", "config.json");
|
|
18457
19195
|
let email = "";
|
|
18458
19196
|
try {
|
|
18459
|
-
const config = JSON.parse((0,
|
|
19197
|
+
const config = JSON.parse((0, import_node_fs31.readFileSync)(configPath, "utf-8"));
|
|
18460
19198
|
email = config.email || "";
|
|
18461
19199
|
} catch {
|
|
18462
19200
|
}
|
|
@@ -18473,9 +19211,9 @@ storeCmd.command("publish <url>").description("Publish a module from a git URL o
|
|
|
18473
19211
|
return;
|
|
18474
19212
|
}
|
|
18475
19213
|
try {
|
|
18476
|
-
const dir = (0,
|
|
18477
|
-
if (!(0,
|
|
18478
|
-
(0,
|
|
19214
|
+
const dir = (0, import_node_path21.join)((0, import_node_os8.homedir)(), ".threatcrush");
|
|
19215
|
+
if (!(0, import_node_fs31.existsSync)(dir)) (0, import_node_fs31.mkdirSync)(dir, { recursive: true });
|
|
19216
|
+
(0, import_node_fs31.writeFileSync)(configPath, JSON.stringify({ email }, null, 2));
|
|
18479
19217
|
console.log(source_default.dim(` Saved email to ${configPath}`));
|
|
18480
19218
|
} catch {
|
|
18481
19219
|
}
|