@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.
- package/dist/collect/__tests__/c11-c18.test.d.ts +1 -0
- package/dist/collect/__tests__/c11-c18.test.js +375 -0
- package/dist/collect/__tests__/c11-c18.test.js.map +1 -0
- package/dist/collect/__tests__/gpu.test.d.ts +1 -0
- package/dist/collect/__tests__/gpu.test.js +196 -0
- package/dist/collect/__tests__/gpu.test.js.map +1 -0
- package/dist/collect/__tests__/systemd.test.js +10 -1
- package/dist/collect/__tests__/systemd.test.js.map +1 -1
- package/dist/collect/cve.d.ts +51 -0
- package/dist/collect/cve.js +327 -0
- package/dist/collect/cve.js.map +1 -0
- package/dist/collect/dmesg-events.d.ts +32 -0
- package/dist/collect/dmesg-events.js +196 -0
- package/dist/collect/dmesg-events.js.map +1 -0
- package/dist/collect/ethtool.d.ts +29 -0
- package/dist/collect/ethtool.js +99 -0
- package/dist/collect/ethtool.js.map +1 -0
- package/dist/collect/gpu.d.ts +13 -0
- package/dist/collect/gpu.js +438 -0
- package/dist/collect/gpu.js.map +1 -0
- package/dist/collect/ipmi.d.ts +19 -1
- package/dist/collect/ipmi.js +39 -2
- package/dist/collect/ipmi.js.map +1 -1
- package/dist/collect/lvm.d.ts +39 -0
- package/dist/collect/lvm.js +102 -0
- package/dist/collect/lvm.js.map +1 -0
- package/dist/collect/smart.d.ts +25 -0
- package/dist/collect/smart.js +36 -0
- package/dist/collect/smart.js.map +1 -1
- package/dist/collect/softnet.d.ts +17 -0
- package/dist/collect/softnet.js +82 -0
- package/dist/collect/softnet.js.map +1 -0
- package/dist/collect/systemd.d.ts +27 -2
- package/dist/collect/systemd.js +98 -5
- package/dist/collect/systemd.js.map +1 -1
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/types.d.ts +248 -1
- 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
|
-
//
|
|
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":"AAAA,6CAA6C;AAC7C,2DAA2D;AAC3D,8BAA8B;AAC9B,2DAA2D;AAC3D,kEAAkE;AAClE,iEAAiE;AACjE,yBAAyB;AAEzB,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAE9D,MAAM,OAAO,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;AACxB,EAAE,CAAC,IAAI,CAAC,mBAAmB,EAAE,GAAG,EAAE,CAAC,CAAC;IAClC,GAAG,EAAE,CAAC,GAAG,IAAe,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC9C,CAAC,CAAC,CAAC;AAEJ,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;AAEzD,UAAU,CAAC,GAAG,EAAE;IACd,OAAO,CAAC,SAAS,EAAE,CAAC;AACtB,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,kDAAkD,EAAE,KAAK,IAAI,EAAE;QAChE,OAAO,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC,2BAA2B;QAC9D,MAAM,GAAG,GAAG,MAAM,cAAc,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACrC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,aAAa,EAAE,CAAC;QAC7C,4CAA4C;QAC5C,MAAM,CAAC,OAAO,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAC;QACzC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0EAA0E,EAAE,KAAK,IAAI,EAAE;QACxF,wCAAwC;QACxC,OAAO,CAAC,qBAAqB,CAC3B,mEAAmE;YACnE,mEAAmE,CACpE,CAAC;QACF,
|
|
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":"AAAA,sDAAsD;AACtD,EAAE;AACF,sEAAsE;AACtE,qEAAqE;AACrE,qEAAqE;AACrE,sEAAsE;AACtE,oEAAoE;AACpE,uCAAuC;AACvC,EAAE;AACF,sBAAsB;AACtB,EAAE;AACF,2BAA2B;AAC3B,sEAAsE;AACtE,qEAAqE;AACrE,gEAAgE;AAChE,6CAA6C;AAC7C,EAAE;AACF,6CAA6C;AAC7C,oEAAoE;AACpE,qEAAqE;AACrE,sEAAsE;AACtE,EAAE;AACF,uBAAuB;AACvB,sEAAsE;AACtE,0DAA0D;AAC1D,EAAE;AACF,kEAAkE;AAClE,qEAAqE;AACrE,mEAAmE;AACnE,gEAAgE;AAChE,eAAe;AACf,EAAE;AACF,6DAA6D;AAE7D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAGrC,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,QAAQ,CAAC;QACd,KAAK,QAAQ;YACX,OAAO,gBAAgB,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC;QACZ,KAAK,QAAQ,CAAC;QACd,KAAK,OAAO,CAAC;QACb,KAAK,MAAM,CAAC;QACZ,KAAK,QAAQ;YACX,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC;QAC5B,KAAK,MAAM,CAAC;QACZ,KAAK,UAAU;YACb,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC;QAC/B;YACE,OAAO;gBACL,SAAS,EAAE,KAAK;gBAChB,MAAM,EAAE,WAAW,MAAM,mCAAmC;gBAC5D,MAAM;gBACN,mBAAmB,EAAE,EAAE;gBACvB,sBAAsB,EAAE,CAAC;gBACzB,uBAAuB,EAAE,CAAC;gBAC1B,cAAc,EAAE,MAAM;aACvB,CAAC;IACN,CAAC;AACH,CAAC;AAED,SAAS,YAAY;IACnB,MAAM,GAAG,GAAG,YAAY,CAAC,iBAAiB,CAAC,CAAC;IAC5C,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,IAAI,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;YACpE,IAAI,EAAE,KAAK,QAAQ;gBAAE,OAAO,QAAQ,CAAC;YACrC,IAAI,EAAE,KAAK,QAAQ;gBAAE,OAAO,QAAQ,CAAC;YACrC,IAAI,EAAE,KAAK,MAAM;gBAAE,OAAO,MAAM,CAAC;YACjC,IAAI,EAAE,KAAK,QAAQ;gBAAE,OAAO,QAAQ,CAAC;YACrC,IAAI,EAAE,KAAK,OAAO;gBAAE,OAAO,OAAO,CAAC;YACnC,IAAI,EAAE,KAAK,WAAW,IAAI,EAAE,KAAK,MAAM;gBAAE,OAAO,MAAM,CAAC;YACvD,IAAI,EAAE,KAAK,QAAQ;gBAAE,OAAO,QAAQ,CAAC;YACrC,IAAI,EAAE,KAAK,MAAM;gBAAE,OAAO,MAAM,CAAC;YACjC,IAAI,EAAE,KAAK,UAAU,IAAI,EAAE,KAAK,eAAe,IAAI,EAAE,KAAK,qBAAqB,EAAE,CAAC;gBAChF,OAAO,UAAU,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,0BAA0B;AAE1B,KAAK,UAAU,gBAAgB;IAC7B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC;IACpD,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,MAAM,EACJ,sFAAsF;YACxF,MAAM,EAAE,QAAQ;YAChB,mBAAmB,EAAE,EAAE;YACvB,sBAAsB,EAAE,CAAC;YACzB,uBAAuB,EAAE,CAAC;YAC1B,cAAc,EAAE,cAAc;SAC/B,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,iBAAiB,EAAE,eAAe,CAAC,CAAC,CAAC;IACnE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,MAAM,EAAE,oFAAoF;YAC5F,MAAM,EAAE,QAAQ;YAChB,mBAAmB,EAAE,EAAE;YACvB,sBAAsB,EAAE,CAAC;YACzB,uBAAuB,EAAE,CAAC;YAC1B,cAAc,EAAE,cAAc;SAC/B,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACvC,OAAO;QACL,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,QAAQ;QAChB,mBAAmB,EAAE,MAAM,CAAC,WAAW;QACvC,sBAAsB,EAAE,MAAM,CAAC,QAAQ;QACvC,uBAAuB,EAAE,MAAM,CAAC,SAAS;QACzC,cAAc,EAAE,cAAc;KAC/B,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAK5C,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACxD,CAAC;IACD,yEAAyE;IACzE,0EAA0E;IAC1E,qEAAqE;IACrE,oEAAoE;IACpE,MAAM,IAAI,GAAG,MAGZ,CAAC;IACF,MAAM,UAAU,GACd,IAAI,EAAE,OAAO,EAAE,CAAC,aAAa,CAAC,EAAE,OAAO;QACtC,IAAI,EAAE,CAAC,aAAa,CAAe;QACpC,EAAE,CAAC;IACL,MAAM,IAAI,GAAgB,EAAE,CAAC;IAC7B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9B,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;YAC/B,MAAM,CAAC,GAAG,KAAgC,CAAC;YAC3C,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,IAAI,CAAC,KAAK;gBAAE,SAAS;YACrB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACrF,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3D,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;YAChF,IAAI,CAAC,IAAI,CAAC;gBACR,MAAM,EAAE,KAAK;gBACb,QAAQ;gBACR,YAAY,EAAE,GAAG;gBACjB,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3C,CAAC,CAAC;YACH,IAAI,QAAQ,KAAK,UAAU;gBAAE,IAAI,EAAE,CAAC;iBAC/B,IAAI,QAAQ,KAAK,WAAW;gBAAE,GAAG,EAAE,CAAC;QAC3C,CAAC;IACH,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;AAC/D,CAAC;AAED,iCAAiC;AAEjC,KAAK,UAAU,UAAU,CAAC,MAAiB;IACzC,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE;QAC3B,YAAY;QACZ,MAAM;QACN,YAAY;QACZ,SAAS;KACV,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,MAAM,EAAE,8FAA8F;YACtG,MAAM;YACN,mBAAmB,EAAE,EAAE;YACvB,sBAAsB,EAAE,CAAC;YACzB,uBAAuB,EAAE,CAAC;YAC1B,cAAc,EAAE,MAAM;SACvB,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,sBAAsB,CAAC,GAAG,CAAC,CAAC;IAC3C,OAAO;QACL,SAAS,EAAE,IAAI;QACf,MAAM;QACN,mBAAmB,EAAE,MAAM,CAAC,WAAW;QACvC,sBAAsB,EAAE,MAAM,CAAC,QAAQ;QACvC,uBAAuB,EAAE,MAAM,CAAC,SAAS;QACzC,6DAA6D;QAC7D,gEAAgE;QAChE,0CAA0C;QAC1C,cAAc,EAAE,MAAM;KACvB,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,GAAW;IAKhD,MAAM,IAAI,GAAgB,EAAE,CAAC;IAC7B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAClB,6CAA6C,CAC9C,CAAC;QACF,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,MAAM,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,SAAS;QACtD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,CAAC,IAAI,CAAC;YACR,MAAM,EAAE,QAAQ,EAAE,2DAA2D;YAC7E,QAAQ;YACR,YAAY,EAAE,GAAG;SAClB,CAAC,CAAC;QACH,IAAI,QAAQ,KAAK,UAAU;YAAE,IAAI,EAAE,CAAC;aAC/B,IAAI,QAAQ,KAAK,WAAW;YAAE,GAAG,EAAE,CAAC;IAC3C,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;AAC/D,CAAC;AAED,oCAAoC;AAEpC,KAAK,UAAU,aAAa,CAAC,MAAiB;IAC5C,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,QAAQ,EAAE;QAC9B,mBAAmB;QACnB,cAAc;QACd,qBAAqB;KACtB,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO;YACL,SAAS,EAAE,KAAK;YAChB,MAAM,EAAE,gFAAgF;YACxF,MAAM;YACN,mBAAmB,EAAE,EAAE;YACvB,sBAAsB,EAAE,CAAC;YACzB,uBAAuB,EAAE,CAAC;YAC1B,cAAc,EAAE,MAAM;SACvB,CAAC;IACJ,CAAC;IACD,MAAM,MAAM,GAAG,0BAA0B,CAAC,GAAG,CAAC,CAAC;IAC/C,OAAO;QACL,SAAS,EAAE,IAAI;QACf,MAAM;QACN,mBAAmB,EAAE,MAAM,CAAC,WAAW;QACvC,sBAAsB,EAAE,MAAM,CAAC,QAAQ;QACvC,uBAAuB,EAAE,MAAM,CAAC,SAAS;QACzC,cAAc,EAAE,MAAM;KACvB,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,0BAA0B,CAAC,GAAW;IAKpD,MAAM,IAAI,GAAgB,EAAE,CAAC;IAC7B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,SAAS;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAClD,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YAAE,SAAS;QAC9B,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC;QAC7C,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;YAAE,SAAS;QAC1C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;YAAE,SAAS;QACpC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI,CAAC;YACR,MAAM,EAAE,IAAI;YACZ,QAAQ;YACR,YAAY,EAAE,IAAI;SACnB,CAAC,CAAC;QACH,IAAI,QAAQ,KAAK,UAAU;YAAE,IAAI,EAAE,CAAC;aAC/B,IAAI,QAAQ,KAAK,WAAW;YAAE,GAAG,EAAE,CAAC;IAC3C,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;AAC/D,CAAC;AAED,yBAAyB;AAEzB,SAAS,iBAAiB,CAAC,GAAW;IACpC,MAAM,CAAC,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC;IACnC,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,MAAM;QAAE,OAAO,UAAU,CAAC;IACxD,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,WAAW,CAAC;IACzE,IAAI,CAAC,KAAK,UAAU,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,UAAU,CAAC;IACzE,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,YAAY;QAAE,OAAO,KAAK,CAAC;IACpD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,YAAY;IACZ,iBAAiB;CAClB,CAAC"}
|
|
@@ -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
|