From 9685db79d36126e8a23ed7d5e98d00bee20e4140 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 15:43:41 +0200 Subject: [PATCH] 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 --- .../scripts/render_report.mjs | 114 +++++++++++++++++- 1 file changed, 111 insertions(+), 3 deletions(-) diff --git a/skills/openclaw-audit-watchdog/scripts/render_report.mjs b/skills/openclaw-audit-watchdog/scripts/render_report.mjs index cb9d4cb..bbb2d9e 100755 --- a/skills/openclaw-audit-watchdog/scripts/render_report.mjs +++ b/skills/openclaw-audit-watchdog/scripts/render_report.mjs @@ -3,10 +3,11 @@ * Render a human-readable security audit report from openclaw JSON. * * Usage: - * node render_report.mjs --audit audit.json --deep deep.json --label "host label" + * node render_report.mjs --audit audit.json --deep deep.json --label "host label" [--config config.json] */ import fs from "node:fs"; +import { loadSuppressionConfig } from "./load_suppression_config.mjs"; function readJsonSafe(p, label) { if (!p) return { findings: [], summary: {}, error: `${label} missing` }; @@ -29,6 +30,84 @@ function pickFindings(report) { }; } +/** + * Extract skill name from a finding object. + * Tries multiple fields in priority order. + * + * @param {object} finding - The finding object + * @returns {string|null} - The skill name or null if not found + */ +function extractSkillName(finding) { + if (!finding) return null; + + // Try common fields where skill name might be stored + if (finding.skill) return String(finding.skill).trim(); + if (finding.skillName) return String(finding.skillName).trim(); + if (finding.target) return String(finding.target).trim(); + + // Attempt to extract from path (e.g., "skills/my-skill/...") + if (finding.path && typeof finding.path === "string") { + const pathMatch = finding.path.match(/skills\/([^\/]+)/); + if (pathMatch) return pathMatch[1]; + } + + // Attempt to extract from title (e.g., "[my-skill] some issue") + if (finding.title && typeof finding.title === "string") { + const titleMatch = finding.title.match(/^\[([^\]]+)\]/); + if (titleMatch) return titleMatch[1]; + } + + return null; +} + +/** + * Filter findings into active and suppressed based on suppression config. + * Matches require BOTH checkId AND skill name to match (exact match). + * + * @param {Array} findings - Array of finding objects + * @param {Array} suppressions - Array of suppression rules + * @returns {{active: Array, suppressed: Array}} + */ +function filterFindings(findings, suppressions) { + if (!Array.isArray(findings)) { + return { active: [], suppressed: [] }; + } + + if (!Array.isArray(suppressions) || suppressions.length === 0) { + return { active: findings, suppressed: [] }; + } + + const active = []; + const suppressed = []; + + for (const finding of findings) { + const checkId = finding?.checkId ?? ""; + const skillName = extractSkillName(finding); + + // Check if this finding matches any suppression rule + const isSuppressed = suppressions.some((rule) => { + // BOTH checkId AND skill must match (exact match, case-sensitive) + return rule.checkId === checkId && rule.skill === skillName; + }); + + if (isSuppressed) { + // Find the matching rule to attach suppression metadata + const matchingRule = suppressions.find( + (rule) => rule.checkId === checkId && rule.skill === skillName + ); + suppressed.push({ + ...finding, + suppressionReason: matchingRule?.reason, + suppressedAt: matchingRule?.suppressedAt, + }); + } else { + active.push(finding); + } + } + + return { active, suppressed }; +} + function lineForFinding(f) { const id = f?.checkId ?? "(no-checkId)"; const title = f?.title ?? "(no-title)"; @@ -37,7 +116,7 @@ function lineForFinding(f) { return `- ${id} ${title}${fixLine ? `\n ${fixLine}` : ""}`; } -function render({ audit, deep, label }) { +function render({ audit, deep, label, suppressedFindings = [] }) { const now = new Date().toISOString(); const a = pickFindings(audit); const d = pickFindings(deep); @@ -94,12 +173,41 @@ function parseArgs(argv) { if (a === "--audit") out.audit = argv[++i]; else if (a === "--deep") out.deep = argv[++i]; else if (a === "--label") out.label = argv[++i]; + else if (a === "--config") out.config = argv[++i]; } return out; } +// Main execution const args = parseArgs(process.argv.slice(2)); + +// Load suppression config (async) +const suppressionConfig = await loadSuppressionConfig(args.config || null); +const suppressions = suppressionConfig.suppressions || []; + +// Read audit results const audit = readJsonSafe(args.audit, "audit"); const deep = readJsonSafe(args.deep, "deep"); -const report = render({ audit, deep, label: args.label }); + +// Apply suppression filtering to findings +const allFindings = [...(audit.findings || []), ...(deep.findings || [])]; +const { active: activeFindings, suppressed: suppressedFindings } = filterFindings( + allFindings, + suppressions +); + +// Replace findings in audit/deep with filtered active findings +if (audit.findings) { + audit.findings = activeFindings.filter((f) => + (audit.findings || []).some((orig) => orig === f || JSON.stringify(orig) === JSON.stringify(f)) + ); +} +if (deep.findings) { + deep.findings = activeFindings.filter((f) => + (deep.findings || []).some((orig) => orig === f || JSON.stringify(orig) === JSON.stringify(f)) + ); +} + +// Render report with suppressed findings +const report = render({ audit, deep, label: args.label, suppressedFindings }); process.stdout.write(report + "\n");