mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-21 17:31:22 +03:00
Revert "feat(clawsec-suite): integrate audit-watchdog and add email-gated setup"
This reverts commit 1ba55dd69ecb7a248a53123277158ce27474d5f7.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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\""
|
||||
@@ -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");
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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@<hostname>)
|
||||
*
|
||||
* Args:
|
||||
* --to <email>
|
||||
* --subject <text>
|
||||
*
|
||||
* 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 <CRLF>.<CRLF>
|
||||
// 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);
|
||||
});
|
||||
@@ -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
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user