diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2fddc957..0469b168 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -13,7 +13,7 @@ Rafter is a security CLI for AI coding agents. It ships as two feature-identical - `node/src/commands/` — CLI commands (commander.js) - `node/src/core/` — Command interceptor, audit logger, config manager -- `node/src/scanners/` — Gitleaks integration + regex-based secret scanner +- `node/src/scanners/` — Betterleaks integration + regex-based secret scanner - `node/src/commands/agent/init.ts` — Per-platform installation logic (8 platforms) - `python/rafter_cli/` — Mirrors the Node structure with typer - `shared-docs/CLI_SPEC.md` — Canonical output contracts and exit codes @@ -29,7 +29,7 @@ Rafter is a security CLI for AI coding agents. It ships as two feature-identical ## Key Patterns - Commands export a `createXCommand()` factory (Node) or use `@app.command()` decorators (Python) -- Scanners use dual-engine: Gitleaks binary first, regex fallback. Patterns defined in `secret-patterns.ts` / `secret_patterns.py` +- Scanners use dual-engine: Betterleaks binary first, regex fallback. Patterns defined in `secret-patterns.ts` / `secret_patterns.py` - Risk classification: critical > high > medium > low - Audit log: JSONL format, append-only, documented schema in CLI_SPEC.md - MCP server: 4 tools + 2 resources over stdio transport diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b6082e60..13a6a35f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -154,6 +154,88 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} run: python -m twine upload dist/* + publish-clawhub: + # Publishes the rafter-security skill to ClawHub + # (https://clawhub.ai), the OpenClaw skill registry. The SKILL.md + # frontmatter version (validated by validate-release.yml to match the + # package version) becomes the ClawHub release version. + # + # Skips on forks where the secret isn't configured. On the canonical + # repo, fails loudly on auth or publish errors so a broken token + # surfaces immediately rather than silently skipping releases. + needs: [publish-node] + runs-on: ubuntu-latest + if: github.repository == 'Raftersecurity/rafter-cli' + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Skip if CLAWHUB_TOKEN is not configured + id: gate + env: + CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }} + run: | + if [ -z "$CLAWHUB_TOKEN" ]; then + echo "CLAWHUB_TOKEN secret is not set — skipping ClawHub publish." + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Stage SKILL.md in a publish directory + if: steps.gate.outputs.skip != 'true' + run: | + # ClawHub expects a directory containing SKILL.md (canonical + # filename). The Node and Python resources are kept in sync by + # validate-release.yml — pick either one as the source of truth. + mkdir -p /tmp/rafter-skill/rafter-security + cp node/resources/rafter-security-skill.md /tmp/rafter-skill/rafter-security/SKILL.md + # Sanity-check: the version in the SKILL.md must match the npm + # publish version. validate-release.yml already enforces this on + # PRs/main, but we re-check here so a release with bypassed + # validation can't ship a stale skill. + SKILL_VERSION=$(sed -n 's/^version: *\(.*\)$/\1/p' /tmp/rafter-skill/rafter-security/SKILL.md | head -1 | tr -d ' ') + PACKAGE_VERSION="${{ needs.publish-node.outputs.version }}" + if [ "$SKILL_VERSION" != "$PACKAGE_VERSION" ]; then + echo "FAIL: SKILL.md version=$SKILL_VERSION but package=$PACKAGE_VERSION" + exit 1 + fi + echo "Publishing rafter-security@$SKILL_VERSION to ClawHub" + + - name: Authenticate clawhub CLI + if: steps.gate.outputs.skip != 'true' + env: + CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }} + run: | + # Use the token directly via env var rather than a login step — + # `clawhub` reads CLAWHUB_TOKEN automatically when present + # (clawhub login --token persists to disk; we want stateless CI). + npx -y clawhub@latest whoami + + - name: Publish to ClawHub + if: steps.gate.outputs.skip != 'true' + env: + CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }} + run: | + # `clawhub skill publish` is idempotent: if the version+content + # fingerprint matches what's already published, it no-ops. + # If the version is unchanged but content differs, the command + # fails (intentional — bumps must come with a version change). + npx -y clawhub@latest skill publish /tmp/rafter-skill/rafter-security \ + --version "${{ needs.publish-node.outputs.version }}" \ + --owner rafter + + - name: Verify the publish landed + if: steps.gate.outputs.skip != 'true' + run: | + # Smoke-check: the published version should be reachable via the + # public registry. Wait a few seconds for index propagation. + sleep 5 + npx -y clawhub@latest skill show rafter-security --version "${{ needs.publish-node.outputs.version }}" + create-release: needs: [publish-node, publish-python] runs-on: ubuntu-latest @@ -181,6 +263,11 @@ jobs: pip install rafter-cli==${{ needs.publish-python.outputs.version }} ``` + **OpenClaw (via ClawHub):** + ```bash + clawhub skill install rafter-security + ``` + See [CHANGELOG.md](https://github.com/raftersecurity/rafter-cli/blob/main/CHANGELOG.md) for details. smoke-test-node: diff --git a/.github/workflows/test-comprehensive.yml b/.github/workflows/test-comprehensive.yml index 71cdd925..225aca9e 100644 --- a/.github/workflows/test-comprehensive.yml +++ b/.github/workflows/test-comprehensive.yml @@ -42,7 +42,12 @@ jobs: run: pnpm run build - name: Verify build - run: node -e "import('./dist/index.js').then(() => console.log('Build OK')).catch(e => { console.error(e); process.exit(1); })" + # Smoke-test that dist/index.js loads and the CLI runs. We can't use + # `node -e "import(...)"` because importing index.js triggers + # `program.parse()` synchronously; with no args Commander prints help + # and exits 1 (Node 20 / Commander 11), so the .then() never runs. + # `--version` is a real Commander action that exits 0 cleanly. + run: node ./dist/index.js --version - name: Run all tests run: pnpm test @@ -251,7 +256,12 @@ jobs: pnpm run build - name: Verify build - run: node -e "import('./dist/index.js').then(() => console.log('Build OK')).catch(e => { console.error(e); process.exit(1); })" + # Smoke-test that dist/index.js loads and the CLI runs. We can't use + # `node -e "import(...)"` because importing index.js triggers + # `program.parse()` synchronously; with no args Commander prints help + # and exits 1 (Node 20 / Commander 11), so the .then() never runs. + # `--version` is a real Commander action that exits 0 cleanly. + run: node ./dist/index.js --version - name: Run tests run: pnpm test diff --git a/.github/workflows/validate-release.yml b/.github/workflows/validate-release.yml index 013a6a3c..dcdd0666 100644 --- a/.github/workflows/validate-release.yml +++ b/.github/workflows/validate-release.yml @@ -41,6 +41,27 @@ jobs: fi echo "Versions match: $NODE_VERSION" + - name: Ensure ClawHub skill version matches package version + # rf-zgwj — the SKILL.md frontmatter version is what ClawHub publishes + # under. Drift here would silently ship a stale version on the next + # `clawhub skill publish` (publish.yml). Both the Node and Python + # resource copies must match the package version exactly. + run: | + PACKAGE_VERSION="${{ steps.node-version.outputs.VERSION }}" + for skill_file in node/resources/rafter-security-skill.md python/rafter_cli/resources/rafter-security-skill.md; do + SKILL_VERSION=$(sed -n 's/^version: *\(.*\)$/\1/p' "$skill_file" | head -1 | tr -d ' ') + if [ -z "$SKILL_VERSION" ]; then + echo "FAIL: $skill_file has no top-level 'version:' field" + exit 1 + fi + if [ "$SKILL_VERSION" != "$PACKAGE_VERSION" ]; then + echo "FAIL: $skill_file version=$SKILL_VERSION but package=$PACKAGE_VERSION" + echo "Update the version: line in $skill_file to match." + exit 1 + fi + echo "OK: $skill_file version matches ($SKILL_VERSION)" + done + - name: Check CHANGELOG updated run: | VERSION="${{ steps.node-version.outputs.VERSION }}" diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 39bc3018..c813669e 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -1,7 +1,7 @@ - id: rafter-scan name: Rafter Secret Scanner - description: Scan staged files for secrets (21+ patterns, Gitleaks integration) - entry: rafter scan local --staged --quiet + description: Scan staged files for secrets (21+ patterns, Betterleaks integration) + entry: rafter secrets --staged --quiet language: system stages: [pre-commit] pass_filenames: false @@ -9,7 +9,7 @@ - id: rafter-scan-node name: Rafter Secret Scan (Node) description: Scan staged files for hardcoded secrets - entry: rafter scan local --staged --quiet + entry: rafter secrets --staged --quiet language: node additional_dependencies: ['@rafter-security/cli'] always_run: true @@ -19,7 +19,7 @@ - id: rafter-scan-python name: Rafter Secret Scan (Python) description: Scan staged files for hardcoded secrets - entry: rafter scan local --staged --quiet + entry: rafter secrets --staged --quiet language: python additional_dependencies: ['rafter-cli'] always_run: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 9479e414..26d0a5f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.8.0] - 2026-05-10 + +### Changed +- **Secret-scanning engine migrated from gitleaks to betterleaks** (Node + Python, rc-ksy / rc-963). [Betterleaks](https://github.com/betterleaks/betterleaks) v1.1.2 is the gitleaks successor maintained by the same authors. JSON report shape is unchanged; what changed is the binary, the CLI subcommand (`detect --no-git -s` → `dir `), the release URL, and the checksum filename. + - **Breaking:** the legacy CLI surface has been removed entirely. `--with-gitleaks`, `--engine gitleaks`, and `rafter agent update-gitleaks` now error out (unknown option / invalid engine / unknown command). Use `--with-betterleaks`, `--engine betterleaks`, and `rafter agent update-betterleaks`. **This is the reason for the 0.8.0 minor bump on a 0.x line.** + - **Soft landing for existing installs:** `rafter agent verify` and `rafter agent status` continue to detect a leftover `~/.rafter/bin/gitleaks` (or `gitleaks` on PATH) and emit "legacy gitleaks at X — run: rafter agent update-betterleaks" instead of a confusing "not found". Verify exits 0 in this case (was a hard fail before this fix). + - **Supply-chain hardening:** SHA256 hashes for the bundled `BETTERLEAKS_VERSION` are pinned in source, so the default install no longer trusts the release-page `checksums.txt` to authenticate itself. Tar/zip extraction now rejects symlink/hardlink/device entries (mitigates a malicious-release symlink-redirect that the subsequent `chmod +x` would have followed). Downloads refuse non-https URLs. The optional `--version` flag is validated against `^[A-Za-z0-9._-]+$` to neutralize URL injection. Targets passed to betterleaks are preceded by `--` so a path beginning with `-` isn't parsed as a flag. + - Internal renames: `GitleaksScanner` → `BetterleaksScanner`, `*_gitleaks` methods → `*_betterleaks`, `GITLEAKS_VERSION` → `BETTERLEAKS_VERSION`. New tests cover pinned-hash table completeness, `--version` validation, non-https refusal, and the alias-removal contract. +- **Purge user-facing `scan local` references** (rc-dmp). The Commander/Typer subcommand was already hidden behind `rafter secrets` — this pass mops up the surfaces that still recommended the alias: `.pre-commit-hooks.yaml` (3 hook entries), `fixtures/vulnerable-repo/README.md` demo commands, `node/.claude/skills/*` dev copies (9 refs), `node/src/commands/issues/from-scan.ts` `--from-local` help text, and `shared-docs/CLI_SPEC.md` baseline example. Alias plumbing, internal comments, and the alias-test path are intentionally retained for backward compat. + +### Added +- **`rafter agent init --dry-run`** (Node + Python, rf-hrtd). Prints every file path the command would create, modify, or download — without making any changes. Lists the always-written `~/.rafter/config.json` and bin/patterns dirs, then per-enabled-platform sections (Claude Code, Codex, Gemini, Cursor, Windsurf, Continue.dev, Aider, OpenClaw) with file paths and short notes about what each write contains. Optional Betterleaks binary download is listed as `DOWNLOAD`. The plan is built from the same resolved `want_*` / `has_*` booleans the install path uses, so the listing mirrors what would actually run. Three new Node tests + three new Python tests confirm `--dry-run` writes nothing (not even the always-create `~/.rafter/config.json`) and lists every section under `--local --all`. Closes the rf-v85b P0-1 review concern: security-conscious adopters can preview every edit before accepting. +- **ClawHub auto-publish on release** (CI). `.github/workflows/publish.yml` now runs `clawhub skill publish` against the rafter-security SKILL.md after every `prod`-branch deploy. Skips on forks (gated on `secrets.CLAWHUB_TOKEN`); fails loudly on auth or publish errors on the canonical repo. `validate-release.yml` was extended to enforce that `version:` in both Node and Python copies of `rafter-security-skill.md` matches the package version — drift would silently ship a stale ClawHub release. OpenClaw users can now install rafter via `clawhub skill install rafter-security` as an alternative to `rafter agent init --with-openclaw`. + +### Fixed +- **GitHub Action `finding-count` always 0** (rf-cfjc). The composite action's jq count query (`[.[].matches[]] | length`) errored on the wrapped JSON shape introduced in v0.7.7 (rf-0pch: `{_note, scan_mode, triage_applied, results: [...]}`) and silently fell through to `"0"`. Test Composite Action's `detect secrets in fixture` job had been failing on every push since betterleaks merged, even though the scanner was correctly detecting the AKIA fixture. Replaced with a type-aware query that handles both the wrapped object (current) and the bare array (older `version:` pins). +- **CI ClawHub publish handle** (#96). The actual ClawHub owner handle is `rafter`, not `raftersecurity`. Without this, the first real ClawHub publish would have failed with "owner not found". +- **README pre-commit rev pins** (rf-z6sv, #97). Both pre-commit examples in README.md were stuck at v0.7.1; bumped to track the latest published tag so new adopters get the rf-zfhj GitHub Action fix, the rf-zgwj OpenClaw ClawHub-shape fix, and the audit-log hash-chain hardening. + ## [0.7.9] - 2026-05-08 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index d98563d9..d52bd9c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,7 +32,7 @@ cd python && poetry install && pytest │ │ │ ├── audit-logger.ts # JSONL audit trail │ │ │ └── config-manager.ts # .rafter.yml + global config │ │ └── scanners/ -│ │ ├── gitleaks.ts # Gitleaks binary integration +│ │ ├── betterleaks.ts # Betterleaks binary integration │ │ ├── secret-patterns.ts # DEFAULT_SECRET_PATTERNS array (21+ patterns) │ │ └── regex-scanner.ts # RegexScanner class (imports secret-patterns) │ └── tests/ # Vitest test files @@ -40,7 +40,7 @@ cd python && poetry install && pytest │ ├── rafter_cli/ │ │ ├── commands/ # CLI commands (typer) │ │ ├── core/ # Mirrors node/src/core/ -│ │ └── scanners/ # secret_patterns.py + regex_scanner.py + gitleaks.py +│ │ └── scanners/ # secret_patterns.py + regex_scanner.py + betterleaks.py │ └── tests/ # pytest test files ├── shared-docs/ # Canonical specs (both implementations follow these) │ └── CLI_SPEC.md # Output contracts, exit codes, JSON schemas @@ -58,7 +58,7 @@ cd python && poetry install && pytest **Risk classification**: Commands are classified into 4 tiers (critical/high/medium/low) by pattern matching in `command-interceptor.ts`. Policy files (`.rafter.yml`) can override defaults. -**Secret scanning**: Dual-engine — tries Gitleaks binary first (higher accuracy), falls back to built-in regex patterns (21+ patterns, zero dependencies). Deterministic for a given version. +**Secret scanning**: Dual-engine — tries Betterleaks binary first (higher accuracy), falls back to built-in regex patterns (21+ patterns, zero dependencies). Deterministic for a given version. Betterleaks is the gitleaks successor maintained by the original gitleaks authors. Existing installs with a leftover `~/.rafter/bin/gitleaks` are detected by `agent verify`/`status` so users get an upgrade hint, but the legacy CLI flags (`--with-gitleaks`, `--engine gitleaks`, `update-gitleaks`) have been removed. **MCP server**: `rafter mcp serve` exposes 4 tools (`scan_secrets`, `evaluate_command`, `read_audit_log`, `get_config`) and 2 resources (`rafter://config`, `rafter://policy`) over stdio. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6cb8a230..0cbc8dab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,6 +41,17 @@ poetry install pytest # 400+ tests, ~2s ``` +### (Optional) Dogfood the skills locally + +If you want Claude Code to auto-load the rafter skills while working *in* this repo, install them from the local build into `node/.claude/skills/` (gitignored): + +```bash +cd node && pnpm run build +node dist/index.js agent init --with-claude-code --local +``` + +This is the same install path users run. The previous checked-in dev copies were removed because they drifted from `node/resources/skills/` (the shipped source of truth). + ## Dual Implementation Rafter ships as both `@rafter-security/cli` (npm) and `rafter-cli` (PyPI) with full feature parity. **Every change must be implemented in both Node.js and Python.** diff --git a/Dockerfile b/Dockerfile index 63c6f875..a46b7f2d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ FROM node:22-alpine RUN apk add --no-cache git \ && npm install -g @rafter-security/cli \ - && rafter agent init --with-gitleaks 2>/dev/null || true + && rafter agent init --with-betterleaks WORKDIR /workspace diff --git a/README.md b/README.md index 9bc18ce8..2e385a2d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ rafter secrets . ```sh rafter agent init --all # → Installs all detected integrations -# → Downloads Gitleaks (or falls back to built-in scanner) +# → Downloads Betterleaks (or falls back to built-in scanner) ``` **3. Try to commit—hook blocks it** @@ -184,7 +184,7 @@ This command: - Creates `~/.rafter/` config and audit log (or `./.rafter/` with `--local` for ephemeral / containerized / benchmark setups) - Auto-detects Claude Code, Codex CLI, OpenClaw, Gemini, Cursor, Windsurf, Continue.dev, and Aider - With `--with-*` or `--all`: installs Rafter skills/extensions to opted-in agents -- With `--with-gitleaks` or `--all`: downloads [Gitleaks](https://github.com/gitleaks/gitleaks) for enhanced secret scanning (falls back to built-in 21-pattern regex scanner) +- With `--with-betterleaks` or `--all`: downloads [Betterleaks](https://github.com/betterleaks/betterleaks) (the gitleaks successor maintained by the original gitleaks authors) for enhanced secret scanning. Falls back to built-in 21-pattern regex scanner. Use `rafter agent list/enable/disable` for granular per-component control after the initial install — toggle any platform on or off without re-running `init`. @@ -197,7 +197,7 @@ rafter secrets . # scan directory rafter secrets ./config.js # scan specific file rafter secrets --staged # scan git staged files only rafter secrets --diff HEAD~1 # scan files changed since a git ref -rafter secrets --history # scan full git history (requires gitleaks engine) +rafter secrets --history # scan full git history (requires betterleaks engine) rafter secrets --json # structured output rafter secrets --quiet # silent unless secrets found (CI-friendly) ``` @@ -223,7 +223,7 @@ Exit code 1 if secrets found, 0 if clean. Raw secret values are never included in output. Pipe to `jq`, feed to CI gates, or hand to any tool that reads JSON. -**Engine selection:** Uses Gitleaks when available (more patterns), falls back to built-in regex. Override with `--engine gitleaks|patterns|auto`. +**Engine selection:** Uses Betterleaks when available (more patterns), falls back to built-in regex. Override with `--engine betterleaks|patterns|auto`. ### Pre-Commit Hook @@ -243,7 +243,7 @@ Rafter works as a [pre-commit](https://pre-commit.com) hook. Add to your `.pre-c ```yaml repos: - repo: https://github.com/Raftersecurity/rafter-cli - rev: v0.7.1 + rev: v0.7.9 hooks: - id: rafter-scan-node ``` @@ -430,7 +430,7 @@ Add to `.pre-commit-config.yaml`: ```yaml repos: - repo: https://github.com/Raftersecurity/rafter-cli - rev: v0.7.1 + rev: v0.7.9 hooks: - id: rafter-scan-node # auto-installs via npm # - id: rafter-scan-python # auto-installs via pip @@ -524,7 +524,7 @@ Exit codes are part of Rafter's output contract — CI pipelines and orchestrato ~/.rafter/ ├── config.json # Configuration ├── audit.jsonl # Security event log (JSON lines) -├── bin/gitleaks # Gitleaks binary +├── bin/betterleaks # Betterleaks binary ├── patterns/ # Custom patterns (reserved) └── git-hooks/ # Global pre-commit hook (if --global) ``` diff --git a/SKILL.md b/SKILL.md index 2700a7a1..c7d6da9f 100644 --- a/SKILL.md +++ b/SKILL.md @@ -32,8 +32,8 @@ rafter agent init --with-continue # MCP server rafter agent init --with-aider # MCP server rafter agent init --with-openclaw # Skills -# Also download Gitleaks for enhanced scanning (optional, falls back to built-in 21-pattern regex) -rafter agent init --with-claude-code --with-gitleaks +# Also download Betterleaks for enhanced scanning (optional, falls back to built-in 21-pattern regex) +rafter agent init --with-claude-code --with-betterleaks ``` **What init does per platform:** diff --git a/action.yml b/action.yml index 63ef712a..f270f51f 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,6 @@ # Rafter Security — GitHub Action # -# Deterministic secret scanning for CI. 21+ credential patterns via Gitleaks, +# Deterministic secret scanning for CI. 21+ credential patterns via Betterleaks, # stable exit codes (0 = clean, 1 = findings, 2 = error), structured JSON output. # No API key required. No code leaves the runner. # @@ -94,9 +94,12 @@ runs: echo "${OUTPUT}" >> "$GITHUB_OUTPUT" echo "RAFTER_EOF" >> "$GITHUB_OUTPUT" - # Count findings from output + # Count findings from output. rafter ≥0.7.0 wraps results in an object + # ({_note, scan_mode, triage_applied, results: [...]}); older versions + # emitted a bare array. Handle both so users pinning `version:` to an + # older release don't break. if [ "${{ inputs.format }}" = "json" ]; then - COUNT=$(echo "${OUTPUT}" | jq '[.[].matches[]] | length' 2>/dev/null || echo "0") + COUNT=$(echo "${OUTPUT}" | jq '[(if type == "array" then . else .results end) | .[]?.matches[]?] | length' 2>/dev/null || echo "0") else COUNT=$(echo "${OUTPUT}" | grep -c 'Secret:' 2>/dev/null || echo "0") fi diff --git a/drafts/show-hn/faq.md b/drafts/show-hn/faq.md index ff4ea588..645296e1 100644 --- a/drafts/show-hn/faq.md +++ b/drafts/show-hn/faq.md @@ -4,11 +4,11 @@ Responses written in founder voice. Adapt as needed based on the actual question --- -## "How is this different from gitleaks / trufflehog?" +## "How is this different from betterleaks (or gitleaks) / trufflehog?" -Rafter actually wraps gitleaks when it's available -- if the binary is on your PATH, we use it as the primary scanner because it's excellent. Our built-in regex engine (21+ patterns) is the fallback for zero-dependency environments. +Rafter actually wraps betterleaks (the gitleaks successor) when it's available -- if the binary is on your PATH, we use it as the primary scanner because it's excellent. Our built-in regex engine (21+ patterns) is the fallback for zero-dependency environments. -The difference is everything around the scan. Gitleaks and trufflehog are standalone secret scanners. Rafter adds command interception (blocking `curl | bash` before your agent runs it), audit logging of agent sessions, MCP integration so the agent itself can check for secrets, pre-commit hooks, and one-command setup for 8 different AI platforms. If you're just scanning repos for secrets, gitleaks is great and you don't need us. If you're running AI coding agents and want guardrails around the whole session, that's what Rafter is for. +The difference is everything around the scan. Betterleaks and trufflehog are standalone secret scanners. Rafter adds command interception (blocking `curl | bash` before your agent runs it), audit logging of agent sessions, MCP integration so the agent itself can check for secrets, pre-commit hooks, and one-command setup for 8 different AI platforms. If you're just scanning repos for secrets, betterleaks is great and you don't need us. If you're running AI coding agents and want guardrails around the whole session, that's what Rafter is for. --- diff --git a/drafts/show-hn/post.md b/drafts/show-hn/post.md index 30154c95..1447ba35 100644 --- a/drafts/show-hn/post.md +++ b/drafts/show-hn/post.md @@ -4,7 +4,7 @@ If you use Claude Code, Codex CLI, Cursor, Gemini CLI, or similar tools, your AI **What it does:** -- **Secret scanning**: 21+ built-in regex patterns plus optional Gitleaks integration. Deterministic for a given version. Stable exit codes for CI. +- **Secret scanning**: 21+ built-in regex patterns plus optional Betterleaks integration (the gitleaks successor). Deterministic for a given version. Stable exit codes for CI. - **Command interception**: Classifies commands into risk tiers (critical/high/medium/low) and enforces approval policies before execution. - **Audit logging**: JSONL trail of every command your agent runs and every secret scan result. - **MCP server**: 4 tools (`scan_secrets`, `evaluate_command`, `read_audit_log`, `get_config`) so AI agents can query security status natively. diff --git a/fixtures/vulnerable-repo/README.md b/fixtures/vulnerable-repo/README.md index 231f1fb4..54d3ca37 100644 --- a/fixtures/vulnerable-repo/README.md +++ b/fixtures/vulnerable-repo/README.md @@ -9,13 +9,13 @@ validating Rafter's secret scanning and command interception. ```bash # Scan with Rafter regex scanner -rafter scan local ./fixtures/vulnerable-repo +rafter secrets ./fixtures/vulnerable-repo -# Scan with Gitleaks engine -rafter scan local ./fixtures/vulnerable-repo --engine gitleaks +# Scan with Betterleaks engine +rafter secrets ./fixtures/vulnerable-repo --engine betterleaks # Scan with built-in patterns engine (tests all 21+ patterns) -rafter scan local ./fixtures/vulnerable-repo --engine patterns +rafter secrets ./fixtures/vulnerable-repo --engine patterns # Expected: 27+ findings across all severity levels ``` diff --git a/llms.txt b/llms.txt index 21a1320b..a9b7af7d 100644 --- a/llms.txt +++ b/llms.txt @@ -38,7 +38,7 @@ rafter secrets --diff main # only files changed since a ref rafter secrets --json . # JSON output for piping to jq / orchestrators # Install Rafter into every detected agent platform on this machine -rafter agent init --all # also downloads gitleaks binary +rafter agent init --all # also downloads betterleaks binary rafter agent init --interactive # prompted setup rafter agent init --with-claude-code # one specific platform @@ -118,7 +118,7 @@ Full schema: [docs.rafter.so/policy](https://docs.rafter.so/policy) (or see `sha | Continue.dev | `--with-continue` | MCP server config | | Aider | `--with-aider` | MCP server config | -Plus `--with-gitleaks` to install the upstream Gitleaks binary for higher-recall secret detection (Rafter falls back to 21+ built-in regex patterns if absent). +Plus `--with-betterleaks` to install the upstream Betterleaks binary (the gitleaks successor) for higher-recall secret detection (Rafter falls back to 21+ built-in regex patterns if absent). ## MCP Server diff --git a/node/.claude/skills/rafter-code-review/SKILL.md b/node/.claude/skills/rafter-code-review/SKILL.md deleted file mode 100644 index 39ceae3d..00000000 --- a/node/.claude/skills/rafter-code-review/SKILL.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -name: rafter-code-review -description: "Structured security code review — OWASP / MITRE / ASVS walkthroughs as questions, not audits. Router skill: pick what kind of code you're reviewing (web app, REST/GraphQL API, LLM-integrated, CLI/library/IaC) and Read the matching sub-doc. Designed to pair with `rafter scan` / `rafter run` — the scanner finds known-bad patterns, this skill asks the questions that patterns miss. Use during PR review, refactoring risky modules, or pre-release hardening." -version: 0.7.0 -allowed-tools: [Bash, Read, Glob, Grep] ---- - -# Rafter Code Review — Structured Security Walkthroughs - -A reviewer's skill, not an audit generator. Each sub-doc is a set of **questions** to run against the code — what to grep for, what to trace, what to ask before you sign off. No monolithic reports. - -> Pair with the `rafter` skill (detection: `rafter scan`, `rafter run`) and `rafter-secure-design` (prevention: design-phase walks). This skill is the middle stage — review before merge. - -## How to use this skill - -1. Identify the category of code in front of you (below). -2. `Read` only the matching sub-doc — do not preload them all. -3. Work through its questions against the specific files/diff. Cite file:line evidence as you go. -4. When in doubt on a single finding, jump to `docs/investigation-playbook.md` for canonical follow-up questions. -5. Finish with `rafter run --mode plus` on the same diff if the stakes warrant a deep automated pass. - ---- - -## Choose Your Adventure - -### (1) Web application (server-rendered, session-based, or SPA backend) - -For: login flows, session/cookie handling, form handlers, template rendering, admin panels, anything browser-facing. - -- **Read `docs/web-app.md`** — OWASP Top 10 (2021) walk: broken access control, crypto failures, injection, insecure design, misconfig, vulnerable components, authn failures, integrity failures, logging gaps, SSRF. - -### (2) REST / GraphQL / gRPC API (machine-to-machine, mobile backend, public API) - -For: endpoint surface that isn't primarily rendering HTML — tokens instead of sessions, authz-per-endpoint, rate limiting. - -- **Read `docs/api.md`** — OWASP API Security Top 10 (2023): BOLA, broken authn, BOPLA, unrestricted resource consumption, BFLA, unrestricted access to sensitive business flows, SSRF, misconfig, improper inventory, unsafe consumption of third-party APIs. - -### (3) LLM-integrated feature (prompts, agents, tools, RAG, embeddings) - -For: anything that sends user text to a model, uses tool calls, retrieves untrusted context, or ships model output to a downstream system. - -- **Read `docs/llm.md`** — OWASP LLM Top 10 (2025): prompt injection, sensitive info disclosure, supply chain, data/model poisoning, improper output handling, excessive agency, system prompt leakage, vector/embedding weaknesses, misinformation, unbounded consumption. - -### (4) CLI, library, or infra-as-code - -For: build tooling, developer CLIs, shared SDK packages, Terraform / CloudFormation / Kubernetes manifests, shell scripts. - -- **Read `docs/cwe-top25.md`** — MITRE CWE Top 25, keyed by language (Python / JS / Go / Rust / Java) and by IaC primitive. Focus on injection, memory safety, path traversal, race conditions, privilege mismanagement. - -### (5) I need to pick the right depth for this review - -For: "how hard should I look?", scoping a review before starting, compliance-adjacent changes. - -- **Read `docs/asvs.md`** — OWASP ASVS L1 / L2 / L3. Picks the level based on risk tier of the code, then gives spot-check questions per level. - -### (6) I have one specific question to investigate - -For: single-finding follow-up, tracing a suspicious call, "is this input reachable from outside?". - -- **Read `docs/investigation-playbook.md`** — canonical questions: reachability, authz coverage, data-flow direction, trust boundary placement. - ---- - -## What this skill will NOT do - -- It will not generate a monolithic "security audit report". If you need a report, run `rafter run --mode plus` — the backend is better at that. -- It will not replace automated scanning. Always pair with `rafter scan local .` (secrets) and `rafter run` (SAST/SCA) before review. -- It will not produce recommendations without evidence. Every question expects a file:line answer before moving on. - ---- - -## Fast path for a typical PR review - -```bash -# 1. Run deterministic checks first — cheap, catches the obvious -rafter scan local . -rafter run # remote SAST/SCA, if RAFTER_API_KEY set - -# 2. Then pick the category and walk the questions -# Read docs/.md -``` - -If the diff spans categories (e.g. a web app that also has an LLM feature), Read both sub-docs and walk them sequentially. Don't try to merge the checklists. - ---- - -## Tie-backs - -- Finding from the scanner you don't understand? → `rafter` skill, `docs/finding-triage.md`. -- Designing a new feature instead of reviewing one? → `rafter-secure-design`. -- Risky command came up mid-review? → `rafter` skill, `docs/guardrails.md`. diff --git a/node/.claude/skills/rafter-code-review/docs/api.md b/node/.claude/skills/rafter-code-review/docs/api.md deleted file mode 100644 index 74b26ed6..00000000 --- a/node/.claude/skills/rafter-code-review/docs/api.md +++ /dev/null @@ -1,90 +0,0 @@ -# API Review — OWASP API Security Top 10 (2023) - -REST / GraphQL / gRPC review: authz-per-endpoint, per-object and per-field; rate limiting; bulk operations. Walk each category as questions. Cite file:line before moving on. - -## API1 — Broken Object Level Authorization (BOLA) - -The most common API vuln. Per-object authz, not per-endpoint. - -- For every handler that takes an id (`/orders/:id`, `/users/:user_id/settings`), is there a check that the id belongs to the caller? "Authenticated" is not "authorized". -- Grep patterns: `findById`, `SELECT ... WHERE id = ?`, `get_object_or_404`. Is the caller's identity in the query, or compared after? -- GraphQL: authz at the resolver level for *each* field that returns a user-owned object. Schema-level auth is not enough if resolvers fan out. -- UUIDs do not save you. They only slow discovery; they do not provide authorization. - -## API2 — Broken Authentication - -- Every unauthenticated endpoint — is it supposed to be? List them: grep for `@AnonymousAllowed`, `permission_classes = []`, middleware skips. -- Token lifetime, refresh, and revocation: where is a token invalidated on logout / password change / user deletion? -- JWT-specific: is `alg` pinned? Is the key rotated? Is `iss` / `aud` checked? Is clock skew bounded? -- API keys: how are they generated (entropy), stored (hashed?), scoped (per-tenant? per-capability?), rotated? -- Credential endpoints (login, reset, MFA enroll) — rate-limited separately from normal endpoints? Return generic errors? Constant-time compare? - -## API3 — Broken Object Property Level Authorization (BOPLA) - -Covers both mass-assignment and excessive data exposure. - -- Serialization: when returning an object, are sensitive fields (`password_hash`, `mfa_secret`, `internal_notes`, `role`) explicitly excluded? "Return the model" is a red flag; "return a DTO" is the fix. -- Mass assignment: can the client set fields they shouldn't? `User.objects.update(**request.data)`, `req.body` spread into an ORM constructor, Rails `params.permit!`. Check every update/create path. -- GraphQL: schema exposes fields; are resolvers authz-checked per field? Can a non-admin introspect admin-only fields? - -## API4 — Unrestricted Resource Consumption - -- Pagination on every list endpoint? Max page size enforced server-side (not just a default)? -- Rate limits: per-user, per-IP, per-endpoint. Token bucket? What happens at the limit — 429 with `Retry-After`, or silent 500? -- Request size limits: body size, file upload size, JSON depth, GraphQL query depth / complexity. -- Expensive operations: image processing, PDF generation, report export — are they queued, timeboxed, cost-accounted? -- Amplification: does one API call trigger N outbound calls (email, SMS, push)? Can that N be user-controlled? - -## API5 — Broken Function Level Authorization (BFLA) - -Different from BOLA — this is "can a regular user invoke an admin function at all?", not "can user A touch user B's data?". - -- List admin / privileged endpoints. For each, is there a role check? Is the role from a trusted source (session/token claim) or from the request (`X-Role: admin`)? -- HTTP verb confusion: does the handler accept PUT/PATCH/DELETE when only GET was authz'd? Are method restrictions on the router or in the handler? -- Feature flags: does the flag gate *access* or only *visibility*? If the endpoint is reachable when the flag is off, the flag isn't security. - -## API6 — Unrestricted Access to Sensitive Business Flows - -- Identify flows worth abusing: signup, promo code redemption, ticket/inventory purchase, "add friend", "send invite". -- Per-flow: is there anti-automation (captcha, proof of work, device fingerprint, delay between steps)? Rate limit per account *and* per payment instrument *and* per IP range? -- Does the flow leak enumeration? Signup "email already registered" is a known tradeoff — is it the right one here? - -## API7 — Server-Side Request Forgery - -(Same question set as web-app A10 — see `web-app.md`.) - -- Webhook configurators, URL-based imports, OAuth discovery endpoints, image fetchers: any user-supplied URL that the server fetches? -- DNS rebinding: is the URL resolved once and then reused, or re-resolved on each redirect? Are redirects followed blindly? -- Cloud metadata (`169.254.169.254`, `metadata.google.internal`) explicitly blocked? - -## API8 — Security Misconfiguration - -- Error responses: do they include stack traces, SQL fragments, internal hostnames? Production should return stable error shapes only. -- CORS per-endpoint: any endpoint with `Allow-Credentials: true` *and* a reflected / wildcard origin? -- Default routes from frameworks still mounted (`/actuator/*`, `/debug/*`, `/_next/*` in dev mode)? -- Are OPTIONS responses correctly restrictive? Do HEAD and OPTIONS follow the same authz as GET? -- TLS: are older APIs allowed to accept plain HTTP for backwards compat? If yes — is that documented and scoped? - -## API9 — Improper Inventory Management - -A governance issue, but reviewable: - -- Is there an API version registry? When this PR adds or changes an endpoint, is it documented (OpenAPI / GraphQL schema committed)? -- Are deprecated endpoints marked and scheduled for removal? Still reachable in production? -- Non-prod environments (staging, sandbox) — do they share data, credentials, or network paths with prod? Often the weakest link. - -## API10 — Unsafe Consumption of Third-Party APIs - -- Outbound API calls: is the response validated before use (schema, size, type)? "Trust the third party" is the failure mode. -- Credentials to third parties: scoped to least privilege? Rotated? Not shared across tenants? -- What happens on timeout / 5xx from the third party? Fallback to cached data? Log and surface? -- If the third party is compromised, what is the blast radius here? Does our data flow into untrusted callbacks? - ---- - -## Exit criteria - -- Every endpoint touched by the diff has a documented answer for API1 (per-object authz) and API5 (per-function authz). -- Every new third-party integration has answers for API10. -- Every new flow has a rate-limit story (API4) and an abuse story (API6). -- Scanner cross-check: run `rafter run` and reconcile SAST findings against this walk. diff --git a/node/.claude/skills/rafter-code-review/docs/asvs.md b/node/.claude/skills/rafter-code-review/docs/asvs.md deleted file mode 100644 index dd3df5ff..00000000 --- a/node/.claude/skills/rafter-code-review/docs/asvs.md +++ /dev/null @@ -1,120 +0,0 @@ -# OWASP ASVS — Picking a Level, Spot-Checking It - -The Application Security Verification Standard (ASVS 4.0 / 5.0) is a catalog of verification requirements. Unlike Top 10 lists, it's exhaustive — which is why you pick a level first and spot-check, not walk every item. - -## Step 1 — Pick the right level - -| Level | Use for | Rough test | -|---|---|---| -| **L1** — Opportunistic | Low-value apps, internal tooling, marketing sites. "Protect against casual, opportunistic attackers." | Would losing this data be annoying but not damaging? | -| **L2** — Standard | Most apps that handle user data, B2B SaaS, line-of-business apps. "Default for apps handling sensitive data." | PII, payment, auth, health-adjacent, B2B tenants? | -| **L3** — Advanced | Apps where compromise leads to real harm: financial transactions, healthcare records, critical infrastructure, high-trust platforms. | Regulatory scrutiny? Lives/money at risk? | - -**Rule of thumb**: pick the lowest level that matches the *highest-sensitivity* data flow in the scope. Don't average. A single admin endpoint that touches payment data pulls the whole service to L2 for that endpoint. - ---- - -## Step 2 — Spot-check (not walk-every-item) - -ASVS has 280+ requirements. You will not walk them all in a PR review. Instead, for the level you picked, ask the three questions below per category. - -### V1 — Architecture, Design, Threat Modeling - -- Is there a threat model for this feature? (L2+: yes; L3: reviewed and signed.) -- Are all components inventoried (deps, services, data stores)? -- Does the design document trust boundaries and assumptions? - -### V2 — Authentication - -- Password policy: min length 8 (L1) / 12 (L2) / 12+MFA (L3); hashed with argon2/bcrypt/scrypt; never truncated or case-normalized in storage. -- MFA: not required at L1; required for admin/sensitive at L2; required for all at L3. -- Credential recovery: does not bypass MFA; uses time-limited, single-use tokens; does not leak account existence. - -### V3 — Session Management - -- Server-side session store (or stateless token with revocation)? -- Session rotation on login / privilege change / logout? -- Cookie flags: `Secure`, `HttpOnly`, `SameSite=Lax` or stricter; `Domain` scoped tightly. - -### V4 — Access Control - -- Deny-by-default on new endpoints? -- Per-object authz (BOLA) enforced where user ids appear in URLs? -- Admin functions require re-authentication at L2+; require MFA step-up at L3. - -### V5 — Validation, Sanitization, Encoding - -- Input: validated at the trust boundary (not downstream) against a positive spec (allowlist, schema, regex anchored). -- Output: context-aware encoding (HTML / URL / JSON / shell / SQL). Templating auto-escapes? -- Parsers: safe loaders for YAML/XML/JSON; no string-concat into query languages. - -### V6 — Stored Cryptography - -- Algorithms: AES-GCM, ChaCha20-Poly1305, SHA-256+, HMAC-SHA-256+, PBKDF2/argon2 for passwords. -- Key management: keys not in source, rotated, scoped per-environment, stored in a managed KMS at L2+. -- Randomness: `secrets` / `crypto.randomBytes` / `crypto/rand` — never `rand()` / `Math.random()`. - -### V7 — Error Handling & Logging - -- No secrets / PII in logs (grep log statements). -- No stack traces to the user in production. -- Authn, authz, and sensitive actions logged with who/when/what. - -### V8 — Data Protection - -- PII classified? Access to it logged? -- Data at rest encrypted (disk, DB, backups, cache)? Keys managed separately? -- Data in transit: TLS 1.2+ with modern ciphers; HSTS. - -### V9 — Communications - -- TLS everywhere, including internal hops? At L3, mutual TLS between services. -- Certificate validation not disabled anywhere (grep `InsecureSkipVerify`, `verify=False`, `rejectUnauthorized: false`). - -### V10 — Malicious Code - -- No hardcoded backdoors, debug logins, "magic" accounts. -- Build pipeline integrity: signed artifacts, locked deps, reproducible where possible. - -### V11 — Business Logic - -- Sequential flows (checkout, signup, reset): can steps be skipped? Replayed? Reordered? -- Anti-automation on abuse-prone flows (captcha, proof of work, device fingerprint)? - -### V12 — Files & Resources - -- Uploads: type-sniffed (not extension-trusted), size-limited, stored outside web root, name-randomized. -- Downloads: path-confined; no `../` traversal; MIME set explicitly. -- Archives: zip-slip / tar-slip prevention when extracting. - -### V13 — API & Web Service - -- OpenAPI / GraphQL schema present and matches implementation? -- Auth per-endpoint, not per-service? -- Rate limits per-endpoint tier? - -### V14 — Configuration - -- Production config reviewed (debug off, defaults rotated, unused features off)? -- Dependencies: SCA in CI, lockfile committed, base images pinned. -- Secrets: none in source, rotated on exposure, scoped per environment. - ---- - -## Step 3 — What to produce - -Not an ASVS report. A list of **citations** keyed by (level, category, requirement), plus **gaps**. Example: - -``` -L2 / V4.1.3 (deny by default) — OK, `authMiddleware` registered globally in app.ts:41 -L2 / V4.2.1 (per-object authz) — GAP, /orders/:id handler (orders.ts:88) lacks owner check -L2 / V5.1.1 (schema validation) — OK, zod schemas in routes/*.ts -``` - ---- - -## Tie-backs - -- Concrete vulnerabilities (not requirements): go to `web-app.md` / `api.md` / `llm.md`. -- Specific finding investigation: `investigation-playbook.md`. -- Automated coverage: `rafter run --mode plus` produces ASVS-tagged findings. diff --git a/node/.claude/skills/rafter-code-review/docs/cwe-top25.md b/node/.claude/skills/rafter-code-review/docs/cwe-top25.md deleted file mode 100644 index 945b0292..00000000 --- a/node/.claude/skills/rafter-code-review/docs/cwe-top25.md +++ /dev/null @@ -1,78 +0,0 @@ -# CWE Top 25 — Language-Keyed Checklist - -MITRE's CWE Top 25 is weakness-level, not risk-level. Use this for CLI tools, libraries, IaC, and anything where OWASP's web/API framing doesn't fit. Pick the language section; pair with language-specific linters. - -## How to use - -- Grep the patterns below against the diff. Each hit is a question, not a verdict. -- Cross-reference with `rafter run` — the backend catches many of these via SAST; this doc covers the ones that take context to judge. - ---- - -## Cross-language (applies everywhere) - -- **CWE-79 XSS / CWE-89 SQLi / CWE-78 OS Command Injection** — any user input reaching a query language, shell, or HTML sink. Fix at the sink: parameterize, array-exec, autoescape. -- **CWE-22 Path Traversal** — any `open(path)`, `fs.readFile(path)`, `os.path.join(base, user_input)`. Canonicalize (`realpath` / `filepath.Abs`) and verify the result stays under an allow-root. -- **CWE-352 CSRF** — state-changing endpoints: is there a token check? SameSite cookies are necessary but not sufficient for cross-site POSTs in older browsers / API clients. -- **CWE-287 Improper Authentication / CWE-862 Missing Authorization** — covered in web-app.md / api.md. -- **CWE-798 Hardcoded Credentials** — `rafter scan local .` catches literal secrets; manually check env-var defaults (`API_KEY = os.environ.get("KEY", "dev-fallback-abc123")` ships the fallback). -- **CWE-918 SSRF** — any user-supplied URL fetched server-side. See web-app.md A10. - ---- - -## Python - -- **CWE-502 Insecure Deserialization** — `pickle.load`, `pickle.loads`, `yaml.load` without `SafeLoader`, `shelve`, `marshal`. Any of these on untrusted bytes is RCE. -- **CWE-78 / Subprocess** — `subprocess.run(..., shell=True)` with user input. Use list form: `subprocess.run(["cmd", arg])`, never `shell=True` with interpolated input. -- **CWE-94 Code Injection** — `eval`, `exec`, `compile`, `__import__` with user input. Also `pd.eval`, `numexpr.evaluate`. -- **CWE-611 XXE** — `xml.etree.ElementTree` is safe by default in 3.7+, but `lxml.etree.parse` with `resolve_entities=True` is not. Prefer `defusedxml`. -- **CWE-327 Weak Crypto** — `hashlib.md5` / `sha1` on passwords; `random` (not `secrets`) for tokens; `Crypto.Cipher.DES`, `AES.MODE_ECB`. -- **CWE-20 Input Validation** — type coercion pitfalls: `int(x)` raises, `int(x, 16)` accepts leading `0x`, `float("inf")`. - -## JavaScript / TypeScript - -- **CWE-1321 Prototype Pollution** — `_.merge`, `Object.assign` with user-controlled source, recursive deep-merge on user JSON. Node: affects the whole process. -- **CWE-79 XSS** — `innerHTML`, `outerHTML`, `document.write`, `dangerouslySetInnerHTML`, `v-html`, `$sce.trustAsHtml`. React's default is safe; anything that bypasses it is the finding. -- **CWE-94 Code Injection** — `eval`, `new Function(str)`, `setTimeout(str, ...)`, `setInterval(str, ...)`, `vm.runInThisContext` with user input. -- **CWE-22 Path Traversal** — `path.join(base, userInput)` does not protect. Must resolve and verify containment. -- **CWE-400 Regex DoS (ReDoS)** — catastrophic backtracking patterns: `(a+)+`, `(.*)*`. Especially user-provided regexes. -- **CWE-346 Origin Validation** — `postMessage` handlers that don't check `event.origin`. `addEventListener("message", ...)` without origin check is the bug. - -## Go - -- **CWE-369 Divide-by-Zero / CWE-190 Integer Overflow** — Go doesn't panic on overflow, it wraps. Slice indexing with computed sizes: `make([]byte, headerLen)` where `headerLen` is attacker-controlled. -- **CWE-362 Race Conditions** — map writes without mutex; goroutines sharing non-channel state; `context.Value` for mutable data. Run `go test -race`. -- **CWE-74 Injection / CWE-78** — `exec.Command(name, args...)` is safe; `sh -c ` is not. Check `exec.Command("sh", "-c", userInput)`. -- **CWE-295 Improper Certificate Validation** — `tls.Config{InsecureSkipVerify: true}` outside tests. -- **CWE-400 Resource Consumption** — `io.ReadAll` on untrusted streams with no `io.LimitReader`. Goroutine leaks: for every `go f()`, how does it exit? -- **CWE-665 Improper Initialization** — zero-value structs used as "valid" config; `sync.Mutex` copied by value. - -## Rust - -- **CWE-119 Buffer Issues** — `unsafe` blocks. Every `unsafe` needs a comment explaining the invariant; missing comments are findings. -- **CWE-362 Race Conditions** — despite borrow checker, `Arc>` misuse (holding across `.await`), `RefCell` in multi-threaded code (→ `RwLock`). -- **CWE-674 Uncontrolled Recursion** — `serde` with deeply nested JSON, manual recursive parsers without depth limit. -- **CWE-400 Resource Consumption** — `.collect::>()` on untrusted iterator; `Bytes::from` without length cap. -- **CWE-704 Incorrect Type Conversion** — `as` casts that truncate silently (`u64 as u32`). Prefer `try_into()`. - -## Java / Kotlin - -- **CWE-502 Insecure Deserialization** — `ObjectInputStream.readObject` on untrusted bytes; XMLDecoder; Jackson with default typing (`@JsonTypeInfo(use = Id.CLASS)` + polymorphic). -- **CWE-611 XXE** — `DocumentBuilderFactory` / `SAXParserFactory` without disabling external entities. Default is unsafe in older Java. -- **CWE-22 Path Traversal** — `Paths.get(base, userInput)` doesn't check containment; use `toRealPath().startsWith(base)`. -- **CWE-917 Expression Language Injection** — SpEL, OGNL, MVEL with user input (classic Struts-style RCE). - -## IaC (Terraform / CloudFormation / Kubernetes) - -- **CWE-284 Improper Access Control** — security groups with `0.0.0.0/0` on admin ports (22, 3389, db ports); S3 buckets public; IAM policies with `Resource: "*"` + `Action: "*"`. -- **CWE-732 Incorrect Permissions** — file modes `0777`, world-writable volumes, ConfigMaps holding secrets. -- **CWE-319 Cleartext Transmission** — ELB listeners on port 80 without redirect; storage without encryption at rest; TLS versions < 1.2. -- **CWE-798 Hardcoded Credentials** — secrets in `*.tf`, `*.yaml` environment, `docker-compose.yml`. -- **CWE-1104 Unmaintained 3rd-Party** — Docker base images pinned to `latest` or unpinned digests; Helm charts from unreviewed repos. - ---- - -## Exit criteria - -- For each language in the diff, walked the relevant section. For each hit, either a file:line citation showing it's safe, or a finding filed. -- Run language-specific linters in CI (`bandit`, `semgrep`, `golangci-lint`, `cargo clippy`, `spotbugs`) — this skill complements, doesn't replace them. diff --git a/node/.claude/skills/rafter-code-review/docs/investigation-playbook.md b/node/.claude/skills/rafter-code-review/docs/investigation-playbook.md deleted file mode 100644 index 72ee405c..00000000 --- a/node/.claude/skills/rafter-code-review/docs/investigation-playbook.md +++ /dev/null @@ -1,101 +0,0 @@ -# Investigation Playbook — Canonical Questions per Category - -When one finding, suspicious pattern, or vague "this looks wrong" needs a follow-up — use this. Each section is a question you can actually answer with Grep / Read / trace. - -## Reachability: "Can untrusted input get here?" - -Before fixing anything, prove it's reachable. - -- Where does the input originate? Trace upward from the sink: `app.post(...)` → handler → service → ... → the line in question. -- Are there layers that filter or transform along the way? Allowlist validator? JSON schema? ORM serializer? -- Is this code path actually called in production? Or is it a dead branch left from a refactor? -- If it's an internal service — is it exposed via a misconfigured ingress, reachable from the internet, accessible from a compromised pod? "Internal" is a policy, not a security boundary. -- Test: can you write a failing request/input that triggers the line? If you can write it in 5 minutes, an attacker can. - ---- - -## Authz coverage: "Is every path checked?" - -For an authz-critical operation (read user X, delete resource Y, invoke admin action): - -- List every entry point (HTTP handler, gRPC method, queue worker, CLI command, background job). For each: is the check present? -- For HTTP: is the check in middleware (applies to all), or per-handler (easy to miss)? Grep for the check; count matches; count routes; compare. -- Does the check use *request-supplied* identity or *session-derived* identity? `X-User-Id: 42` header is not identity. -- Privilege escalation corner: can a user modify their own `role`, `tenant_id`, `permissions` via the same endpoint that updates their profile? (mass-assignment + authz = disaster.) -- Is authz re-checked after redirects / async continuations / token refreshes? Identity is not sticky across those. - ---- - -## Data flow: "Does tainted data reach a dangerous sink?" - -Source → sinks, both directions: - -- **Top-down**: pick a source (request body, query string, file upload, DB read from a user-writable table). Grep how that variable flows. Does it reach a sink (SQL, shell, HTML, URL fetch, deserializer)? -- **Bottom-up**: pick a sink (every `subprocess.run`, every `db.raw`, every `innerHTML`). Trace backward. Is the input at the sink derivable from a source? -- Don't trust "it's validated upstream" without proof. Read the validator; check that the type after validation is strong enough (strings are weak, `UUID` is strong). -- Does the data go through serialization round-trips that could re-introduce metacharacters? JSON round-trip, URL-decoding at the wrong layer, base64 → string → SQL. - ---- - -## Trust boundaries: "Where does untrusted become trusted?" - -- Draw the boundary. What's on each side? -- At the boundary: is there validation (shape, type, range, allowlist)? Is there normalization (Unicode NFKC, lowercasing, path canonicalization)? -- Is the same boundary crossed more than once? (Controller → service → repo — does the repo re-validate, or trust the service?) -- Cross-service: does Service B trust Service A's payload? If A is compromised, what can B do? -- Cross-tenant: if a single process serves multiple tenants, where is the tenant id enforced? On every query? Or only at the top of the handler? - ---- - -## Error paths: "What happens when this fails?" - -- Every try/except / error branch: does it leak information (stack, internal IDs, DB errors) to the caller? -- Does the failure leave the system in a broken state (half-written file, partial DB row, orphaned session)? -- Does the failure log enough for you to debug a real incident? Generic "failed to process" without context is a blind spot. -- Are retries bounded? Does the retry code path itself re-authenticate, or reuse a possibly-stale token? - ---- - -## Concurrency: "What happens with two of these at once?" - -- Is there shared mutable state (module globals, singletons, caches, files)? Protected by a lock? -- Check-then-act races: `if not exists: create` — two requests can both pass the check. Use `INSERT ... ON CONFLICT` or transactions. -- Idempotency: can the client retry safely? Is there an idempotency key? Repeated payment, duplicate email, double-spend patterns. -- Async/await holding locks across `.await`: in Rust/Python, this deadlocks. In Go, it's fine but can cause fairness issues. - ---- - -## Secrets lifecycle: "Where does this credential live, and who can read it?" - -- Creation: how is it generated (entropy source)? Who knows it at creation time? -- Storage: env var, config file, KMS, DB, vault? File permissions? -- Transit: does it appear in logs, metrics, error messages, request bodies? -- Rotation: is there a story for rotating it? Automated or manual? What breaks during rotation? -- Revocation: if it leaks today, what's the time-to-revoke? Minutes, hours, or "we'd have to redeploy"? - ---- - -## Input shape: "Can I break the parser?" - -- Size: is there a max? What happens at the max+1? At 10×max? -- Depth: for JSON/XML/nested structures — max depth? Billion-laughs / deeply nested dicts can OOM. -- Encoding: UTF-8 vs UTF-16 vs Latin-1; BOM handling; surrogate pairs; null bytes in paths. -- Numeric: NaN, Infinity, -0, integer overflow, very large floats losing precision. -- Arrays: empty, one element, duplicate keys, sparse arrays, non-integer indices. - ---- - -## How to record the outcome - -For each finding that survives investigation, produce a one-line summary in this shape: - -``` -[severity] [ruleId or ad-hoc tag] file:line — -``` - -Example: -``` -[high] IDOR /orders/:id (orders.ts:88) — handler loads order by URL id without comparing to session user — add owner check before load, 404 (not 403) on mismatch -``` - -Feed these into the PR review comment or back to `rafter` for triage follow-up (`rafter/docs/finding-triage.md`). diff --git a/node/.claude/skills/rafter-code-review/docs/llm.md b/node/.claude/skills/rafter-code-review/docs/llm.md deleted file mode 100644 index 0a8715a0..00000000 --- a/node/.claude/skills/rafter-code-review/docs/llm.md +++ /dev/null @@ -1,87 +0,0 @@ -# LLM-Integrated Code Review — OWASP LLM Top 10 (2025) - -For any code that sends prompts to a model, exposes tool calls, retrieves context (RAG), or ships model output to a downstream system. Walk as questions. Cite file:line. - -## LLM01 — Prompt Injection - -Assume every string that reaches the prompt — user input, retrieved documents, tool output, file contents, web pages — is adversarial. - -- Trace the prompt build. Concatenation of user input into the system prompt? String interpolation of retrieved chunks? Find every `system + user` join site. -- Are there *structural* defenses? (Delimiters the model is trained to respect, role separation, XML tags, instruction hierarchies.) Note: none are airtight — defense is layered, not singular. -- Indirect injection: is retrieved content (web page, email, PDF, repo file) ever fed to the model? Treat it as untrusted input, same as the user's message. -- Output gating: is the model's output used to decide authz, invoke tools, or send messages? If yes — LLM01 merges with LLM06 (Excessive Agency). - -## LLM02 — Sensitive Information Disclosure - -- What goes into the prompt? Grep for prompts that include: PII, internal URLs, database rows, credentials, full request objects. "Just pass context" is the failure mode. -- Is there a redaction step between "application data" and "prompt"? Can it be turned off by flag? -- Does the model provider retain logs? Which tenant's data is crossing into the provider? Is that contractually allowed? -- Model output: before returning to the user, is it scanned for data the caller shouldn't see (e.g. other tenants' data leaked from the context)? - -## LLM03 — Supply Chain - -- Model source: where does the model come from? Provider API (which account?) or self-hosted? If self-hosted, from which registry? Is the weights file checksummed? -- Embedding model: same questions. Many RAG pipelines have *two* models; both are supply chain. -- Prompt templates: if loaded from a shared registry (LangChain Hub, custom store), pinned and verified? Or pulled by name? -- Plugins / tools / MCP servers registered with the agent — are they audited (see `rafter agent audit`) before install? - -## LLM04 — Data & Model Poisoning - -- Training / fine-tuning data: where from, how reviewed, who can write to the source? Can a user of the system influence future training (feedback loops)? -- RAG corpus: same question. Can a user add documents to the retrieval index? If yes — those documents can issue instructions via LLM01. -- Vector store: who can write? Who can update metadata (which drives filtering)? Metadata poisoning can bypass the retrieval filter. - -## LLM05 — Improper Output Handling - -Treat model output as untrusted input to whatever consumes it. - -- Markdown → HTML rendering: is the markdown sanitized? `![img](javascript:...)`, `