From 691c03f2b4480ac7366fa7f51e45c7a87a9c9d7e Mon Sep 17 00:00:00 2001 From: davida-ps Date: Sun, 15 Feb 2026 12:54:41 +0000 Subject: [PATCH] feat(clawsec-suite): integrate audit-watchdog and add email-gated setup --- skills/clawsec-suite/CHANGELOG.md | 14 + skills/clawsec-suite/SKILL.md | 19 +- .../scripts/audit-watchdog/codex_review.sh | 20 ++ .../scripts/audit-watchdog/render_report.mjs | 105 ++++++++ .../audit-watchdog/run_audit_and_format.sh | 67 +++++ .../scripts/audit-watchdog/runner.sh | 52 ++++ .../scripts/audit-watchdog/send_smtp.mjs | 157 +++++++++++ .../scripts/audit-watchdog/sendmail_report.sh | 57 ++++ .../scripts/setup_audit_watchdog.mjs | 253 ++++++++++++++++++ skills/clawsec-suite/skill.json | 66 ++++- 10 files changed, 803 insertions(+), 7 deletions(-) create mode 100755 skills/clawsec-suite/scripts/audit-watchdog/codex_review.sh create mode 100755 skills/clawsec-suite/scripts/audit-watchdog/render_report.mjs create mode 100755 skills/clawsec-suite/scripts/audit-watchdog/run_audit_and_format.sh create mode 100755 skills/clawsec-suite/scripts/audit-watchdog/runner.sh create mode 100755 skills/clawsec-suite/scripts/audit-watchdog/send_smtp.mjs create mode 100755 skills/clawsec-suite/scripts/audit-watchdog/sendmail_report.sh create mode 100644 skills/clawsec-suite/scripts/setup_audit_watchdog.mjs diff --git a/skills/clawsec-suite/CHANGELOG.md b/skills/clawsec-suite/CHANGELOG.md index 4458db9..ed03e60 100644 --- a/skills/clawsec-suite/CHANGELOG.md +++ b/skills/clawsec-suite/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to the ClawSec Suite will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.0.11] - 2026-02-15 + +### Added +- Integrated `openclaw-audit-watchdog` into `clawsec-suite` under `scripts/audit-watchdog/`. +- Added `scripts/setup_audit_watchdog.mjs` to create/update daily audit cron directly from suite. + +### Changed +- `clawsec-suite` now treats audit watchdog as an embedded component that complements `healthcheck` with read-only daily audit reporting. +- Watchdog setup now uses suite-local install paths (no standalone path assumptions). + +### Security / UX +- Watchdog setup attempts to auto-discover report email from OpenClaw-known context (env/config/git identity). +- If email cannot be discovered, setup prompts the user and refuses to create/update cron until a valid email is provided. + ## [0.0.10] - 2026-02-11 ### Security diff --git a/skills/clawsec-suite/SKILL.md b/skills/clawsec-suite/SKILL.md index 3617d1d..23cee25 100644 --- a/skills/clawsec-suite/SKILL.md +++ b/skills/clawsec-suite/SKILL.md @@ -1,6 +1,6 @@ --- name: clawsec-suite -version: 0.0.10 +version: 0.0.11 description: ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills. homepage: https://clawsec.prompt.security clawdis: @@ -27,9 +27,10 @@ This means `clawsec-suite` can: - OpenClaw advisory guardian hook package: `hooks/clawsec-advisory-guardian/` - Setup scripts for hook and optional cron scheduling: `scripts/` - Guarded installer: `scripts/guarded_skill_install.mjs` +- Integrated OpenClaw audit watchdog scripts: `scripts/audit-watchdog/` +- Watchdog cron bootstrap with email discovery/prompt guard: `scripts/setup_audit_watchdog.mjs` ### installed separately -- `openclaw-audit-watchdog` - `soul-guardian` - `clawtributor` (explicit opt-in) @@ -159,6 +160,18 @@ SUITE_DIR="${INSTALL_ROOT:-$HOME/.openclaw/skills}/clawsec-suite" node "$SUITE_DIR/scripts/setup_advisory_cron.mjs" ``` +Optional: enable the integrated daily audit watchdog cron (complements `healthcheck` by running read-only audits and reporting findings): + +```bash +SUITE_DIR="${INSTALL_ROOT:-$HOME/.openclaw/skills}/clawsec-suite" +node "$SUITE_DIR/scripts/setup_audit_watchdog.mjs" +``` + +Email behavior in watchdog setup: +- If an email is already known (OpenClaw env/config/git identity), it is auto-populated. +- If no email is known, setup prompts for one. +- Cron job is not created/updated until a valid email is provided. + What this adds: - scan on `agent:bootstrap` and `/new` (`command:new`), - compare advisory `affected` entries against installed skills, @@ -276,7 +289,7 @@ The suite hook and heartbeat guidance are intentionally non-destructive by defau Install additional protections as needed: ```bash -npx clawhub@latest install openclaw-audit-watchdog +# audit watchdog is integrated in clawsec-suite npx clawhub@latest install soul-guardian # opt-in only: npx clawhub@latest install clawtributor diff --git a/skills/clawsec-suite/scripts/audit-watchdog/codex_review.sh b/skills/clawsec-suite/scripts/audit-watchdog/codex_review.sh new file mode 100755 index 0000000..aec9b3a --- /dev/null +++ b/skills/clawsec-suite/scripts/audit-watchdog/codex_review.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Run a Codex CLI code review for this skill. +# Safe by default: read-only sandbox. + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +CODEX_BIN="/opt/homebrew/bin/codex" +if [[ ! -x "$CODEX_BIN" ]]; then + echo "codex not found at $CODEX_BIN" >&2 + exit 127 +fi + +# Use GPT-5.1 Codex Max (high reasoning). Note: some models (e.g. o3) may be blocked +# depending on the account type. +exec "$CODEX_BIN" review -s read-only -m gpt-5.1-codex-max \ + "Review this skill for security/reliability issues. Focus on: shell quoting, command injection, sendmail header injection, dependency checks, cron payload safety, and failure modes. Provide concrete patch suggestions (with diffs if possible)." \ + -c "workdir=\"$ROOT_DIR\"" \ + -c "reasoning_effort=\"xhigh\"" diff --git a/skills/clawsec-suite/scripts/audit-watchdog/render_report.mjs b/skills/clawsec-suite/scripts/audit-watchdog/render_report.mjs new file mode 100755 index 0000000..cb9d4cb --- /dev/null +++ b/skills/clawsec-suite/scripts/audit-watchdog/render_report.mjs @@ -0,0 +1,105 @@ +#!/usr/bin/env node +/** + * Render a human-readable security audit report from openclaw JSON. + * + * Usage: + * node render_report.mjs --audit audit.json --deep deep.json --label "host label" + */ + +import fs from "node:fs"; + +function readJsonSafe(p, label) { + if (!p) return { findings: [], summary: {}, error: `${label} missing` }; + try { + const s = fs.readFileSync(p, "utf8"); + return JSON.parse(s); + } catch (e) { + return { findings: [], summary: {}, error: `${label} parse failed: ${e?.message || String(e)}` }; + } +} + +function pickFindings(report) { + const findings = Array.isArray(report?.findings) ? report.findings : []; + const bySev = (sev) => findings.filter((f) => f?.severity === sev); + return { + critical: bySev("critical"), + warn: bySev("warn"), + info: bySev("info"), + summary: report?.summary ?? null, + }; +} + +function lineForFinding(f) { + const id = f?.checkId ?? "(no-checkId)"; + const title = f?.title ?? "(no-title)"; + const fix = (f?.remediation ?? "").trim(); + const fixLine = fix ? `Fix: ${fix}` : ""; + return `- ${id} ${title}${fixLine ? `\n ${fixLine}` : ""}`; +} + +function render({ audit, deep, label }) { + const now = new Date().toISOString(); + const a = pickFindings(audit); + const d = pickFindings(deep); + + const summary = a.summary || d.summary || { critical: 0, warn: 0, info: 0 }; + + const lines = []; + lines.push(`openclaw security audit report${label ? ` -- ${label}` : ""}`); + lines.push(`Time: ${now}`); + lines.push(`Summary: ${summary.critical ?? 0} critical · ${summary.warn ?? 0} warn · ${summary.info ?? 0} info`); + + const top = []; + top.push(...a.critical, ...a.warn); + const seen = new Set(); + const deduped = []; + for (const f of top) { + const key = `${f?.severity}:${f?.checkId}`; + if (seen.has(key)) continue; + seen.add(key); + deduped.push(f); + } + + if (deduped.length) { + lines.push(""); + lines.push("Findings (critical/warn):"); + for (const f of deduped.slice(0, 25)) lines.push(lineForFinding(f)); + if (deduped.length > 25) lines.push(`…${deduped.length - 25} more`); + } + + // Surface deep probe failure if present + const deepProbe = Array.isArray(deep?.findings) + ? deep.findings.find((f) => f?.checkId === "gateway.probe_failed") + : null; + if (deepProbe) { + lines.push(""); + lines.push("Deep probe:"); + lines.push(lineForFinding(deepProbe)); + } + + const errors = [audit?.error, deep?.error].filter(Boolean); + if (errors.length) { + lines.push(""); + lines.push("Errors:"); + for (const e of errors) lines.push(`- ${e}`); + } + + return lines.join("\n"); +} + +function parseArgs(argv) { + const out = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === "--audit") out.audit = argv[++i]; + else if (a === "--deep") out.deep = argv[++i]; + else if (a === "--label") out.label = argv[++i]; + } + return out; +} + +const args = parseArgs(process.argv.slice(2)); +const audit = readJsonSafe(args.audit, "audit"); +const deep = readJsonSafe(args.deep, "deep"); +const report = render({ audit, deep, label: args.label }); +process.stdout.write(report + "\n"); diff --git a/skills/clawsec-suite/scripts/audit-watchdog/run_audit_and_format.sh b/skills/clawsec-suite/scripts/audit-watchdog/run_audit_and_format.sh new file mode 100755 index 0000000..ad91c43 --- /dev/null +++ b/skills/clawsec-suite/scripts/audit-watchdog/run_audit_and_format.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runs openclaw security audits and prints a formatted report to stdout. +# +# Usage: +# ./run_audit_and_format.sh [--label "custom label"] + +LABEL="" +while [[ $# -gt 0 ]]; do + case "$1" in + --label) + LABEL="${2:-}"; shift 2 ;; + *) + echo "Unknown arg: $1" >&2 + exit 2 + ;; + esac +done + +TMPDIR="${TMPDIR:-/tmp}" +AUDIT_JSON="$(mktemp "${TMPDIR%/}/openclaw_audit.XXXXXX.audit.json")" +DEEP_JSON="$(mktemp "${TMPDIR%/}/openclaw_audit.XXXXXX.deep.json")" + +cleanup() { + rm -f "$AUDIT_JSON" "$DEEP_JSON" 2>/dev/null || true +} +trap cleanup EXIT + +command -v openclaw >/dev/null 2>&1 || { echo "openclaw not found in PATH" >&2; exit 127; } +command -v node >/dev/null 2>&1 || { echo "node not found in PATH" >&2; exit 127; } + +run_audit() { + local kind="$1" outfile="$2" + local errfile + errfile="$(mktemp "${TMPDIR%/}/openclaw_audit.XXXXXX.err")" + + # kind is either: "audit" or "deep" + if [[ "$kind" == "audit" ]]; then + if ! openclaw security audit --json >"$outfile" 2>"$errfile"; then + printf '{"findings":[],"summary":{"critical":0,"warn":0,"info":0},"error":"audit failed: %s"}\n' \ + "$(head -n 20 "$errfile" | tr '\n' ' ')" >"$outfile" + fi + else + if ! openclaw security audit --deep --json >"$outfile" 2>"$errfile"; then + printf '{"findings":[],"summary":{"critical":0,"warn":0,"info":0},"error":"deep failed: %s"}\n' \ + "$(head -n 20 "$errfile" | tr '\n' ' ')" >"$outfile" + fi + fi + + rm -f "$errfile" 2>/dev/null || true +} + +run_audit "audit" "$AUDIT_JSON" +run_audit "deep" "$DEEP_JSON" + +# Host id: prefer short hostname; fall back to full hostname +HOST_ID="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo unknown-host)" + +if [[ -z "$LABEL" ]]; then + LABEL="$HOST_ID" +else + LABEL="$LABEL ($HOST_ID)" +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +node "$SCRIPT_DIR/render_report.mjs" --audit "$AUDIT_JSON" --deep "$DEEP_JSON" --label "$LABEL" diff --git a/skills/clawsec-suite/scripts/audit-watchdog/runner.sh b/skills/clawsec-suite/scripts/audit-watchdog/runner.sh new file mode 100755 index 0000000..9ee9a18 --- /dev/null +++ b/skills/clawsec-suite/scripts/audit-watchdog/runner.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runner for Prompt Security daily audit job. +# - Optionally git-pulls repo (if PROMPTSEC_GIT_PULL=1) +# - Runs openclaw security audit + deep audit +# - Emails report to target@example.com via local sendmail +# - Prints the report to stdout (so cron delivery can DM it) + +COMPANY_EMAIL="${PROMPTSEC_EMAIL_TO:-target@example.com}" +HOST_LABEL="${PROMPTSEC_HOST_LABEL:-}" +DO_PULL="${PROMPTSEC_GIT_PULL:-0}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +if [[ "$DO_PULL" == "1" ]]; then + if command -v git >/dev/null 2>&1 && [[ -d "$ROOT_DIR/.git" ]]; then + git -C "$ROOT_DIR" pull --ff-only >/dev/null 2>&1 || true + fi +fi + +args=( ) +if [[ -n "$HOST_LABEL" ]]; then + args+=(--label "$HOST_LABEL") +fi +REPORT="$($SCRIPT_DIR/run_audit_and_format.sh "${args[@]}")" + +SUBJECT_HOST="${HOST_LABEL:-$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo unknown-host)}" +EMAIL_OK=1 + +# Prefer sendmail-compatible delivery if available; otherwise fallback to local SMTP (localhost:25 by default). +if printf '%s\n' "$REPORT" | "$SCRIPT_DIR/sendmail_report.sh" --to "$COMPANY_EMAIL" --subject "[$SUBJECT_HOST] openclaw daily security audit"; then + EMAIL_OK=1 +else + if command -v node >/dev/null 2>&1; then + if printf '%s\n' "$REPORT" | node "$SCRIPT_DIR/send_smtp.mjs" --to "$COMPANY_EMAIL" --subject "[$SUBJECT_HOST] openclaw daily security audit"; then + EMAIL_OK=1 + else + EMAIL_OK=0 + fi + else + EMAIL_OK=0 + fi +fi + +if [[ "$EMAIL_OK" -eq 0 ]]; then + printf '%s\n\n' "$REPORT" + echo "NOTE: could not deliver email to ${COMPANY_EMAIL} via local sendmail" +else + printf '%s\n' "$REPORT" +fi diff --git a/skills/clawsec-suite/scripts/audit-watchdog/send_smtp.mjs b/skills/clawsec-suite/scripts/audit-watchdog/send_smtp.mjs new file mode 100755 index 0000000..7c95760 --- /dev/null +++ b/skills/clawsec-suite/scripts/audit-watchdog/send_smtp.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node +/** + * Minimal SMTP sender (no auth) intended for localhost-relay MTAs. + * + * Env: + * - PROMPTSEC_SMTP_HOST (default 127.0.0.1) + * - PROMPTSEC_SMTP_PORT (default 25) + * - PROMPTSEC_SMTP_HELO (default hostname) + * - PROMPTSEC_SMTP_FROM (default security-checkup@) + * + * Args: + * --to + * --subject + * + * Body is read from stdin. + */ + +import net from "node:net"; +import os from "node:os"; + +function argVal(name) { + const i = process.argv.indexOf(name); + if (i === -1) return null; + return process.argv[i + 1] ?? null; +} + +const to = argVal("--to"); +const subjectRaw = argVal("--subject") ?? "openclaw daily security audit"; +if (!to) { + process.stderr.write("--to is required\n"); + process.exit(2); +} + +const host = (process.env.PROMPTSEC_SMTP_HOST || "127.0.0.1").trim(); +const port = Number(process.env.PROMPTSEC_SMTP_PORT || "25"); +const hostname = (os.hostname?.() || "unknown-host").trim(); +const helo = (process.env.PROMPTSEC_SMTP_HELO || hostname).trim(); +const from = (process.env.PROMPTSEC_SMTP_FROM || `security-checkup@${hostname}`).trim(); + +function stripCrlf(s) { + return String(s ?? "").replace(/[\r\n]+/g, " ").trim(); +} + +const subject = stripCrlf(subjectRaw); +const toClean = stripCrlf(to); +const fromClean = stripCrlf(from); + +async function readStdin() { + return await new Promise((resolve, reject) => { + let data = ""; + process.stdin.setEncoding("utf8"); + process.stdin.on("data", (c) => (data += c)); + process.stdin.on("end", () => resolve(data)); + process.stdin.on("error", reject); + }); +} + +function expectCode(line, okPrefixes) { + const code = line.slice(0, 3); + if (!okPrefixes.includes(code)) { + throw new Error(`SMTP unexpected response: ${line}`); + } +} + +function dotStuff(body) { + // SMTP DATA terminates on . + // Dot-stuff any line that begins with '.' + return body.replace(/(^|\r?\n)\./g, "$1.."); +} + +async function send() { + const body = await readStdin(); + const msg = [ + `From: ${fromClean}`, + `To: ${toClean}`, + `Subject: ${subject}`, + `Content-Type: text/plain; charset=UTF-8`, + "", + dotStuff(body).replace(/\r?\n/g, "\r\n"), + ].join("\r\n"); + + const socket = net.createConnection({ host, port }); + socket.setTimeout(10000); + + let buffer = ""; + const readLine = () => + new Promise((resolve, reject) => { + const onData = (chunk) => { + buffer += chunk.toString("utf8"); + const idx = buffer.indexOf("\r\n"); + if (idx !== -1) { + const line = buffer.slice(0, idx); + buffer = buffer.slice(idx + 2); + cleanup(); + resolve(line); + } + }; + const onError = (e) => { + cleanup(); + reject(e); + }; + const onTimeout = () => { + cleanup(); + reject(new Error("SMTP timeout")); + }; + const cleanup = () => { + socket.off("data", onData); + socket.off("error", onError); + socket.off("timeout", onTimeout); + }; + socket.on("data", onData); + socket.on("error", onError); + socket.on("timeout", onTimeout); + }); + + const write = (line) => socket.write(line + "\r\n"); + + try { + const greet = await readLine(); + expectCode(greet, ["220"]); + + write(`EHLO ${helo}`); + // Consume EHLO multi-line: 250-..., then 250 ... + while (true) { + const l = await readLine(); + if (l.startsWith("250-")) continue; + expectCode(l, ["250"]); + break; + } + + write(`MAIL FROM:<${fromClean}>`); + expectCode(await readLine(), ["250"]); + + write(`RCPT TO:<${toClean}>`); + expectCode(await readLine(), ["250", "251"]); + + write("DATA"); + expectCode(await readLine(), ["354"]); + + socket.write(msg + "\r\n.\r\n"); + expectCode(await readLine(), ["250"]); + + write("QUIT"); + // best-effort + try { await readLine(); } catch {} + + socket.end(); + } catch (e) { + try { socket.destroy(); } catch {} + throw e; + } +} + +send().catch((e) => { + process.stderr.write(String(e?.stack || e) + "\n"); + process.exit(1); +}); diff --git a/skills/clawsec-suite/scripts/audit-watchdog/sendmail_report.sh b/skills/clawsec-suite/scripts/audit-watchdog/sendmail_report.sh new file mode 100755 index 0000000..f04f58c --- /dev/null +++ b/skills/clawsec-suite/scripts/audit-watchdog/sendmail_report.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Sends report text (stdin) via local sendmail. +# +# Usage: +# ./sendmail_report.sh --to target@example.com [--subject "..."] + +TO="" +SUBJECT="openclaw daily security audit" + +while [[ $# -gt 0 ]]; do + case "$1" in + --to) + TO="${2:-}"; shift 2 ;; + --subject) + SUBJECT="${2:-}"; shift 2 ;; + *) + echo "Unknown arg: $1" >&2 + exit 2 + ;; + esac +done + +if [[ -z "$TO" ]]; then + echo "--to is required" >&2 + exit 2 +fi + +# Resolve sendmail: +# - explicit override via PROMPTSEC_SENDMAIL_BIN +# - macOS default /usr/sbin/sendmail (often not in PATH for non-login shells) +# - fallback to PATH lookup +SENDMAIL_BIN="${PROMPTSEC_SENDMAIL_BIN:-}" +if [[ -z "$SENDMAIL_BIN" ]] && [[ -x "/usr/sbin/sendmail" ]]; then + SENDMAIL_BIN="/usr/sbin/sendmail" +fi +if [[ -z "$SENDMAIL_BIN" ]]; then + SENDMAIL_BIN="$(command -v sendmail || true)" +fi +if [[ -z "$SENDMAIL_BIN" ]] || [[ ! -x "$SENDMAIL_BIN" ]]; then + echo "sendmail not found (tried PROMPTSEC_SENDMAIL_BIN, /usr/sbin/sendmail, and sendmail in PATH)" >&2 + exit 1 +fi + +# Prevent header injection: strip CR/LF from header fields +TO_CLEAN="$(printf '%s' "$TO" | tr -d '\r\n')" +SUBJECT_CLEAN="$(printf '%s' "$SUBJECT" | tr -d '\r\n')" + +# Basic RFC2822 +{ + echo "To: ${TO_CLEAN}" + echo "Subject: ${SUBJECT_CLEAN}" + echo "Content-Type: text/plain; charset=UTF-8" + echo + cat +} | "$SENDMAIL_BIN" -oi -oem -t diff --git a/skills/clawsec-suite/scripts/setup_audit_watchdog.mjs b/skills/clawsec-suite/scripts/setup_audit_watchdog.mjs new file mode 100644 index 0000000..fc49cc5 --- /dev/null +++ b/skills/clawsec-suite/scripts/setup_audit_watchdog.mjs @@ -0,0 +1,253 @@ +#!/usr/bin/env node +/** + * Setup: create/update a daily security-audit watchdog cron from clawsec-suite. + * + * Requirements: + * - DM target is required (channel + id) + * - Email recipient is required; if known from OpenClaw config/runtime, auto-populate + * otherwise prompt interactively. In non-interactive mode, fail with actionable error. + */ + +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import readline from "node:readline"; +import { fileURLToPath } from "node:url"; + +const JOB_NAME = "Daily security audit (Prompt Security)"; +const DEFAULT_TZ = "UTC"; +const DEFAULT_EXPR = "0 23 * * *"; + +const SCRIPT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const WATCHDOG_DIR = path.join(SCRIPT_ROOT, "scripts", "audit-watchdog"); + +const EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i; + +function sh(cmd, args, { input } = {}) { + const res = spawnSync(cmd, args, { + encoding: "utf8", + input: input ?? undefined, + stdio: [input ? "pipe" : "ignore", "pipe", "pipe"], + }); + if (res.error) throw res.error; + if (res.status !== 0) { + const msg = (res.stderr || res.stdout || "").trim(); + throw new Error(`${cmd} ${args.join(" ")} failed (code ${res.status})${msg ? `: ${msg}` : ""}`); + } + return res.stdout; +} + +async function prompt(question, { defaultValue = "" } = {}) { + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const q = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `; + const answer = await new Promise((resolve) => rl.question(q, resolve)); + rl.close(); + const trimmed = String(answer ?? "").trim(); + return trimmed || defaultValue; +} + +function envOrEmpty(name) { + const v = process.env[name]; + return typeof v === "string" ? v.trim() : ""; +} + +function oneline(v) { + return String(v ?? "") + .replace(/[\r\n]+/g, " ") + .replace(/"/g, '\\"') + .trim(); +} + +function looksLikeEmail(value) { + return EMAIL_RE.test(String(value ?? "").trim()); +} + +function collectEmailsFromObject(obj, found = new Set()) { + if (!obj) return found; + if (typeof obj === "string") { + const match = obj.match(EMAIL_RE); + if (match) found.add(match[0]); + return found; + } + if (Array.isArray(obj)) { + for (const item of obj) collectEmailsFromObject(item, found); + return found; + } + if (typeof obj === "object") { + for (const [k, v] of Object.entries(obj)) { + if (typeof v === "string") { + const key = String(k).toLowerCase(); + if (key.includes("email") && looksLikeEmail(v)) found.add(v.trim()); + const match = v.match(EMAIL_RE); + if (match) found.add(match[0]); + } else { + collectEmailsFromObject(v, found); + } + } + } + return found; +} + +function discoverKnownEmail() { + const candidates = [ + envOrEmpty("PROMPTSEC_EMAIL_TO"), + envOrEmpty("OPENCLAW_USER_EMAIL"), + envOrEmpty("USER_EMAIL"), + ].filter(Boolean); + + try { + const gitEmail = sh("git", ["config", "--get", "user.email"]).trim(); + if (gitEmail) candidates.push(gitEmail); + } catch {} + + // Scan OpenClaw config/state for known email addresses. + const cfgPaths = [ + path.join(os.homedir(), ".openclaw", "openclaw.json"), + path.join(os.homedir(), ".openclaw", "config.json"), + ]; + + for (const p of cfgPaths) { + try { + if (!fs.existsSync(p)) continue; + const raw = fs.readFileSync(p, "utf8"); + const parsed = JSON.parse(raw); + for (const e of collectEmailsFromObject(parsed)) candidates.push(e); + } catch {} + } + + return candidates.find(looksLikeEmail) || ""; +} + +function buildAgentMessage({ dmChannel, dmTo, hostLabel, installDir, emailTo }) { + const safeDir = oneline(installDir || ""); + return [ + "Run daily openclaw security audits and deliver report (DM + email).", + "", + `Delivery DM: ${oneline(dmChannel)}:${oneline(dmTo)}`, + `Email: ${oneline(emailTo)} (sendmail/SMTP fallback)`, + + "", + "Execute:", + `- Run via exec: cd \"${safeDir}\" && PROMPTSEC_HOST_LABEL=\"${oneline(hostLabel)}\" PROMPTSEC_EMAIL_TO=\"${oneline(emailTo)}\" ./scripts/audit-watchdog/runner.sh`, + "", + "Output requirements:", + "- Print the report to stdout (cron deliver will DM it).", + `- Also email the same report to ${oneline(emailTo)}; if email fails, append a NOTE line to stdout.`, + + "- Do not apply fixes automatically.", + "- Keep findings aligned with openclaw security audit / healthcheck workflows.", + ].join("\n"); +} + +function findExistingJobId(listJson) { + const jobs = Array.isArray(listJson?.jobs) ? listJson.jobs : []; + const match = jobs.find((j) => j?.name === JOB_NAME); + return match?.id ?? null; +} + +async function run() { + const tzEnv = envOrEmpty("PROMPTSEC_TZ"); + const dmChannelEnv = envOrEmpty("PROMPTSEC_DM_CHANNEL"); + const dmToEnv = envOrEmpty("PROMPTSEC_DM_TO"); + const hostLabelEnv = envOrEmpty("PROMPTSEC_HOST_LABEL"); + const emailEnv = envOrEmpty("PROMPTSEC_EMAIL_TO"); + const knownEmail = discoverKnownEmail(); + + const interactive = !(tzEnv && dmChannelEnv && dmToEnv); + + const tz = interactive + ? await prompt("Timezone for daily 11pm run (IANA)", { defaultValue: tzEnv || DEFAULT_TZ }) + : tzEnv || DEFAULT_TZ; + + const dmChannel = interactive + ? await prompt("DM channel (e.g. telegram, slack, discord)", { defaultValue: dmChannelEnv }) + : dmChannelEnv; + + const dmTo = interactive + ? await prompt("DM recipient id (Telegram numeric chatId/userId preferred)", { defaultValue: dmToEnv }) + : dmToEnv; + + const hostLabel = interactive + ? await prompt("Optional host label to include in report", { defaultValue: hostLabelEnv }) + : hostLabelEnv; + + const installDir = SCRIPT_ROOT; + + let emailTo = emailEnv || knownEmail; + if (interactive) { + emailTo = await prompt("Email recipient for audit reports", { defaultValue: emailTo }); + } + + if (!dmChannel || !dmTo) { + throw new Error("Missing DM target. Set PROMPTSEC_DM_CHANNEL and PROMPTSEC_DM_TO (or run interactively)."); + } + + if (!looksLikeEmail(emailTo)) { + throw new Error( + "Missing/invalid email recipient. Provide PROMPTSEC_EMAIL_TO or run interactively and enter an email. " + + "Cron job was not created." + ); + } + + const runnerPath = path.join(WATCHDOG_DIR, "runner.sh"); + if (!fs.existsSync(runnerPath)) { + throw new Error(`runner.sh not found at ${runnerPath}; reinstall clawsec-suite`); + } + + const listOut = sh("openclaw", ["cron", "list", "--json"]); + const listJson = JSON.parse(listOut); + const existingId = findExistingJobId(listJson); + + const agentMessage = buildAgentMessage({ dmChannel, dmTo, hostLabel, installDir, emailTo }); + const description = `Runs openclaw security audit daily and delivers to ${dmChannel}:${dmTo} + ${emailTo}.`; + + if (!existingId) { + const args = [ + "cron", "add", + "--name", JOB_NAME, + "--description", description, + "--session", "isolated", + "--wake", "now", + "--cron", DEFAULT_EXPR, + "--tz", tz, + "--message", agentMessage, + "--deliver", + "--channel", dmChannel, + "--to", dmTo, + "--best-effort-deliver", + "--post-prefix", "[daily security audit]", + "--post-mode", "summary", + "--json", + ]; + const out = sh("openclaw", args); + const job = JSON.parse(out); + process.stdout.write(`Created cron job ${job.id}: ${JOB_NAME}\n`); + process.stdout.write(`Email recipient: ${emailTo}\n`); + } else { + const args = [ + "cron", "edit", existingId, + "--name", JOB_NAME, + "--description", description, + "--enable", + "--session", "isolated", + "--wake", "now", + "--cron", DEFAULT_EXPR, + "--tz", tz, + "--message", agentMessage, + "--deliver", + "--channel", dmChannel, + "--to", dmTo, + "--best-effort-deliver", + "--post-prefix", "[daily security audit]", + ]; + sh("openclaw", args); + process.stdout.write(`Updated cron job ${existingId}: ${JOB_NAME}\n`); + process.stdout.write(`Email recipient: ${emailTo}\n`); + } +} + +run().catch((err) => { + process.stderr.write(String(err?.stack || err) + "\n"); + process.exit(1); +}); diff --git a/skills/clawsec-suite/skill.json b/skills/clawsec-suite/skill.json index 5db2417..5fe619e 100644 --- a/skills/clawsec-suite/skill.json +++ b/skills/clawsec-suite/skill.json @@ -1,6 +1,6 @@ { "name": "clawsec-suite", - "version": "0.0.10", + "version": "0.0.11", "description": "ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.", "author": "prompt-security", "license": "MIT", @@ -134,6 +134,41 @@ "path": "scripts/generate_checksums_json.mjs", "required": false, "description": "Utility script for generating SHA-256 checksum manifests" + }, + { + "path": "scripts/setup_audit_watchdog.mjs", + "required": true, + "description": "Installer script for integrated daily audit watchdog cron with email auto-discovery and required-email guard" + }, + { + "path": "scripts/audit-watchdog/runner.sh", + "required": true, + "description": "Integrated watchdog runner for daily security audits and report delivery" + }, + { + "path": "scripts/audit-watchdog/run_audit_and_format.sh", + "required": true, + "description": "Runs openclaw security audits and formats concise report output" + }, + { + "path": "scripts/audit-watchdog/render_report.mjs", + "required": false, + "description": "Render concise security audit report from JSON findings" + }, + { + "path": "scripts/audit-watchdog/sendmail_report.sh", + "required": false, + "description": "Sendmail-based report delivery helper" + }, + { + "path": "scripts/audit-watchdog/send_smtp.mjs", + "required": false, + "description": "SMTP fallback report delivery helper" + }, + { + "path": "scripts/audit-watchdog/codex_review.sh", + "required": false, + "description": "Optional codex-driven review helper" } ] }, @@ -159,6 +194,28 @@ ], "standalone_available": true, "deprecation_plan": "standalone skill may be retired after suite migration is verified" + }, + "openclaw-audit-watchdog": { + "source_skill": "openclaw-audit-watchdog", + "source_version": "0.0.4", + "paths": [ + "scripts/setup_audit_watchdog.mjs", + "scripts/audit-watchdog/runner.sh", + "scripts/audit-watchdog/run_audit_and_format.sh", + "scripts/audit-watchdog/render_report.mjs", + "scripts/audit-watchdog/sendmail_report.sh", + "scripts/audit-watchdog/send_smtp.mjs", + "scripts/audit-watchdog/codex_review.sh" + ], + "capabilities": [ + "daily openclaw security audit automation", + "healthcheck-complement read-only audit reporting", + "email + DM report delivery", + "email auto-discovery with required-email setup gating" + ], + "integrated_in_suite": true, + "standalone_available": true, + "deprecation_plan": "remove standalone listing after suite integration rollout is confirmed" } }, "catalog": { @@ -177,14 +234,15 @@ ] }, "openclaw-audit-watchdog": { - "description": "Automated daily audits with email reporting", + "description": "Integrated in clawsec-suite; automated daily audits with email/DM reporting", "default_install": true, "compatible": [ "openclaw", "moltbot", "clawdbot" ], - "note": "Tailored for OpenClaw/MoltBot family" + "note": "Tailored for OpenClaw/MoltBot family", + "integrated_in_suite": true }, "soul-guardian": { "description": "Drift detection and file integrity guard", @@ -210,7 +268,7 @@ } }, "openclaw": { - "emoji": "📦", + "emoji": "\ud83d\udce6", "category": "security", "requires": { "bins": [