From bea48675de76bf64649c594f64d48e4ff4b9e46d Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Mon, 23 Feb 2026 19:15:51 -0500 Subject: [PATCH] refactor: move integrity status parsing into node check --- bin/checks/integrity-status.mjs | 78 ++++++++++++++++++++++++++ bin/doctor.sh | 79 +++++++++++++++++++-------- bin/security-audit.sh | 82 +++++++++++++++++++--------- package.json | 2 +- test/integrity-status-check.test.mjs | 72 ++++++++++++++++++++++++ vitest.config.mjs | 1 + 6 files changed, 262 insertions(+), 52 deletions(-) create mode 100755 bin/checks/integrity-status.mjs create mode 100644 test/integrity-status-check.test.mjs diff --git a/bin/checks/integrity-status.mjs b/bin/checks/integrity-status.mjs new file mode 100755 index 0000000..5090042 --- /dev/null +++ b/bin/checks/integrity-status.mjs @@ -0,0 +1,78 @@ +#!/usr/bin/env node + +import fs from "node:fs"; + +function asString(value, fallback) { + return typeof value === "string" && value.length > 0 ? value : fallback; +} + +function asCountString(value) { + if (typeof value === "number" && Number.isFinite(value) && value >= 0) { + return String(Math.trunc(value)); + } + if (typeof value === "string" && /^\d+$/.test(value)) { + return value; + } + return "0"; +} + +const statusPath = process.argv[2] || ""; + +if (!statusPath) { + process.stdout.write( + JSON.stringify({ + ok: "0", + exists: "0", + status: "unknown", + checked_at: "unknown", + missing_files: "0", + hash_mismatches: "0", + error: "missing_path_argument", + }), + ); + process.exit(0); +} + +if (!fs.existsSync(statusPath)) { + process.stdout.write( + JSON.stringify({ + ok: "1", + exists: "0", + status: "missing", + checked_at: "unknown", + missing_files: "0", + hash_mismatches: "0", + error: "", + }), + ); + process.exit(0); +} + +try { + const raw = fs.readFileSync(statusPath, "utf8"); + const parsed = JSON.parse(raw); + + process.stdout.write( + JSON.stringify({ + ok: "1", + exists: "1", + status: asString(parsed?.status, "unknown"), + checked_at: asString(parsed?.checked_at, "unknown"), + missing_files: asCountString(parsed?.missing_files), + hash_mismatches: asCountString(parsed?.hash_mismatches), + error: "", + }), + ); +} catch { + process.stdout.write( + JSON.stringify({ + ok: "0", + exists: "1", + status: "unknown", + checked_at: "unknown", + missing_files: "0", + hash_mismatches: "0", + error: "parse_error", + }), + ); +} diff --git a/bin/doctor.sh b/bin/doctor.sh index cf908a9..e48b3df 100755 --- a/bin/doctor.sh +++ b/bin/doctor.sh @@ -301,31 +301,62 @@ else fi INTEGRITY_STATUS_FILE="$BAUDBOT_INTEGRITY_STATUS_FILE" -if [ -f "$INTEGRITY_STATUS_FILE" ]; then - integrity_status="$(jq -r '.status // "unknown"' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")" - integrity_checked_at="$(jq -r '.checked_at // "unknown"' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")" - integrity_missing="$(jq -r '.missing_files // 0' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")" - integrity_mismatches="$(jq -r '.hash_mismatches // 0' "$INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")" - - case "$integrity_status" in - pass) - pass "startup manifest integrity passed ($integrity_checked_at)" - ;; - warn) - warn "startup manifest integrity reported issues at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)" - ;; - fail) - fail "startup manifest integrity failed at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)" - ;; - skipped) - warn "startup manifest integrity check is disabled/skipped" - ;; - *) - warn "startup manifest integrity status unknown (file: $INTEGRITY_STATUS_FILE)" - ;; - esac +INTEGRITY_CHECK_SCRIPT="$BAUDBOT_ROOT/bin/checks/integrity-status.mjs" +INTEGRITY_CHECK_NODE_BIN="" +if [ -n "${NODE_BIN:-}" ] && [ -x "${NODE_BIN:-}" ]; then + INTEGRITY_CHECK_NODE_BIN="$NODE_BIN" +elif command -v node >/dev/null 2>&1; then + INTEGRITY_CHECK_NODE_BIN="$(command -v node)" +fi + +if [ -n "$INTEGRITY_CHECK_NODE_BIN" ] && [ -f "$INTEGRITY_CHECK_SCRIPT" ]; then + integrity_payload="$($INTEGRITY_CHECK_NODE_BIN "$INTEGRITY_CHECK_SCRIPT" "$INTEGRITY_STATUS_FILE" 2>/dev/null || true)" +else + integrity_payload="" +fi + +if [ -n "$integrity_payload" ]; then + integrity_exists="$(printf '%s' "$integrity_payload" | json_get_string_stdin "exists" 2>/dev/null || true)" + integrity_status="$(printf '%s' "$integrity_payload" | json_get_string_stdin "status" 2>/dev/null || true)" + integrity_checked_at="$(printf '%s' "$integrity_payload" | json_get_string_stdin "checked_at" 2>/dev/null || true)" + integrity_missing="$(printf '%s' "$integrity_payload" | json_get_string_stdin "missing_files" 2>/dev/null || true)" + integrity_mismatches="$(printf '%s' "$integrity_payload" | json_get_string_stdin "hash_mismatches" 2>/dev/null || true)" + + [ -n "$integrity_exists" ] || integrity_exists="0" + [ -n "$integrity_status" ] || integrity_status="unknown" + [ -n "$integrity_checked_at" ] || integrity_checked_at="unknown" + [ -n "$integrity_missing" ] || integrity_missing="0" + [ -n "$integrity_mismatches" ] || integrity_mismatches="0" + + if [ "$integrity_exists" != "1" ]; then + if [ "$IS_ROOT" -ne 1 ] && [ -d "$BAUDBOT_HOME/.pi/agent" ]; then + warn "cannot verify startup manifest integrity status as non-root (run: sudo baudbot doctor)" + else + warn "startup manifest integrity status file missing ($INTEGRITY_STATUS_FILE)" + fi + else + case "$integrity_status" in + pass) + pass "startup manifest integrity passed ($integrity_checked_at)" + ;; + warn) + warn "startup manifest integrity reported issues at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)" + ;; + fail) + fail "startup manifest integrity failed at $integrity_checked_at ($integrity_missing missing, $integrity_mismatches mismatched)" + ;; + skipped) + warn "startup manifest integrity check is disabled/skipped" + ;; + *) + warn "startup manifest integrity status unknown (file: $INTEGRITY_STATUS_FILE)" + ;; + esac + fi else - if [ "$IS_ROOT" -ne 1 ] && [ -d "$BAUDBOT_HOME/.pi/agent" ]; then + if [ -f "$INTEGRITY_STATUS_FILE" ]; then + warn "startup manifest integrity status unreadable (file: $INTEGRITY_STATUS_FILE)" + elif [ "$IS_ROOT" -ne 1 ] && [ -d "$BAUDBOT_HOME/.pi/agent" ]; then warn "cannot verify startup manifest integrity status as non-root (run: sudo baudbot doctor)" else warn "startup manifest integrity status file missing ($INTEGRITY_STATUS_FILE)" diff --git a/bin/security-audit.sh b/bin/security-audit.sh index 720f75f..970dbd2 100755 --- a/bin/security-audit.sh +++ b/bin/security-audit.sh @@ -289,34 +289,62 @@ else "Run deploy.sh to generate" fi -if [ -f "$BAUDBOT_INTEGRITY_STATUS_FILE" ]; then - status_value="$(jq -r '.status // "unknown"' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")" - status_checked_at="$(jq -r '.checked_at // "unknown"' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "unknown")" - status_missing="$(jq -r '.missing_files // 0' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")" - status_mismatches="$(jq -r '.hash_mismatches // 0' "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || echo "0")" - - case "$status_value" in - pass) - ok "Last startup integrity check passed ($status_checked_at)" - ;; - warn) - finding "WARN" "Last startup integrity check reported issues" \ - "$status_checked_at — missing: $status_missing, mismatched: $status_mismatches" - ;; - fail) - finding "CRITICAL" "Last startup integrity check failed" \ - "$status_checked_at — missing: $status_missing, mismatched: $status_mismatches" - ;; - skipped) - finding "WARN" "Last startup integrity check was skipped/disabled" "$status_checked_at" - ;; - *) - finding "INFO" "Startup integrity status unknown" "$BAUDBOT_INTEGRITY_STATUS_FILE" - ;; - esac +INTEGRITY_CHECK_SCRIPT="$BAUDBOT_SRC/bin/checks/integrity-status.mjs" +INTEGRITY_CHECK_NODE_BIN="$(bb_resolve_runtime_node_bin "$BAUDBOT_HOME" 2>/dev/null || true)" +if [ -z "$INTEGRITY_CHECK_NODE_BIN" ] && command -v node >/dev/null 2>&1; then + INTEGRITY_CHECK_NODE_BIN="$(command -v node)" +fi + +if [ -n "$INTEGRITY_CHECK_NODE_BIN" ] && [ -f "$INTEGRITY_CHECK_SCRIPT" ]; then + integrity_payload="$($INTEGRITY_CHECK_NODE_BIN "$INTEGRITY_CHECK_SCRIPT" "$BAUDBOT_INTEGRITY_STATUS_FILE" 2>/dev/null || true)" +else + integrity_payload="" +fi + +if [ -n "$integrity_payload" ]; then + status_exists="$(printf '%s' "$integrity_payload" | json_get_string_stdin "exists" 2>/dev/null || true)" + status_value="$(printf '%s' "$integrity_payload" | json_get_string_stdin "status" 2>/dev/null || true)" + status_checked_at="$(printf '%s' "$integrity_payload" | json_get_string_stdin "checked_at" 2>/dev/null || true)" + status_missing="$(printf '%s' "$integrity_payload" | json_get_string_stdin "missing_files" 2>/dev/null || true)" + status_mismatches="$(printf '%s' "$integrity_payload" | json_get_string_stdin "hash_mismatches" 2>/dev/null || true)" + + [ -n "$status_exists" ] || status_exists="0" + [ -n "$status_value" ] || status_value="unknown" + [ -n "$status_checked_at" ] || status_checked_at="unknown" + [ -n "$status_missing" ] || status_missing="0" + [ -n "$status_mismatches" ] || status_mismatches="0" + + if [ "$status_exists" != "1" ]; then + finding "WARN" "No startup integrity status found" \ + "Expected: $BAUDBOT_INTEGRITY_STATUS_FILE (restart agent after deploy)" + else + case "$status_value" in + pass) + ok "Last startup integrity check passed ($status_checked_at)" + ;; + warn) + finding "WARN" "Last startup integrity check reported issues" \ + "$status_checked_at — missing: $status_missing, mismatched: $status_mismatches" + ;; + fail) + finding "CRITICAL" "Last startup integrity check failed" \ + "$status_checked_at — missing: $status_missing, mismatched: $status_mismatches" + ;; + skipped) + finding "WARN" "Last startup integrity check was skipped/disabled" "$status_checked_at" + ;; + *) + finding "INFO" "Startup integrity status unknown" "$BAUDBOT_INTEGRITY_STATUS_FILE" + ;; + esac + fi else - finding "WARN" "No startup integrity status found" \ - "Expected: $BAUDBOT_INTEGRITY_STATUS_FILE (restart agent after deploy)" + if [ -f "$BAUDBOT_INTEGRITY_STATUS_FILE" ]; then + finding "INFO" "Startup integrity status unreadable" "$BAUDBOT_INTEGRITY_STATUS_FILE" + else + finding "WARN" "No startup integrity status found" \ + "Expected: $BAUDBOT_INTEGRITY_STATUS_FILE (restart agent after deploy)" + fi fi echo "" diff --git a/package.json b/package.json index 54121ea..8bcd955 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "test": "vitest run --config vitest.config.mjs", - "test:js": "vitest run --config vitest.config.mjs pi/extensions/heartbeat.test.mjs pi/extensions/memory.test.mjs test/legacy-node-tests.test.mjs test/broker-bridge.integration.test.mjs", + "test:js": "vitest run --config vitest.config.mjs pi/extensions/heartbeat.test.mjs pi/extensions/memory.test.mjs test/legacy-node-tests.test.mjs test/broker-bridge.integration.test.mjs test/integrity-status-check.test.mjs", "test:shell": "vitest run --config vitest.config.mjs test/shell-scripts.test.mjs test/security-audit.test.mjs", "test:coverage": "vitest run --config vitest.config.mjs --coverage pi/extensions/heartbeat.test.mjs pi/extensions/memory.test.mjs test/legacy-node-tests.test.mjs", "lint": "npm run lint:js && npm run lint:shell", diff --git a/test/integrity-status-check.test.mjs b/test/integrity-status-check.test.mjs new file mode 100644 index 0000000..af1ed6c --- /dev/null +++ b/test/integrity-status-check.test.mjs @@ -0,0 +1,72 @@ +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +const scriptPath = path.resolve("bin/checks/integrity-status.mjs"); + +const tmpDirs = []; + +function runCheck(statusPath) { + const result = spawnSync("node", [scriptPath, statusPath], { + cwd: path.resolve("."), + encoding: "utf8", + }); + + expect(result.status).toBe(0); + return JSON.parse(result.stdout); +} + +afterEach(() => { + for (const dir of tmpDirs.splice(0)) { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +describe("bin/checks/integrity-status.mjs", () => { + it("reports missing status file", () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "baudbot-integrity-check-")); + tmpDirs.push(tmpDir); + + const payload = runCheck(path.join(tmpDir, "missing.json")); + expect(payload.exists).toBe("0"); + expect(payload.status).toBe("missing"); + }); + + it("normalizes valid status payload", () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "baudbot-integrity-check-")); + tmpDirs.push(tmpDir); + + const statusPath = path.join(tmpDir, "status.json"); + fs.writeFileSync( + statusPath, + JSON.stringify({ + status: "warn", + checked_at: "2026-02-24T00:00:00Z", + missing_files: 2, + hash_mismatches: 1, + }), + ); + + const payload = runCheck(statusPath); + expect(payload.exists).toBe("1"); + expect(payload.status).toBe("warn"); + expect(payload.checked_at).toBe("2026-02-24T00:00:00Z"); + expect(payload.missing_files).toBe("2"); + expect(payload.hash_mismatches).toBe("1"); + }); + + it("handles invalid JSON safely", () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "baudbot-integrity-check-")); + tmpDirs.push(tmpDir); + + const statusPath = path.join(tmpDir, "status.json"); + fs.writeFileSync(statusPath, "{not-json"); + + const payload = runCheck(statusPath); + expect(payload.exists).toBe("1"); + expect(payload.ok).toBe("0"); + expect(payload.error).toBe("parse_error"); + }); +}); diff --git a/vitest.config.mjs b/vitest.config.mjs index 37db185..f30921a 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -7,6 +7,7 @@ export default defineConfig({ "pi/extensions/memory.test.mjs", "test/legacy-node-tests.test.mjs", "test/broker-bridge.integration.test.mjs", + "test/integrity-status-check.test.mjs", "test/shell-scripts.test.mjs", "test/security-audit.test.mjs", ],