mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
63de5ce08d
* auto-claude: subtask-1-1 - Create config loading utility with multi-path fallback Created load_suppression_config.mjs with: - Multi-path fallback: ~/.openclaw/security-audit.json -> .clawsec/allowlist.json - Environment variable support (OPENCLAW_AUDIT_CONFIG) - Custom path support via CLI argument - Schema validation (checkId, skill, reason, suppressedAt required) - Malformed JSON error handling - Graceful fallback to empty suppressions when no config exists - ISO 8601 date format validation with warnings Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * auto-claude: subtask-1-2 - Create example config file template - Added security-audit-config.example.json with two suppression examples - Included examples for clawsec-suite and openclaw-audit-watchdog - Created comprehensive README.md explaining configuration format - All required fields documented (checkId, skill, reason, suppressedAt) - ISO 8601 date format demonstrated - JSON validated successfully * auto-claude: subtask-1-3 - Add unit tests for config loading Added comprehensive unit tests for suppression config loading: - Valid config with all required fields - Malformed date warning (non-blocking) - Missing required field validation - Malformed JSON error handling - File not found graceful fallback - Custom path priority - Environment variable override - Missing/empty suppressions array handling All 10 tests passing. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * auto-claude: subtask-2-1 - Add suppression filtering to render_report.mjs Implements suppression filtering logic for security audit findings: - Import loadSuppressionConfig for config loading - Add --config CLI argument for custom config paths - Create extractSkillName() to extract skill names from findings (tries multiple fields) - Create filterFindings() to split findings into active/suppressed - Match suppressions by BOTH checkId AND skill name (exact match required) - Attach suppression metadata (reason, suppressedAt) to suppressed findings - Modify render() to accept suppressedFindings parameter - Apply filtering in main execution before rendering Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * auto-claude: subtask-2-2 - Add INFO-SUPPRESSED section to report output - Added lineForSuppressedFinding() to format suppressed findings - Added INFO-SUPPRESSED section showing suppressed findings with reason and date - Suppressed findings are not counted in summary (already filtered) - Follows existing code patterns for report sections Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * auto-claude: subtask-3-1 - Add --config flag to run_audit_and_format.sh - Added --config flag to accept path to config file - Added --help flag with usage documentation - Config flag is passed to openclaw audit commands when provided - Follows existing pattern for --label flag * auto-claude: subtask-4-1 - Create integration tests for render_report with suppressions Created comprehensive integration tests covering: - Suppressed findings appear in INFO-SUPPRESSED section - Active findings appear in CRITICAL/WARN section - Summary counts exclude suppressed findings - Backward compatibility (no config) - Partial matches don't suppress (checkId or skill alone) - Multiple suppressions work correctly - Skill name extraction from path field - Skill name extraction from title field - Empty suppressions array behaves like no config Bug fix in render_report.mjs: - Summary counts now recalculated after filtering suppressed findings - Previously summary showed original counts instead of filtered counts All 10 tests passing. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * auto-claude: subtask-4-2 - Manual E2E test with real openclaw audit - Fixed run_audit_and_format.sh to pass --config flag to render_report.mjs - Enhanced lineForFinding() to display skill names for better clarity - Enhanced lineForSuppressedFinding() to display skill names consistently - Created comprehensive E2E test documentation in E2E-TEST-RESULTS.md - All E2E verification points passed: * Config loading from custom paths * Suppression matching by checkId + skill name * INFO-SUPPRESSED section display * Suppression reason and date display * Summary count accuracy (excludes suppressed findings) * Non-suppressed findings preservation * Skill name display in all findings - All integration tests still passing (10/10) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * auto-claude: subtask-5-1 - Update README.md with suppression feature * auto-claude: subtask-5-2 - Update SKILL.md with usage examples * - Add backslash escaping before quote escaping in oneline() function - Prevents incomplete string escaping vulnerability - Resolves CodeQL alert: https://github.com/prompt-security/clawsec/security/code-scanning/16 * Fix regex in extractSkillName function and simplify error handling in suppression config tests * Enhance suppression mechanism in OpenClaw Audit Watchdog - Updated README.md to clarify suppression configuration and activation requirements. - Improved SKILL.md with examples for suppressing known findings. - Refactored load_suppression_config.mjs to implement opt-in gating for suppressions. - Modified render_report.mjs to support suppression flag in report generation. - Enhanced run_audit_and_format.sh and runner.sh scripts to accept --enable-suppressions flag. - Added test cases for suppression configuration, including validation for enabledFor sentinel and opt-in behavior. - Introduced new test files for empty and invalid suppression configurations. * Fix type assertion for checksums file entries in Checksums component * Update ESLint configuration and dependencies to pin @eslint/js to version 9.28.0 * Update CHANGELOG.md for advisory suppression module and OpenClaw Audit Watchdog enhancements * Refactor finding comparison logic in render_report.mjs to simplify equality checks * chore(clawsec-suite): bump version to 0.1.2 * chore(openclaw-audit-watchdog): bump version to 0.1.0 * Remove suppressed matches tracking from state to prevent re-evaluation alerts --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
221 lines
6.4 KiB
JavaScript
Executable File
221 lines
6.4 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Setup: create/update a daily 23:00 cron job that
|
|
* - runs openclaw security audits
|
|
* - DMs a chosen recipient (channel+id)
|
|
* - emails target@example.com via local sendmail
|
|
*
|
|
* Uses the `openclaw cron` CLI so it can run on a host without direct Gateway RPC access.
|
|
*/
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import readline from "node:readline";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const JOB_NAME = "Daily security audit (Prompt Security)";
|
|
const COMPANY_EMAIL = "target@example.com";
|
|
const DEFAULT_TZ = "UTC";
|
|
const DEFAULT_EXPR = "0 23 * * *"; // 23:00 daily
|
|
|
|
const SCRIPT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
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, "\\\\")
|
|
.replace(/"/g, "\\\"")
|
|
.trim();
|
|
}
|
|
|
|
function escapeForShellEnvVar(v) {
|
|
return String(v ?? "")
|
|
.replace(/[\r\n]+/g, " ")
|
|
.replace(/\\/g, "\\\\")
|
|
.replace(/\$/g, "\\$")
|
|
.replace(/`/g, "\\`")
|
|
.replace(/"/g, "\\\"")
|
|
.trim();
|
|
}
|
|
|
|
function defaultInstallDir() {
|
|
const env = envOrEmpty("PROMPTSEC_INSTALL_DIR");
|
|
if (env) return env;
|
|
const home = envOrEmpty("HOME");
|
|
if (home) return path.join(home, ".config", "security-checkup");
|
|
return SCRIPT_ROOT;
|
|
}
|
|
|
|
function buildAgentMessage({ dmChannel, dmTo, hostLabel, installDir }) {
|
|
const safeDir = escapeForShellEnvVar(installDir || "");
|
|
const escapedHostLabel = escapeForShellEnvVar(hostLabel);
|
|
|
|
return [
|
|
"Run daily openclaw security audits and deliver report (DM + email).",
|
|
"",
|
|
`Delivery DM: ${oneline(dmChannel)}:${oneline(dmTo)}`,
|
|
`Email: ${COMPANY_EMAIL} (local sendmail)`,
|
|
"",
|
|
"Execute:",
|
|
`- Run via exec: cd "${safeDir}" && PROMPTSEC_HOST_LABEL="${escapedHostLabel}" ./scripts/runner.sh`,
|
|
"",
|
|
"Output requirements:",
|
|
"- Print the report to stdout (cron deliver will DM it).",
|
|
`- Also email the same report to ${COMPANY_EMAIL}; if email fails, append a NOTE line to stdout.`,
|
|
"- Do not apply fixes automatically.",
|
|
].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() {
|
|
// Non-interactive first (MDM-friendly)
|
|
const tzEnv = envOrEmpty("PROMPTSEC_TZ");
|
|
const dmChannelEnv = envOrEmpty("PROMPTSEC_DM_CHANNEL");
|
|
const dmToEnv = envOrEmpty("PROMPTSEC_DM_TO");
|
|
const hostLabelEnv = envOrEmpty("PROMPTSEC_HOST_LABEL");
|
|
|
|
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 installDirDefault = defaultInstallDir();
|
|
const installDir = interactive
|
|
? await prompt("Install dir containing scripts/runner.sh", { defaultValue: installDirDefault })
|
|
: installDirDefault;
|
|
|
|
if (!dmChannel || !dmTo) {
|
|
throw new Error("Missing DM target. Set PROMPTSEC_DM_CHANNEL and PROMPTSEC_DM_TO (or run interactively). ");
|
|
}
|
|
|
|
const runnerPath = path.join(installDir, "scripts", "runner.sh");
|
|
if (!fs.existsSync(runnerPath)) {
|
|
throw new Error(`runner.sh not found at ${runnerPath}; set PROMPTSEC_INSTALL_DIR to the deployed path`);
|
|
}
|
|
|
|
const listOut = sh("openclaw", ["cron", "list", "--json"]);
|
|
const listJson = JSON.parse(listOut);
|
|
const existingId = findExistingJobId(listJson);
|
|
|
|
const agentMessage = buildAgentMessage({ dmChannel, dmTo, hostLabel, installDir });
|
|
const description = `Runs openclaw security audit daily and delivers to ${dmChannel}:${dmTo} + ${COMPANY_EMAIL}.`;
|
|
|
|
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`);
|
|
} 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`);
|
|
}
|
|
}
|
|
|
|
run().catch((err) => {
|
|
process.stderr.write(String(err?.stack || err) + "\n");
|
|
process.exit(1);
|
|
});
|