Skip to content

Release v0.9.1#207

Merged
Raftersecurity merged 14 commits into
prodfrom
main
Jul 22, 2026
Merged

Release v0.9.1#207
Raftersecurity merged 14 commits into
prodfrom
main

Conversation

@Rome-1

@Rome-1 Rome-1 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Promotes mainprod, publishing v0.9.1 to npm (@rafter-security/cli) and PyPI (rafter-cli) via publish.yaml.

Headline

Also included since 0.9.0

Version

Bumped to 0.9.1 across node/package.json, python/pyproject.toml, and both rafter-security-skill.md frontmatters (#206); CHANGELOG rolled to [0.9.1] - 2026-07-21. validate-release enforces parity on this PR.

Verification

  • Node suite green (the one pre-existing AuditLogger red is a non-hermetic test unrelated to this release, tracked in sable-883t); Python suite green.
  • The plus-scan gate has dedicated tests in both impls.

Minh-Nguyen-2k7 and others added 14 commits July 8, 2026 19:07
…g (ob-y5ep) (#191)

The audit logger wrote command lines through `scanner.redact()`, which only
masked substrings matching a known secret *pattern*. A command run as
`RAFTER_API_KEY=<value> rafter ...` logged the value in plaintext: the key's
shape matches no built-in pattern, so `redact()` left it untouched — leaking
real API keys into `~/.rafter/audit.jsonl` despite `redact_secrets: true`.

Add env-assignment redaction to the shared redaction path
(`PatternEngine.redactText` / `redact_text`), so every `scanner.redact(command)`
call site benefits (both `logCommandIntercepted` and `logPolicyOverride`). Any
`NAME=VALUE` token whose NAME looks secret-bearing
(`/(?:^|_)(KEY|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|PWD|API[_-]?KEY|ACCESS[_-]?KEY|CREDENTIALS?|AUTH)$/i`,
which includes RAFTER_API_KEY) has its VALUE masked with the codebase's existing
char-mask. This is additive to the existing pattern-based redaction — it catches
values that match no pattern but sit behind a secret-named env var. Benign
assignments like `FOO=bar` and `NODE_ENV=production` are left untouched.

Both implementations updated in lockstep (identical regex, ordering, and mask).
Tests added in both suites: a `RAFTER_API_KEY=<val> rafter scan .` command must
not leak the raw value into the log, and benign assignments must be preserved.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ow-up to #190) (#194)

Follow-up polish on @Minh-Nguyen-2k7's Mailchimp pattern (#190): asserts a bare
32-char hex with no -us datacenter suffix does NOT match, in both Node and Python,
and removes trailing whitespace on the Python test. Test-only; pattern unchanged.
Verified: node secret-patterns 58 passed, python test_regex_scanner 16 passed.

Co-authored-by: Minh-Nguyen-2k7 <Minh-Nguyen-2k7@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#195)

The rafter skill prompts were all-escalation with no scoping ("DO NOT
stop at local", "an un-evaluated 'done' is not done", "default: run
rafter run") and never said WHAT counts as security-relevant or when the
gate does NOT apply. Agents writing research/experimental code with zero
security surface (training scripts, data analysis, plotting, model eval,
notebooks, pure computation over trusted local data) kept getting the
gate in the way.

Fix (prompting only — no executable code changes): add a "When this
applies (and when it doesn't)" framing driven by the security SURFACE of
the diff, not the task's label.

- ENGAGE fully (unchanged, not weakened): auth, credentials/secrets/
  tokens/sessions, user/untrusted input, SQL, shell/exec, file paths +
  uploads, deserialization, crypto, network-facing endpoints, data
  deletion, dependency/manifest changes.
- BACK OFF: research/experimental/local-only/throwaway code with NONE of
  that surface — a quick surface check suffices, and with genuinely no
  surface it is fine to proceed without the full rafter-code-review +
  rafter run.
- Key rule landed in every edited skill: judge by the actual security
  surface of the diff, not by whether the work is called "research."
  Research code that touches secrets/shell/network/untrusted input still
  fully engages.

Also surface-conditioned the overly-broad `description:` front-matter
(that string drives auto-invocation).

Files: node/resources/skills/{rafter,rafter-code-review,
rafter-secure-design}/SKILL.md and their byte-identical python mirrors
under python/rafter_cli/resources/skills/. rafter-skill-review is left
unchanged on purpose: vetting third-party executable context before
install is always a genuine security surface, so a back-off there would
weaken a real gate.

This REDUCES false engagement on no-surface code without removing or
softening protection for any genuine security surface.

Refs: sable-hjd3

AI-assisted (per repo AI Policy): authored with Claude Opus 4.8.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…el (sable-m9n6) (#196)

`rafter agent init` injects RAFTER_INSTRUCTION_BLOCK into agents' instruction
files (CLAUDE.md, and ~/.codex/AGENTS.md — a GLOBAL Codex instructions file).
The block was a blanket gate ("A security-relevant task is not complete until
reviewed... Stop and invoke before continuing") with no scoping. Because Codex
reads ~/.codex/AGENTS.md as global instructions, every Codex session was ordered
to treat its task as rafter-gated — agents went to read rafter's SKILL.md instead
of doing the requested (often no-surface, research) work. Instruction-level
hijacking.

This applies the same surface-based scoping that PR #195 gave the skill prompts,
to the injected block itself, so block and skills tell the same story:

- Engage the gate when the change touches a real security surface (auth, secrets/
  tokens, untrusted input, SQL/query, shell/exec, file paths, deserialization,
  crypto, network endpoints/SSRF, data deletion, dependencies).
- Back off when NONE of those are present — research/experimental/local-only/
  throwaway code (training scripts, analysis, plotting, model eval, notebooks,
  pure computation over trusted local data): a quick surface check is enough.
- Decisive rule: judge by the diff's actual surface, not the "research" label —
  research code that reads a secret, shells out, hits the network, parses
  untrusted input, or bumps a dependency still gets the full gate.

Guardrail: this reduces false engagement on no-surface code without removing or
softening protection for any listed surface — the routing bullets were expanded
(crypto, network/SSRF, data deletion, dependencies) so coverage strengthens, not
weakens. node and python RAFTER_INSTRUCTION_BLOCK remain byte-identical.

Also (sable-h0ah): narrow the Codex .codex/hooks.json PostToolUse matcher from
`.*` to `Bash|apply_patch` so posttool stops firing on every Read/MCP call
(log noise + latency), matching Codex's own PreToolUse surface and the
claude-code posttool scoping. Applied in init.ts, components.ts, and the python
mirror; the one test asserting the old value is updated.

AI-assisted (Claude Opus 4.8) per repo AI Policy.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… PostToolUse matcher (#197)

The Python dry-run preview still printed 'PostToolUse: .*' after #196 narrowed the
actual Codex PostToolUse matcher to Bash|apply_patch (Node's equivalent plan string
was updated; Python's was missed). Cosmetic parity fix — the installed matcher was
already correct; only the printed plan was stale.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… stale poetry.lock (#198)

Adds an explicit cryptography constraint to pin the in-major fix for
GHSA-537c-gmf6-5ccf (held to 46.x to avoid the unrelated 49.x jump).

While regenerating, found poetry.lock was STALE and violated its own pyproject
constraints — the previously-declared Python security bumps were never locked,
so the vulnerable versions were still what actually installed:
  python-dotenv  ^1.2.2      -> lock had 1.0.1   (now 1.2.2)
  requests       ^2.33.0     -> lock had 2.32.4  (now 2.34.2)
  urllib3        >=2.7.0,<3  -> lock had 2.6.3   (now 2.7.0)
  cryptography   (new pin)   -> lock had 46.0.5  (now 46.0.7)

lock-version 2.1 preserved (regenerated with Poetry 2.4.1). `poetry check --lock`
passes. Python suite: 1495 passed against the new locked deps.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#195 added the surface-scoping section and grew the rafter router SKILL.md from
118 -> 142 lines, blowing BOTH line budgets (node <120, python <130). With no CI,
those two tests have been red on main since it merged.

- Condense SKILL.md 142 -> 128 lines: the 11-bullet engage list becomes a compact
  inline list (every surface kept) and the tier section is tightened. No content
  dropped — the scoping rule stays in the router, since it decides whether the
  skill is entered at all and can't be pushed into a sub-doc.
- Align the node budget 120 -> 130 to match python's. The limits had drifted:
  python was bumped 120 -> 130 for the scanner-scope section; node never was.

Tests: node brief.test.ts 28 passed; python test_brief.py 35 passed.
node/python SKILL.md byte-identical.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…as a command (sable-4v6e) (#200)

The pretool hook hard-denied `gh pr create --body "…git push --force…"` and
`git commit -m "don't git push --force"` because it matched risk/policy patterns
against the RAW command line — quoted DATA (a PR body, a commit message) was
treated as if it were the command. Two compounding defects:

1. Naive matching over the whole line (command-interceptor `matchesPattern` and
   risk-rules `assessCommandRisk`) saw inside quoted arguments.
2. A `policy.blockedPatterns` hit hard-coded riskLevel:"critical" — a hard deny
   that ignored the assessed risk — and the default deny-list held loose literals
   (the substring "rm -rf /" also denied `rm -rf /tmp/build`, which is only HIGH).

Fix — an argument-aware sanitizer (`sanitizeCommandForMatching`, new in both
risk-rules.ts and risk_rules.py) that all risk/policy matching now runs against:

- Tokenize respecting single/double quotes, escapes, `$(…)`/backticks, redirects;
  split on chain operators (`;` `&&` `||` `|` `&`), kept in place so pipeline
  rules (`curl … | bash`) still match.
- Redact only spans that are DATA: operands of text commands (echo/printf/grep/…),
  values of prose flags (`-m`/`--message`/`--body`/`--title`/…), and quoted
  multi-word prose arguments to ordinary commands.
- Preserve and RECURSIVELY sanitize spans that are CODE: a shell's `-c` argument
  (bash/sh/zsh/…), `$(…)`/backticks outside single quotes, eval-style flags
  (`-c`/`-e`/`--eval`/`-exec`) and execs (eval/exec/ssh/xargs); prefix wrappers
  (sudo/env/timeout/nice/…) delegate to the command they wrap. The executable
  token is unquoted so quoting the command name (`"rm" -rf /`, `watch "rm -rf /"`)
  can't break the anchors. Recursion depth-capped at 8; fails safe (stays flagged).

So `bash -c "rm -rf /"` still hard-blocks (its quoted arg IS a command), while
`echo "rm -rf /"` is DATA and allowed. Defect 2: a deny-list match now reports
the ASSESSED risk (still allowed:false), and DEFAULT_BLOCKED_PATTERNS is exactly
the unconditional CRITICAL_PATTERNS set — so `git push --force` stays HIGH /
approval-gated as intended, not silently coerced to a critical hard-deny.

Node and Python mirrored (0 divergences across a 50-case differential corpus).
Tests both directions in both suites; the old "quoting breaks rm -rf" known-gap
tests now assert it's caught.

Security: reviewed with the rafter agent (adversarial, ~20k differential-fuzz
cases). It confirmed no catastrophic command moved from denied to silently
allowed, no Node/Python divergence, and no DoS. It surfaced the exec-name
quoting bypass (`"rm" -rf /`) — fixed here with tests — and two pre-existing,
out-of-scope pattern-coverage gaps filed as sable-urvj (long-form `rm
--recursive --force`) and sable-2t0w (Node audit-logger snake_case config).

AI-assisted (Claude Opus 4.8) per the repo AI Policy.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds a SendGrid API key detection pattern (SG. prefixed, critical severity) to the built-in regex scanner in both the Node and Python implementations, with positive and negative tests in each suite and a CHANGELOG entry. Mirrors the existing DigitalOcean provider-key pattern.

Written with Claude Code.

Co-authored-by: Claude <noreply@anthropic.com>
#201: blank line before the new Python SendGrid test; blank line before the
## [0.9.0] CHANGELOG header.
#202: add @vitest/coverage-v8 so `test:coverage` works, and restore package.json's
trailing newline. Pinned coverage-v8 to 4.1.0 (not ^4.1.0) because it requires an
EXACT vitest peer, and vitest is locked at 4.1.0 — a caret pulled 4.1.10 and tripped
the peer check. Minimal: no vitest bump.

Verified: `vitest run --coverage` produces a v8 report; node secret-patterns and
python test_regex_scanner suites green.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds a config flag `scan.plus_requires_approval` (OFF by default) that
requires explicit confirmation before a paid `rafter run --mode plus`
scan spends credits. Motivated by an external user whose AI agent
auto-ran a Plus scan and consumed credits without asking.

When enabled, a Plus scan (via `rafter run`, `rafter scan`, or
`rafter scan remote`) prompts `[y/N]` on a TTY and refuses in a
non-interactive/agent context with new exit code 5 unless `--yes`/`-y`
or `RAFTER_CONFIRM=1` is present. Fast scans are never gated.

Precedence is additive (OR): a project `.rafter.yml` can turn the gate
ON but can never turn OFF a gate the machine owner set in global config,
so a hostile repo can't silently re-open the credit-burn hole. Fails
open (gate off) on a config/policy read error, matching the OFF default.

The rafter agent skill and injected instruction block now tell agents
Plus is a paid tier and to ask the user before running it.

Dual-impl Node + Python with full parity; tests in both suites; CHANGELOG
and CLI_SPEC (exit-code matrix, `--yes`, config schema) updated.
Cut v0.9.1 from main → prod. Ships the Plus-scan approval gate
(scan.plus_requires_approval, #204) and the SendGrid secret pattern
(#201), plus everything accumulated on main since the 0.9.0 tag
(OpenCode support, multi-provider remote scan, Mailchimp pattern,
hook/audit/skill fixes).

Bumps node/package.json, python/pyproject.toml, and both
rafter-security-skill.md frontmatters to 0.9.1 (validate-release
parity), and rolls CHANGELOG [Unreleased] → [0.9.1].
@Raftersecurity
Raftersecurity merged commit 1f612e6 into prod Jul 22, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants