@glassmkr/crucible 0.10.3 → 0.11.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__/c1-c6.test.d.ts +1 -0
  2. package/dist/collect/__tests__/c1-c6.test.js +160 -0
  3. package/dist/collect/__tests__/c1-c6.test.js.map +1 -0
  4. package/dist/collect/__tests__/c7-c10.test.d.ts +1 -0
  5. package/dist/collect/__tests__/c7-c10.test.js +271 -0
  6. package/dist/collect/__tests__/c7-c10.test.js.map +1 -0
  7. package/dist/collect/bonding.d.ts +37 -0
  8. package/dist/collect/bonding.js +246 -0
  9. package/dist/collect/bonding.js.map +1 -0
  10. package/dist/collect/conntrack.d.ts +19 -0
  11. package/dist/collect/conntrack.js +82 -1
  12. package/dist/collect/conntrack.js.map +1 -1
  13. package/dist/collect/edac.d.ts +2 -0
  14. package/dist/collect/edac.js +104 -0
  15. package/dist/collect/edac.js.map +1 -0
  16. package/dist/collect/fd.d.ts +46 -0
  17. package/dist/collect/fd.js +148 -0
  18. package/dist/collect/fd.js.map +1 -1
  19. package/dist/collect/hardware-raid.d.ts +2 -0
  20. package/dist/collect/hardware-raid.js +152 -0
  21. package/dist/collect/hardware-raid.js.map +1 -0
  22. package/dist/collect/psi.d.ts +20 -0
  23. package/dist/collect/psi.js +90 -0
  24. package/dist/collect/psi.js.map +1 -0
  25. package/dist/collect/reboot-evidence.d.ts +2 -0
  26. package/dist/collect/reboot-evidence.js +109 -0
  27. package/dist/collect/reboot-evidence.js.map +1 -0
  28. package/dist/collect/tcp-stats.d.ts +37 -0
  29. package/dist/collect/tcp-stats.js +153 -0
  30. package/dist/collect/tcp-stats.js.map +1 -0
  31. package/dist/collect/vmstat.d.ts +22 -0
  32. package/dist/collect/vmstat.js +94 -0
  33. package/dist/collect/vmstat.js.map +1 -0
  34. package/dist/collect/zfs.js +94 -0
  35. package/dist/collect/zfs.js.map +1 -1
  36. package/dist/index.js +49 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/types.d.ts +211 -0
  39. package/package.json +1 -1
@@ -1,4 +1,18 @@
1
+ // File descriptor collection.
2
+ //
3
+ // Host-wide path (collectFileDescriptors): reads /proc/sys/fs/file-nr.
4
+ // Predates C7; still consumed by dashboard's host-wide fd_exhaustion path.
5
+ //
6
+ // Per-process path (collectProcessFd): reads /proc/<pid>/fd/ + /proc/<pid>/limits
7
+ // to surface processes approaching their RLIMIT_NOFILE soft limit. Per
8
+ // CC_SPEC_CRUCIBLE_C7_C10_NETWORK_PROCESS_COLLECTION_2026-05-19.md §1.
9
+ //
10
+ // Two-pass strategy: cheap readdir over /proc to count FDs per PID, then
11
+ // expensive read of limits + comm only for the top 50 consumers. Process-
12
+ // disappeared races are tolerated silently (ENOENT swallowed).
13
+ import { readdirSync, readFileSync } from "fs";
1
14
  import { readProcFile } from "../lib/parse.js";
