diff --git a/skills/clawsec-suite/CHANGELOG.md b/skills/clawsec-suite/CHANGELOG.md index 8cc8a31..4cd5859 100644 --- a/skills/clawsec-suite/CHANGELOG.md +++ b/skills/clawsec-suite/CHANGELOG.md @@ -15,20 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated `SKILL.md` to use dynamic catalog discovery commands instead of hard-coded optional-skill names. - Documented explicit fallback behavior to suite-local `skill.json` catalog metadata when remote index fetch fails. -## [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 2d62a9a..68d08b2 100644 --- a/skills/clawsec-suite/SKILL.md +++ b/skills/clawsec-suite/SKILL.md @@ -1,6 +1,6 @@ --- name: clawsec-suite -version: 0.0.11 +version: 0.0.10 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: @@ -28,11 +28,9 @@ This means `clawsec-suite` can: - Setup scripts for hook and optional cron scheduling: `scripts/` - Guarded installer: `scripts/guarded_skill_install.mjs` - Dynamic catalog discovery for installable skills: `scripts/discover_skill_catalog.mjs` -- Integrated OpenClaw audit watchdog scripts: `scripts/audit-watchdog/` -- Watchdog cron bootstrap with email discovery/prompt guard: `scripts/setup_audit_watchdog.mjs` ### Installed separately (dynamic catalog) -`clawsec-suite` no longer hard-codes add-on skill names in this document. +`clawsec-suite` does not hard-code add-on skill names in this document. Discover the current catalog from the authoritative index (`https://clawsec.prompt.security/skills/index.json`) at runtime: @@ -147,18 +145,6 @@ 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, diff --git a/skills/clawsec-suite/scripts/audit-watchdog/codex_review.sh b/skills/clawsec-suite/scripts/audit-watchdog/codex_review.sh deleted file mode 100755 index aec9b3a..0000000 --- a/skills/clawsec-suite/scripts/audit-watchdog/codex_review.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/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 deleted file mode 100755 index cb9d4cb..0000000 --- a/skills/clawsec-suite/scripts/audit-watchdog/render_report.mjs +++ /dev/null @@ -1,105 +0,0 @@ -#!/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 deleted file mode 100755 index ad91c43..0000000 --- a/skills/clawsec-suite/scripts/audit-watchdog/run_audit_and_format.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/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 deleted file mode 100755 index 9ee9a18..0000000 --- a/skills/clawsec-suite/scripts/audit-watchdog/runner.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/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 deleted file mode 100755 index 7c95760..0000000 --- a/skills/clawsec-suite/scripts/audit-watchdog/send_smtp.mjs +++ /dev/null @@ -1,157 +0,0 @@ -#!/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 deleted file mode 100755 index f04f58c..0000000 --- a/skills/clawsec-suite/scripts/audit-watchdog/sendmail_report.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/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 deleted file mode 100644 index fc49cc5..0000000 --- a/skills/clawsec-suite/scripts/setup_audit_watchdog.mjs +++ /dev/null @@ -1,253 +0,0 @@ -#!/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 0af6922..de86054 100644 --- a/skills/clawsec-suite/skill.json +++ b/skills/clawsec-suite/skill.json @@ -1,6 +1,6 @@ { "name": "clawsec-suite", - "version": "0.0.11", + "version": "0.0.10", "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", @@ -139,41 +139,6 @@ "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" } ] }, @@ -199,28 +164,6 @@ ], "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": { @@ -239,15 +182,14 @@ ] }, "openclaw-audit-watchdog": { - "description": "Integrated in clawsec-suite; automated daily audits with email/DM reporting", + "description": "Automated daily audits with email reporting", "default_install": true, "compatible": [ "openclaw", "moltbot", "clawdbot" ], - "note": "Tailored for OpenClaw/MoltBot family", - "integrated_in_suite": true + "note": "Tailored for OpenClaw/MoltBot family" }, "soul-guardian": { "description": "Drift detection and file integrity guard", @@ -273,7 +215,7 @@ } }, "openclaw": { - "emoji": "\ud83d\udce6", + "emoji": "📦", "category": "security", "requires": { "bins": [