@glassmkr/crucible 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/dist/alerts/__tests__/rules.test.d.ts +1 -0
  2. package/dist/alerts/__tests__/rules.test.js +325 -0
  3. package/dist/alerts/__tests__/rules.test.js.map +1 -0
  4. package/dist/alerts/rules.d.ts +8 -0
  5. package/dist/alerts/rules.js +139 -32
  6. package/dist/alerts/rules.js.map +1 -1
  7. package/dist/api.d.ts +2 -0
  8. package/dist/api.js +7 -0
  9. package/dist/api.js.map +1 -0
  10. package/dist/collect/__tests__/dmi.test.d.ts +1 -0
  11. package/dist/collect/__tests__/dmi.test.js +114 -0
  12. package/dist/collect/__tests__/dmi.test.js.map +1 -0
  13. package/dist/collect/__tests__/ipmi.test.js +47 -1
  14. package/dist/collect/__tests__/ipmi.test.js.map +1 -1
  15. package/dist/collect/__tests__/thermal.test.d.ts +1 -0
  16. package/dist/collect/__tests__/thermal.test.js +164 -0
  17. package/dist/collect/__tests__/thermal.test.js.map +1 -0
  18. package/dist/collect/dmi.d.ts +19 -0
  19. package/dist/collect/dmi.js +109 -0
  20. package/dist/collect/dmi.js.map +1 -0
  21. package/dist/collect/ipmi.d.ts +27 -2
  22. package/dist/collect/ipmi.js +90 -2
  23. package/dist/collect/ipmi.js.map +1 -1
  24. package/dist/collect/thermal.d.ts +10 -0
  25. package/dist/collect/thermal.js +187 -0
  26. package/dist/collect/thermal.js.map +1 -0
  27. package/dist/config.d.ts +10 -0
  28. package/dist/config.js +2 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/index.js +52 -14
  31. package/dist/index.js.map +1 -1
  32. package/dist/lib/__tests__/capability.test.d.ts +1 -0
  33. package/dist/lib/__tests__/capability.test.js +87 -0
  34. package/dist/lib/__tests__/capability.test.js.map +1 -0
  35. package/dist/lib/__tests__/vendor-sensors.test.d.ts +1 -0
  36. package/dist/lib/__tests__/vendor-sensors.test.js +49 -0
  37. package/dist/lib/__tests__/vendor-sensors.test.js.map +1 -0
  38. package/dist/lib/capability.d.ts +21 -0
  39. package/dist/lib/capability.js +110 -0
  40. package/dist/lib/capability.js.map +1 -0
  41. package/dist/lib/cpu-thermal-chips.d.ts +2 -0
  42. package/dist/lib/cpu-thermal-chips.js +28 -0
  43. package/dist/lib/cpu-thermal-chips.js.map +1 -0
  44. package/dist/lib/types.d.ts +58 -0
  45. package/dist/lib/vendor-sensors.d.ts +27 -0
  46. package/dist/lib/vendor-sensors.js +63 -0
  47. package/dist/lib/vendor-sensors.js.map +1 -0
  48. package/dist/lib/version-check.js +1 -1
  49. package/dist/lib/version-check.js.map +1 -1
  50. package/dist/lib/version.d.ts +1 -0
  51. package/dist/lib/version.js +32 -0
  52. package/dist/lib/version.js.map +1 -0
  53. package/dist/notify/email.js +2 -1
  54. package/dist/notify/email.js.map +1 -1
  55. package/dist/notify/slack.js +2 -1
  56. package/dist/notify/slack.js.map +1 -1
  57. package/dist/notify/telegram.js +1 -1
  58. package/dist/notify/telegram.js.map +1 -1
  59. package/package.json +16 -1
  60. package/rule-ids.json +29 -0
  61. package/.dockerignore +0 -13
  62. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
  63. package/.github/ISSUE_TEMPLATE/no_data.md +0 -26
  64. package/.github/workflows/docker.yml +0 -53
  65. package/.github/workflows/publish.yml +0 -25
  66. package/Dockerfile +0 -59
  67. package/config/collector.example.yaml +0 -43
  68. package/docker-compose.yml +0 -26
  69. package/scripts/sign-release.sh +0 -29
  70. package/src/__tests__/cli.test.ts +0 -74
  71. package/src/__tests__/reboot-marker.test.ts +0 -122
  72. package/src/alerts/evaluator.ts +0 -15
  73. package/src/alerts/rules.ts +0 -283
  74. package/src/alerts/state.ts +0 -92
  75. package/src/cli.ts +0 -112
  76. package/src/collect/__tests__/ipmi.test.ts +0 -96
  77. package/src/collect/__tests__/smart.test.ts +0 -68
  78. package/src/collect/__tests__/system.test.ts +0 -29
  79. package/src/collect/__tests__/zfs.test.ts +0 -72
  80. package/src/collect/conntrack.ts +0 -27
  81. package/src/collect/cpu.ts +0 -92
  82. package/src/collect/disks.ts +0 -91
  83. package/src/collect/fd.ts +0 -31
  84. package/src/collect/io-errors.ts +0 -23
  85. package/src/collect/io-latency.ts +0 -103
  86. package/src/collect/ipmi.ts +0 -207
  87. package/src/collect/memory.ts +0 -30
  88. package/src/collect/network.ts +0 -193
  89. package/src/collect/ntp.ts +0 -114
  90. package/src/collect/os-alerts.ts +0 -43
  91. package/src/collect/raid.ts +0 -40
  92. package/src/collect/security.ts +0 -268
  93. package/src/collect/smart.ts +0 -72
  94. package/src/collect/system.ts +0 -32
  95. package/src/collect/systemd.ts +0 -33
  96. package/src/collect/zfs.ts +0 -66
  97. package/src/config.ts +0 -65
  98. package/src/index.ts +0 -233
  99. package/src/lib/__tests__/parse.test.ts +0 -28
  100. package/src/lib/exec.ts +0 -16
  101. package/src/lib/parse.ts +0 -29
  102. package/src/lib/reboot-marker.ts +0 -88
  103. package/src/lib/types.ts +0 -226
  104. package/src/lib/version-check.ts +0 -38
  105. package/src/metrics-server.ts +0 -123
  106. package/src/notify/email.ts +0 -68
  107. package/src/notify/slack.ts +0 -46
  108. package/src/notify/telegram.ts +0 -65
  109. package/src/push/forge.ts +0 -109
  110. package/tsconfig.json +0 -15
  111. package/vitest.config.ts +0 -12