15
+ const TOP_N = 50;
2
16
  export function collectFileDescriptors() {
3
17
  const raw = readProcFile("/proc/sys/fs/file-nr");
4
18
  if (!raw) {
@@ -17,4 +31,138 @@ export function collectFileDescriptors() {
17
31
  const percent = Math.round(((allocated / max) * 100) * 10) / 10;
18
32
  return { allocated, free: isNaN(free) ? 0 : free, max, percent };
19
33
  }
34
+ /**
35
+ * Per-process FD scan. Walks /proc, counts FDs per PID, then reads
36
+ * `/proc/<pid>/limits` for the top-N consumers to compute proximity
37
+ * to each process's RLIMIT_NOFILE soft limit.
38
+ *
39
+ * Returns `{ available: false }` when /proc/1 is not a directory
40
+ * (essentially never on Linux; defensive).
41
+ */
42
+ export function collectProcessFd() {
43
+ let pidNames;
44
+ try {
45
+ pidNames = readdirSync("/proc");
46
+ }
47
+ catch (err) {
48
+ return {
49
+ available: false,
50
+ reason: `/proc not accessible: ${errCode(err)}`,
51
+ top_consumers: [],
52
+ total_processes_scanned: 0,
53
+ highest_percent_of_limit: null,
54
+ };
55
+ }
56
+ // Cheap pass: just count FDs per numeric PID directory.
57
+ const fdCounts = [];
58
+ let scanned = 0;
59
+ for (const entry of pidNames) {
60
+ const pid = Number(entry);
61
+ if (!Number.isInteger(pid) || pid <= 0)
62
+ continue;
63
+ scanned++;
64
+ try {
65
+ const fds = readdirSync(`/proc/${pid}/fd`);
66
+ fdCounts.push({ pid, fd_count: fds.length });
67
+ }
68
+ catch {
69
+ // Process disappeared or no permission to read fd/; skip silently.
70
+ }
71
+ }
72
+ // Top-N by fd_count.
73
+ fdCounts.sort((a, b) => b.fd_count - a.fd_count);
74
+ const candidates = fdCounts.slice(0, TOP_N);
75
+ // Expensive pass: read comm + limits for the candidates.
76
+ const top_consumers = [];
77
+ for (const { pid, fd_count } of candidates) {
78
+ const comm = safeReadTrimmed(`/proc/${pid}/comm`);
79
+ if (comm === null)
80
+ continue; // Process disappeared.
81
+ const limits = safeReadTrimmed(`/proc/${pid}/limits`);
82
+ if (limits === null)
83
+ continue;
84
+ const parsed = parseOpenFilesLimit(limits);
85
+ if (!parsed)
86
+ continue; // Malformed limits; skip.
87
+ const { soft, hard } = parsed;
88
+ const percent = soft > 0
89
+ ? Math.round((fd_count / soft) * 1000) / 10
90
+ : 0;
91
+ top_consumers.push({
92
+ pid,
93
+ comm,
94
+ fd_count,
95
+ rlimit_nofile_soft: soft,
96
+ rlimit_nofile_hard: hard,
97
+ percent_of_soft_limit: percent,
98
+ });
99
+ }
100
+ const highest = top_consumers.length > 0
101
+ ? top_consumers.reduce((m, e) => (e.percent_of_soft_limit > m ? e.percent_of_soft_limit : m), 0)
102
+ : null;
103
+ return {
104
+ available: true,
105
+ top_consumers,
106
+ total_processes_scanned: scanned,
107
+ highest_percent_of_limit: highest,
108
+ };
109
+ }
110
+ function safeReadTrimmed(path) {
111
+ try {
112
+ return readFileSync(path, "utf-8").trim();
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
118
+ function errCode(err) {
119
+ if (err && typeof err === "object" && "code" in err) {
120
+ const code = err.code;
121
+ if (typeof code === "string")
122
+ return code;
123
+ }
124
+ return "unknown";
125
+ }
126
+ /**
127
+ * Parse the "Max open files" line from /proc/<pid>/limits.
128
+ *
129
+ * Format (fixed-width, header + rows):
130
+ * Limit Soft Limit Hard Limit Units
131
+ * Max open files 1024 4096 files
132
+ *
133
+ * Returns null when the line is missing or fields are unparseable.
134
+ * "unlimited" maps to Infinity which Number.MAX_SAFE_INTEGER would
135
+ * misrepresent; we use 0 as a sentinel meaning "no useful soft limit"
136
+ * which makes percent_of_soft_limit zero (and the dashboard rule
137
+ * treats that as a no-emission case).
138
+ */
139
+ function parseOpenFilesLimit(raw) {
140
+ for (const line of raw.split("\n")) {
141
+ if (!line.startsWith("Max open files"))
142
+ continue;
143
+ // After the label there are at least two whitespace-separated values.
144
+ const rest = line.slice("Max open files".length).trim();
145
+ const parts = rest.split(/\s+/);
146
+ if (parts.length < 2)
147
+ return null;
148
+ const soft = parseLimitValue(parts[0]);
149
+ const hard = parseLimitValue(parts[1]);
150
+ if (soft === null || hard === null)
151
+ return null;
152
+ return { soft, hard };
153
+ }
154
+ return null;
155
+ }
156
+ function parseLimitValue(v) {
157
+ if (v === "unlimited")
158
+ return 0; // sentinel; see parseOpenFilesLimit comment
159
+ const n = Number(v);
160
+ if (!Number.isFinite(n) || n < 0)
161
+ return null;
162
+ return n;
163
+ }
164
+ export const __test_only = {
165
+ parseOpenFilesLimit,
166
+ TOP_N,
167
+ };
20
168
  //# sourceMappingURL=fd.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fd.js","sourceRoot":"","sources":["../../src/collect/fd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAS/C,MAAM,UAAU,sBAAsB;IACpC,MAAM,GAAG,GAAG,YAAY,CAAC,sBAAsB,CAAC,CAAC;IACjD,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACvD,CAAC;IAED,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACvD,CAAC;IAED,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEnC,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IACvD,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;IAChE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AACnE,CAAC"}
1
+ {"version":3,"file":"fd.js","sourceRoot":"","sources":["../../src/collect/fd.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { HardwareRaidSnapshot } from "../lib/types.js";
2
+ export declare function collectHardwareRaid(): Promise<HardwareRaidSnapshot | null>;
@@ -0,0 +1,152 @@
1
+ // Hardware RAID controller scraping.
2
+ //
3
+ // Per CC_SPEC_FORGE_FOLLOWUP_C1_C6_ACTIVATION_2026-05-19.md (C5).
4
+ //
5
+ // Detects four vendor CLIs:
6
+ // - perccli (Dell PERC)
7
+ // - ssacli (HPE Smart Array)
8
+ // - storcli (LSI / Broadcom MegaRAID)
9
+ // - arcconf (Adaptec)
10
+ //
11
+ // For each installed CLI, queries the controller state and returns
12
+ // a normalized HardwareRaidController. The vendor-specific output
13
+ // formats vary considerably; this module's parsers are intentionally
14
+ // conservative — they extract a state string ("Optimal", "Degraded",
15
+ // etc.) plus an optional degraded_disks counter when the vendor output
16
+ // makes it easy to find. The dashboard's raid_degraded evaluator pages
17
+ // on any state != "Optimal"; that's the contract.
18
+ //
19
+ // Implementation scope (2026-05-19): perccli + storcli parsers are
20
+ // best-effort because the validation fleet has no hardware RAID
21
+ // controllers to verify against. Real parsing precision lands in
22
+ // follow-up PRs as customers with each vendor surface. The framework
23
+ // here ensures:
24
+ // - empty controllers[] on hosts without any vendor CLI (capability
25
+ // gate; dashboard rule no-ops),
26
+ // - empty controllers[] on hosts with the CLI but no controllers
27
+ // present (rare configurations).
28
+ //
29
+ // The dashboard's mdadm path is unaffected by this module.
30
+ import { run } from "../lib/exec.js";
31
+ async function hasCli(name) {
32
+ const out = await run("which", [name], 2000);
33
+ return !!(out && out.trim());
34
+ }
35
+ async function scrapePerccli() {
36
+ // perccli /c0 show all J — JSON output for controller 0.
37
+ // Multi-controller hosts are rare; query c0 only and let follow-ups
38
+ // expand if a customer surfaces multi-controller hardware.
39
+ const raw = await run("perccli", ["/c0", "show", "all", "J"], 10000);
40
+ if (!raw)
41
+ return [];
42
+ try {
43
+ const obj = JSON.parse(raw);
44
+ const ctrlList = obj?.Controllers ?? [];
45
+ return ctrlList.map((c) => ({
46
+ vendor: "dell",
47
+ controller_id: String(c?.["Command Status"]?.Controller ?? "0"),
48
+ state: String(c?.["Response Data"]?.["Status"]?.["Controller Status"] ?? "Unknown"),
49
+ degraded_disks: null,
50
+ raw_summary: null,
51
+ }));
52
+ }
53
+ catch {
54
+ // Output wasn't JSON (older perccli, or controller missing).
55
+ return [];
56
+ }
57
+ }
58
+ async function scrapeStorcli() {
59
+ const raw = await run("storcli", ["/call", "show", "all", "J"], 10000);
60
+ if (!raw)
61
+ return [];
62
+ try {
63
+ const obj = JSON.parse(raw);
64
+ const ctrlList = obj?.Controllers ?? [];
65
+ return ctrlList.map((c) => ({
66
+ vendor: "lsi",
67
+ controller_id: String(c?.["Command Status"]?.Controller ?? "?"),
68
+ state: String(c?.["Response Data"]?.["Status"]?.["Controller Status"] ?? "Unknown"),
69
+ degraded_disks: null,
70
+ raw_summary: null,
71
+ }));
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ }
77
+ async function scrapeSsacli() {
78
+ // ssacli ctrl all show — text format. Conservative: extract one
79
+ // line per "in slot X" entry; status reported on a "Controller Status"
80
+ // line. Real parsing lands when an HPE customer surfaces.
81
+ const raw = await run("ssacli", ["ctrl", "all", "show", "status"], 10000);
82
+ if (!raw)
83
+ return [];
84
+ const lines = raw.split("\n").map((l) => l.trim()).filter(Boolean);
85
+ const controllers = [];
86
+ let pending = null;
87
+ for (const line of lines) {
88
+ const slotMatch = line.match(/in Slot (\S+)/);
89
+ if (slotMatch) {
90
+ if (pending)
91
+ controllers.push(pendingToController(pending));
92
+ pending = { slot: slotMatch[1], status: "Unknown" };
93
+ continue;
94
+ }
95
+ const statusMatch = line.match(/Controller Status:\s*(.+)/);
96
+ if (statusMatch && pending)
97
+ pending.status = statusMatch[1].trim();
98
+ }
99
+ if (pending)
100
+ controllers.push(pendingToController(pending));
101
+ return controllers;
102
+ }
103
+ function pendingToController(p) {
104
+ return {
105
+ vendor: "hpe",
106
+ controller_id: p.slot,
107
+ state: p.status,
108
+ degraded_disks: null,
109
+ raw_summary: null,
110
+ };
111
+ }
112
+ async function scrapeArcconf() {
113
+ // arcconf has no JSON mode. Best-effort: detect controllers via
114
+ // `arcconf list` and surface a placeholder state. Real parser
115
+ // for Adaptec lands when a customer surfaces.
116
+ const raw = await run("arcconf", ["list"], 10000);
117
+ if (!raw)
118
+ return [];
119
+ const controllers = [];
120
+ for (const line of raw.split("\n")) {
121
+ const m = line.match(/Controller (\d+):/i);
122
+ if (m) {
123
+ controllers.push({
124
+ vendor: "adaptec",
125
+ controller_id: m[1],
126
+ state: "Unknown",
127
+ degraded_disks: null,
128
+ raw_summary: "arcconf parsing pending — surface a customer with Adaptec hardware",
129
+ });
130
+ }
131
+ }
132
+ return controllers;
133
+ }
134
+ export async function collectHardwareRaid() {
135
+ const hasPerccli = await hasCli("perccli");
136
+ const hasStorcli = await hasCli("storcli");
137
+ const hasSsacli = await hasCli("ssacli");
138
+ const hasArcconf = await hasCli("arcconf");
139
+ if (!hasPerccli && !hasStorcli && !hasSsacli && !hasArcconf)
140
+ return null;
141
+ const controllers = [];
142
+ if (hasPerccli)
143
+ controllers.push(...(await scrapePerccli()));
144
+ if (hasStorcli)
145
+ controllers.push(...(await scrapeStorcli()));
146
+ if (hasSsacli)
147
+ controllers.push(...(await scrapeSsacli()));
148
+ if (hasArcconf)
149
+ controllers.push(...(await scrapeArcconf()));
150
+ return { controllers };
151
+ }
152
+ //# sourceMappingURL=hardware-raid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hardware-raid.js","sourceRoot":"","sources":["../../src/collect/hardware-raid.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { PsiResource, PsiSnapshot } from "../lib/types.js";
2
+ declare function parsePsiLine(line: string): PsiResource | null;
3
+ interface PsiFile {
4
+ some: PsiResource;
5
+ /** Only present for memory + io; cpu has no "full" line in current kernels. */
6
+ full?: PsiResource;
7
+ }
8
+ declare function parsePsiFile(contents: string): PsiFile | null;
9
+ /**
10
+ * Collect PSI for all three resources. Returns null if /proc/pressure/
11
+ * is unavailable (older kernel or PSI disabled). Per resource: returns
12
+ * undefined if that specific file is unreadable (rare; usually all
13
+ * three present or none).
14
+ */
15
+ export declare function collectPsi(): PsiSnapshot | null;
16
+ export declare const __test_only: {
17
+ parsePsiLine: typeof parsePsiLine;
18
+ parsePsiFile: typeof parsePsiFile;
19
+ };
20
+ export {};
@@ -0,0 +1,90 @@
1
+ // PSI (Pressure Stall Information) collection.
2
+ //
3
+ // Reads /proc/pressure/{cpu,memory,io}. The kernel exposes this on
4
+ // 4.20+ when PSI is compiled in (most modern distros). Older kernels
5
+ // or those built without `CONFIG_PSI=y` return an empty result; the
6
+ // snapshot then omits the `psi` field entirely, and the dashboard
7
+ // alert evaluator's capability gates treat that as `available: false`.
8
+ //
9
+ // Per CC_SPEC_FORGE_FOLLOWUP_C1_C6_ACTIVATION_2026-05-19.md (C2).
10
+ //
11
+ // File format (all three resources share the same shape):
12
+ //
13
+ // some avg10=0.06 avg60=0.04 avg300=0.05 total=12345
14
+ // full avg10=0.00 avg60=0.00 avg300=0.00 total=678
15
+ //
16
+ // "some" = % of time at least one task was stalled on the resource.
17
+ // "full" = % of time ALL non-idle tasks were stalled (only present
18
+ // for memory and io; cpu has only "some" per kernel docs).
19
+ // "total" = cumulative microseconds since boot.
20
+ import { readProcFile } from "../lib/parse.js";
21
+ function parsePsiLine(line) {
22
+ // Expect: "some avg10=N avg60=N avg300=N total=N"
23
+ const parts = line.trim().split(/\s+/);
24
+ if (parts.length < 5)
25
+ return null;
26
+ const out = {};
27
+ for (const part of parts.slice(1)) {
28
+ const [k, v] = part.split("=");
29
+ if (k === "avg10")
30
+ out.avg10 = parseFloat(v);
31
+ else if (k === "avg60")
32
+ out.avg60 = parseFloat(v);
33
+ else if (k === "avg300")
34
+ out.avg300 = parseFloat(v);
35
+ else if (k === "total")
36
+ out.total = parseInt(v, 10);
37
+ }
38
+ if (out.avg10 === undefined ||
39
+ out.avg60 === undefined ||
40
+ out.avg300 === undefined ||
41
+ out.total === undefined) {
42
+ return null;
43
+ }
44
+ return out;
45
+ }
46
+ function parsePsiFile(contents) {
47
+ let some = null;
48
+ let full = null;
49
+ for (const line of contents.split("\n")) {
50
+ if (line.startsWith("some "))
51
+ some = parsePsiLine(line);
52
+ else if (line.startsWith("full "))
53
+ full = parsePsiLine(line);
54
+ }
55
+ if (!some)
56
+ return null;
57
+ return full ? { some, full } : { some };
58
+ }
59
+ /**
60
+ * Collect PSI for all three resources. Returns null if /proc/pressure/
61
+ * is unavailable (older kernel or PSI disabled). Per resource: returns
62
+ * undefined if that specific file is unreadable (rare; usually all
63
+ * three present or none).
64
+ */
65
+ export function collectPsi() {
66
+ const cpu = readProcFile("/proc/pressure/cpu");
67
+ const memory = readProcFile("/proc/pressure/memory");
68
+ const io = readProcFile("/proc/pressure/io");
69
+ if (!cpu && !memory && !io)
70
+ return null;
71
+ const out = {};
72
+ if (cpu) {
73
+ const parsed = parsePsiFile(cpu);
74
+ if (parsed)
75
+ out.cpu = parsed;
76
+ }
77
+ if (memory) {
78
+ const parsed = parsePsiFile(memory);
79
+ if (parsed)
80
+ out.memory = parsed;
81
+ }
82
+ if (io) {
83
+ const parsed = parsePsiFile(io);
84
+ if (parsed)
85
+ out.io = parsed;
86
+ }
87
+ return Object.keys(out).length > 0 ? out : null;
88
+ }
89
+ export const __test_only = { parsePsiLine, parsePsiFile };
90
+ //# sourceMappingURL=psi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"psi.js","sourceRoot":"","sources":["../../src/collect/psi.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,mEAAmE;AACnE,qEAAqE;AACrE,oEAAoE;AACpE,kEAAkE;AAClE,uEAAuE;AACvE,EAAE;AACF,kEAAkE;AAClE,EAAE;AACF,0DAA0D;AAC1D,EAAE;AACF,uDAAuD;AACvD,qDAAqD;AACrD,EAAE;AACF,oEAAoE;AACpE,mEAAmE;AACnE,oEAAoE;AACpE,gDAAgD;AAEhD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAG/C,SAAS,YAAY,CAAC,IAAY;IAChC,kDAAkD;IAClD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACvC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAClC,MAAM,GAAG,GAAyB,EAAE,CAAC;IACrC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;QAClC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,CAAC,KAAK,OAAO;YAAE,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;aACxC,IAAI,CAAC,KAAK,OAAO;YAAE,GAAG,CAAC,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;aAC7C,IAAI,CAAC,KAAK,QAAQ;YAAE,GAAG,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;aAC/C,IAAI,CAAC,KAAK,OAAO;YAAE,GAAG,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,IACE,GAAG,CAAC,KAAK,KAAK,SAAS;QACvB,GAAG,CAAC,KAAK,KAAK,SAAS;QACvB,GAAG,CAAC,MAAM,KAAK,SAAS;QACxB,GAAG,CAAC,KAAK,KAAK,SAAS,EACvB,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,GAAkB,CAAC;AAC5B,CAAC;AAQD,SAAS,YAAY,CAAC,QAAgB;IACpC,IAAI,IAAI,GAAuB,IAAI,CAAC;IACpC,IAAI,IAAI,GAAuB,IAAI,CAAC;IACpC,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;aACnD,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAC/D,CAAC;IACD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;AAC1C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,GAAG,GAAG,YAAY,CAAC,oBAAoB,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,YAAY,CAAC,uBAAuB,CAAC,CAAC;IACrD,MAAM,EAAE,GAAG,YAAY,CAAC,mBAAmB,CAAC,CAAC;IAC7C,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAExC,MAAM,GAAG,GAAgB,EAAE,CAAC;IAC5B,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM;YAAE,GAAG,CAAC,GAAG,GAAG,MAAM,CAAC;IAC/B,CAAC;IACD,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACpC,IAAI,MAAM;YAAE,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC;IAClC,CAAC;IACD,IAAI,EAAE,EAAE,CAAC;QACP,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC;QAChC,IAAI,MAAM;YAAE,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC;IAC9B,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAClD,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { RebootEvidence } from "../lib/types.js";
2
+ export declare function collectRebootEvidence(): Promise<RebootEvidence>;
@@ -0,0 +1,109 @@
1
+ // Reboot-evidence collection: pstore + kdump + wtmp.
2
+ //
3
+ // Per CC_SPEC_FORGE_FOLLOWUP_C1_C6_ACTIVATION_2026-05-19.md (C4).
4
+ //
5
+ // Three independent signals corroborate a reboot:
6
+ //
7
+ // - /sys/fs/pstore/ — persistent storage records the kernel
8
+ // wrote during the previous shutdown / panic. Presence of
9
+ // dmesg-* records indicates the prior kernel left a forensic
10
+ // trail before halting.
11
+ //
12
+ // - /var/crash/ — kdump's vmcore dump from the prior
13
+ // kernel panic. Path varies by distro; check the standard
14
+ // location plus the systemd default.
15
+ //
16
+ // - wtmp — accounting log. `last reboot -F` shows
17
+ // `reboot system boot ...` events plus optional `shutdown`
18
+ // events that preceded them. A clean shutdown (poweroff/halt)
19
+ // produces a `shutdown` record; a hard reset or power loss
20
+ // leaves only the boot record with no prior shutdown.
21
+ //
22
+ // The dashboard's unexpected_reboot rule consumes this to enrich
23
+ // the alert's evidence; the kernel_panic_detected rule (C4
24
+ // follow-up) consumes pstore_present + vmcore_present to fire its
25
+ // own P0 alert.
26
+ import { existsSync, readdirSync, statSync } from "node:fs";
27
+ import { run } from "../lib/exec.js";
28
+ const PSTORE_PATH = "/sys/fs/pstore";
29
+ const VAR_CRASH_PATH = "/var/crash";
30
+ function pstoreRecords() {
31
+ try {
32
+ const entries = readdirSync(PSTORE_PATH);
33
+ return entries.filter((n) => n.startsWith("dmesg-") || n.startsWith("console-") || n.startsWith("ftrace-"));
34
+ }
35
+ catch {
36
+ return [];
37
+ }
38
+ }
39
+ function vmcorePresent() {
40
+ try {
41
+ if (!existsSync(VAR_CRASH_PATH))
42
+ return false;
43
+ // /var/crash typically contains dated subdirectories per crash
44
+ // (e.g. /var/crash/2026-05-13-04:22:54/vmcore). Recursive check
45
+ // would be expensive; we just check if any subdirectory contains
46
+ // a file named `vmcore` or `vmcore.flat`.
47
+ const top = readdirSync(VAR_CRASH_PATH);
48
+ for (const sub of top) {
49
+ try {
50
+ const subPath = `${VAR_CRASH_PATH}/${sub}`;
51
+ const st = statSync(subPath);
52
+ if (!st.isDirectory())
53
+ continue;
54
+ const children = readdirSync(subPath);
55
+ if (children.some((c) => c === "vmcore" || c === "vmcore.flat" || c.startsWith("vmcore."))) {
56
+ return true;
57
+ }
58
+ }
59
+ catch {
60
+ // Skip unreadable subdirs.
61
+ }
62
+ }
63
+ return false;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ async function readWtmp() {
70
+ const output = await run("last", ["reboot", "-F"], 5000);
71
+ if (!output) {
72
+ return { last_reboot_raw: null, prior_shutdown_clean: false };
73
+ }
74
+ const lines = output.split("\n").filter((l) => l.trim().length > 0);
75
+ // The first non-empty line is the most recent reboot record.
76
+ const lastReboot = lines[0] ?? null;
77
+ // Look for a `shutdown` record between the most recent reboot and
78
+ // the second-most-recent reboot. `last -F` interleaves shutdown
79
+ // records when present; on a clean shutdown they sit immediately
80
+ // before the boot record.
81
+ const shutdownOutput = await run("last", ["shutdown", "-F"], 5000);
82
+ let priorShutdownClean = false;
83
+ if (shutdownOutput && lastReboot) {
84
+ // Extract a date marker from the lastReboot line; if any shutdown
85
+ // record predates this reboot by <= 5 minutes, treat as clean.
86
+ // Conservative: if any shutdown record exists in the wtmp at all,
87
+ // assume the most recent one was clean. Defensive against parsing
88
+ // glitches; refined by the dashboard evaluator.
89
+ const sLines = shutdownOutput.split("\n").filter((l) => l.trim().length > 0 && l.startsWith("shutdown"));
90
+ priorShutdownClean = sLines.length > 0;
91
+ }
92
+ return {
93
+ last_reboot_raw: lastReboot,
94
+ prior_shutdown_clean: priorShutdownClean,
95
+ };
96
+ }
97
+ export async function collectRebootEvidence() {
98
+ const records = pstoreRecords();
99
+ const vmcore = vmcorePresent();
100
+ const wtmp = await readWtmp();
101
+ return {
102
+ pstore_present: records.length > 0,
103
+ pstore_record_count: records.length,
104
+ vmcore_present: vmcore,
105
+ wtmp_reboot_record: wtmp.last_reboot_raw,
106
+ prior_shutdown_clean: wtmp.prior_shutdown_clean,
107
+ };
108
+ }
109
+ //# sourceMappingURL=reboot-evidence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reboot-evidence.js","sourceRoot":"","sources":["../../src/collect/reboot-evidence.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ export interface TcpStatsSnapshot {
2
+ available: boolean;
3
+ reason?: string;
4
+ out_segs_total?: number;
5
+ retrans_segs_total?: number;
6
+ in_segs_total?: number;
7
+ retrans_ratio?: number | null;
8
+ retrans_rate_per_sec?: number | null;
9
+ listen_overflows_total?: number;
10
+ listen_drops_total?: number;
11
+ listen_overflows_rate_per_sec?: number | null;
12
+ listen_drops_rate_per_sec?: number | null;
13
+ }
14
+ export declare function collectTcpStats(): TcpStatsSnapshot;
15
+ /**
16
+ * Parse /proc/net/snmp's `Tcp:` two-line section (header + values).
17
+ *
18
+ * Lines look like:
19
+ * Tcp: RtoAlgorithm RtoMin ... InSegs OutSegs RetransSegs InErrs OutRsts InCsumErrors
20
+ * Tcp: 1 200 ... 9876543 8765432 1234 0 100 0
21
+ */
22
+ export declare function parseTcpSnmp(): {
23
+ out_segs: number;
24
+ retrans_segs: number;
25
+ in_segs: number;
26
+ } | null;
27
+ /**
28
+ * Parse /proc/net/netstat's `TcpExt:` two-line section for the listen-
29
+ * queue counters.
30
+ */
31
+ export declare function parseTcpExt(): {
32
+ listen_overflows: number;
33
+ listen_drops: number;
34
+ } | null;
35
+ export declare const __test_only: {
36
+ resetForTests: () => void;
37
+ };