diff --git a/clawpinch.sh b/clawpinch.sh index 3f45e26..d05189a 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/sarif.sh" # ─── Signal trap for animation cleanup ────────────────────────────────────── @@ -23,6 +24,7 @@ trap '_cleanup_animation; exit 130' INT TERM DEEP=0 JSON_OUTPUT=0 +SARIF_OUTPUT=0 SHOW_FIX=0 QUIET=0 NO_INTERACTIVE=0 @@ -39,6 +41,7 @@ Usage: clawpinch [OPTIONS] Options: --deep Run thorough / deep scans --json Output findings as JSON array only + --sarif Output findings in SARIF format --fix Show auto-fix commands in report --quiet Print summary line only --sequential Run scanners sequentially (default is parallel) @@ -60,6 +63,7 @@ while [[ $# -gt 0 ]]; do case "$1" in --deep) DEEP=1; shift ;; --json) JSON_OUTPUT=1; shift ;; + --sarif) SARIF_OUTPUT=1; shift ;; --fix) SHOW_FIX=1; shift ;; --quiet) QUIET=1; shift ;; --sequential) PARALLEL_SCANNERS=0; shift ;; @@ -88,6 +92,12 @@ export CLAWPINCH_SHOW_FIX="$SHOW_FIX" export CLAWPINCH_CONFIG_DIR="$CONFIG_DIR" export QUIET +# Determine if we should show terminal UI (not in JSON/SARIF/quiet mode) +_SHOW_UI=1 +if [[ "$JSON_OUTPUT" -eq 1 ]] || [[ "$SARIF_OUTPUT" -eq 1 ]] || [[ "$QUIET" -eq 1 ]]; then + _SHOW_UI=0 +fi + # ─── Validate security config (early check for --remediate) ────────────────── # Fail fast with a clear setup message instead of per-command failures later. @@ -131,7 +141,7 @@ export OPENCLAW_CONFIG # ─── Banner ────────────────────────────────────────────────────────────────── -if [[ "$JSON_OUTPUT" -eq 0 ]] && [[ "$QUIET" -eq 0 ]]; then +if [[ "$_SHOW_UI" -eq 1 ]]; then print_header_animated log_info "OS detected: $CLAWPINCH_OS" if [[ -n "$OPENCLAW_CONFIG" ]]; then @@ -240,7 +250,7 @@ _scan_start="${EPOCHSECONDS:-$(date +%s)}" if [[ "$PARALLEL_SCANNERS" -eq 1 ]]; then # Parallel execution - if [[ "$JSON_OUTPUT" -eq 0 ]] && [[ "$QUIET" -eq 0 ]]; then + if [[ "$_SHOW_UI" -eq 1 ]]; then start_spinner "Running ${scanner_count} scanners in parallel..." fi @@ -256,7 +266,7 @@ if [[ "$PARALLEL_SCANNERS" -eq 1 ]]; then # Count findings from merged results _parallel_count="$(echo "$ALL_FINDINGS" | jq 'length')" - if [[ "$JSON_OUTPUT" -eq 0 ]] && [[ "$QUIET" -eq 0 ]]; then + if [[ "$_SHOW_UI" -eq 1 ]]; then stop_spinner "Parallel scan" "$_parallel_count" "$_parallel_elapsed" fi else @@ -269,7 +279,7 @@ else # Record scanner start time _scanner_start="${EPOCHSECONDS:-$(date +%s)}" - if [[ "$JSON_OUTPUT" -eq 0 ]] && [[ "$QUIET" -eq 0 ]]; then + if [[ "$_SHOW_UI" -eq 1 ]]; then # Print section header for this scanner print_section_header "$scanner_name" @@ -290,7 +300,7 @@ else elif has_cmd python; then output="$(python "$scanner" 2>/dev/null)" || true else - if [[ "$JSON_OUTPUT" -eq 0 ]] && [[ "$QUIET" -eq 0 ]]; then + if [[ "$_SHOW_UI" -eq 1 ]]; then stop_spinner "$local_name" 0 0 fi log_warn "Skipping $scanner_name (python not found)" @@ -315,7 +325,7 @@ else _scanner_end="${EPOCHSECONDS:-$(date +%s)}" _scanner_elapsed=$(( _scanner_end - _scanner_start )) - if [[ "$JSON_OUTPUT" -eq 0 ]] && [[ "$QUIET" -eq 0 ]]; then + if [[ "$_SHOW_UI" -eq 1 ]]; then stop_spinner "$local_name" "$local_count" "$_scanner_elapsed" fi done @@ -325,7 +335,7 @@ fi _scan_end="${EPOCHSECONDS:-$(date +%s)}" _scan_elapsed=$(( _scan_end - _scan_start )) -if [[ "$JSON_OUTPUT" -eq 0 ]] && [[ "$QUIET" -eq 0 ]]; then +if [[ "$_SHOW_UI" -eq 1 ]]; then printf '\n' fi @@ -352,7 +362,10 @@ count_ok="$(echo "$SORTED_FINDINGS" | jq '[.[] | select(.severity == "ok") # ─── Output ────────────────────────────────────────────────────────────────── -if [[ "$JSON_OUTPUT" -eq 1 ]]; then +if [[ "$SARIF_OUTPUT" -eq 1 ]]; then + # SARIF v2.1.0 output (for GitHub Code Scanning, etc.) + convert_to_sarif "$SORTED_FINDINGS" +elif [[ "$JSON_OUTPUT" -eq 1 ]]; then # Pure JSON output (compact for piping efficiency) echo "$SORTED_FINDINGS" | jq -c . else diff --git a/docs/github-actions-sarif.md b/docs/github-actions-sarif.md new file mode 100644 index 0000000..315a16b --- /dev/null +++ b/docs/github-actions-sarif.md @@ -0,0 +1,405 @@ +# GitHub Actions SARIF Integration + +This guide shows you how to integrate ClawPinch with GitHub Code Scanning using SARIF output. This enables security findings to appear in the repository's Security tab as code scanning alerts. + +--- + +## Quick Start + +Add this workflow to `.github/workflows/clawpinch.yml`: + +```yaml +name: ClawPinch Security Scan + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + schedule: + # Run weekly on Monday at 9am UTC + - cron: '0 9 * * 1' + +permissions: + contents: read + security-events: write # Required for uploading SARIF results + +jobs: + security-scan: + name: ClawPinch Security Audit + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run ClawPinch security scan + run: | + npx clawpinch --sarif --no-interactive > clawpinch.sarif + continue-on-error: true # Don't fail the build on findings + + - name: Upload SARIF results to GitHub + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: clawpinch.sarif + category: clawpinch +``` + +--- + +## How It Works + +### 1. Running the Scan + +The workflow runs ClawPinch with the `--sarif` and `--no-interactive` flags: + +```bash +npx clawpinch --sarif --no-interactive > clawpinch.sarif +``` + +- `--sarif` produces SARIF v2.1.0 JSON output instead of the standard terminal UI +- `--no-interactive` skips the post-scan menu (required for CI/CD) +- Output is redirected to `clawpinch.sarif` + +### 2. Uploading Results + +The `github/codeql-action/upload-sarif@v3` action uploads the SARIF file to GitHub: + +```yaml +- name: Upload SARIF results to GitHub + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: clawpinch.sarif + category: clawpinch +``` + +**Parameters:** +- `sarif_file`: Path to the SARIF output file +- `category`: Identifies this as a ClawPinch scan (allows multiple analysis tools) + +**Requirements:** +- The workflow must have `security-events: write` permission +- The repository must have GitHub Advanced Security enabled (free for public repos, requires license for private repos) + +### 3. Viewing Results in Pull Requests + +Once uploaded, security findings appear in the repository's **Security tab** under Code Scanning: + +- **Security tab alerts**: Findings are listed as code scanning alerts, filterable by tool ("ClawPinch") +- **PR checks**: A "Code scanning results / ClawPinch" check appears in the PR status +- **Severity badges**: Critical, warning, and info findings are color-coded + +> **Note:** Because ClawPinch scans runtime configurations rather than source files, findings use the repository root as their location (`"uri": "."`) and do not include specific file or line information. As a result, findings appear in the Security tab as repository-level alerts rather than as inline PR annotations on specific lines of code. + +### 4. Viewing Results in the Security Tab + +All findings across all scans are tracked in the repository's Security tab: + +1. Navigate to your repository → **Security** tab +2. Click **Code scanning** in the left sidebar +3. Filter by tool: **ClawPinch** + +**Features:** +- **Timeline view**: Track when findings were introduced and fixed +- **Trend analysis**: See security posture improving over time +- **Filter by severity**: Focus on critical findings first +- **Dismissal workflow**: Mark findings as false positives or won't-fix with comments + +--- + +## Advanced Configuration + +### Run on Specific Directories + +If your OpenClaw deployment is in a subdirectory: + +```yaml +- name: Run ClawPinch security scan + run: | + npx clawpinch --sarif --no-interactive --config-dir ./infra/openclaw > clawpinch.sarif +``` + +### Deep Scan Mode + +Enable supply-chain verification and skill decompilation: + +```yaml +- name: Run ClawPinch deep scan + run: | + npx clawpinch --sarif --no-interactive --deep > clawpinch.sarif +``` + +### Fail Build on Critical Findings + +By default, `continue-on-error: true` prevents failing the build. To enforce a security gate: + +```yaml +- name: Run ClawPinch security scan + run: | + npx clawpinch --sarif --no-interactive > clawpinch.sarif + + # Check if any critical findings exist + if jq -e '.runs[0]?.results[]? | select(.level == "error")' clawpinch.sarif > /dev/null; then + echo "❌ Critical security findings detected" + exit 1 + fi +``` + +### Multiple SARIF Uploads + +If you run multiple scans (e.g., different environments), use unique categories: + +```yaml +- name: Scan production config + run: npx clawpinch --sarif --config-dir ./prod > clawpinch-prod.sarif + +- name: Upload production results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: clawpinch-prod.sarif + category: clawpinch-prod + +- name: Scan staging config + run: npx clawpinch --sarif --config-dir ./staging > clawpinch-staging.sarif + +- name: Upload staging results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: clawpinch-staging.sarif + category: clawpinch-staging +``` + +--- + +## Combining with Other Scanners + +SARIF allows you to aggregate results from multiple static analysis tools: + +```yaml +name: Security Scanning Suite + +jobs: + security-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # ClawPinch for OpenClaw-specific checks + - run: npx clawpinch --sarif > clawpinch.sarif + - uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: clawpinch.sarif + category: clawpinch + + # Semgrep for general code patterns + - run: semgrep --sarif > semgrep.sarif + - uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: semgrep.sarif + category: semgrep + + # Trivy for container scanning + - run: trivy image --format sarif myapp:latest > trivy.sarif + - uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy.sarif + category: trivy +``` + +All findings appear together in the Security tab, filterable by tool. + +--- + +## Troubleshooting + +### "Resource not accessible by integration" + +**Error:** +``` +Error: Resource not accessible by integration +``` + +**Solution:** +Add `security-events: write` permission to the workflow: + +```yaml +permissions: + contents: read + security-events: write +``` + +### "Advanced Security must be enabled" + +**Error:** +``` +Advanced Security must be enabled for this repository to use code scanning. +``` + +**Solution:** +- For **public repositories**: GitHub Advanced Security is free and automatically available +- For **private repositories**: Enable GitHub Advanced Security in repository settings (requires GitHub Enterprise license) + +### Invalid SARIF File + +**Error:** +``` +Error: Invalid SARIF. The SARIF file is not valid. +``` + +**Solution:** +Validate the SARIF file before uploading: + +```yaml +- name: Validate SARIF output + run: | + # Install SARIF validator + npm install -g @microsoft/sarif-multitool + + # Validate against SARIF v2.1.0 schema + sarif-multitool validate clawpinch.sarif +``` + +If validation fails, [open an issue](https://github.com/MikeeBuilds/clawpinch/issues) with the output. + +### No Findings Appear in PRs + +**Checklist:** +1. Verify the workflow completed successfully in the Actions tab +2. Check that `security-events: write` permission is set +3. Ensure the SARIF file was uploaded (check action logs) +4. Wait 1-2 minutes for GitHub to process the SARIF file +5. Verify findings are for files changed in the PR (GitHub only shows diff-related alerts in PR checks) + +--- + +## SARIF Output Format Reference + +ClawPinch produces SARIF v2.1.0 output with the following structure. The `version` field is dynamically populated from `package.json` at runtime: + +```json +{ + "version": "2.1.0", + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "runs": [{ + "tool": { + "driver": { + "name": "ClawPinch", + "version": "", + "informationUri": "https://github.com/MikeeBuilds/clawpinch", + "rules": [ + { + "id": "CHK-CFG-001", + "name": "GatewayListeningOnAllInterfaces", + "shortDescription": { + "text": "Gateway listening on 0.0.0.0" + }, + "helpUri": "https://github.com/MikeeBuilds/clawpinch/blob/main/references/check-catalog.md", + "defaultConfiguration": { + "level": "error" + } + } + ] + } + }, + "results": [ + { + "ruleId": "CHK-CFG-001", + "level": "error", + "message": { + "text": "Gateway listening on 0.0.0.0", + "markdown": "The gateway is configured to listen on all interfaces (0.0.0.0), exposing it to the network.\n\n**Remediation:** Set gateway.host to '127.0.0.1' in openclaw.json" + } + } + ] + }] +} +``` + +**Severity Mapping:** +- `critical` → SARIF `error` +- `warn` → SARIF `warning` +- `info` → SARIF `note` +- `ok` → Not included in SARIF output (only findings) + +--- + +## Related Documentation + +- [ClawPinch README](../README.md) — Installation and usage +- [Check Catalog](../references/check-catalog.md) — Full list of 63 checks +- [SARIF Specification](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) — Official SARIF v2.1.0 spec +- [GitHub Code Scanning](https://docs.github.com/en/code-security/code-scanning) — GitHub Advanced Security docs + +--- + +## Example: Complete CI/CD Pipeline + +This workflow runs ClawPinch on every PR, uploads results to GitHub, and blocks merging on critical findings: + +```yaml +name: Security Gate + +on: + pull_request: + branches: [main] + +permissions: + contents: read + security-events: write + pull-requests: write + +jobs: + clawpinch-scan: + name: ClawPinch Security Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run ClawPinch + id: scan + run: | + npx clawpinch --sarif --no-interactive > clawpinch.sarif + + # Count critical findings + CRITICAL=$(jq '[.runs[0]?.results[]? | select(.level == "error")] | length' clawpinch.sarif) + echo "critical=$CRITICAL" >> $GITHUB_OUTPUT + continue-on-error: true + + - name: Upload SARIF to GitHub + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: clawpinch.sarif + category: clawpinch + + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const critical = ${{ steps.scan.outputs.critical }}; + const body = critical > 0 + ? `⛔ **ClawPinch found ${critical} critical security finding(s)**\n\nReview the Code Scanning alerts for details.` + : `✅ **ClawPinch scan passed** - No critical findings`; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: body + }); + + - name: Enforce security gate + if: steps.scan.outputs.critical > 0 + run: | + echo "❌ Blocking merge: ${{ steps.scan.outputs.critical }} critical findings detected" + exit 1 +``` + +This enforces a security gate — PRs with critical findings cannot be merged until they're fixed. + +--- + +## License + +MIT diff --git a/scripts/helpers/sarif.sh b/scripts/helpers/sarif.sh new file mode 100644 index 0000000..3293fd7 --- /dev/null +++ b/scripts/helpers/sarif.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ─── ClawPinch SARIF formatter ───────────────────────────────────────────── +# Converts ClawPinch findings JSON to SARIF v2.1.0 format for GitHub Code +# Scanning and other static analysis platforms. + +# Ensure common helpers are available +if [[ -z "${_CLAWPINCH_HAS_COLOR:-}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + # shellcheck source=scripts/helpers/common.sh + source "$SCRIPT_DIR/common.sh" +fi + +# ─── SARIF converter ────────────────────────────────────────────────────── +# Usage: convert_to_sarif +# findings_json: JSON array of ClawPinch findings +# +# Outputs: Valid SARIF v2.1.0 JSON to stdout + +convert_to_sarif() { + local findings_json="${1:-[]}" + + # Require jq for SARIF generation + if ! require_cmd jq; then + log_error "jq is required for SARIF output" + return 1 + fi + + # Get tool version from package.json (fallback to 1.2.1) + local tool_version="1.2.1" + local package_json + package_json="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/package.json" + if [[ -f "$package_json" ]]; then + tool_version="$(jq -r '.version // "1.2.1"' "$package_json" 2>/dev/null || echo "1.2.1")" + fi + + # SARIF schema URL + local sarif_schema="https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json" + + # Tool information URI + local tool_uri="https://github.com/MikeeBuilds/clawpinch" + + # Build SARIF document using jq + echo "$findings_json" | jq -c \ + --arg schema "$sarif_schema" \ + --arg version "2.1.0" \ + --arg tool_name "clawpinch" \ + --arg tool_version "$tool_version" \ + --arg tool_uri "$tool_uri" \ + '{ + "$schema": $schema, + "version": $version, + "runs": [ + { + "tool": { + "driver": { + "name": $tool_name, + "version": $tool_version, + "informationUri": $tool_uri, + "rules": ( + . | [.[] | select(.severity != "ok")] | map({ + id: .id, + name: .title, + shortDescription: { + text: .title + }, + fullDescription: { + text: .description + }, + helpUri: ($tool_uri + "/blob/main/references/check-catalog.md"), + defaultConfiguration: { + level: ( + if .severity == "critical" then "error" + elif .severity == "warn" then "warning" + elif .severity == "info" then "note" + else "note" + end + ) + }, + properties: { + category: ( + if .id | startswith("CHK-CFG-") then "configuration" + elif .id | startswith("CHK-SEC-") then "secrets" + elif .id | startswith("CHK-NET-") then "network" + elif .id | startswith("CHK-SKL-") then "skills" + elif .id | startswith("CHK-PRM-") then "permissions" + elif .id | startswith("CHK-CRN-") then "cron" + elif .id | startswith("CHK-CVE-") then "cve" + elif .id | startswith("CHK-SUP-") then "supply-chain" + else "general" + end + ) + } + }) + | unique_by(.id) + ) + } + }, + "results": ( + . | [.[] | select(.severity != "ok")] | map({ + "ruleId": .id, + "level": ( + if .severity == "critical" then "error" + elif .severity == "warn" then "warning" + elif .severity == "info" then "note" + else "note" + end + ), + "message": { + "text": .title, + "markdown": ( + if .remediation != "" then + (.description + "\n\n**Remediation:** " + .remediation) + else + .description + end + ) + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "." + } + } + } + ], + "properties": { + "evidence": .evidence, + "auto_fix": .auto_fix + } + }) + ) + } + ] + }' +} + +# ─── Main ────────────────────────────────────────────────────────────────── +# If this script is executed directly (not sourced), convert stdin to SARIF + +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + findings="$(cat)" + convert_to_sarif "$findings" +fi