From fc9ba5cfcc331a4deeb0fffac215621cbe6b57c1 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Fri, 6 Feb 2026 21:50:33 -0500 Subject: [PATCH 01/13] auto-claude: subtask-1-1 - Create suppression.sh helper with load_suppressions Implemented suppression.sh helper module with three core functions: - load_suppressions(): Reads .clawpinch-ignore.json and parses suppressions array - is_suppressed(): Checks if a finding ID is currently suppressed (with expiration) - filter_findings(): Splits findings array into active and suppressed based on suppressions Features: - Graceful handling of missing or invalid JSON files - ISO 8601 expiration date support with automatic expiry checking - Fallback behavior when jq or date commands unavailable - Follows patterns from common.sh and redact.sh Co-Authored-By: Claude Sonnet 4.5 --- .clawpinch-ignore.json.example | 15 +++ scripts/helpers/suppression.sh | 208 +++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 .clawpinch-ignore.json.example create mode 100644 scripts/helpers/suppression.sh diff --git a/.clawpinch-ignore.json.example b/.clawpinch-ignore.json.example new file mode 100644 index 0000000..595a95d --- /dev/null +++ b/.clawpinch-ignore.json.example @@ -0,0 +1,15 @@ +{ + "suppressions": [ + { + "id": "CHK-CFG-001", + "reason": "Dev environment - open gateway is intentional", + "expires": "2099-12-31T23:59:59Z", + "suppressed_by": "devops@example.com", + "suppressed_at": "2024-01-15T10:30:00Z" + }, + { + "id": "CHK-SEC-003", + "reason": "Test API key in example config - not used in production" + } + ] +} diff --git a/scripts/helpers/suppression.sh b/scripts/helpers/suppression.sh new file mode 100644 index 0000000..1ea2d17 --- /dev/null +++ b/scripts/helpers/suppression.sh @@ -0,0 +1,208 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ─── ClawPinch suppression helpers ────────────────────────────────────────── +# Manage finding suppressions from .clawpinch-ignore.json +# Source this file from the main orchestrator: +# source "$(dirname "$0")/scripts/helpers/suppression.sh" + +# Global variable to store loaded suppressions (JSON array) +_CLAWPINCH_SUPPRESSIONS="[]" + +# ─── Load suppressions from ignore file ───────────────────────────────────── +# Usage: load_suppressions +# Returns: 0 on success (including when file doesn't exist), 1 on invalid JSON + +load_suppressions() { + local ignore_file="${1:-.clawpinch-ignore.json}" + + # Reset suppressions + _CLAWPINCH_SUPPRESSIONS="[]" + + # If file doesn't exist, that's OK - no suppressions to load + if [[ ! -f "$ignore_file" ]]; then + return 0 + fi + + # Require jq for JSON parsing + if ! command -v jq &>/dev/null; then + # Fallback: if jq not available, disable suppressions + # This is a graceful degradation - better than failing the whole scan + return 0 + fi + + # Parse the JSON file and extract suppressions array + local parsed + if ! parsed="$(jq -c '.suppressions // []' "$ignore_file" 2>/dev/null)"; then + # Invalid JSON - log warning and continue with no suppressions + if [[ -n "${_CLR_YLW:-}" ]]; then + printf "${_CLR_YLW}[warn]${_CLR_RST} Invalid JSON in %s - ignoring suppressions\n" "$ignore_file" >&2 + else + printf "[warn] Invalid JSON in %s - ignoring suppressions\n" "$ignore_file" >&2 + fi + return 1 + fi + + # Validate that we got an array + if ! echo "$parsed" | jq -e 'type == "array"' >/dev/null 2>&1; then + if [[ -n "${_CLR_YLW:-}" ]]; then + printf "${_CLR_YLW}[warn]${_CLR_RST} .suppressions is not an array in %s\n" "$ignore_file" >&2 + else + printf "[warn] .suppressions is not an array in %s\n" "$ignore_file" >&2 + fi + return 1 + fi + + _CLAWPINCH_SUPPRESSIONS="$parsed" + return 0 +} + +# ─── Check if a finding ID is currently suppressed ────────────────────────── +# Usage: is_suppressed +# Returns: 0 if suppressed and not expired, 1 otherwise + +is_suppressed() { + local check_id="$1" + + # If no suppressions loaded, nothing is suppressed + if [[ "$_CLAWPINCH_SUPPRESSIONS" == "[]" ]]; then + return 1 + fi + + # Require jq + if ! command -v jq &>/dev/null; then + return 1 + fi + + # Get current timestamp in ISO 8601 format for expiration checking + local now + if command -v date &>/dev/null; then + # Try to get ISO 8601 timestamp (works on GNU date and macOS date) + if date -u +"%Y-%m-%dT%H:%M:%SZ" &>/dev/null; then + now="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + else + # Fallback if date format fails + now="" + fi + else + now="" + fi + + # Check if the ID is in suppressions and not expired + local result + if [[ -n "$now" ]]; then + # With expiration checking + result="$(echo "$_CLAWPINCH_SUPPRESSIONS" | jq -r --arg id "$check_id" --arg now "$now" ' + map(select(.id == $id)) | + if length > 0 then + .[0] | + if .expires then + if .expires > $now then "suppressed" else "expired" end + else + "suppressed" + end + else + "active" + end + ' 2>/dev/null)" + else + # Without expiration checking (no date command or failed to get timestamp) + result="$(echo "$_CLAWPINCH_SUPPRESSIONS" | jq -r --arg id "$check_id" ' + if (map(select(.id == $id)) | length > 0) then + "suppressed" + else + "active" + end + ' 2>/dev/null)" + fi + + [[ "$result" == "suppressed" ]] +} + +# ─── Filter findings into active and suppressed arrays ────────────────────── +# Usage: filter_findings < findings.json +# Reads findings JSON array from stdin +# Outputs: {"active": [...], "suppressed": [...]} + +filter_findings() { + local ignore_file="${1:-.clawpinch-ignore.json}" + + # Load suppressions if not already loaded + if [[ "$_CLAWPINCH_SUPPRESSIONS" == "[]" ]] && [[ -f "$ignore_file" ]]; then + load_suppressions "$ignore_file" + fi + + # Require jq + if ! command -v jq &>/dev/null; then + # Fallback: all findings are active + local findings + findings="$(cat)" + echo "{\"active\": $findings, \"suppressed\": []}" + return 0 + fi + + # Get current timestamp + local now + if command -v date &>/dev/null && date -u +"%Y-%m-%dT%H:%M:%SZ" &>/dev/null; then + now="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + else + now="" + fi + + # Read findings from stdin and filter + local findings + findings="$(cat)" + + # Use jq to split findings into active and suppressed + if [[ -n "$now" ]]; then + # With expiration checking + echo "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" --arg now "$now" '{ + active: map( + . as $finding | + ($suppressions | map(select(.id == $finding.id)) | .[0]) as $suppression | + if $suppression then + if $suppression.expires then + if $suppression.expires <= $now then $finding else empty end + else + empty + end + else + $finding + end + ), + suppressed: map( + . as $finding | + ($suppressions | map(select(.id == $finding.id)) | .[0]) as $suppression | + if $suppression then + if $suppression.expires then + if $suppression.expires > $now then $finding else empty end + else + $finding + end + else + empty + end + ) + }' + else + # Without expiration checking (treat all as unexpired) + echo "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" '{ + active: map( + . as $finding | + if ($suppressions | map(select(.id == $finding.id)) | length == 0) then + $finding + else + empty + end + ), + suppressed: map( + . as $finding | + if ($suppressions | map(select(.id == $finding.id)) | length > 0) then + $finding + else + empty + end + ) + }' + fi +} From 4c800593203dacad659a707ec43bf87025d85804 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Fri, 6 Feb 2026 21:56:59 -0500 Subject: [PATCH 02/13] auto-claude: subtask-2-1 - Add --show-suppressed and --no-ignore flags to arg parser --- clawpinch.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/clawpinch.sh b/clawpinch.sh index 3f45e26..c9f968b 100755 --- a/clawpinch.sh +++ b/clawpinch.sh @@ -29,6 +29,8 @@ NO_INTERACTIVE=0 REMEDIATE=0 PARALLEL_SCANNERS=1 CONFIG_DIR="" +SHOW_SUPPRESSED=0 +NO_IGNORE=0 # ─── Usage ─────────────────────────────────────────────────────────────────── @@ -45,6 +47,8 @@ Options: --no-interactive Disable interactive post-scan menu --remediate Run scan then pipe findings to Claude for AI remediation --config-dir PATH Explicit path to openclaw config directory + --show-suppressed Include suppressed findings in normal output + --no-ignore Disable all suppressions for full audit scan -h, --help Show this help message Exit codes: @@ -65,6 +69,8 @@ while [[ $# -gt 0 ]]; do --sequential) PARALLEL_SCANNERS=0; shift ;; --no-interactive) NO_INTERACTIVE=1; shift ;; --remediate) REMEDIATE=1; NO_INTERACTIVE=1; shift ;; + --show-suppressed) SHOW_SUPPRESSED=1; shift ;; + --no-ignore) NO_IGNORE=1; shift ;; --config-dir) if [[ -z "${2:-}" ]]; then log_error "--config-dir requires a path argument" @@ -86,6 +92,8 @@ done export CLAWPINCH_DEEP="$DEEP" export CLAWPINCH_SHOW_FIX="$SHOW_FIX" export CLAWPINCH_CONFIG_DIR="$CONFIG_DIR" +export CLAWPINCH_SHOW_SUPPRESSED="$SHOW_SUPPRESSED" +export CLAWPINCH_NO_IGNORE="$NO_IGNORE" export QUIET # ─── Validate security config (early check for --remediate) ────────────────── From d53c404182a7f6c9ba056fd9132e9b42e0d60bea Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Fri, 6 Feb 2026 22:01:14 -0500 Subject: [PATCH 03/13] auto-claude: subtask-2-2 - Source suppression.sh and apply filtering after fi Integrated suppression.sh into the main orchestrator: - Sourced suppression.sh helper following the pattern of other helpers - Applied filter_findings() after sorting to split findings into active and suppressed - Updated JSON output to include both 'findings' and 'suppressed' arrays - Updated display logic to use DISPLAY_FINDINGS (active + suppressed if --show-suppressed) - Updated severity counts to only count active findings for exit code calculation - Updated remediation to use active findings only Co-Authored-By: Claude Sonnet 4.5 --- clawpinch.sh | 52 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/clawpinch.sh b/clawpinch.sh index c9f968b..05c18a6 100755 --- a/clawpinch.sh +++ b/clawpinch.sh @@ -14,6 +14,7 @@ source "$HELPERS_DIR/common.sh" source "$HELPERS_DIR/report.sh" source "$HELPERS_DIR/redact.sh" source "$HELPERS_DIR/interactive.sh" +source "$HELPERS_DIR/suppression.sh" # ─── Signal trap for animation cleanup ────────────────────────────────────── @@ -351,18 +352,47 @@ SORTED_FINDINGS="$(echo "$ALL_FINDINGS" | jq ' sort_by(.severity | sev_order) ')" +# ─── Apply suppression filtering ───────────────────────────────────────────── + +ACTIVE_FINDINGS="$SORTED_FINDINGS" +SUPPRESSED_FINDINGS="[]" + +# Apply filtering unless --no-ignore is set +if [[ "$NO_IGNORE" -eq 0 ]]; then + # Look for .clawpinch-ignore.json in the OpenClaw config directory or current directory + ignore_file=".clawpinch-ignore.json" + if [[ -n "$OPENCLAW_CONFIG" ]] && [[ -f "$OPENCLAW_CONFIG/.clawpinch-ignore.json" ]]; then + ignore_file="$OPENCLAW_CONFIG/.clawpinch-ignore.json" + fi + + # Filter findings into active and suppressed + if [[ -f "$ignore_file" ]]; then + filtered_result="$(echo "$SORTED_FINDINGS" | filter_findings "$ignore_file")" + ACTIVE_FINDINGS="$(echo "$filtered_result" | jq -c '.active')" + SUPPRESSED_FINDINGS="$(echo "$filtered_result" | jq -c '.suppressed')" + fi +fi + +# For --show-suppressed mode, merge suppressed back into active for display +DISPLAY_FINDINGS="$ACTIVE_FINDINGS" +if [[ "$SHOW_SUPPRESSED" -eq 1 ]]; then + DISPLAY_FINDINGS="$(echo "$ACTIVE_FINDINGS" "$SUPPRESSED_FINDINGS" | jq -s '.[0] + .[1] | sort_by(.severity | if . == "critical" then 0 elif . == "warn" then 1 elif . == "info" then 2 elif . == "ok" then 3 else 4 end)')" +fi + # ─── Count by severity ────────────────────────────────────────────────────── +# Count only active findings (not suppressed) for exit code calculation -count_critical="$(echo "$SORTED_FINDINGS" | jq '[.[] | select(.severity == "critical")] | length')" -count_warn="$(echo "$SORTED_FINDINGS" | jq '[.[] | select(.severity == "warn")] | length')" -count_info="$(echo "$SORTED_FINDINGS" | jq '[.[] | select(.severity == "info")] | length')" -count_ok="$(echo "$SORTED_FINDINGS" | jq '[.[] | select(.severity == "ok")] | length')" +count_critical="$(echo "$ACTIVE_FINDINGS" | jq '[.[] | select(.severity == "critical")] | length')" +count_warn="$(echo "$ACTIVE_FINDINGS" | jq '[.[] | select(.severity == "warn")] | length')" +count_info="$(echo "$ACTIVE_FINDINGS" | jq '[.[] | select(.severity == "info")] | length')" +count_ok="$(echo "$ACTIVE_FINDINGS" | jq '[.[] | select(.severity == "ok")] | length')" # ─── Output ────────────────────────────────────────────────────────────────── if [[ "$JSON_OUTPUT" -eq 1 ]]; then - # Pure JSON output (compact for piping efficiency) - echo "$SORTED_FINDINGS" | jq -c . + # Pure JSON output with findings and suppressed arrays + jq -n -c --argjson findings "$ACTIVE_FINDINGS" --argjson suppressed "$SUPPRESSED_FINDINGS" \ + '{findings: $findings, suppressed: $suppressed}' else if [[ "$QUIET" -eq 0 ]]; then # Determine if interactive mode is available @@ -373,13 +403,13 @@ else if [[ "$_is_interactive" -eq 1 ]]; then # Compact grouped table (new v1.1 display) - print_findings_compact "$SORTED_FINDINGS" + print_findings_compact "$DISPLAY_FINDINGS" else # Non-interactive fallback: full card display (v1.0 behavior) - total="$(echo "$SORTED_FINDINGS" | jq 'length')" + total="$(echo "$DISPLAY_FINDINGS" | jq 'length')" if (( total > 0 )); then for i in $(seq 0 $((total - 1))); do - finding="$(echo "$SORTED_FINDINGS" | jq -c ".[$i]")" + finding="$(echo "$DISPLAY_FINDINGS" | jq -c ".[$i]")" print_finding "$finding" done else @@ -396,7 +426,7 @@ else # Launch interactive menu if TTY and not disabled if [[ "$QUIET" -eq 0 ]] && [[ "$NO_INTERACTIVE" -eq 0 ]] && [[ -t 0 ]]; then - interactive_menu "$SORTED_FINDINGS" "$scanner_count" "$_scan_elapsed" + interactive_menu "$DISPLAY_FINDINGS" "$scanner_count" "$_scan_elapsed" fi # ─── AI Remediation pipeline ───────────────────────────────────────────── @@ -413,7 +443,7 @@ else if [[ -z "$_claude_bin" ]]; then log_error "Claude CLI not found. Install it or set CLAWPINCH_CLAUDE_BIN." else - _non_ok_findings="$(echo "$SORTED_FINDINGS" | jq -c '[.[] | select(.severity != "ok")]')" + _non_ok_findings="$(echo "$ACTIVE_FINDINGS" | jq -c '[.[] | select(.severity != "ok")]')" _non_ok_count="$(echo "$_non_ok_findings" | jq 'length')" if (( _non_ok_count > 0 )); then From 9e90970149e5d7c7d97404f337fa3cd108e5a4d9 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Fri, 6 Feb 2026 22:20:47 -0500 Subject: [PATCH 04/13] auto-claude: subtask-3-2 - Update interactive/terminal output to show suppres Added suppressed count to summary dashboard: - Calculate count_suppressed from SUPPRESSED_FINDINGS array - Pass suppressed count to print_summary function - Display suppressed count in summary (only when > 0) - Line appears after timing line in dashboard Co-Authored-By: Claude Sonnet 4.5 --- clawpinch.sh | 3 ++- scripts/helpers/report.sh | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/clawpinch.sh b/clawpinch.sh index 05c18a6..2d963a4 100755 --- a/clawpinch.sh +++ b/clawpinch.sh @@ -386,6 +386,7 @@ count_critical="$(echo "$ACTIVE_FINDINGS" | jq '[.[] | select(.severity == "crit count_warn="$(echo "$ACTIVE_FINDINGS" | jq '[.[] | select(.severity == "warn")] | length')" count_info="$(echo "$ACTIVE_FINDINGS" | jq '[.[] | select(.severity == "info")] | length')" count_ok="$(echo "$ACTIVE_FINDINGS" | jq '[.[] | select(.severity == "ok")] | length')" +count_suppressed="$(echo "$SUPPRESSED_FINDINGS" | jq 'length')" # ─── Output ────────────────────────────────────────────────────────────────── @@ -419,7 +420,7 @@ else fi # Always print summary dashboard (animated when TTY) - print_summary_animated "$count_critical" "$count_warn" "$count_info" "$count_ok" "$scanner_count" "$_scan_elapsed" + print_summary_animated "$count_critical" "$count_warn" "$count_info" "$count_ok" "$scanner_count" "$_scan_elapsed" "$count_suppressed" # Contextual completion message print_completion_message "$count_critical" "$count_warn" diff --git a/scripts/helpers/report.sh b/scripts/helpers/report.sh index c01385e..4d764c4 100755 --- a/scripts/helpers/report.sh +++ b/scripts/helpers/report.sh @@ -730,6 +730,7 @@ print_summary() { local ok="${4:-0}" local scanner_count="${5:-0}" local elapsed="${6:-0}" + local suppressed="${7:-0}" local total=$(( critical + warn + info + ok )) local w @@ -837,6 +838,20 @@ print_summary() { printf '%*s' "$time_rpad" '' printf " %b%s%b\n" "$_CLR_BOX" "$_HBOX_V" "$_CLR_RST" + # Suppressed count line (only show if > 0) + if (( suppressed > 0 )); then + local supp_text + supp_text="$(printf 'Suppressed: %d findings' "$suppressed")" + local supp_len=${#supp_text} + local supp_lpad=2 + local supp_rpad=$(( inner - supp_lpad - supp_len )) + if (( supp_rpad < 0 )); then supp_rpad=0; fi + printf " %b%s%b " "$_CLR_BOX" "$_HBOX_V" "$_CLR_RST" + printf '%*s%b%s%b' "$supp_lpad" '' "$_CLR_DIM" "$supp_text" "$_CLR_RST" + printf '%*s' "$supp_rpad" '' + printf " %b%s%b\n" "$_CLR_BOX" "$_HBOX_V" "$_CLR_RST" + fi + # Empty line printf " %b%s%b " "$_CLR_BOX" "$_HBOX_V" "$_CLR_RST" printf '%*s' "$inner" '' From 7041a3f6ced5262b00e44c7304016736a4e9bdcb Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Fri, 6 Feb 2026 22:30:17 -0500 Subject: [PATCH 05/13] auto-claude: subtask-3-3 - Add --show-suppressed display logic to include sup When --show-suppressed flag is used, suppressed findings now appear in the output with clear visual indicators: 1. clawpinch.sh: Mark suppressed findings with "suppressed": true field when merging them for display 2. scripts/helpers/interactive.sh: In print_findings_compact, check for suppressed field and add [SUPPRESSED] prefix + dim the row 3. scripts/helpers/report.sh: In print_finding and print_finding_ok, check for suppressed field and add [SUPPRESSED] prefix + dim the text Visual indicators: - [SUPPRESSED] prefix on title - Dimmed text color to distinguish from active findings Co-Authored-By: Claude Sonnet 4.5 --- clawpinch.sh | 4 +++- scripts/helpers/interactive.sh | 25 ++++++++++++++++++++++--- scripts/helpers/report.sh | 31 +++++++++++++++++++++++++------ 3 files changed, 50 insertions(+), 10 deletions(-) diff --git a/clawpinch.sh b/clawpinch.sh index 2d963a4..3fc08f6 100755 --- a/clawpinch.sh +++ b/clawpinch.sh @@ -376,7 +376,9 @@ fi # For --show-suppressed mode, merge suppressed back into active for display DISPLAY_FINDINGS="$ACTIVE_FINDINGS" if [[ "$SHOW_SUPPRESSED" -eq 1 ]]; then - DISPLAY_FINDINGS="$(echo "$ACTIVE_FINDINGS" "$SUPPRESSED_FINDINGS" | jq -s '.[0] + .[1] | sort_by(.severity | if . == "critical" then 0 elif . == "warn" then 1 elif . == "info" then 2 elif . == "ok" then 3 else 4 end)')" + # Mark suppressed findings with a "suppressed": true field before merging + MARKED_SUPPRESSED="$(echo "$SUPPRESSED_FINDINGS" | jq '[.[] | . + {suppressed: true}]')" + DISPLAY_FINDINGS="$(echo "$ACTIVE_FINDINGS" "$MARKED_SUPPRESSED" | jq -s '.[0] + .[1] | sort_by(.severity | if . == "critical" then 0 elif . == "warn" then 1 elif . == "info" then 2 elif . == "ok" then 3 else 4 end)')" fi # ─── Count by severity ────────────────────────────────────────────────────── diff --git a/scripts/helpers/interactive.sh b/scripts/helpers/interactive.sh index 1089b5e..b2b015d 100644 --- a/scripts/helpers/interactive.sh +++ b/scripts/helpers/interactive.sh @@ -233,10 +233,16 @@ print_findings_compact() { for (( i=0; i Date: Fri, 6 Feb 2026 22:32:08 -0500 Subject: [PATCH 06/13] auto-claude: subtask-4-1 - Create .clawpinch-ignore.json.example with documentation --- .clawpinch-ignore.json.example | 44 +++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/.clawpinch-ignore.json.example b/.clawpinch-ignore.json.example index 595a95d..fe052a3 100644 --- a/.clawpinch-ignore.json.example +++ b/.clawpinch-ignore.json.example @@ -1,15 +1,53 @@ { + "$schema": "https://clawpinch.dev/schemas/ignore.json", + "_comment": "This file configures finding suppression for ClawPinch security scans", + "_documentation": { + "purpose": "Suppress accepted-risk findings to prevent CI/CD pipeline failures", + "location": "Copy this file to .clawpinch-ignore.json in your project root", + "fields": { + "id": "(required) Check ID to suppress (e.g., CHK-CFG-001)", + "reason": "(required) Justification for suppressing this finding", + "expires": "(optional) ISO 8601 timestamp when suppression expires and finding reactivates", + "suppressed_by": "(optional) Email or identifier of person who approved suppression", + "suppressed_at": "(optional) ISO 8601 timestamp when suppression was created" + }, + "behavior": { + "suppressed_findings": "Moved to 'suppressed' array in JSON output, excluded from severity counts", + "expired_suppressions": "Automatically reactivated if expires date is in the past", + "flags": { + "--show-suppressed": "Include suppressed findings in output with [SUPPRESSED] marker", + "--no-ignore": "Disable all suppressions for full audit scans" + } + } + }, "suppressions": [ { "id": "CHK-CFG-001", - "reason": "Dev environment - open gateway is intentional", - "expires": "2099-12-31T23:59:59Z", + "reason": "Dev environment - open gateway is intentional for local testing", + "expires": "2025-12-31T23:59:59Z", "suppressed_by": "devops@example.com", "suppressed_at": "2024-01-15T10:30:00Z" }, { "id": "CHK-SEC-003", - "reason": "Test API key in example config - not used in production" + "reason": "Test API key in example config - not used in production deployments" + }, + { + "id": "CHK-NET-004", + "reason": "WebSocket endpoint exposed for real-time collaboration feature", + "expires": "2025-06-30T00:00:00Z", + "suppressed_by": "security-team@example.com" + }, + { + "id": "CHK-PRM-002", + "reason": "Wildcard permission required for dynamic skill loader - reviewed by security", + "suppressed_by": "ciso@example.com", + "suppressed_at": "2024-02-20T14:30:00Z" + }, + { + "id": "CHK-CRN-001", + "reason": "Cron job needs elevated permissions for system maintenance tasks", + "expires": "2025-03-01T00:00:00Z" } ] } From 832f3eebbce11fe34147316a33446c02edb01a53 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Fri, 6 Feb 2026 22:33:52 -0500 Subject: [PATCH 07/13] auto-claude: subtask-4-2 - Update README.md with suppression documentation --- README.md | 128 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/README.md b/README.md index 6f29957..5209af3 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,12 @@ bash clawpinch.sh --config-dir /path/to/openclaw/config # Print auto-fix commands (read-only -- does not execute them) bash clawpinch.sh --fix + +# Show suppressed findings in output +bash clawpinch.sh --show-suppressed + +# Disable all suppressions for full audit +bash clawpinch.sh --no-ignore ``` --- @@ -241,6 +247,128 @@ bash clawpinch.sh --sequential --- +## Suppressing Findings + +ClawPinch allows you to suppress specific findings by check ID -- useful for accepted risks in development environments or findings that have been reviewed and approved by your security team. Suppressed findings are still scanned but reported separately and excluded from severity counts and exit codes. + +### Creating a Suppression File + +Create a `.clawpinch-ignore.json` file in your project root: + +```json +{ + "suppressions": [ + { + "id": "CHK-CFG-001", + "reason": "Dev environment - open gateway is intentional for local testing" + }, + { + "id": "CHK-SEC-003", + "reason": "Test API key in example config - not used in production", + "expires": "2025-12-31T23:59:59Z", + "suppressed_by": "security-team@example.com" + } + ] +} +``` + +### Suppression Fields + +- **`id`** (required) -- The check ID to suppress (e.g., `CHK-CFG-001`, `CHK-NET-004`) +- **`reason`** (required) -- Justification for why this finding is being suppressed +- **`expires`** (optional) -- ISO 8601 timestamp when the suppression expires and the finding reactivates +- **`suppressed_by`** (optional) -- Email or identifier of the person who approved the suppression +- **`suppressed_at`** (optional) -- ISO 8601 timestamp when the suppression was created + +### How Suppressions Work + +1. **Automatic Filtering** -- Suppressed findings are moved from the main `findings` array to a separate `suppressed` array in JSON output +2. **Severity Exclusion** -- Suppressed findings do not count toward severity totals or affect exit codes +3. **Expiration** -- If a suppression has an `expires` date in the past, it is automatically reactivated and the finding appears in normal output +4. **Visibility** -- Use `--show-suppressed` to include suppressed findings in output (marked with `[SUPPRESSED]`) +5. **Audit Mode** -- Use `--no-ignore` to disable all suppressions for full audit scans + +### Example Workflow + +```bash +# 1. Run initial scan and identify accepted risks +npx clawpinch --json > findings.json + +# 2. Create .clawpinch-ignore.json with suppressions +cat > .clawpinch-ignore.json < Date: Fri, 6 Feb 2026 22:36:58 -0500 Subject: [PATCH 08/13] auto-claude: subtask-4-3 - Update CLAUDE.md and SKILL.md with suppression feature info Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++ SKILL.md | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d0f911a..29989c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,12 @@ bash clawpinch.sh --remediate # Deep scan bash clawpinch.sh --deep + +# Show suppressed findings +bash clawpinch.sh --show-suppressed + +# Disable all suppressions (full audit) +bash clawpinch.sh --no-ignore ``` ## Architecture @@ -32,6 +38,7 @@ clawpinch/ │ │ ├── report.sh # Terminal UI: banner, finding cards, summary dashboard, spinner │ │ ├── redact.sh # Secret redaction: redact_value(), redact_line(), redact_json_secrets() │ │ ├── safe_exec.sh # Safe command execution: whitelist-based validation, replaces eval() +│ │ ├── suppression.sh # Finding suppression: load_suppressions(), filter_findings(), expiration handling │ │ └── interactive.sh # Post-scan menu: review, auto-fix, handoff export, AI remediation │ ├── scan_config.sh # CHK-CFG-001..010 — gateway, TLS, auth, CORS │ ├── scan_secrets.py # CHK-SEC-001..008 — API keys, passwords, tokens @@ -46,6 +53,7 @@ clawpinch/ │ ├── malicious-patterns.json # Known bad skill hashes │ ├── check-catalog.md # Full check documentation │ └── threat-model.md # Threat model for OpenClaw +├── .clawpinch-ignore.json.example # Example suppression config with documentation ├── package.json # npm package metadata ├── SKILL.md # AI-readable skill documentation ├── CLAUDE.md # This file — project context for Claude Code @@ -72,6 +80,19 @@ Every scanner emits findings via `emit_finding()` from `common.sh`: Severity order: `critical` > `warn` > `info` > `ok`. +### Output Format with Suppressions + +When suppressions are enabled (via `.clawpinch-ignore.json`), the JSON output contains two arrays: + +```json +{ + "findings": [ /* active findings only */ ], + "suppressed": [ /* suppressed findings */ ] +} +``` + +Suppressed findings do not count toward severity totals or exit codes. Use `--show-suppressed` to include them in terminal output (marked with `[SUPPRESSED]`), or `--no-ignore` to disable all suppressions for full audits. + ## How to Add a New Check 1. Choose the appropriate scanner file in `scripts/` (or create a new `scan_*.sh`) @@ -80,6 +101,39 @@ Severity order: `critical` > `warn` > `info` > `ok`. 4. Call `emit_finding "CHK-XXX-NNN" "severity" "title" "description" "evidence" "remediation" "auto_fix"` 5. Add the check to `references/check-catalog.md` and `SKILL.md` category table +## Finding Suppression + +Findings can be suppressed by creating a `.clawpinch-ignore.json` file in the project root: + +```json +{ + "suppressions": [ + { + "id": "CHK-CFG-001", + "reason": "Dev environment - open gateway is intentional", + "expires": "2025-12-31T23:59:59Z", + "suppressed_by": "devops@example.com", + "suppressed_at": "2024-01-15T10:30:00Z" + } + ] +} +``` + +**Behavior:** +- Suppressed findings move to `suppressed` array in JSON output +- Suppressed findings do not count toward severity totals or exit codes +- Expired suppressions (past `expires` date) automatically reactivate +- `--show-suppressed` includes suppressed findings in output with `[SUPPRESSED]` marker +- `--no-ignore` disables all suppressions for full audit scans + +**Use cases:** +- Accepted risks in development environments +- Findings under gradual remediation with expiration tracking +- Security-reviewed exceptions with documented justifications +- CI/CD pipelines that fail on active findings only + +See `.clawpinch-ignore.json.example` for a fully documented template. + ## Conventions - All scanners output a JSON array to stdout diff --git a/SKILL.md b/SKILL.md index b23c163..64282cf 100644 --- a/SKILL.md +++ b/SKILL.md @@ -61,6 +61,12 @@ clawpinch --no-interactive # AI-powered remediation — scan then pipe to Claude for automated fixing clawpinch --remediate +# Show suppressed findings in output (marked with [SUPPRESSED]) +clawpinch --show-suppressed + +# Disable all suppressions for full audit scans +clawpinch --no-ignore + # Target specific config directory clawpinch --config-dir /path/to/openclaw/config @@ -84,6 +90,17 @@ Each finding is a JSON object: } ``` +When suppressions are enabled via `.clawpinch-ignore.json`, the output format changes: + +```json +{ + "findings": [ /* active findings only */ ], + "suppressed": [ /* suppressed findings */ ] +} +``` + +Suppressed findings do not count toward severity totals or exit codes. + ## Check Categories | Category | ID Range | Count | Description | @@ -121,6 +138,47 @@ npx clawpinch --json --no-interactive | jq '[.[] | select(.severity == "critical npx clawpinch --quiet --no-interactive ``` +## Finding Suppression + +Suppress specific findings by creating `.clawpinch-ignore.json`: + +```json +{ + "suppressions": [ + { + "id": "CHK-CFG-001", + "reason": "Dev environment - open gateway is intentional", + "expires": "2025-12-31T23:59:59Z", + "suppressed_by": "devops@example.com", + "suppressed_at": "2024-01-15T10:30:00Z" + } + ] +} +``` + +**Fields:** +- `id` (required): Check ID to suppress +- `reason` (required): Justification for suppression +- `expires` (optional): ISO 8601 timestamp when suppression expires +- `suppressed_by` (optional): Email/identifier of approver +- `suppressed_at` (optional): ISO 8601 timestamp when created + +**Behavior:** +- Suppressed findings appear in `suppressed` array, not `findings` +- Suppressed findings excluded from severity counts and exit codes +- Expired suppressions automatically reactivate +- `--show-suppressed`: Include suppressed findings in output with `[SUPPRESSED]` marker +- `--no-ignore`: Disable all suppressions for full audits + +**Use cases:** +- **CI/CD:** Prevent pipeline failures from accepted risks +- **Development:** Suppress intentional misconfigurations in dev environments +- **Gradual remediation:** Suppress findings with expiration dates for periodic review +- **Security review:** Document accepted risks with justifications +- **Audits:** Use `--no-ignore` for quarterly full scans + +See `.clawpinch-ignore.json.example` for detailed examples. + ## Dependencies - **Required:** `bash` >= 4.0, `jq` @@ -139,5 +197,7 @@ npx clawpinch --quiet --no-interactive | Code | Meaning | |------|---------| -| 0 | No critical findings | -| 1 | One or more critical findings detected | +| 0 | No active critical findings (suppressed findings excluded) | +| 1 | One or more active critical findings detected | + +**Note:** Suppressed findings do not affect exit codes. Use `--no-ignore` to include suppressed findings in exit code calculation. From ee007ef18e0c4cbf1f6d3b9b297772b3a38d1813 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Fri, 6 Feb 2026 22:57:29 -0500 Subject: [PATCH 09/13] auto-claude: subtask-6-1 - Test suppression with no expiry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Integration test for finding suppression feature: - Created .clawpinch-ignore.json with CHK-CFG-001 suppressed (no expiry) - Verified all 5 test scenarios pass: ✅ Finding appears in suppressed array (not findings array) ✅ Suppressed findings don't affect exit code ✅ --show-suppressed flag displays suppressed findings ✅ --no-ignore flag disables suppressions for full audit ✅ Exit code correctly reflects only active critical findings Test results documented in build-progress.txt. Co-Authored-By: Claude Sonnet 4.5 --- .clawpinch-ignore.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .clawpinch-ignore.json diff --git a/.clawpinch-ignore.json b/.clawpinch-ignore.json new file mode 100644 index 0000000..f51d42a --- /dev/null +++ b/.clawpinch-ignore.json @@ -0,0 +1,8 @@ +{ + "suppressions": [ + { + "id": "CHK-CFG-001", + "reason": "Test suppression with no expiry date" + } + ] +} From 22b5b8f1a67f5d5f4996925f0f3cf40bc2dd169b Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Fri, 6 Feb 2026 23:01:09 -0500 Subject: [PATCH 10/13] auto-claude: subtask-6-2 - Test suppression with expiration (future and past) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ✅ Integration test completed successfully Verified expiration behavior: - Future expiry (2030-12-31): Finding stays in suppressed array - Past expiry (2020-01-01): Finding moves back to active findings - Expiration logic correctly uses ISO 8601 timestamp comparison - Finding counts update correctly when suppressions expire Test setup: - OpenClaw config with CHK-CFG-002 trigger (groupPolicy=open) - Suppression file with expiry dates - JSON output validation All verification steps documented in build-progress.txt --- .clawpinch-ignore.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .clawpinch-ignore.json diff --git a/.clawpinch-ignore.json b/.clawpinch-ignore.json deleted file mode 100644 index f51d42a..0000000 --- a/.clawpinch-ignore.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "suppressions": [ - { - "id": "CHK-CFG-001", - "reason": "Test suppression with no expiry date" - } - ] -} From ead3cd23aeb83d18734a8e8d759310663ff7b403 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Sun, 8 Feb 2026 21:44:53 -0500 Subject: [PATCH 11/13] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20fix=20dirname=20bug,=20optimize=20jq=20filtering,?= =?UTF-8?q?=20add=20suppression=20metadata=20to=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- clawpinch.sh | 6 +-- scripts/helpers/suppression.sh | 68 ++++++++++++---------------------- 2 files changed, 26 insertions(+), 48 deletions(-) diff --git a/clawpinch.sh b/clawpinch.sh index 3fc08f6..521ecee 100755 --- a/clawpinch.sh +++ b/clawpinch.sh @@ -361,8 +361,8 @@ SUPPRESSED_FINDINGS="[]" if [[ "$NO_IGNORE" -eq 0 ]]; then # Look for .clawpinch-ignore.json in the OpenClaw config directory or current directory ignore_file=".clawpinch-ignore.json" - if [[ -n "$OPENCLAW_CONFIG" ]] && [[ -f "$OPENCLAW_CONFIG/.clawpinch-ignore.json" ]]; then - ignore_file="$OPENCLAW_CONFIG/.clawpinch-ignore.json" + if [[ -n "$OPENCLAW_CONFIG" ]] && [[ -f "$(dirname "$OPENCLAW_CONFIG")/.clawpinch-ignore.json" ]]; then + ignore_file="$(dirname "$OPENCLAW_CONFIG")/.clawpinch-ignore.json" fi # Filter findings into active and suppressed @@ -378,7 +378,7 @@ DISPLAY_FINDINGS="$ACTIVE_FINDINGS" if [[ "$SHOW_SUPPRESSED" -eq 1 ]]; then # Mark suppressed findings with a "suppressed": true field before merging MARKED_SUPPRESSED="$(echo "$SUPPRESSED_FINDINGS" | jq '[.[] | . + {suppressed: true}]')" - DISPLAY_FINDINGS="$(echo "$ACTIVE_FINDINGS" "$MARKED_SUPPRESSED" | jq -s '.[0] + .[1] | sort_by(.severity | if . == "critical" then 0 elif . == "warn" then 1 elif . == "info" then 2 elif . == "ok" then 3 else 4 end)')" + DISPLAY_FINDINGS="$(echo "$ACTIVE_FINDINGS" "$MARKED_SUPPRESSED" | jq -s 'def sev_order: if . == "critical" then 0 elif . == "warn" then 1 elif . == "info" then 2 elif . == "ok" then 3 else 4 end; .[0] + .[1] | sort_by(.severity | sev_order)')" fi # ─── Count by severity ────────────────────────────────────────────────────── diff --git a/scripts/helpers/suppression.sh b/scripts/helpers/suppression.sh index 1ea2d17..b51de8d 100644 --- a/scripts/helpers/suppression.sh +++ b/scripts/helpers/suppression.sh @@ -77,11 +77,7 @@ is_suppressed() { # Get current timestamp in ISO 8601 format for expiration checking local now if command -v date &>/dev/null; then - # Try to get ISO 8601 timestamp (works on GNU date and macOS date) - if date -u +"%Y-%m-%dT%H:%M:%SZ" &>/dev/null; then - now="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" - else - # Fallback if date format fails + if ! now="$(date -u +'%Y-%m-%dT%H:%M:%SZ' 2>/dev/null)"; then now="" fi else @@ -143,8 +139,10 @@ filter_findings() { # Get current timestamp local now - if command -v date &>/dev/null && date -u +"%Y-%m-%dT%H:%M:%SZ" &>/dev/null; then - now="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + if command -v date &>/dev/null; then + if ! now="$(date -u +'%Y-%m-%dT%H:%M:%SZ' 2>/dev/null)"; then + now="" + fi else now="" fi @@ -156,53 +154,33 @@ filter_findings() { # Use jq to split findings into active and suppressed if [[ -n "$now" ]]; then # With expiration checking - echo "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" --arg now "$now" '{ - active: map( - . as $finding | - ($suppressions | map(select(.id == $finding.id)) | .[0]) as $suppression | - if $suppression then - if $suppression.expires then - if $suppression.expires <= $now then $finding else empty end - else - empty - end - else - $finding - end - ), - suppressed: map( - . as $finding | - ($suppressions | map(select(.id == $finding.id)) | .[0]) as $suppression | - if $suppression then - if $suppression.expires then - if $suppression.expires > $now then $finding else empty end + echo "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" --arg now "$now" ' + ($suppressions | map({(.id): .}) | add // {}) as $smap | + reduce .[] as $f ({active: [], suppressed: []}; + $smap[$f.id] as $s | + if $s then + if $s.expires and $s.expires <= $now then + .active += [$f] else - $finding + .suppressed += [$f + {suppression: ($s | del(.id))}] end else - empty + .active += [$f] end ) - }' + ' else # Without expiration checking (treat all as unexpired) - echo "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" '{ - active: map( - . as $finding | - if ($suppressions | map(select(.id == $finding.id)) | length == 0) then - $finding - else - empty - end - ), - suppressed: map( - . as $finding | - if ($suppressions | map(select(.id == $finding.id)) | length > 0) then - $finding + echo "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" ' + ($suppressions | map({(.id): .}) | add // {}) as $smap | + reduce .[] as $f ({active: [], suppressed: []}; + $smap[$f.id] as $s | + if $s then + .suppressed += [$f + {suppression: ($s | del(.id))}] else - empty + .active += [$f] end ) - }' + ' fi } From bbe77b0aecfae9cc369ee4dd4776097586bff555 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Sun, 8 Feb 2026 23:09:29 -0500 Subject: [PATCH 12/13] =?UTF-8?q?fix:=20address=20round=203=20review=20?= =?UTF-8?q?=E2=80=94=20harden=20suppression,=20consolidate=20jq,=20remove?= =?UTF-8?q?=20dead=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace echo with printf for JSON construction in suppression.sh (security) - Add null check for suppression entries missing id field to prevent jq crash - Remove unused is_suppressed() function - Consolidate 8 separate jq calls into single call with read in print_finding() - Extract duplicated sev_order jq function into reusable JQ_SEV_ORDER_FUNC variable Co-Authored-By: Claude Opus 4.6 --- clawpinch.sh | 16 +++----- scripts/helpers/report.sh | 27 ++++++++++---- scripts/helpers/suppression.sh | 68 +++------------------------------- 3 files changed, 29 insertions(+), 82 deletions(-) diff --git a/clawpinch.sh b/clawpinch.sh index 521ecee..a28c249 100755 --- a/clawpinch.sh +++ b/clawpinch.sh @@ -33,6 +33,9 @@ CONFIG_DIR="" SHOW_SUPPRESSED=0 NO_IGNORE=0 +# Reusable jq severity ordering function (used in sort and merge operations) +JQ_SEV_ORDER_FUNC='def sev_order: if . == "critical" then 0 elif . == "warn" then 1 elif . == "info" then 2 elif . == "ok" then 3 else 4 end;' + # ─── Usage ─────────────────────────────────────────────────────────────────── usage() { @@ -341,16 +344,7 @@ fi # ─── Sort findings by severity ─────────────────────────────────────────────── # Order: critical > warn > info > ok -SORTED_FINDINGS="$(echo "$ALL_FINDINGS" | jq ' - def sev_order: - if . == "critical" then 0 - elif . == "warn" then 1 - elif . == "info" then 2 - elif . == "ok" then 3 - else 4 - end; - sort_by(.severity | sev_order) -')" +SORTED_FINDINGS="$(echo "$ALL_FINDINGS" | jq "${JQ_SEV_ORDER_FUNC} sort_by(.severity | sev_order)")" # ─── Apply suppression filtering ───────────────────────────────────────────── @@ -378,7 +372,7 @@ DISPLAY_FINDINGS="$ACTIVE_FINDINGS" if [[ "$SHOW_SUPPRESSED" -eq 1 ]]; then # Mark suppressed findings with a "suppressed": true field before merging MARKED_SUPPRESSED="$(echo "$SUPPRESSED_FINDINGS" | jq '[.[] | . + {suppressed: true}]')" - DISPLAY_FINDINGS="$(echo "$ACTIVE_FINDINGS" "$MARKED_SUPPRESSED" | jq -s 'def sev_order: if . == "critical" then 0 elif . == "warn" then 1 elif . == "info" then 2 elif . == "ok" then 3 else 4 end; .[0] + .[1] | sort_by(.severity | sev_order)')" + DISPLAY_FINDINGS="$(echo "$ACTIVE_FINDINGS" "$MARKED_SUPPRESSED" | jq -s "${JQ_SEV_ORDER_FUNC} .[0] + .[1] | sort_by(.severity | sev_order)")" fi # ─── Count by severity ────────────────────────────────────────────────────── diff --git a/scripts/helpers/report.sh b/scripts/helpers/report.sh index 0d1f90c..432aec8 100755 --- a/scripts/helpers/report.sh +++ b/scripts/helpers/report.sh @@ -562,14 +562,25 @@ print_finding() { local inner=$(( w - 6 )) # " ┃ " left (4) + " ┃" right (2) local id severity title description evidence remediation auto_fix suppressed - id="$(echo "$json" | jq -r '.id // ""')" - severity="$(echo "$json" | jq -r '.severity // "info"')" - title="$(echo "$json" | jq -r '.title // ""')" - description="$(echo "$json" | jq -r '.description // ""')" - evidence="$(echo "$json" | jq -r '.evidence // ""')" - remediation="$(echo "$json" | jq -r '.remediation // ""')" - auto_fix="$(echo "$json" | jq -r '.auto_fix // ""')" - suppressed="$(echo "$json" | jq -r '.suppressed // false')" + { + read -r id + read -r severity + read -r title + read -r description + read -r evidence + read -r remediation + read -r auto_fix + read -r suppressed + } < <(echo "$json" | jq -r ' + .id // "", + .severity // "info", + .title // "", + .description // "", + .evidence // "", + .remediation // "", + .auto_fix // "", + .suppressed // false + ') # For OK findings, use compact single-line format if [[ "$severity" == "ok" ]]; then diff --git a/scripts/helpers/suppression.sh b/scripts/helpers/suppression.sh index b51de8d..47f0217 100644 --- a/scripts/helpers/suppression.sh +++ b/scripts/helpers/suppression.sh @@ -57,64 +57,6 @@ load_suppressions() { return 0 } -# ─── Check if a finding ID is currently suppressed ────────────────────────── -# Usage: is_suppressed -# Returns: 0 if suppressed and not expired, 1 otherwise - -is_suppressed() { - local check_id="$1" - - # If no suppressions loaded, nothing is suppressed - if [[ "$_CLAWPINCH_SUPPRESSIONS" == "[]" ]]; then - return 1 - fi - - # Require jq - if ! command -v jq &>/dev/null; then - return 1 - fi - - # Get current timestamp in ISO 8601 format for expiration checking - local now - if command -v date &>/dev/null; then - if ! now="$(date -u +'%Y-%m-%dT%H:%M:%SZ' 2>/dev/null)"; then - now="" - fi - else - now="" - fi - - # Check if the ID is in suppressions and not expired - local result - if [[ -n "$now" ]]; then - # With expiration checking - result="$(echo "$_CLAWPINCH_SUPPRESSIONS" | jq -r --arg id "$check_id" --arg now "$now" ' - map(select(.id == $id)) | - if length > 0 then - .[0] | - if .expires then - if .expires > $now then "suppressed" else "expired" end - else - "suppressed" - end - else - "active" - end - ' 2>/dev/null)" - else - # Without expiration checking (no date command or failed to get timestamp) - result="$(echo "$_CLAWPINCH_SUPPRESSIONS" | jq -r --arg id "$check_id" ' - if (map(select(.id == $id)) | length > 0) then - "suppressed" - else - "active" - end - ' 2>/dev/null)" - fi - - [[ "$result" == "suppressed" ]] -} - # ─── Filter findings into active and suppressed arrays ────────────────────── # Usage: filter_findings < findings.json # Reads findings JSON array from stdin @@ -133,7 +75,7 @@ filter_findings() { # Fallback: all findings are active local findings findings="$(cat)" - echo "{\"active\": $findings, \"suppressed\": []}" + printf '{"active": %s, "suppressed": []}\n' "$findings" return 0 fi @@ -154,8 +96,8 @@ filter_findings() { # Use jq to split findings into active and suppressed if [[ -n "$now" ]]; then # With expiration checking - echo "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" --arg now "$now" ' - ($suppressions | map({(.id): .}) | add // {}) as $smap | + printf '%s\n' "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" --arg now "$now" ' + ($suppressions | map(select(.id != null) | {(.id): .}) | add // {}) as $smap | reduce .[] as $f ({active: [], suppressed: []}; $smap[$f.id] as $s | if $s then @@ -171,8 +113,8 @@ filter_findings() { ' else # Without expiration checking (treat all as unexpired) - echo "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" ' - ($suppressions | map({(.id): .}) | add // {}) as $smap | + printf '%s\n' "$findings" | jq -c --argjson suppressions "$_CLAWPINCH_SUPPRESSIONS" ' + ($suppressions | map(select(.id != null) | {(.id): .}) | add // {}) as $smap | reduce .[] as $f ({active: [], suppressed: []}; $smap[$f.id] as $s | if $s then From 3b7a7d3c678de7e137a25e38003b3ccb682fb284 Mon Sep 17 00:00:00 2001 From: Black Circle Sentinel Date: Tue, 10 Feb 2026 09:41:00 -0500 Subject: [PATCH 13/13] =?UTF-8?q?fix:=20address=20PR=20#17=20review=20?= =?UTF-8?q?=E2=80=94=20revert=20to=20individual=20jq=20calls,=20remove=20d?= =?UTF-8?q?ead=20code,=20simplify=20timestamp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - report.sh: Revert consolidated jq/read to individual jq calls per field to prevent newline injection manipulating the suppressed flag (security-high) - interactive.sh: Remove unused fix_mark variable (dead code) - suppression.sh: Simplify timestamp logic — replace nested if with single date call using || true fallback Co-Authored-By: Claude Opus 4.6 --- scripts/helpers/interactive.sh | 8 -------- scripts/helpers/report.sh | 27 ++++++++------------------- scripts/helpers/suppression.sh | 8 +------- 3 files changed, 9 insertions(+), 34 deletions(-) diff --git a/scripts/helpers/interactive.sh b/scripts/helpers/interactive.sh index b2b015d..c48e756 100644 --- a/scripts/helpers/interactive.sh +++ b/scripts/helpers/interactive.sh @@ -250,14 +250,6 @@ print_findings_compact() { f_title="${f_title:0:$((max_title_len - 3))}..." fi - # Fix indicator (fixed-width, no color padding issues) - local fix_mark - if [[ -n "$f_auto_fix" ]]; then - fix_mark=" ${_CLR_OK}✓${_CLR_RST} " - else - fix_mark=" ─ " - fi - # Dim the row if suppressed local row_color="" if [[ "$f_suppressed" == "true" ]]; then diff --git a/scripts/helpers/report.sh b/scripts/helpers/report.sh index 432aec8..5181fb1 100755 --- a/scripts/helpers/report.sh +++ b/scripts/helpers/report.sh @@ -562,25 +562,14 @@ print_finding() { local inner=$(( w - 6 )) # " ┃ " left (4) + " ┃" right (2) local id severity title description evidence remediation auto_fix suppressed - { - read -r id - read -r severity - read -r title - read -r description - read -r evidence - read -r remediation - read -r auto_fix - read -r suppressed - } < <(echo "$json" | jq -r ' - .id // "", - .severity // "info", - .title // "", - .description // "", - .evidence // "", - .remediation // "", - .auto_fix // "", - .suppressed // false - ') + id="$(echo "$json" | jq -r '.id // ""')" + severity="$(echo "$json" | jq -r '.severity // "info"')" + title="$(echo "$json" | jq -r '.title // ""')" + description="$(echo "$json" | jq -r '.description // ""')" + evidence="$(echo "$json" | jq -r '.evidence // ""')" + remediation="$(echo "$json" | jq -r '.remediation // ""')" + auto_fix="$(echo "$json" | jq -r '.auto_fix // ""')" + suppressed="$(echo "$json" | jq -r '.suppressed // false')" # For OK findings, use compact single-line format if [[ "$severity" == "ok" ]]; then diff --git a/scripts/helpers/suppression.sh b/scripts/helpers/suppression.sh index 47f0217..759239a 100644 --- a/scripts/helpers/suppression.sh +++ b/scripts/helpers/suppression.sh @@ -81,13 +81,7 @@ filter_findings() { # Get current timestamp local now - if command -v date &>/dev/null; then - if ! now="$(date -u +'%Y-%m-%dT%H:%M:%SZ' 2>/dev/null)"; then - now="" - fi - else - now="" - fi + now="$(date -u +'%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || true)" # Read findings from stdin and filter local findings