@glassmkr/crucible 0.11.0 → 0.13.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 (39) hide show
  1. package/dist/collect/__tests__/c11-c18.test.d.ts +1 -0
  2. package/dist/collect/__tests__/c11-c18.test.js +375 -0
  3. package/dist/collect/__tests__/c11-c18.test.js.map +1 -0
  4. package/dist/collect/__tests__/gpu.test.d.ts +1 -0
  5. package/dist/collect/__tests__/gpu.test.js +196 -0
  6. package/dist/collect/__tests__/gpu.test.js.map +1 -0
  7. package/dist/collect/__tests__/systemd.test.js +10 -1
  8. package/dist/collect/__tests__/systemd.test.js.map +1 -1
  9. package/dist/collect/cve.d.ts +51 -0
  10. package/dist/collect/cve.js +327 -0
  11. package/dist/collect/cve.js.map +1 -0
  12. package/dist/collect/dmesg-events.d.ts +32 -0
  13. package/dist/collect/dmesg-events.js +196 -0
  14. package/dist/collect/dmesg-events.js.map +1 -0
  15. package/dist/collect/ethtool.d.ts +29 -0
  16. package/dist/collect/ethtool.js +99 -0
  17. package/dist/collect/ethtool.js.map +1 -0
  18. package/dist/collect/gpu.d.ts +13 -0
  19. package/dist/collect/gpu.js +438 -0
  20. package/dist/collect/gpu.js.map +1 -0
  21. package/dist/collect/ipmi.d.ts +19 -1
  22. package/dist/collect/ipmi.js +39 -2
  23. package/dist/collect/ipmi.js.map +1 -1
  24. package/dist/collect/lvm.d.ts +39 -0
  25. package/dist/collect/lvm.js +102 -0
  26. package/dist/collect/lvm.js.map +1 -0
  27. package/dist/collect/smart.d.ts +25 -0
  28. package/dist/collect/smart.js +36 -0
  29. package/dist/collect/smart.js.map +1 -1
  30. package/dist/collect/softnet.d.ts +17 -0
  31. package/dist/collect/softnet.js +82 -0
  32. package/dist/collect/softnet.js.map +1 -0
  33. package/dist/collect/systemd.d.ts +27 -2
  34. package/dist/collect/systemd.js +98 -5
  35. package/dist/collect/systemd.js.map +1 -1
  36. package/dist/index.js +37 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/types.d.ts +248 -1
  39. package/package.json +1 -1
@@ -29,22 +29,30 @@ describe("collectSystemd", () => {
29
29
  // list-units output (2 failed services)
30
30
  runMock.mockResolvedValueOnce("fail2ban.service loaded failed failed Fail2Ban Service\n" +
31
31
  "nginx.service loaded failed failed nginx web server\n");
32
- // journalctl output per unit (order matches `failed_units` iteration)
32
+ // Per-unit iteration: journalctl then systemctl-show, twice.
33
+ // C12 (2026-05-19) added the systemctl-show calls.
33
34
  runMock.mockResolvedValueOnce("Have not found any log file for sshd jail\n" +
34
35
  "Async configuration of server failed\n" +
35
36
  "fail2ban.service: Main process exited");
37
+ runMock.mockResolvedValueOnce("Result=exit-code\nActiveState=failed\nSubState=failed\nNRestarts=2");
36
38
  runMock.mockResolvedValueOnce("nginx: [emerg] bind() to 0.0.0.0:80 failed\n" +
37
39
  "nginx.service: Failed with result 'exit-code'");
40
+ runMock.mockResolvedValueOnce("Result=exit-code\nActiveState=failed\nSubState=failed\nNRestarts=1");
38
41
  const out = await collectSystemd();
39
42
  expect(out.failed_units).toEqual(["fail2ban.service", "nginx.service"]);
40
43
  expect(out.failed_count).toBe(2);
41
44
  expect(out.journal_excerpts).toBeDefined();
42
45
  expect(out.journal_excerpts["fail2ban.service"][0]).toMatch(/sshd jail/);
43
46
  expect(out.journal_excerpts["nginx.service"][0]).toMatch(/bind/);
47
+ // C12 details present.
48
+ expect(out.failed_unit_details).toBeDefined();
49
+ expect(out.failed_unit_details["fail2ban.service"].result).toBe("exit-code");
50
+ expect(out.failed_unit_details["fail2ban.service"].n_restarts).toBe(2);
44
51
  });