@@ -1,30 +0,0 @@
1
- import { readProcFile, parseKb } from "../lib/parse.js";
2
- import type { MemoryInfo } from "../lib/types.js";
3
-
4
- export async function collectMemory(): Promise<MemoryInfo> {
5
- const raw = readProcFile("/proc/meminfo") || "";
6
- const kv: Record<string, string> = {};
7
- for (const line of raw.split("\n")) {
8
- const match = line.match(/^(\w+):\s+(.+)/);
9
- if (match) kv[match[1]] = match[2];
10
- }
11
-
12
- const totalKb = parseKb(kv["MemTotal"]);
13
- const availableKb = parseKb(kv["MemAvailable"]);
14
- const swapTotalKb = parseKb(kv["SwapTotal"]);
15
- const swapFreeKb = parseKb(kv["SwapFree"]);
16
-
17
- const totalMb = Math.round(totalKb / 1024);
18
- const availableMb = Math.round(availableKb / 1024);
19
- const usedMb = totalMb - availableMb;
20
- const swapTotalMb = Math.round(swapTotalKb / 1024);
21
- const swapUsedMb = Math.round((swapTotalKb - swapFreeKb) / 1024);
22
-
23
- return {
24
- total_mb: totalMb,
25
- used_mb: usedMb,
26
- available_mb: availableMb,
27
- swap_total_mb: swapTotalMb,
28
- swap_used_mb: swapUsedMb,
29
- };
30
- }
@@ -1,193 +0,0 @@
1
- import { readProcFile, sleep } from "../lib/parse.js";
2
- import { readFileSync, readdirSync } from "fs";
3
- import type { NetworkInfo } from "../lib/types.js";
4
-
5
- interface IfaceStats {
6
- rx_bytes: number; rx_packets: number; rx_errors: number; rx_drops: number;
7
- tx_bytes: number; tx_packets: number; tx_errors: number; tx_drops: number;
8
- }
9
-
10
- // Previous cumulative counters for delta computation (persists in process memory across cycles)
11
- interface PreviousCounters {
12
- rx_errors: number;
13
- tx_errors: number;
14
- rx_drops: number;
15
- tx_drops: number;
16
- rx_packets: number;
17
- tx_packets: number;
18
- rx_crc_errors?: number;
19
- rx_frame_errors?: number;
20
- rx_length_errors?: number;
21
- tx_carrier_errors?: number;
22
- }
23
-
24
- const previousCounters = new Map<string, PreviousCounters>();
25
-
26
- function readStatCounter(iface: string, name: string): number | undefined {
27
- try {
28
- const raw = readFileSync(`/sys/class/net/${iface}/statistics/${name}`, "utf-8").trim();
29
- const val = parseInt(raw, 10);
30
- return Number.isFinite(val) ? val : undefined;
31
- } catch {
32
- return undefined;
33
- }
34
- }
35
-
36
- function parseNetDev(): Record<string, IfaceStats> {
37
- const raw = readProcFile("/proc/net/dev") || "";
38
- const result: Record<string, IfaceStats> = {};
39
- for (const line of raw.split("\n").slice(2)) {
40
- const match = line.match(/^\s*(\S+):\s+(.*)/);
41
- if (!match) continue;
42
- const name = match[1];
43
- // Skip virtual interfaces
44
- if (name === "lo" || name.startsWith("veth") || name.startsWith("docker") || name.startsWith("br-") || name.startsWith("virbr")) continue;
45
- const parts = match[2].trim().split(/\s+/).map(Number);
46
- result[name] = {
47
- rx_bytes: parts[0] || 0, rx_packets: parts[1] || 0, rx_errors: parts[2] || 0, rx_drops: parts[3] || 0,
48
- tx_bytes: parts[8] || 0, tx_packets: parts[9] || 0, tx_errors: parts[10] || 0, tx_drops: parts[11] || 0,
49
- };
50
- }
51
- return result;
52
- }
53
-
54
- function getSpeed(iface: string): number {
55
- try {
56
- const speed = readFileSync(`/sys/class/net/${iface}/speed`, "utf-8").trim();
57
- const val = parseInt(speed, 10);
58
- return isNaN(val) || val <= 0 ? 0 : val;
59
- } catch {
60
- return 0;
61
- }
62
- }
63
-
64
- function getOperstate(iface: string): string {
65
- try {
66
- return readFileSync(`/sys/class/net/${iface}/operstate`, "utf-8").trim();
67
- } catch {
68
- return "unknown";
69
- }
70
- }
71
-
72
- function getBondMaster(iface: string): string | undefined {
73
- try {
74
- const bonds = readdirSync("/proc/net/bonding/");
75
- for (const bond of bonds) {
76
- const content = readFileSync(`/proc/net/bonding/${bond}`, "utf-8");
77
- if (content.includes(`Slave Interface: ${iface}`)) return bond;
78
- }
79
- } catch {
80
- // No bonds or /proc/net/bonding doesn't exist
81
- }
82
- return undefined;
83
- }
84
-
85
- function isBondMaster(iface: string): boolean {
86
- try {
87
- return readdirSync("/proc/net/bonding/").includes(iface);
88
- } catch {
89
- return false;
90
- }
91
- }
92
-
93
- // Compute delta, handling counter wraps (current < previous means reset, use current as delta)
94
- function delta(current: number, previous: number): number {
95
- if (current >= previous) return current - previous;
96
- return current; // counter wrapped or reset
97
- }
98
-
99
- export async function collectNetwork(): Promise<NetworkInfo[]> {
100
- const stats1 = parseNetDev();
101
- await sleep(1000);
102
- const stats2 = parseNetDev();
103
-
104
- const currentIfaces = new Set<string>();
105
- const results: NetworkInfo[] = [];
106
-
107
- for (const [name, s2] of Object.entries(stats2)) {
108
- const s1 = stats1[name];
109
- if (!s1) continue;
110
- currentIfaces.add(name);
111
-
112
- const prev = previousCounters.get(name);
113
-
114
- // /sys/class/net/*/statistics/ exposes finer-grained RX/TX subtype
115
- // counters than /proc/net/dev. Read cumulative values here; delta is
116
- // derived below against the previous cycle's snapshot.
117
- const rxCrcCum = readStatCounter(name, "rx_crc_errors");
118
- const rxFrameCum = readStatCounter(name, "rx_frame_errors");
119
- const rxLenCum = readStatCounter(name, "rx_length_errors");
120
- const txCarrierCum = readStatCounter(name, "tx_carrier_errors");
121
-
122
- // Compute error/drop deltas (0 on first cycle after start or new interface)
123
- let rxErrorsDelta = 0;
124
- let txErrorsDelta = 0;
125
- let rxDropsDelta = 0;
126
- let txDropsDelta = 0;
127
- let rxPacketsDelta = 0;
128
- let txPacketsDelta = 0;
129
- let rxCrcDelta: number | undefined;
130
- let rxFrameDelta: number | undefined;
131
- let rxLenDelta: number | undefined;
132
- let txCarrierDelta: number | undefined;
133
-
134
- if (prev) {
135
- rxErrorsDelta = delta(s2.rx_errors, prev.rx_errors);
136
- txErrorsDelta = delta(s2.tx_errors, prev.tx_errors);
137
- rxDropsDelta = delta(s2.rx_drops, prev.rx_drops);
138
- txDropsDelta = delta(s2.tx_drops, prev.tx_drops);
139
- rxPacketsDelta = delta(s2.rx_packets, prev.rx_packets);
140
- txPacketsDelta = delta(s2.tx_packets, prev.tx_packets);
141
- if (rxCrcCum != null && prev.rx_crc_errors != null) rxCrcDelta = delta(rxCrcCum, prev.rx_crc_errors);
142
- if (rxFrameCum != null && prev.rx_frame_errors != null) rxFrameDelta = delta(rxFrameCum, prev.rx_frame_errors);
143
- if (rxLenCum != null && prev.rx_length_errors != null) rxLenDelta = delta(rxLenCum, prev.rx_length_errors);
144
- if (txCarrierCum != null && prev.tx_carrier_errors != null) txCarrierDelta = delta(txCarrierCum, prev.tx_carrier_errors);
145
- }
146
-
147
- // Store current cumulative values for next cycle
148
- previousCounters.set(name, {
149
- rx_errors: s2.rx_errors,
150
- tx_errors: s2.tx_errors,
151
- rx_drops: s2.rx_drops,
152
- tx_drops: s2.tx_drops,
153
- rx_packets: s2.rx_packets,
154
- tx_packets: s2.tx_packets,
155
- rx_crc_errors: rxCrcCum,
156
- rx_frame_errors: rxFrameCum,
157
- rx_length_errors: rxLenCum,
158
- tx_carrier_errors: txCarrierCum,
159
- });
160
-
161
- const entry: NetworkInfo = {
162
- interface: name,
163
- speed_mbps: getSpeed(name),
164
- rx_bytes_sec: s2.rx_bytes - s1.rx_bytes, // already a 1-second delta
165
- tx_bytes_sec: s2.tx_bytes - s1.tx_bytes,
166
- rx_errors: rxErrorsDelta,
167
- tx_errors: txErrorsDelta,
168
- rx_drops: rxDropsDelta,
169
- tx_drops: txDropsDelta,
170
- rx_packets: rxPacketsDelta,
171
- tx_packets: txPacketsDelta,
172
- operstate: getOperstate(name),
173
- };
174
- if (rxCrcDelta !== undefined) entry.rx_crc_errors = rxCrcDelta;
175
- if (rxFrameDelta !== undefined) entry.rx_frame_errors = rxFrameDelta;
176
- if (rxLenDelta !== undefined) entry.rx_length_errors = rxLenDelta;
177
- if (txCarrierDelta !== undefined) entry.tx_carrier_errors = txCarrierDelta;
178
- const master = getBondMaster(name);
179
- if (master) entry.bond_master = master;
180
- // Identify bond masters (have at least one slave pointing at them).
181
- if (isBondMaster(name)) entry.is_bond_master = true;
182
- results.push(entry);
183
- }
184
-
185
- // Remove stale interfaces that disappeared
186
- for (const name of previousCounters.keys()) {
187
- if (!currentIfaces.has(name)) {
188
- previousCounters.delete(name);
189
- }
190
- }
191
-
192
- return results;
193
- }
@@ -1,114 +0,0 @@
1
- import { run } from "../lib/exec.js";
2
-
3
- export interface NtpData {
4
- synced: boolean;
5
- offset_seconds: number;
6
- source: string;
7
- daemon_running: boolean;
8
- daemon_name: string;
9
- }
10
-
11
- // Check whether a systemd unit is active. Returns false for missing units, not-found, etc.
12
- async function isUnitActive(unit: string): Promise<boolean> {
13
- const out = await run("systemctl", ["is-active", unit], 3000);
14
- return out?.trim() === "active";
15
- }
16
-
17
- // Detect which time-sync daemon unit is currently active on the host, if any.
18
- // Returns "" when none are. We check the common names in order of preference.
19
- async function detectActiveDaemon(): Promise<string> {
20
- const candidates = ["chrony", "chronyd", "systemd-timesyncd", "ntp", "ntpsec", "ntpd"];
21
- for (const unit of candidates) {
22
- if (await isUnitActive(unit)) return unit;
23
- }
24
- return "";
25
- }
26
-
27
- export async function collectNtp(): Promise<NtpData> {
28
- // The authoritative "is the daemon running" check is systemctl is-active,
29
- // not any derived flag from timedatectl. This catches daemon crashes and
30
- // manual stops where the kernel clock is still synced.
31
- const daemonName = await detectActiveDaemon();
32
- const daemonRunning = daemonName !== "";
33
-
34
- // Try timedatectl first (works for systemd-timesyncd and records the kernel
35
- // NTPSynchronized flag regardless of which daemon set it).
36
- const tdctl = await run("timedatectl", ["show", "--property=NTPSynchronized", "--value"]);
37
- if (tdctl !== null && (tdctl.trim() === "yes" || tdctl.trim() === "no")) {
38
- const synced = tdctl.trim() === "yes";
39
-
40
- let offset = 0;
41
- try {
42
- const tsStatus = await run("timedatectl", ["timesync-status"]);
43
- if (tsStatus) {
44
- const match = tsStatus.match(/Offset:\s*([+-]?\d+\.?\d*)(us|ms|s)/);
45
- if (match) {
46
- const val = parseFloat(match[1]);
47
- const unit = match[2];
48
- if (unit === "us") offset = val / 1_000_000;
49
- else if (unit === "ms") offset = val / 1000;
50
- else offset = val;
51
- }
52
- }
53
- } catch { /* offset stays 0 */ }
54
-
55
- // Prefer an explicitly detected daemon name; fall back to systemd-timesyncd
56
- // since timedatectl is most commonly the timesyncd frontend.
57
- const source = daemonName || "systemd-timesyncd";
58
- return { synced, offset_seconds: offset, source, daemon_running: daemonRunning, daemon_name: daemonName };
59
- }
60
-
61
- // Chrony tracking. Validate Leap status so we do not misread error text.
62
- const chronyOut = await run("chronyc", ["tracking"]);
63
- if (chronyOut) {
64
- const leapMatch = chronyOut.match(/Leap status\s*:\s*(.+)/);
65
- if (leapMatch) {
66
- const synced = leapMatch[1].trim() === "Normal";
67
- let offset = 0;
68
- const offsetMatch = chronyOut.match(/Last offset\s*:\s*([+-]?\d+\.?\d*)\s*seconds/);
69
- if (offsetMatch) offset = parseFloat(offsetMatch[1]);
70
- return {
71
- synced,
72
- offset_seconds: Math.abs(offset),
73
- source: daemonName || "chrony",
74
- daemon_running: daemonRunning,
75
- daemon_name: daemonName,
76
- };
77
- }
78
- }
79
-
80
- // ntpq peer table. Header check avoids false positives on error messages.
81
- const ntpqOut = await run("ntpq", ["-pn"]);
82
- if (ntpqOut) {
83
- const hasHeader = ntpqOut.split("\n").some((line) => line.includes("remote"));
84
- if (hasHeader) {
85
- const synced = ntpqOut.split("\n").some((line) => line.startsWith("*"));
86
- let offset = 0;
87
- const selectedLine = ntpqOut.split("\n").find((line) => line.startsWith("*"));
88
- if (selectedLine) {
89
- const fields = selectedLine.trim().split(/\s+/);
90
- if (fields.length >= 9) {
91
- const rawOffset = parseFloat(fields[8]);
92
- if (!isNaN(rawOffset)) offset = Math.abs(rawOffset) / 1000;
93
- }
94
- }
95
- return {
96
- synced,
97
- offset_seconds: offset,
98
- source: daemonName || "ntpd",
99
- daemon_running: daemonRunning,
100
- daemon_name: daemonName,
101
- };
102
- }
103
- }
104
-
105
- // No usable probe output. If systemd still reports a daemon as active, trust that;
106
- // otherwise report fully down.
107
- return {
108
- synced: false,
109
- offset_seconds: 0,
110
- source: daemonName || "none",
111
- daemon_running: daemonRunning,
112
- daemon_name: daemonName,
113
- };
114
- }
@@ -1,43 +0,0 @@
1
- import { run } from "../lib/exec.js";
2
- import { readProcFile } from "../lib/parse.js";
3
- import { readdirSync, readFileSync } from "fs";
4
- import type { OsAlerts } from "../lib/types.js";
5
-
6
- export async function collectOsAlerts(): Promise<OsAlerts> {
7
- // OOM kills
8
- let oomKills = 0;
9
- const dmesg = await run("dmesg", ["--level=err,crit", "--since", "5 min ago"]);
10
- if (dmesg) {
11
- oomKills = (dmesg.match(/Out of memory/gi) || []).length;
12
- }
13
-
14
- // Zombie processes
15
- let zombies = 0;
16
- try {
17
- const pids = readdirSync("/proc").filter((f) => /^\d+$/.test(f));
18
- for (const pid of pids) {
19
- try {
20
- const stat = readFileSync(`/proc/${pid}/stat`, "utf-8");
21
- // Field 3 is the state character
22
- const state = stat.split(" ")[2];
23
- if (state === "Z") zombies++;
24
- } catch { /* process disappeared */ }
25
- }
26
- } catch { /* /proc not readable */ }
27
-
28
- // Time drift (simple: check if chrony/ntp reports drift)
29
- let timeDriftMs = 0;
30
- const chrony = await run("chronyc", ["tracking"]);
31
- if (chrony) {
32
- const match = chrony.match(/System time\s*:\s*([\d.]+)\s*seconds\s*(slow|fast)/);
33
- if (match) {
34
- timeDriftMs = parseFloat(match[1]) * 1000;
35
- }
36
- }
37
-
38
- return {
39
- oom_kills_recent: oomKills,
40
- zombie_processes: zombies,
41
- time_drift_ms: Math.round(timeDriftMs * 100) / 100,
42
- };
43
- }
@@ -1,40 +0,0 @@
1
- import { readProcFile } from "../lib/parse.js";
2
- import type { RaidInfo } from "../lib/types.js";
3
-
4
- export async function collectRaid(): Promise<RaidInfo[]> {
5
- const raw = readProcFile("/proc/mdstat");
6
- if (!raw) return [];
7
-
8
- const results: RaidInfo[] = [];
9
- const lines = raw.split("\n");
10
-
11
- for (let i = 0; i < lines.length; i++) {
12
- const match = lines[i].match(/^(md\d+)\s*:\s*(\w+)\s+(\w+)\s+(.*)/);
13
- if (!match) continue;
14
-
15
- const device = match[1];
16
- const status = match[2]; // "active" or "inactive"
17
- const level = match[3]; // "raid1", "raid5", etc.
18
- const disksPart = match[4];
19
-
20
- // Parse component disks (e.g., "sda1[0] sdb1[1]")
21
- const disks = (disksPart.match(/\w+\[\d+\]/g) || []).map((d) => d.replace(/\[\d+\]/, ""));
22
-
23
- // Check next line for degraded status (e.g., "[UU_]" means one drive missing)
24
- const statusLine = lines[i + 1] || "";
25
- const bracketMatch = statusLine.match(/\[([U_]+)\]/);
26
- const degraded = bracketMatch ? bracketMatch[1].includes("_") : false;
27
-
28
- const failedDisks: string[] = [];
29
- if (degraded && bracketMatch) {
30
- const pattern = bracketMatch[1];
31
- pattern.split("").forEach((c, idx) => {
32
- if (c === "_" && disks[idx]) failedDisks.push(disks[idx]);
33
- });
34
- }
35
-
36
- results.push({ device, level, status, degraded, disks, failed_disks: failedDisks });
37
- }
38
-
39
- return results;
40
- }
@@ -1,268 +0,0 @@
1
- import { run } from "../lib/exec.js";
2
- import { readFileSync, existsSync, readdirSync } from "fs";
3
-
4
- export interface SshSecurityStatus {
5
- permitRootLogin: string;
6
- passwordAuthentication: string;
7
- rootPasswordExposed: boolean;
8
- }
9
-
10
- export interface FirewallStatus {
11
- active: boolean;
12
- source: string;
13
- details: string;
14
- }
15
-
16
- export interface SecurityUpdateStatus {
17
- distro: string;
18
- pendingCount: number;
19
- available: boolean;
20
- }
21
-
22
- export interface VulnerabilityStatus {
23
- name: string;
24
- status: string;
25
- mitigated: boolean;
26
- }
27
-
28
- export interface KernelRebootStatus {
29
- running: string;
30
- installed: string;
31
- needsReboot: boolean;
32
- }
33
-
34
- export interface AutoUpdateStatus {
35
- configured: boolean;
36
- mechanism: string;
37
- details: string;
38
- }
39
-
40
- export interface SecurityData {
41
- ssh: SshSecurityStatus | null;
42
- firewall: FirewallStatus;
43
- pending_updates: SecurityUpdateStatus | null;
44
- kernel_vulns: VulnerabilityStatus[];
45
- kernel_reboot: KernelRebootStatus | null;
46
- auto_updates: AutoUpdateStatus;
47
- }
48
-
49
- export async function collectSecurity(): Promise<SecurityData> {
50
- const [ssh, firewall, pendingUpdates, kernelVulns, kernelReboot, autoUpdates] = await Promise.all([
51
- checkSshConfig(),
52
- checkFirewall(),
53
- checkSecurityUpdates(),
54
- checkKernelVulnerabilities(),
55
- checkKernelReboot(),
56
- checkAutoUpdates(),
57
- ]);
58
-
59
- return { ssh, firewall, pending_updates: pendingUpdates, kernel_vulns: kernelVulns, kernel_reboot: kernelReboot, auto_updates: autoUpdates };
60
- }
61
-
62
- // === SSH ===
63
-
64
- async function checkSshConfig(): Promise<SshSecurityStatus | null> {
65
- // Prefer sshd -T (resolves includes and match blocks)
66
- const output = await run("sshd", ["-T"], 5000);
67
- if (output) {
68
- const getVal = (key: string): string => {
69
- const line = output.split("\n").find((l) => l.startsWith(key + " "));
70
- return line ? line.split(" ")[1].trim() : "";
71
- };
72
- const permitRootLogin = getVal("permitrootlogin");
73
- const passwordAuth = getVal("passwordauthentication");
74
- const rootPasswordExposed = permitRootLogin === "yes" && passwordAuth !== "no";
75
- return { permitRootLogin, passwordAuthentication: passwordAuth, rootPasswordExposed };
76
- }
77
-
78
- // Fallback: parse sshd_config directly
79
- try {
80
- const config = readFileSync("/etc/ssh/sshd_config", "utf-8");
81
- const lines = config.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
82
- const find = (key: string): string | null => {
83
- const line = lines.find((l) => l.toLowerCase().startsWith(key.toLowerCase()));
84
- return line ? line.split(/\s+/)[1] : null;
85
- };
86
- const permitRootLogin = find("PermitRootLogin") || "prohibit-password";
87
- const passwordAuth = find("PasswordAuthentication") || "yes";
88
- const rootPasswordExposed = permitRootLogin.toLowerCase() === "yes" && passwordAuth.toLowerCase() !== "no";
89
- return { permitRootLogin, passwordAuthentication: passwordAuth, rootPasswordExposed };
90
- } catch {
91
- return null;
92
- }
93
- }
94
-
95
- // === Firewall ===
96
-
97
- async function checkFirewall(): Promise<FirewallStatus> {
98
- // UFW: if installed, its status is authoritative (ignores Docker iptables chains)
99
- const ufw = await run("ufw", ["status"], 5000);
100
- if (ufw && ufw.includes("Status:")) {
101
- const active = ufw.includes("Status: active");
102
- return { active, source: "ufw", details: active ? "UFW is active" : "UFW is inactive" };
103
- }
104
-
105
- // firewalld: if installed, its status is authoritative
106
- const fwd = await run("firewall-cmd", ["--state"], 5000);
107
- if (fwd) {
108
- if (fwd.trim() === "running") {
109
- return { active: true, source: "firewalld", details: "firewalld is running" };
110
- }
111
- if (fwd.includes("not running") || fwd.includes("dead")) {
112
- return { active: false, source: "firewalld", details: "firewalld is not running" };
113
- }
114
- }
115
-
116
- // nftables (only if no managed firewall found)
117
- const nft = await run("nft", ["list", "ruleset"], 5000);
118
- if (nft) {
119
- const ruleLines = nft.split("\n").filter((l) => l.trim().match(/^\s*(meta|ip |ip6 |tcp |udp |ct |drop|reject|accept)/));
120
- if (ruleLines.length > 0) {
121
- return { active: true, source: "nftables", details: `${ruleLines.length} nftables rules` };
122
- }
123
- }
124
-
125
- // iptables fallback: filter out Docker/container chains to avoid false positives
126
- const ipt = await run("iptables", ["-L", "-n"], 5000);
127
- if (ipt) {
128
- const lines = ipt.split("\n").filter((l) =>
129
- l.trim() &&
130
- !l.startsWith("Chain ") &&
131
- !l.startsWith("target ") &&
132
- !l.includes("DOCKER") &&
133
- !l.includes("docker") &&
134
- !l.includes("br-") &&
135
- !l.includes("f2b-")
136
- );
137
- if (lines.length > 0) return { active: true, source: "iptables", details: `${lines.length} user iptables rules` };
138
- if (ipt.includes("policy DROP") || ipt.includes("policy REJECT")) {
139
- return { active: true, source: "iptables", details: "Default policy is DROP/REJECT" };
140
- }
141
- }
142
-
143
- return { active: false, source: "none", details: "No firewall detected (checked ufw, firewalld, nftables, iptables)" };
144
- }
145
-
146
- // === Pending Security Updates ===
147
-
148
- async function checkSecurityUpdates(): Promise<SecurityUpdateStatus | null> {
149
- let osRelease = "";
150
- try { osRelease = readFileSync("/etc/os-release", "utf-8").toLowerCase(); } catch { return null; }
151
-
152
- if (osRelease.includes("debian") || osRelease.includes("ubuntu") || osRelease.includes("mint")) {
153
- const output = await run("bash", ["-c", 'apt list --upgradable 2>/dev/null | grep -i "security" | wc -l'], 30000);
154
- if (output) {
155
- const count = parseInt(output.trim()) || 0;
156
- return { distro: osRelease.includes("ubuntu") ? "ubuntu" : "debian", pendingCount: count, available: true };
157
- }
158
- return { distro: "debian", pendingCount: 0, available: false };
159
- }
160
-
161
- if (osRelease.includes("rhel") || osRelease.includes("rocky") || osRelease.includes("alma") || osRelease.includes("fedora") || osRelease.includes("centos")) {
162
- const cmd = existsSync("/usr/bin/dnf") ? "dnf" : "yum";
163
- const output = await run("bash", ["-c", `${cmd} updateinfo list security --available 2>/dev/null | grep -c "/"`], 60000);
164
- if (output) {
165
- const count = parseInt(output.trim()) || 0;
166
- const distro = osRelease.includes("rocky") ? "rocky" : osRelease.includes("alma") ? "alma" : osRelease.includes("fedora") ? "fedora" : "rhel";
167
- return { distro, pendingCount: count, available: true };
168
- }
169
- return { distro: "rhel", pendingCount: 0, available: false };
170
- }
171
-
172
- return null;
173
- }
174
-
175
- // === Kernel Vulnerabilities ===
176
-
177
- function checkKernelVulnerabilities(): VulnerabilityStatus[] {
178
- const vulnDir = "/sys/devices/system/cpu/vulnerabilities";
179
- if (!existsSync(vulnDir)) return [];
180
-
181
- try {
182
- const files = readdirSync(vulnDir);
183
- return files.map((file) => {
184
- try {
185
- const status = readFileSync(`${vulnDir}/${file}`, "utf-8").trim();
186
- const mitigated = status.includes("Not affected") || status.includes("Mitigation:");
187
- return { name: file, status, mitigated };
188
- } catch {
189
- return { name: file, status: "unknown", mitigated: true };
190
- }
191
- });
192
- } catch {
193
- return [];
194
- }
195
- }
196
-
197
- // === Kernel Reboot ===
198
-
199
- async function checkKernelReboot(): Promise<KernelRebootStatus | null> {
200
- const running = (await run("uname", ["-r"]))?.trim();
201
- if (!running) return null;
202
-
203
- // Method 1: reboot-required flag (Debian/Ubuntu)
204
- if (existsSync("/var/run/reboot-required")) {
205
- // Filter to versioned images only (e.g. linux-image-6.8.0-107-generic),
206
- // excluding metapackages like linux-image-generic, linux-image-virtual.
207
- const installed = (await run("bash", ["-c", 'dpkg -l "linux-image-*" 2>/dev/null | grep "^ii" | awk \'{print $2}\' | grep "linux-image-[0-9]" | sed "s/linux-image-//" | sort -V | tail -1']))?.trim() || "unknown";
208
- return { running, installed, needsReboot: true };
209
- }
210
-
211
- // Method 2: Compare packages (Debian/Ubuntu)
212
- // Same filter: only versioned images, no metapackages.
213
- const debPkg = (await run("bash", ["-c", 'dpkg -l "linux-image-*" 2>/dev/null | grep "^ii" | awk \'{print $2}\' | grep "linux-image-[0-9]" | sed "s/linux-image-//" | sort -V | tail -1']))?.trim();
214
- if (debPkg) {
215
- return { running, installed: debPkg, needsReboot: debPkg !== running };
216
- }
217
-
218
- // Method 3: RPM-based
219
- const rpmPkg = (await run("bash", ["-c", 'rpm -q kernel --queryformat "%{VERSION}-%{RELEASE}.%{ARCH}\\n" 2>/dev/null | sort -V | tail -1']))?.trim();
220
- if (rpmPkg) {
221
- return { running, installed: rpmPkg, needsReboot: rpmPkg !== running };
222
- }
223
-
224
- return null;
225
- }
226
-
227
- // === Auto Updates ===
228
-
229
- async function checkAutoUpdates(): Promise<AutoUpdateStatus> {
230
- // Debian/Ubuntu: unattended-upgrades
231
- const uuInstalled = await run("bash", ["-c", 'dpkg -l unattended-upgrades 2>/dev/null | grep "^ii"'], 5000);
232
- if (uuInstalled) {
233
- // Check config file
234
- const autoConf = "/etc/apt/apt.conf.d/20auto-upgrades";
235
- let configEnabled = false;
236
- if (existsSync(autoConf)) {
237
- const content = readFileSync(autoConf, "utf-8");
238
- configEnabled = content.includes('Update-Package-Lists "1"') && content.includes('Unattended-Upgrade "1"');
239
- }
240
-
241
- // Check systemd service state
242
- const serviceEnabled = (await run("bash", ["-c", "systemctl is-enabled unattended-upgrades 2>/dev/null"], 5000))?.trim() === "enabled";
243
- const serviceActive = (await run("bash", ["-c", "systemctl is-active unattended-upgrades 2>/dev/null"], 5000))?.trim() === "active";
244
-
245
- if (configEnabled && serviceEnabled) {
246
- return { configured: true, mechanism: "unattended-upgrades", details: serviceActive ? "Installed, enabled, and running" : "Installed and enabled (service not active)" };
247
- }
248
- if (!configEnabled && !serviceEnabled) {
249
- return { configured: false, mechanism: "unattended-upgrades", details: "Installed but not configured and service disabled" };
250
- }
251
- if (!serviceEnabled) {
252
- return { configured: false, mechanism: "unattended-upgrades", details: "Installed and configured but service disabled" };
253
- }
254
- return { configured: false, mechanism: "unattended-upgrades", details: "Installed but not enabled in 20auto-upgrades" };
255
- }
256
-
257
- // RHEL/Rocky/Alma: dnf-automatic
258
- const dnfAuto = await run("bash", ["-c", "rpm -q dnf-automatic 2>/dev/null"], 5000);
259
- if (dnfAuto && !dnfAuto.includes("not installed")) {
260
- const timerActive = await run("bash", ["-c", "systemctl is-enabled dnf-automatic-install.timer 2>/dev/null || systemctl is-enabled dnf-automatic.timer 2>/dev/null"], 5000);
261
- if (timerActive && timerActive.includes("enabled")) {
262
- return { configured: true, mechanism: "dnf-automatic", details: "Installed and timer enabled" };
263
- }
264
- return { configured: false, mechanism: "dnf-automatic", details: "Installed but timer not enabled" };
265
- }
266
-
267
- return { configured: false, mechanism: "none", details: "No automatic security update mechanism detected" };
268
- }