Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions bin/checks/integrity-status.mjs
Original file line number Diff line number Diff line change
@@ -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",
}),
);
}
79 changes: 55 additions & 24 deletions bin/doctor.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
82 changes: 55 additions & 27 deletions bin/security-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
72 changes: 72 additions & 0 deletions test/integrity-status-check.test.mjs
Original file line number Diff line number Diff line change
@@ -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");
});
});
1 change: 1 addition & 0 deletions vitest.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down