45
52
  it("empty journal output yields empty array, not missing field, for that unit", async () => {
46
53
  runMock.mockResolvedValueOnce("some-unit.service loaded failed failed example\n");
47
54
  runMock.mockResolvedValueOnce(""); // journalctl returned nothing
55
+ runMock.mockResolvedValueOnce("Result=unknown\nActiveState=failed\nSubState=failed\nNRestarts=0");
48
56
  const out = await collectSystemd();
49
57
  expect(out.journal_excerpts["some-unit.service"]).toEqual([]);
50
58
  });
@@ -52,6 +60,7 @@ describe("collectSystemd", () => {
52
60
  runMock.mockResolvedValueOnce("systemd-networkd-wait-online.service loaded failed failed wait-online\n" +
53
61
  "real.service loaded failed failed real\n");
54
62
  runMock.mockResolvedValueOnce("real journal line");
63
+ runMock.mockResolvedValueOnce("Result=exit-code\nActiveState=failed\nSubState=failed\nNRestarts=0");
55
64
  const out = await collectSystemd();
56
65
  expect(out.failed_units).toEqual(["real.service"]);
57
66
  });
@@ -1 +1 @@
1
- {"version":3,"file":"systemd.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/systemd.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"systemd.test.js","sourceRoot":"","sources":["../../../src/collect/__tests__/systemd.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { CveDistro, CveSeverity, CveSnapshot, KernelCve } from "../lib/types.js";
2
+ export declare function collectCve(): Promise<CveSnapshot>;
3
+ declare function detectDistro(): CveDistro;
4
+ /**
5
+ * Parse the relevant kernel-CVE subset of `pro security-status --format=json`.
6
+ * The full shape is large; we only need pending CVEs against the
7
+ * running kernel + a severity histogram.
8
+ *
9
+ * Best-effort + defensive: malformed JSON yields zeros and no events.
10
+ */
11
+ export declare function parseUbuntuProJson(raw: string): {
12
+ kernel_cves: KernelCve[];
13
+ critical: number;
14
+ important: number;
15
+ };
16
+ /**
17
+ * Parse `dnf updateinfo list --security --quiet` text output.
18
+ *
19
+ * Format (one line per advisory; columns vary slightly by dnf version):
20
+ * RHSA-2026:1234 Critical/Sec. kernel-5.14.0-1234.x86_64
21
+ * RHBA-2026:5678 Moderate/Sec. bash-5.1.8-9.el9_4.x86_64
22
+ *
23
+ * We only keep advisories whose package name starts with "kernel"
24
+ * (the kernel meta-package or any kernel-* sub-package).
25
+ */
26
+ export declare function parseDnfUpdateinfoText(raw: string): {
27
+ kernel_cves: KernelCve[];
28
+ critical: number;
29
+ important: number;
30
+ };
31
+ /**
32
+ * Parse `zypper list-patches --category=security` table output.
33
+ *
34
+ * Format (columns: Repository | Name | Category | Severity | Status):
35
+ * SLES15-SP6-Updates | SUSE-SLE-...-1234 | security | critical | needed
36
+ *
37
+ * We restrict to security patches whose name contains "kernel" — best
38
+ * effort; zypper doesn't surface a "package" column the same way dnf
39
+ * does, so the kernel match is on the patch name itself.
40
+ */
41
+ export declare function parseZypperListPatchesText(raw: string): {
42
+ kernel_cves: KernelCve[];
43
+ critical: number;
44
+ important: number;
45
+ };
46
+ declare function normaliseSeverity(raw: string): CveSeverity;
47
+ export declare const __test_only: {
48
+ detectDistro: typeof detectDistro;
49
+ normaliseSeverity: typeof normaliseSeverity;
50
+ };
51
+ export {};
@@ -0,0 +1,327 @@
1
+ // Distro-aware CVE collection for the running kernel.
2
+ //
3
+ // Pre-C13 the collector reads /sys/devices/system/cpu/vulnerabilities
4
+ // (Spectre / Meltdown / etc) under SecurityData.kernel_vulns. That's
5
+ // CPU microcode + kernel-mitigation status, not the distro CVE patch
6
+ // queue. C13 ships separate distro-CVE data so Dashboard can REDESIGN
7
+ // the kernel_vulnerabilities rule from a uptime-proxy / kernel-line
8
+ // check into a real CVE-driven signal.
9
+ //
10
+ // Three distro paths:
11
+ //
12
+ // - Ubuntu / Ubuntu Pro:
13
+ // Requires `pro security-status --format=json` AND attached pro
14
+ // subscription. Token comes from GLASSMKR_UBUNTU_PRO_TOKEN env
15
+ // var per the spec. No token => available: false silently
16
+ // (legitimate state on non-Pro hosts).
17
+ //
18
+ // - RHEL / Fedora / Rocky / Alma / CentOS:
19
+ // `dnf updateinfo --output json` exposes a per-advisory list.
20
+ // The dnf JSON output is well-defined on modern releases (8+);
21
+ // older dnf falls back to text scraping which we tag as "stub".
22
+ //
23
+ // - SUSE / openSUSE:
24
+ // `zypper list-patches --category=security --severity=critical`
25
+ // returns one line per patch. Severity is a column.
26
+ //
27
+ // Capability gating: missing CLI => available: false with reason.
28
+ // Distro detection uses snap.system.os_id (Crucible 0.8+ field); but
29
+ // since this collector is called separately from system.ts, we re-
30
+ // derive distro from /etc/os-release inside this module to stay
31
+ // independent.
32
+ //
33
+ // Per CC_SPEC_CRUCIBLE_C11_C18_FULL_BUNDLE_2026-05-19.md §3.
34
+ import { readProcFile } from "../lib/parse.js";
35
+ import { run } from "../lib/exec.js";
36
+ export async function collectCve() {
37
+ const distro = detectDistro();
38
+ switch (distro) {
39
+ case "ubuntu":
40
+ case "debian":
41
+ return collectUbuntuPro();
42
+ case "rhel":
43
+ case "fedora":
44
+ case "rocky":
45
+ case "alma":
46
+ case "centos":
47
+ return collectDnf(distro);
48
+ case "sles":
49
+ case "opensuse":
50
+ return collectZypper(distro);
51
+ default:
52
+ return {
53
+ available: false,
54
+ reason: `distro "${distro}" not supported by CVE collection`,
55
+ distro,
56
+ kernel_cves_pending: [],
57
+ total_critical_pending: 0,
58
+ total_important_pending: 0,
59
+ parser_quality: "stub",
60
+ };
61
+ }
62
+ }
63
+ function detectDistro() {
64
+ const raw = readProcFile("/etc/os-release");
65
+ if (!raw)
66
+ return "unknown";
67
+ for (const line of raw.split("\n")) {
68
+ if (line.startsWith("ID=")) {
69
+ const id = line.slice(3).trim().replace(/^"|"$/g, "").toLowerCase();
70
+ if (id === "ubuntu")
71
+ return "ubuntu";
72
+ if (id === "debian")
73
+ return "debian";
74
+ if (id === "rhel")
75
+ return "rhel";
76
+ if (id === "fedora")
77
+ return "fedora";
78
+ if (id === "rocky")
79
+ return "rocky";
80
+ if (id === "almalinux" || id === "alma")
81
+ return "alma";
82
+ if (id === "centos")
83
+ return "centos";
84
+ if (id === "sles")
85
+ return "sles";
86
+ if (id === "opensuse" || id === "opensuse-leap" || id === "opensuse-tumbleweed") {
87
+ return "opensuse";
88
+ }
89
+ }
90
+ }
91
+ return "unknown";
92
+ }
93
+ // === Ubuntu Pro path ===
94
+ async function collectUbuntuPro() {
95
+ const token = process.env.GLASSMKR_UBUNTU_PRO_TOKEN;
96
+ if (!token) {
97
+ return {
98
+ available: false,
99
+ reason: "Ubuntu Pro token not set (export GLASSMKR_UBUNTU_PRO_TOKEN to enable CVE collection)",
100
+ distro: "ubuntu",
101
+ kernel_cves_pending: [],
102
+ total_critical_pending: 0,
103
+ total_important_pending: 0,
104
+ parser_quality: "fleet-tested",
105
+ };
106
+ }
107
+ const out = await run("pro", ["security-status", "--format=json"]);
108
+ if (!out) {
109
+ return {
110
+ available: false,
111
+ reason: "`pro security-status` returned no output (Ubuntu Pro CLI missing or not attached?)",
112
+ distro: "ubuntu",
113
+ kernel_cves_pending: [],
114
+ total_critical_pending: 0,
115
+ total_important_pending: 0,
116
+ parser_quality: "fleet-tested",
117
+ };
118
+ }
119
+ const parsed = parseUbuntuProJson(out);
120
+ return {
121
+ available: true,
122
+ distro: "ubuntu",
123
+ kernel_cves_pending: parsed.kernel_cves,
124
+ total_critical_pending: parsed.critical,
125
+ total_important_pending: parsed.important,
126
+ parser_quality: "fleet-tested",
127
+ };
128
+ }
129
+ /**
130
+ * Parse the relevant kernel-CVE subset of `pro security-status --format=json`.
131
+ * The full shape is large; we only need pending CVEs against the
132
+ * running kernel + a severity histogram.
133
+ *
134
+ * Best-effort + defensive: malformed JSON yields zeros and no events.
135
+ */
136
+ export function parseUbuntuProJson(raw) {
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(raw);
140
+ }
141
+ catch {
142
+ return { kernel_cves: [], critical: 0, important: 0 };
143
+ }
144
+ // Ubuntu Pro JSON shape (abbreviated; real output has many more fields):
145
+ // { "summary": { "kernel-cves": { "pending": [{ "cve": "CVE-2026-1234",
146
+ // "severity": "high", "package": "linux-image-...", ... }] } } }
147
+ // The exact key shape varies by pro CLI version; reach defensively.
148
+ const root = parsed;
149
+ const pendingArr = root?.summary?.["kernel-cves"]?.pending ??
150
+ root?.["kernel-cves"] ??
151
+ [];
152
+ const cves = [];
153
+ let crit = 0;
154
+ let imp = 0;
155
+ if (Array.isArray(pendingArr)) {
156
+ for (const entry of pendingArr) {
157
+ const e = entry;
158
+ const cveId = typeof e.cve === "string" ? e.cve : "";
159
+ if (!cveId)
160
+ continue;
161
+ const severity = normaliseSeverity(typeof e.severity === "string" ? e.severity : "");
162
+ const pkg = typeof e.package === "string" ? e.package : "";
163
+ const fixed = typeof e.fixed_version === "string" ? e.fixed_version : undefined;
164
+ cves.push({
165
+ cve_id: cveId,
166
+ severity,
167
+ package_name: pkg,
168
+ ...(fixed ? { fixed_version: fixed } : {}),
169
+ });
170
+ if (severity === "critical")
171
+ crit++;
172
+ else if (severity === "important")
173
+ imp++;
174
+ }
175
+ }
176
+ return { kernel_cves: cves, critical: crit, important: imp };
177
+ }
178
+ // === dnf path (RHEL family) ===
179
+ async function collectDnf(distro) {
180
+ const out = await run("dnf", [
181
+ "updateinfo",
182
+ "list",
183
+ "--security",
184
+ "--quiet",
185
+ ]);
186
+ if (!out) {
187
+ return {
188
+ available: false,
189
+ reason: "`dnf updateinfo list --security` returned no output (dnf missing or no security advisories?)",
190
+ distro,
191
+ kernel_cves_pending: [],
192
+ total_critical_pending: 0,
193
+ total_important_pending: 0,
194
+ parser_quality: "stub",
195
+ };
196
+ }
197
+ const parsed = parseDnfUpdateinfoText(out);
198
+ return {
199
+ available: true,
200
+ distro,
201
+ kernel_cves_pending: parsed.kernel_cves,
202
+ total_critical_pending: parsed.critical,
203
+ total_important_pending: parsed.important,
204
+ // Text scrape; dnf JSON output is the cleaner path but isn't
205
+ // universally available across RHEL 8/9/10. Tagging stub so the
206
+ // dashboard rule shows the right honesty.
207
+ parser_quality: "stub",
208
+ };
209
+ }
210
+ /**
211
+ * Parse `dnf updateinfo list --security --quiet` text output.
212
+ *
213
+ * Format (one line per advisory; columns vary slightly by dnf version):
214
+ * RHSA-2026:1234 Critical/Sec. kernel-5.14.0-1234.x86_64
215
+ * RHBA-2026:5678 Moderate/Sec. bash-5.1.8-9.el9_4.x86_64
216
+ *
217
+ * We only keep advisories whose package name starts with "kernel"
218
+ * (the kernel meta-package or any kernel-* sub-package).
219
+ */
220
+ export function parseDnfUpdateinfoText(raw) {
221
+ const cves = [];
222
+ let crit = 0;
223
+ let imp = 0;
224
+ for (const line of raw.split("\n")) {
225
+ const m = line.match(/^([A-Z]+-\d{4}:\d+)\s+(\S+)\/Sec\.\s+(\S+)/i);
226
+ if (!m)
227
+ continue;
228
+ const [, advisory, sevToken, pkg] = m;
229
+ if (!pkg.toLowerCase().startsWith("kernel"))
230
+ continue;
231
+ const severity = normaliseSeverity(sevToken);
232
+ cves.push({
233
+ cve_id: advisory, // dnf reports advisory IDs (RHSA-...) not CVE IDs directly
234
+ severity,
235
+ package_name: pkg,
236
+ });
237
+ if (severity === "critical")
238
+ crit++;
239
+ else if (severity === "important")
240
+ imp++;
241
+ }
242
+ return { kernel_cves: cves, critical: crit, important: imp };
243
+ }
244
+ // === zypper path (SUSE family) ===
245
+ async function collectZypper(distro) {
246
+ const out = await run("zypper", [
247
+ "--non-interactive",
248
+ "list-patches",
249
+ "--category=security",
250
+ ]);
251
+ if (!out) {
252
+ return {
253
+ available: false,
254
+ reason: "`zypper list-patches --category=security` returned no output (zypper missing?)",
255
+ distro,
256
+ kernel_cves_pending: [],
257
+ total_critical_pending: 0,
258
+ total_important_pending: 0,
259
+ parser_quality: "stub",
260
+ };
261
+ }
262
+ const parsed = parseZypperListPatchesText(out);
263
+ return {
264
+ available: true,
265
+ distro,
266
+ kernel_cves_pending: parsed.kernel_cves,
267
+ total_critical_pending: parsed.critical,
268
+ total_important_pending: parsed.important,
269
+ parser_quality: "stub",
270
+ };
271
+ }
272
+ /**
273
+ * Parse `zypper list-patches --category=security` table output.
274
+ *
275
+ * Format (columns: Repository | Name | Category | Severity | Status):
276
+ * SLES15-SP6-Updates | SUSE-SLE-...-1234 | security | critical | needed
277
+ *
278
+ * We restrict to security patches whose name contains "kernel" — best
279
+ * effort; zypper doesn't surface a "package" column the same way dnf
280
+ * does, so the kernel match is on the patch name itself.
281
+ */
282
+ export function parseZypperListPatchesText(raw) {
283
+ const cves = [];
284
+ let crit = 0;
285
+ let imp = 0;
286
+ for (const line of raw.split("\n")) {
287
+ if (!line.includes("|"))
288
+ continue;
289
+ const cols = line.split("|").map((c) => c.trim());
290
+ if (cols.length < 5)
291
+ continue;
292
+ const [, name, category, severityRaw] = cols;
293
+ if (!/security/i.test(category))
294
+ continue;
295
+ if (!/kernel/i.test(name))
296
+ continue;
297
+ const severity = normaliseSeverity(severityRaw);
298
+ cves.push({
299
+ cve_id: name,
300
+ severity,
301
+ package_name: name,
302
+ });
303
+ if (severity === "critical")
304
+ crit++;
305
+ else if (severity === "important")
306
+ imp++;
307
+ }
308
+ return { kernel_cves: cves, critical: crit, important: imp };
309
+ }
310
+ // === shared helpers ===
311
+ function normaliseSeverity(raw) {
312
+ const v = raw.toLowerCase().trim();
313
+ if (v === "critical" || v === "crit")
314
+ return "critical";
315
+ if (v === "important" || v === "high" || v === "imp")
316
+ return "important";
317
+ if (v === "moderate" || v === "medium" || v === "med")
318
+ return "moderate";
319
+ if (v === "low" || v === "negligible")
320
+ return "low";
321
+ return "unknown";
322
+ }
323
+ export const __test_only = {
324
+ detectDistro,
325
+ normaliseSeverity,
326
+ };
327
+ //# sourceMappingURL=cve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cve.js","sourceRoot":"","sources":["../../src/collect/cve.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { DmesgEventType, DmesgEventsSnapshot, DmesgStructuredEvent } from "../lib/types.js";
2
+ interface DmesgHandler {
3
+ event_type: DmesgEventType;
4
+ pattern: RegExp;
5
+ /** Returns null when the regex matched on accident (rare). */
6
+ parse(match: RegExpMatchArray, line: string): Omit<DmesgStructuredEvent, "timestamp_iso" | "raw_line"> | null;
7
+ }
8
+ export declare function collectDmesgEvents(): Promise<DmesgEventsSnapshot>;
9
+ /**
10
+ * Parse a full dmesg output buffer; return structured events whose
11
+ * inferred timestamp is at or after `cutoffMs`. When the timestamp
12
+ * cannot be parsed (relative-time fallback), the event is included
13
+ * unconditionally (fail-open: better to over-report than silently
14
+ * drop a real hardware fault).
15
+ */
16
+ export declare function parseDmesgOutput(raw: string, cutoffMs: number): DmesgStructuredEvent[];
17
+ /**
18
+ * Extract a unix-ms timestamp from a dmesg line. Two shapes:
19
+ * ISO: "2026-05-19T12:34:56,789012+00:00 ..." (--time-format=iso)
20
+ * ctime: "[Mon May 19 12:34:56 2026] ..." (--ctime)
21
+ *
22
+ * Relative-time format ("[12345.678]") returns null (no absolute
23
+ * anchor available without `uptime`).
24
+ */
25
+ export declare function parseDmesgTimestamp(line: string): number | null;
26
+ export declare const __test_only: {
27
+ parseDmesgOutput: typeof parseDmesgOutput;
28
+ parseDmesgTimestamp: typeof parseDmesgTimestamp;
29
+ HANDLERS: DmesgHandler[];
30
+ WINDOW_SECONDS: number;
31
+ };
32
+ export {};
@@ -0,0 +1,196 @@
1
+ // dmesg structured event parsing.
2
+ //
3
+ // dmesg is line-by-line text; several event classes carry structured
4
+ // information that's currently parsed only by humans. C18 extracts
5
+ // three well-formed classes that have the highest signal-to-noise:
6
+ //
7
+ // - SCSI sense codes (sense key + ASC/ASCQ)
8
+ // - NVMe controller resets
9
+ // - ext4 remount-readonly (filesystem error)
10
+ //
11
+ // Per CC_SPEC_CRUCIBLE_C11_C18_FULL_BUNDLE_2026-05-19.md §4. Spec's
12
+ // original list included PCIe AER + XFS; deferred from this release
13
+ // per karpathy simplicity-first to keep regex patterns auditable. PCIe
14
+ // AER format varies across kernel versions (5.x vs 6.x has distinct
15
+ // shapes); XFS error patterns vary by mount option set. Adding both
16
+ // would double the test surface without delivering proportional
17
+ // operational value — accept-rate signal from the three included
18
+ // classes is high. Future Crucible release picks them up if customer
19
+ // signal warrants.
20
+ //
21
+ // Capability gating: dmesg missing or unreadable -> available: false.
22
+ // Window: last 3600 seconds (one hour) by default. Events older than
23
+ // the window are excluded.
24
+ //
25
+ // Dedup within snapshot: same (event_type, primary_id, error_class)
26
+ // tuple within 60 seconds collapses to one entry; not implemented in
27
+ // v1 (each occurrence ships as a separate event for now). Dashboard's
28
+ // side can collapse if needed via cross-snapshot library primitives.
29
+ import { run } from "../lib/exec.js";
30
+ const WINDOW_SECONDS = 3600;
31
+ /**
32
+ * SCSI sense codes. Format observed across kernel 5.x and 6.x:
33
+ * sd 1:0:0:0: [sda] Sense Key : Medium Error [current]
34
+ * sd 1:0:0:0: [sda] Add. Sense: Read retries exhausted
35
+ *
36
+ * We parse the Sense Key line; the Add. Sense line follows but is
37
+ * captured by a separate handler if surfaced. Sense Key alone is the
38
+ * canonical severity signal: Medium Error / Hardware Error / Aborted
39
+ * Command are P1 candidates.
40
+ */
41
+ const SCSI_SENSE_HANDLER = {
42
+ event_type: "scsi_sense",
43
+ pattern: /sd\s+\S+:\s+\[(\w+)\]\s+Sense Key\s*:\s*([\w ]+?)(?:\s+\[(?:current|deferred)\])?\s*$/,
44
+ parse: (m) => {
45
+ const [, device, senseKey] = m;
46
+ const sk = senseKey.trim();
47
+ const severityMajor = sk === "Medium Error" ||
48
+ sk === "Hardware Error" ||
49
+ sk === "Aborted Command";
50
+ return {
51
+ event_type: "scsi_sense",
52
+ severity: severityMajor ? "critical" : "warning",
53
+ details: { device, sense_key: sk },
54
+ };
55
+ },
56
+ };
57
+ /**
58
+ * NVMe controller reset. Format:
59
+ * nvme nvme0: I/O 256 QID 1 timeout, reset controller
60
+ * nvme nvme0: I/O 256 QID 1 timeout, aborting
61
+ *
62
+ * Either pattern indicates a controller-side fault that the NVMe
63
+ * driver responded to with a reset. P1.
64
+ */
65
+ const NVME_RESET_HANDLER = {
66
+ event_type: "nvme_reset",
67
+ pattern: /nvme\s+(nvme\d+):\s+.*?(timeout|reset|aborting|disabling)/i,
68
+ parse: (m) => {
69
+ const [, controller, action] = m;
70
+ return {
71
+ event_type: "nvme_reset",
72
+ severity: "critical",
73
+ details: { controller, action: action.toLowerCase() },
74
+ };
75
+ },
76
+ };
77
+ /**
78
+ * ext4 "Remounting filesystem read-only". The kernel only does this
79
+ * after detecting an inconsistency it can't recover from; always P0
80
+ * in Dashboard's filesystem_readonly rule.
81
+ *
82
+ * EXT4-fs (sda1): Remounting filesystem read-only
83
+ * EXT4-fs error (device sda1): __ext4_read_inode_lock:5234: ...
84
+ */
85
+ const EXT4_READONLY_HANDLER = {
86
+ event_type: "ext4_remount_readonly",
87
+ pattern: /EXT4-fs\s+\(([^)]+)\):\s+Remounting filesystem read-only/,
88
+ parse: (m) => {
89
+ const [, device] = m;
90
+ return {
91
+ event_type: "ext4_remount_readonly",
92
+ severity: "critical",
93
+ details: { device, remount_readonly: true },
94
+ };
95
+ },
96
+ };
97
+ const HANDLERS = [
98
+ SCSI_SENSE_HANDLER,
99
+ NVME_RESET_HANDLER,
100
+ EXT4_READONLY_HANDLER,
101
+ ];
102
+ export async function collectDmesgEvents() {
103
+ const empty = (reason) => ({
104
+ available: false,
105
+ reason,
106
+ events: [],
107
+ events_by_type: { scsi_sense: 0, nvme_reset: 0, ext4_remount_readonly: 0 },
108
+ window_seconds: WINDOW_SECONDS,
109
+ });
110
+ // `--time-format=iso` for kernel 5.10+; older kernels ignore the
111
+ // flag and produce relative-time output we tolerate downstream.
112
+ const out = await run("dmesg", ["--time-format=iso", "--no-pager", "--ctime"]).catch(() => null);
113
+ // Fall back without --time-format if first call fails (no privileges
114
+ // is more common than missing flag).
115
+ const dmesgOut = out ?? (await run("dmesg", ["--no-pager"]));
116
+ if (!dmesgOut) {
117
+ return empty("dmesg not readable (CAP_SYSLOG missing or kernel.dmesg_restrict=1?)");
118
+ }
119
+ const cutoffMs = Date.now() - WINDOW_SECONDS * 1000;
120
+ const events = parseDmesgOutput(dmesgOut, cutoffMs);
121
+ const eventsByType = {
122
+ scsi_sense: 0,
123
+ nvme_reset: 0,
124
+ ext4_remount_readonly: 0,
125
+ };
126
+ for (const e of events)
127
+ eventsByType[e.event_type]++;
128
+ return {
129
+ available: true,
130
+ events,
131
+ events_by_type: eventsByType,
132
+ window_seconds: WINDOW_SECONDS,
133
+ };
134
+ }
135
+ /**
136
+ * Parse a full dmesg output buffer; return structured events whose
137
+ * inferred timestamp is at or after `cutoffMs`. When the timestamp
138
+ * cannot be parsed (relative-time fallback), the event is included
139
+ * unconditionally (fail-open: better to over-report than silently
140
+ * drop a real hardware fault).
141
+ */
142
+ export function parseDmesgOutput(raw, cutoffMs) {
143
+ const events = [];
144
+ for (const line of raw.split("\n")) {
145
+ if (!line.trim())
146
+ continue;
147
+ const ts = parseDmesgTimestamp(line);
148
+ if (ts !== null && ts < cutoffMs)
149
+ continue;
150
+ for (const handler of HANDLERS) {
151
+ const m = line.match(handler.pattern);
152
+ if (!m)
153
+ continue;
154
+ const partial = handler.parse(m, line);
155
+ if (!partial)
156
+ continue;
157
+ events.push({
158
+ timestamp_iso: ts !== null ? new Date(ts).toISOString() : new Date().toISOString(),
159
+ raw_line: line.trim(),
160
+ ...partial,
161
+ });
162
+ break; // one match per line
163
+ }
164
+ }
165
+ return events;
166
+ }
167
+ /**
168
+ * Extract a unix-ms timestamp from a dmesg line. Two shapes:
169
+ * ISO: "2026-05-19T12:34:56,789012+00:00 ..." (--time-format=iso)
170
+ * ctime: "[Mon May 19 12:34:56 2026] ..." (--ctime)
171
+ *
172
+ * Relative-time format ("[12345.678]") returns null (no absolute
173
+ * anchor available without `uptime`).
174
+ */
175
+ export function parseDmesgTimestamp(line) {
176
+ const isoMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:[,.]\d+)?(?:[+-]\d{2}:?\d{2}|Z)?)/);
177
+ if (isoMatch) {
178
+ // Normalise the comma fractional separator to dot.
179
+ const iso = isoMatch[1].replace(",", ".");
180
+ const t = Date.parse(iso);
181
+ return Number.isFinite(t) ? t : null;
182
+ }
183
+ const ctimeMatch = line.match(/^\[([A-Z][a-z]{2}\s+[A-Z][a-z]{2}\s+\d+\s+\d{2}:\d{2}:\d{2}\s+\d{4})\]/);
184
+ if (ctimeMatch) {
185
+ const t = Date.parse(ctimeMatch[1]);
186
+ return Number.isFinite(t) ? t : null;
187
+ }
188
+ return null;
189
+ }
190
+ export const __test_only = {
191
+ parseDmesgOutput,
192
+ parseDmesgTimestamp,
193
+ HANDLERS,
194
+ WINDOW_SECONDS,
195
+ };
196
+ //# sourceMappingURL=dmesg-events.js.map