From ab6c8182aa6d62f837b5b8dacd515b945f2d681b Mon Sep 17 00:00:00 2001 From: prabu-openclaw Date: Sat, 15 Aug 2026 12:35:57 -0600 Subject: [PATCH 1/3] ci: free security scanning to replace CodeQL CodeQL is licensed for open source only unless the org holds GitHub Code Security ($30/active committer/month), so it cannot legally run against these private repos - hosted or via the CLI. This adds permissively-licensed scanners that can: gitleaks secrets, full history bandit python SAST, high severity + high confidence only semgrep OSS multi-language SAST, ERROR severity only osv-scanner dependency CVEs, advisory only All the logic lives in scripts/security-scan.sh, which the workflow simply calls. That means you can run the identical scan locally at any time: ./scripts/security-scan.sh Local and CI results match by construction, and neither of us is blocked waiting on the other's machine to learn whether our code is clean. Runs on the self-hosted Macs, so it costs nothing and does not depend on the Actions billing state. Switch to ubuntu-latest once billing is healthy. .gitleaks.toml suppresses only shapes triaged as false positives across the org (20 findings, 0 real secrets: Swift UserDefaults keys, golden-snapshot fixture ids, and synthetic PEMs in tests that assert they get blocked). The allowlist is restricted to dot-separated reverse-DNS values, so sk_live_*, ghp_*, AKIA* and friends are still reported - verified against planted credentials. Detection matches gitleaks' stock config on that canary. Nothing here uploads source code. --- .github/workflows/security.yml | 38 ++++++++++++ .gitleaks.toml | 39 +++++++++++++ scripts/security-scan.sh | 104 +++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 .github/workflows/security.yml create mode 100644 .gitleaks.toml create mode 100755 scripts/security-scan.sh diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..a377363c --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,38 @@ +name: security + +# Free replacement for CodeQL, which is licensed for open source only unless +# you hold GitHub Code Security. +# +# This workflow deliberately contains no scanning logic — it just calls +# scripts/security-scan.sh, the same script you run locally: +# +# ./scripts/security-scan.sh +# +# so local and CI results are identical by construction, and neither of you is +# ever blocked waiting on a runner to find out whether your code is clean. +# +# Runs on the self-hosted Macs: no billed minutes, and no dependency on the +# Actions billing state. Once billing is healthy, switch to ubuntu-latest for +# cleaner isolation — Linux minutes fit inside the included tier. + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + scan: + name: security scan + runs-on: [self-hosted, macOS] + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + # gitleaks scans history, not just the tip + fetch-depth: 0 + + - name: Run security scan + run: ./scripts/security-scan.sh diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..f9f6c012 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,39 @@ +title = "Zero-State-LLC secret scanning" + +[extend] +# All default rules stay on, including generic-api-key. Rather than disabling +# that noisy rule wholesale, the allowlist below suppresses only the specific +# shapes that were triaged as false positives across the org on 2026-08-15 +# (20 findings, 0 real secrets). +useDefault = true + +[allowlist] +description = "Triaged false-positive shapes. Deliberately narrow." + +regexTarget = "match" +regexes = [ + # Swift persistence key constants, e.g. + # private let bestFloorKey = "HexWire.Gauntlet.BestFloor.v1" + # defaults.set(x, forKey: "App.Thing.v1") + # + # Restricted to DOT-separated reverse-DNS values on purpose. Real credentials + # (sk_live_..., ghp_..., AKIA..., xox...) are not dot-separated, so they are + # still reported. Verified: this suppresses the 6 UserDefaults keys in Hexwire + # and none of the sample credentials tested against it. + '''(?i)\w*key\w*\s*=\s*"[A-Za-z0-9]+(\.[A-Za-z0-9]+)+"''', + '''(?i)for\s*key:\s*"[A-Za-z0-9]+(\.[A-Za-z0-9]+)+"''', +] + +paths = [ + # Golden/snapshot test fixtures: identifiers like "key": "lsd_open_42". + '''(?i).*/fixtures?/.*''', + '''(?i).*golden.*\.json$''', + + # Synthetic PEM and "sk-abc123" literals that exist so tests can assert the + # privacy preflight BLOCKS them. Provider-specific rules still apply here. + '''.*test_privacy_preflight\.py$''', + + # Prose that discusses credentials rather than containing them. + '''(?i).*PRIVACY\.md$''', + '''(?i)^docs/.*\.md$''', +] diff --git a/scripts/security-scan.sh b/scripts/security-scan.sh new file mode 100755 index 00000000..807fc4fc --- /dev/null +++ b/scripts/security-scan.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash +# +# Security scan — run this locally, exactly as CI runs it. +# +# ./scripts/security-scan.sh # scan, fail on findings +# REPORT_ONLY=1 ./scripts/security-scan.sh # scan, always exit 0 +# +# CodeQL is licensed for open source only, so these private repos use +# permissively-licensed scanners instead. Nothing here uploads source code: +# gitleaks and bandit are entirely local, osv-scanner sends package names and +# versions, semgrep downloads rules and runs with metrics disabled. +# +# Tools are cached outside the repo, so the working tree stays clean. + +set -euo pipefail + +GITLEAKS_VERSION="${GITLEAKS_VERSION:-8.30.1}" +OSV_VERSION="${OSV_VERSION:-2.5.0}" +REPORT_ONLY="${REPORT_ONLY:-0}" + +ROOT="$(git rev-parse --show-toplevel)" +TOOLDIR="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/zsl-security-tools" +BIN="$TOOLDIR/bin" +VENV="$TOOLDIR/venv" +mkdir -p "$BIN" + +failed=() +note() { printf '\n\033[1m== %s\033[0m\n' "$1"; } +record() { failed+=("$1"); printf '\033[31mFAIL: %s\033[0m\n' "$1"; } + +case "$(uname -s)" in Darwin) OS=darwin ;; *) OS=linux ;; esac +case "$(uname -m)" in arm64|aarch64) ARCH=arm64 ;; *) ARCH=amd64 ;; esac + +# ---------------------------------------------------------------- install --- +if [ ! -x "$BIN/gitleaks" ]; then + note "installing gitleaks $GITLEAKS_VERSION" + GL_ARCH="$ARCH"; [ "$ARCH" = amd64 ] && GL_ARCH=x64 + curl -fsSL -o "$TOOLDIR/gl.tar.gz" \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_${OS}_${GL_ARCH}.tar.gz" + tar xzf "$TOOLDIR/gl.tar.gz" -C "$BIN" gitleaks +fi + +if [ ! -x "$BIN/osv-scanner" ]; then + note "installing osv-scanner $OSV_VERSION" + curl -fsSL -o "$BIN/osv-scanner" \ + "https://github.com/google/osv-scanner/releases/download/v${OSV_VERSION}/osv-scanner_${OS}_${ARCH}" + chmod +x "$BIN/osv-scanner" +fi + +has_python=0 +if find "$ROOT" -name '*.py' -not -path '*/.git/*' -print -quit 2>/dev/null | grep -q .; then + has_python=1 +fi + +if [ ! -x "$VENV/bin/semgrep" ]; then + note "installing semgrep + bandit" + python3 -m venv "$VENV" + "$VENV/bin/pip" install --quiet --upgrade pip + # Deliberately unpinned: stale rules are worse than non-reproducible ones + # for a security scanner. + "$VENV/bin/pip" install --quiet semgrep bandit +fi + +EXCLUDES=(--exclude=.git --exclude=build --exclude=DerivedData + --exclude=node_modules --exclude=.venv --exclude=Pods) + +# ------------------------------------------------------------------ scans --- +note "secrets (gitleaks, full history)" +GL_CONF=() +[ -f "$ROOT/.gitleaks.toml" ] && GL_CONF=(--config "$ROOT/.gitleaks.toml") +"$BIN/gitleaks" detect --source "$ROOT" "${GL_CONF[@]}" \ + --redact --no-banner || record "gitleaks: secrets detected" + +if [ "$has_python" = 1 ]; then + note "python SAST (bandit)" + # High severity AND high confidence only. Noisier settings get the whole + # gate switched off within a week. + "$VENV/bin/bandit" -r "$ROOT" -ll -ii -q \ + --exclude "$ROOT/.git,$ROOT/build,$ROOT/DerivedData,$ROOT/.venv,$ROOT/node_modules" \ + || record "bandit: high-severity python findings" +else + note "python SAST (bandit) — no python sources, skipped" +fi + +note "multi-language SAST (semgrep)" +"$VENV/bin/semgrep" scan --config=p/security-audit \ + --metrics=off --error --severity=ERROR --quiet \ + "${EXCLUDES[@]}" "$ROOT" || record "semgrep: ERROR-severity findings" + +note "dependencies (osv-scanner) — advisory only" +# Never blocking: a fresh CVE in a transitive dependency should not stop an +# unrelated PR. Review these, do not gate on them. +"$BIN/osv-scanner" scan source --recursive "$ROOT" || true + +# ----------------------------------------------------------------- result --- +echo +if [ ${#failed[@]} -eq 0 ]; then + printf '\033[32mAll blocking checks passed.\033[0m\n' + exit 0 +fi +printf '\033[31m%d blocking check(s) failed:\033[0m\n' "${#failed[@]}" +printf ' - %s\n' "${failed[@]}" +[ "$REPORT_ONLY" = "1" ] && { echo "REPORT_ONLY=1 — exiting 0 anyway."; exit 0; } +exit 1 From c94c2d3873ad026a39697c2f944eb0c154113693 Mon Sep 17 00:00:00 2001 From: prabu-openclaw Date: Sat, 15 Aug 2026 12:39:52 -0600 Subject: [PATCH 2/3] ci: rename workflow to avoid colliding with existing security.yml Patch-Hive already had a .github/workflows/security.yml and the first version of this change silently overwrote it. Renaming to free-security-scan.yml so this can never clobber a repo's own security workflow. --- .github/workflows/{security.yml => free-security-scan.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{security.yml => free-security-scan.yml} (100%) diff --git a/.github/workflows/security.yml b/.github/workflows/free-security-scan.yml similarity index 100% rename from .github/workflows/security.yml rename to .github/workflows/free-security-scan.yml From 9de64a1bd24ff2e84a7c1b97408fab0acbc373ff Mon Sep 17 00:00:00 2001 From: prabu-openclaw Date: Sat, 15 Aug 2026 12:55:10 -0600 Subject: [PATCH 3/3] fix: gate bandit on high severity, not medium The bandit flags did not match the documented intent. -ll -ii means severity and confidence MEDIUM and above, not HIGH -- so the gate failed on Psy-Fi (High: 0, Medium: 9) and Abraxas-Orchestra (High: 0, Medium: 11), neither of which has a single high-severity finding. The gate now runs -lll -iii (high severity AND high confidence). Medium-and- above still runs immediately before it as informational output, so those findings stay visible without blocking a merge. --- scripts/security-scan.sh | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/security-scan.sh b/scripts/security-scan.sh index 807fc4fc..1355cd59 100755 --- a/scripts/security-scan.sh +++ b/scripts/security-scan.sh @@ -71,13 +71,19 @@ GL_CONF=() "$BIN/gitleaks" detect --source "$ROOT" "${GL_CONF[@]}" \ --redact --no-banner || record "gitleaks: secrets detected" +BANDIT_EXCLUDE="$ROOT/.git,$ROOT/build,$ROOT/DerivedData,$ROOT/.venv,$ROOT/node_modules" + if [ "$has_python" = 1 ]; then - note "python SAST (bandit)" - # High severity AND high confidence only. Noisier settings get the whole - # gate switched off within a week. - "$VENV/bin/bandit" -r "$ROOT" -ll -ii -q \ - --exclude "$ROOT/.git,$ROOT/build,$ROOT/DerivedData,$ROOT/.venv,$ROOT/node_modules" \ - || record "bandit: high-severity python findings" + # Note on flags: -l/-ll/-lll is severity LOW/MEDIUM/HIGH and above, and + # -i/-ii/-iii is the same for confidence. So -ll -ii means MEDIUM and above, + # NOT high -- which is why this gate first failed on repos whose worst + # finding was medium. + note "python SAST (bandit) — informational, medium severity and above" + "$VENV/bin/bandit" -r "$ROOT" -ll -ii -q --exclude "$BANDIT_EXCLUDE" || true + + note "python SAST (bandit) — gate, high severity AND high confidence" + "$VENV/bin/bandit" -r "$ROOT" -lll -iii -q --exclude "$BANDIT_EXCLUDE" \ + || record "bandit: high-severity, high-confidence python findings" else note "python SAST (bandit) — no python sources, skipped" fi