rafter-cli 0.11.2__tar.gz → 0.11.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/PKG-INFO +2 -2
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/README.md +1 -1
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/pyproject.toml +1 -1
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/agent.py +48 -5
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/agent_components.py +3 -2
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/hook.py +24 -3
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/issues/issue_builder.py +40 -15
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/issues/issues_app.py +13 -4
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/notify.py +84 -30
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/policy.py +1 -1
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/risk_rules.py +226 -2
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/rafter-security-skill.md +1 -1
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter/SKILL.md +1 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/scanners/betterleaks.py +135 -80
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/scanners/regex_scanner.py +1 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/utils/api.py +6 -1
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/__main__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/backend.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/brief.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/ci.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/docs.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/issues/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/issues/dedup.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/issues/github_client.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/mcp_server.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/report.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/scan.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/sites.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/skill.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/commands/skill_remote.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/audit_logger.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/command_interceptor.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/config_manager.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/config_schema.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/custom_patterns.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/docs_loader.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/hook_control.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/pattern_engine.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/policy_loader.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/core/suppression_writer.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/agents/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/agents/rafter.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/continue-rules/rafter-code-review.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/continue-rules/rafter-secure-design.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/continue-rules/rafter-skill-review.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/continue-rules/rafter.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/cursor-rules/rafter-code-review.mdc +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/cursor-rules/rafter-secure-design.mdc +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/cursor-rules/rafter-skill-review.mdc +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/cursor-rules/rafter.mdc +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/pre-commit-hook.sh +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/pre-push-hook.sh +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter/docs/backend.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter/docs/cli-reference.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter/docs/finding-triage.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter/docs/guardrails.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter/docs/shift-left.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-code-review/SKILL.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-code-review/docs/api.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-code-review/docs/asvs.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-code-review/docs/cwe-top25.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-code-review/docs/investigation-playbook.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-code-review/docs/llm.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-code-review/docs/web-app.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-secure-design/SKILL.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-secure-design/docs/api-design.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-secure-design/docs/auth.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-secure-design/docs/data-storage.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-secure-design/docs/dependencies.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-secure-design/docs/deployment.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-secure-design/docs/ingestion.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-secure-design/docs/standards-pointers.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-secure-design/docs/threat-modeling.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-skill-review/SKILL.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-skill-review/docs/authorship-provenance.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-skill-review/docs/changelog-review.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-skill-review/docs/data-practices.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-skill-review/docs/malware-indicators.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-skill-review/docs/prompt-injection.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/skills/rafter-skill-review/docs/telemetry.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/windsurf-rules/rafter-code-review.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/windsurf-rules/rafter-secure-design.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/windsurf-rules/rafter-skill-review.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/resources/windsurf-rules/rafter.md +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/scanners/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/scanners/git_diff_scan.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/scanners/git_history_scan.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/scanners/secret_patterns.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/scanners/skill_scanner.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/scanners/union.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/utils/__init__.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/utils/binary_manager.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/utils/formatter.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/utils/git.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/utils/git_diff.py +0 -0
- {rafter_cli-0.11.2 → rafter_cli-0.11.4}/rafter_cli/utils/skill_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rafter-cli
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.4
|
|
4
4
|
Summary: Rafter CLI — the default security agent for AI workflows. Free for individuals and open source.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Rafter Team
|
|
@@ -166,7 +166,7 @@ Export Rafter policy for agent platforms.
|
|
|
166
166
|
|
|
167
167
|
```bash
|
|
168
168
|
# Filter high-severity vulnerabilities (SARIF levels: error, warning, note)
|
|
169
|
-
rafter get SCAN_ID --format json | jq '.vulnerabilities[] | select(.
|
|
169
|
+
rafter get SCAN_ID --format json | jq '.vulnerabilities[] | select(.severity=="error")'
|
|
170
170
|
|
|
171
171
|
# CI gate
|
|
172
172
|
if rafter get SCAN_ID --format json | jq -e '.vulnerabilities | length > 0'; then
|
|
@@ -139,7 +139,7 @@ Export Rafter policy for agent platforms.
|
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
141
|
# Filter high-severity vulnerabilities (SARIF levels: error, warning, note)
|
|
142
|
-
rafter get SCAN_ID --format json | jq '.vulnerabilities[] | select(.
|
|
142
|
+
rafter get SCAN_ID --format json | jq '.vulnerabilities[] | select(.severity=="error")'
|
|
143
143
|
|
|
144
144
|
# CI gate
|
|
145
145
|
if rafter get SCAN_ID --format json | jq -e '.vulnerabilities | length > 0'; then
|
|
@@ -223,7 +223,7 @@ def _install_claude_code_hooks(root: Path, quiet: bool = False) -> None:
|
|
|
223
223
|
post_hook = {"type": "command", "command": post_command}
|
|
224
224
|
settings["hooks"]["PreToolUse"].extend([
|
|
225
225
|
{"matcher": "Bash", "hooks": [pre_hook]},
|
|
226
|
-
{"matcher": "Write|Edit", "hooks": [pre_hook]},
|
|
226
|
+
{"matcher": "Write|Edit|MultiEdit", "hooks": [pre_hook]},
|
|
227
227
|
])
|
|
228
228
|
# Narrow to tools that produce scannable output (shell output + file
|
|
229
229
|
# writes). Firing posttool on every tool — including Read and MCP calls —
|
|
@@ -733,9 +733,9 @@ def _install_gemini_mcp(root: Path, quiet: bool = False) -> bool:
|
|
|
733
733
|
|
|
734
734
|
# Cursor hook events covered by rafter (rf-svn3).
|
|
735
735
|
_CURSOR_HOOK_EVENTS: tuple[tuple[str, str], ...] = (
|
|
736
|
-
("preToolUse", "
|
|
737
|
-
("postToolUse", "
|
|
738
|
-
("beforeShellExecution", "
|
|
736
|
+
("preToolUse", "hook pretool --format cursor"),
|
|
737
|
+
("postToolUse", "hook posttool --format cursor"),
|
|
738
|
+
("beforeShellExecution", "hook pretool --format cursor"),
|
|
739
739
|
)
|
|
740
740
|
|
|
741
741
|
# Skills shipped as both Cursor rules and Claude Code / Codex / Gemini skills.
|
|
@@ -783,6 +783,44 @@ def _install_codex_hooks(root: Path, quiet: bool = False) -> None:
|
|
|
783
783
|
_init_print(quiet, fmt.success(f"Installed PostToolUse hooks to {hooks_path}"))
|
|
784
784
|
|
|
785
785
|
|
|
786
|
+
def _install_gemini_hooks(root: Path, quiet: bool = False) -> None:
|
|
787
|
+
"""Install Rafter BeforeTool/AfterTool hooks into <root>/.gemini/settings.json.
|
|
788
|
+
|
|
789
|
+
Mirrors Node's installGeminiHooks. Gemini matchers are regexes against its
|
|
790
|
+
built-in tool names; the mutating ones are matched explicitly.
|
|
791
|
+
"""
|
|
792
|
+
gemini_dir = root / ".gemini"
|
|
793
|
+
gemini_dir.mkdir(parents=True, exist_ok=True)
|
|
794
|
+
settings_path = gemini_dir / "settings.json"
|
|
795
|
+
|
|
796
|
+
settings: dict[str, Any] = {}
|
|
797
|
+
if settings_path.exists():
|
|
798
|
+
try:
|
|
799
|
+
settings = json.loads(settings_path.read_text())
|
|
800
|
+
except (json.JSONDecodeError, ValueError):
|
|
801
|
+
_init_alert(quiet, fmt.warning("Existing Gemini settings.json was unreadable, creating new one"))
|
|
802
|
+
|
|
803
|
+
hooks = settings.setdefault("hooks", {})
|
|
804
|
+
rafter_bin = _resolve_rafter_path()
|
|
805
|
+
for event, sub, matcher in (
|
|
806
|
+
("BeforeTool", "pretool", "run_shell_command|write_file|replace"),
|
|
807
|
+
("AfterTool", "posttool", ".*"),
|
|
808
|
+
):
|
|
809
|
+
entries = hooks.get(event) or []
|
|
810
|
+
entries = [
|
|
811
|
+
e for e in entries
|
|
812
|
+
if not any(f"hook {sub}" in str(h.get("command", "")) for h in (e.get("hooks") or []))
|
|
813
|
+
]
|
|
814
|
+
entries.append({
|
|
815
|
+
"matcher": matcher,
|
|
816
|
+
"hooks": [{"type": "command", "command": f"{rafter_bin} hook {sub} --format gemini", "timeout": 5000}],
|
|
817
|
+
})
|
|
818
|
+
hooks[event] = entries
|
|
819
|
+
|
|
820
|
+
settings_path.write_text(json.dumps(settings, indent=2))
|
|
821
|
+
_init_print(quiet, fmt.success(f"Installed hooks to {settings_path}"))
|
|
822
|
+
|
|
823
|
+
|
|
786
824
|
def _install_cursor_hooks(root: Path, quiet: bool = False) -> None:
|
|
787
825
|
"""Install Cursor hooks at <root>/.cursor/hooks.json.
|
|
788
826
|
|
|
@@ -804,7 +842,11 @@ def _install_cursor_hooks(root: Path, quiet: bool = False) -> None:
|
|
|
804
842
|
config.setdefault("version", 1)
|
|
805
843
|
config.setdefault("hooks", {})
|
|
806
844
|
|
|
807
|
-
|
|
845
|
+
# The resolved path, as for the Claude Code and Codex hooks: Cursor runs
|
|
846
|
+
# hooks without the user's shell PATH, where a bare `rafter` may not resolve.
|
|
847
|
+
rafter_bin = _resolve_rafter_path()
|
|
848
|
+
for event, args in _CURSOR_HOOK_EVENTS:
|
|
849
|
+
command = f"{rafter_bin} {args}"
|
|
808
850
|
entries = config["hooks"].get(event)
|
|
809
851
|
if not isinstance(entries, list):
|
|
810
852
|
entries = []
|
|
@@ -1505,6 +1547,7 @@ def init(
|
|
|
1505
1547
|
_init_alert(quiet, fmt.error(f"Failed to install Gemini CLI skills: {skills_error}"))
|
|
1506
1548
|
else:
|
|
1507
1549
|
_register_gemini_skills(root / ".agents" / "skills", quiet=quiet)
|
|
1550
|
+
_install_gemini_hooks(root, quiet=quiet)
|
|
1508
1551
|
if gemini_ok and scope == "user":
|
|
1509
1552
|
manager.set("agent.environments.gemini.enabled", True)
|
|
1510
1553
|
except Exception as e:
|
|
@@ -205,7 +205,7 @@ def _claude_code_hooks() -> ComponentSpec:
|
|
|
205
205
|
del hooks["SessionStart"]
|
|
206
206
|
hooks["PreToolUse"].extend([
|
|
207
207
|
{"matcher": "Bash", "hooks": [pre]},
|
|
208
|
-
{"matcher": "Write|Edit", "hooks": [pre]},
|
|
208
|
+
{"matcher": "Write|Edit|MultiEdit", "hooks": [pre]},
|
|
209
209
|
])
|
|
210
210
|
# Narrow to tools that produce scannable output (shell output + file
|
|
211
211
|
# writes); avoids firing posttool on every Read/MCP call (latency).
|
|
@@ -594,8 +594,9 @@ def _gemini_hooks() -> ComponentSpec:
|
|
|
594
594
|
h["AfterTool"] = _filter_hooks(h["AfterTool"], lambda e: _hook_entry_has_rafter(e, "rafter hook posttool"))
|
|
595
595
|
# Explicit Gemini built-in tool names per geminicli.com/docs/hooks/reference
|
|
596
596
|
# (rf-044o verification).
|
|
597
|
+
# No tool is named `edit`: Gemini's edit tool is `replace`.
|
|
597
598
|
h["BeforeTool"].append({
|
|
598
|
-
"matcher": "run_shell_command|write_file|replace
|
|
599
|
+
"matcher": "run_shell_command|write_file|replace",
|
|
599
600
|
"hooks": [{"type": "command", "command": "rafter hook pretool --format gemini", "timeout": 5000}],
|
|
600
601
|
})
|
|
601
602
|
h["AfterTool"].append({
|
|
@@ -455,8 +455,9 @@ def _writes_rafter_security_config(file_path: str) -> bool:
|
|
|
455
455
|
def _evaluate_write(tool_input: dict) -> dict:
|
|
456
456
|
# rf-vnxs: checked FIRST, and before the no-content early return, because a
|
|
457
457
|
# truncating write with empty content disables the hook just as well as one
|
|
458
|
-
# carrying JSON.
|
|
459
|
-
|
|
458
|
+
# carrying JSON. apply_patch names its targets only in the patch headers.
|
|
459
|
+
targets = [tool_input.get("file_path", ""), *tool_input.get("patch_paths", [])]
|
|
460
|
+
if any(_writes_rafter_security_config(t) for t in targets):
|
|
460
461
|
return {
|
|
461
462
|
"decision": "deny",
|
|
462
463
|
"reason": (
|
|
@@ -572,7 +573,8 @@ def _normalize_write_input(tool_name: str, tool_input: dict) -> dict:
|
|
|
572
573
|
|
|
573
574
|
Continue's Write names its path ``filepath``; MultiEdit and Windsurf's
|
|
574
575
|
pre_write_code carry ``edits[].new_string``; Codex's apply_patch carries
|
|
575
|
-
the whole patch in ``command
|
|
576
|
+
the whole patch in ``command``, with its target paths in the patch
|
|
577
|
+
headers (``patch_paths``).
|
|
576
578
|
"""
|
|
577
579
|
out = dict(tool_input)
|
|
578
580
|
if "file_path" not in out and isinstance(tool_input.get("filepath"), str):
|
|
@@ -585,9 +587,28 @@ def _normalize_write_input(tool_name: str, tool_input: dict) -> dict:
|
|
|
585
587
|
)
|
|
586
588
|
elif tool_name == "apply_patch" and isinstance(tool_input.get("command"), str):
|
|
587
589
|
out["content"] = tool_input["command"]
|
|
590
|
+
if tool_name == "apply_patch" and isinstance(tool_input.get("command"), str):
|
|
591
|
+
out["patch_paths"] = _apply_patch_paths(tool_input["command"])
|
|
588
592
|
return out
|
|
589
593
|
|
|
590
594
|
|
|
595
|
+
# The apply_patch format carries its targets only in hunk headers, which Codex
|
|
596
|
+
# matches on the trimmed line (codex-rs/apply-patch, apply_patch.lark).
|
|
597
|
+
_APPLY_PATCH_HEADERS = ("*** Add File: ", "*** Update File: ", "*** Delete File: ", "*** Move to: ")
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _apply_patch_paths(patch: str) -> list[str]:
|
|
601
|
+
"""Every path an apply_patch names."""
|
|
602
|
+
paths = []
|
|
603
|
+
for raw in patch.split("\n"):
|
|
604
|
+
line = raw.strip()
|
|
605
|
+
for header in _APPLY_PATCH_HEADERS:
|
|
606
|
+
if line.startswith(header):
|
|
607
|
+
paths.append(line[len(header):].strip())
|
|
608
|
+
break
|
|
609
|
+
return paths
|
|
610
|
+
|
|
611
|
+
|
|
591
612
|
def _deny_on_error(exc: Exception) -> dict:
|
|
592
613
|
reason = f"rafter hook error: {type(exc).__name__}: {exc}; denying to be safe"
|
|
593
614
|
print(reason, file=sys.stderr)
|
|
@@ -17,10 +17,28 @@ class IssueDraft:
|
|
|
17
17
|
|
|
18
18
|
@dataclass
|
|
19
19
|
class BackendVulnerability:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
"""Backend vulnerability from `GET /api/static/scan` (`rafter get <id> --format json`).
|
|
21
|
+
|
|
22
|
+
The real API shape (`toRafterFindings()` on the backend) is
|
|
23
|
+
`rule_id`/`severity`/`title`/`description`/`file_path`/`line_start`/
|
|
24
|
+
`line_end` plus optional `cwe`/`confidence`. `level`/`message`/`file`/
|
|
25
|
+
`line` are the pre-rename names, kept as a fallback for old cached
|
|
26
|
+
payloads and existing tests, never the primary source.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
rule_id: str = ""
|
|
30
|
+
severity: str = ""
|
|
31
|
+
title: str = ""
|
|
32
|
+
description: str = ""
|
|
33
|
+
file_path: str | None = None
|
|
34
|
+
line_start: int | None = None
|
|
35
|
+
line_end: int | None = None
|
|
36
|
+
cwe: str | None = None
|
|
37
|
+
confidence: str | None = None
|
|
38
|
+
# Pre-rename names.
|
|
39
|
+
level: str = ""
|
|
40
|
+
message: str = ""
|
|
41
|
+
file: str = ""
|
|
24
42
|
line: int | None = None
|
|
25
43
|
|
|
26
44
|
|
|
@@ -67,24 +85,31 @@ def _truncate(s: str, max_len: int) -> str:
|
|
|
67
85
|
|
|
68
86
|
|
|
69
87
|
def build_from_backend_vulnerability(vuln: BackendVulnerability) -> IssueDraft:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
88
|
+
rule_id = vuln.rule_id or "unknown-rule"
|
|
89
|
+
level = vuln.severity or vuln.level or "note"
|
|
90
|
+
summary = vuln.title or vuln.message or "Security finding"
|
|
91
|
+
details = vuln.description or vuln.message or summary
|
|
92
|
+
file = vuln.file_path or vuln.file or "unknown file"
|
|
93
|
+
line = vuln.line_start if vuln.line_start is not None else vuln.line
|
|
94
|
+
|
|
95
|
+
sev = _severity_label(level)
|
|
96
|
+
emoji = _severity_emoji(level)
|
|
97
|
+
fp = fingerprint(file, rule_id)
|
|
73
98
|
|
|
74
|
-
title = f"{emoji} [{sev.upper()}] {
|
|
99
|
+
title = f"{emoji} [{sev.upper()}] {rule_id}: {_truncate(summary, 80)}"
|
|
75
100
|
|
|
76
101
|
body = f"## Security Finding\n\n"
|
|
77
|
-
body += f"**Rule:** `{
|
|
102
|
+
body += f"**Rule:** `{rule_id}`\n"
|
|
78
103
|
body += f"**Severity:** {sev}\n"
|
|
79
|
-
body += f"**File:** `{
|
|
80
|
-
if
|
|
81
|
-
body += f" (line {
|
|
104
|
+
body += f"**File:** `{file}`"
|
|
105
|
+
if line:
|
|
106
|
+
body += f" (line {line})"
|
|
82
107
|
body += "\n\n"
|
|
83
|
-
body += f"### Description\n\n{
|
|
84
|
-
body += f"### Remediation\n\nReview and fix the finding in `{
|
|
108
|
+
body += f"### Description\n\n{details}\n\n"
|
|
109
|
+
body += f"### Remediation\n\nReview and fix the finding in `{file}`.\n"
|
|
85
110
|
body += "\n---\n*Created by [Rafter CLI](https://rafter.so) — security for AI builders*\n"
|
|
86
111
|
|
|
87
|
-
labels = ["security", f"severity:{sev}", f"rule:{
|
|
112
|
+
labels = ["security", f"severity:{sev}", f"rule:{rule_id}"]
|
|
88
113
|
|
|
89
114
|
return IssueDraft(
|
|
90
115
|
title=title,
|
|
@@ -268,10 +268,19 @@ def _drafts_from_backend(scan_id: str, api_key: str | None) -> list[IssueDraft]:
|
|
|
268
268
|
return [
|
|
269
269
|
build_from_backend_vulnerability(
|
|
270
270
|
BackendVulnerability(
|
|
271
|
-
rule_id=v
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
rule_id=v.get("rule_id") or v.get("ruleId") or "",
|
|
272
|
+
severity=v.get("severity") or "",
|
|
273
|
+
title=v.get("title") or "",
|
|
274
|
+
description=v.get("description") or "",
|
|
275
|
+
file_path=v.get("file_path"),
|
|
276
|
+
line_start=v.get("line_start"),
|
|
277
|
+
line_end=v.get("line_end"),
|
|
278
|
+
cwe=v.get("cwe"),
|
|
279
|
+
confidence=v.get("confidence"),
|
|
280
|
+
# Pre-rename names — fallback for old cached payloads.
|
|
281
|
+
level=v.get("level") or "",
|
|
282
|
+
message=v.get("message") or "",
|
|
283
|
+
file=v.get("file") or "",
|
|
275
284
|
line=v.get("line"),
|
|
276
285
|
)
|
|
277
286
|
)
|
|
@@ -57,19 +57,81 @@ def _detect_platform(url: str) -> str:
|
|
|
57
57
|
return "generic"
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def _severity_tier(raw: str | None) -> str:
|
|
61
|
+
"""Map a raw severity/level to a display tier.
|
|
62
|
+
|
|
63
|
+
Uses the same rules as the GitHub Action (github-action/action.yml) and
|
|
64
|
+
``rafter get``, so notify agrees with both. A value outside the known set is
|
|
65
|
+
kept verbatim so a finding is never silently dropped.
|
|
66
|
+
"""
|
|
67
|
+
s = (raw or "").lower()
|
|
68
|
+
if s == "critical":
|
|
69
|
+
return "critical"
|
|
70
|
+
if s in ("error", "high"):
|
|
71
|
+
return "high"
|
|
72
|
+
if s in ("warning", "medium"):
|
|
73
|
+
return "medium"
|
|
74
|
+
if s in ("note", "low"):
|
|
75
|
+
return "low"
|
|
76
|
+
return s or "unknown"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _normalize_scan(scan_data: dict) -> dict:
|
|
80
|
+
"""Read findings from the real scan shape (``vulnerabilities[]``).
|
|
81
|
+
|
|
82
|
+
Falls back to the legacy ``findings[]`` for old cached payloads. Counts are
|
|
83
|
+
derived from each finding's own severity — never from ``summary`` — so a
|
|
84
|
+
finding cannot be lost to a summary field this code does not read. "Clean"
|
|
85
|
+
is therefore reported only when there are genuinely zero findings.
|
|
86
|
+
"""
|
|
87
|
+
raw = scan_data.get("vulnerabilities")
|
|
88
|
+
if not isinstance(raw, list):
|
|
89
|
+
raw = scan_data.get("findings")
|
|
90
|
+
if not isinstance(raw, list):
|
|
91
|
+
raw = []
|
|
92
|
+
|
|
93
|
+
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0}
|
|
94
|
+
findings = []
|
|
95
|
+
for v in raw:
|
|
96
|
+
tier = _severity_tier(v.get("severity") or v.get("level"))
|
|
97
|
+
if tier in counts:
|
|
98
|
+
counts[tier] += 1
|
|
99
|
+
title = (
|
|
100
|
+
v.get("title")
|
|
101
|
+
or v.get("rule_id")
|
|
102
|
+
or v.get("ruleId")
|
|
103
|
+
or v.get("message")
|
|
104
|
+
or "Unknown"
|
|
105
|
+
)
|
|
106
|
+
file = v.get("file_path") or v.get("file")
|
|
107
|
+
line = v.get("line_start") if v.get("line_start") is not None else v.get("line")
|
|
108
|
+
location = v.get("location") or file or ""
|
|
109
|
+
if not v.get("location") and file and line is not None:
|
|
110
|
+
location = f"{file}:{line}"
|
|
111
|
+
findings.append({"severity": tier, "title": title, "location": location})
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
"findings": findings,
|
|
115
|
+
"critical": counts["critical"],
|
|
116
|
+
"high": counts["high"],
|
|
117
|
+
"medium": counts["medium"],
|
|
118
|
+
"low": counts["low"],
|
|
119
|
+
"total": len(findings),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
60
123
|
def _format_slack_payload(scan_data: dict) -> dict:
|
|
61
124
|
"""Format scan results as a Slack message with Block Kit."""
|
|
62
125
|
status = scan_data.get("status", "unknown")
|
|
63
126
|
repo = scan_data.get("repository_name", "unknown")
|
|
64
127
|
scan_id = scan_data.get("scan_id", "")
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
total = critical + high + medium + low
|
|
128
|
+
norm = _normalize_scan(scan_data)
|
|
129
|
+
findings = norm["findings"]
|
|
130
|
+
critical = norm["critical"]
|
|
131
|
+
high = norm["high"]
|
|
132
|
+
medium = norm["medium"]
|
|
133
|
+
low = norm["low"]
|
|
134
|
+
total = norm["total"]
|
|
73
135
|
|
|
74
136
|
# Status emoji
|
|
75
137
|
if status == "completed" and total == 0:
|
|
@@ -140,12 +202,9 @@ def _format_slack_payload(scan_data: dict) -> dict:
|
|
|
140
202
|
if findings:
|
|
141
203
|
finding_lines = []
|
|
142
204
|
for f in findings[:5]:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
line = f"• `[{sev}]` {title}"
|
|
147
|
-
if location:
|
|
148
|
-
line += f" — {location}"
|
|
205
|
+
line = f"• `[{f['severity'].upper()}]` {f['title']}"
|
|
206
|
+
if f["location"]:
|
|
207
|
+
line += f" — {f['location']}"
|
|
149
208
|
finding_lines.append(line)
|
|
150
209
|
|
|
151
210
|
if len(findings) > 5:
|
|
@@ -179,14 +238,13 @@ def _format_discord_payload(scan_data: dict) -> dict:
|
|
|
179
238
|
status = scan_data.get("status", "unknown")
|
|
180
239
|
repo = scan_data.get("repository_name", "unknown")
|
|
181
240
|
scan_id = scan_data.get("scan_id", "")
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
total = critical + high + medium + low
|
|
241
|
+
norm = _normalize_scan(scan_data)
|
|
242
|
+
findings = norm["findings"]
|
|
243
|
+
critical = norm["critical"]
|
|
244
|
+
high = norm["high"]
|
|
245
|
+
medium = norm["medium"]
|
|
246
|
+
low = norm["low"]
|
|
247
|
+
total = norm["total"]
|
|
190
248
|
|
|
191
249
|
# Embed color
|
|
192
250
|
if status == "completed" and total == 0:
|
|
@@ -237,12 +295,9 @@ def _format_discord_payload(scan_data: dict) -> dict:
|
|
|
237
295
|
if findings:
|
|
238
296
|
finding_lines = []
|
|
239
297
|
for f in findings[:5]:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
line = f"• `[{sev}]` {title}"
|
|
244
|
-
if location:
|
|
245
|
-
line += f" — {location}"
|
|
298
|
+
line = f"• `[{f['severity'].upper()}]` {f['title']}"
|
|
299
|
+
if f["location"]:
|
|
300
|
+
line += f" — {f['location']}"
|
|
246
301
|
finding_lines.append(line)
|
|
247
302
|
|
|
248
303
|
if len(findings) > 5:
|
|
@@ -271,8 +326,7 @@ def _format_generic_payload(scan_data: dict) -> dict:
|
|
|
271
326
|
"""Format scan results as a plain JSON payload with text field."""
|
|
272
327
|
status = scan_data.get("status", "unknown")
|
|
273
328
|
repo = scan_data.get("repository_name", "unknown")
|
|
274
|
-
|
|
275
|
-
total = sum(summary.get(k, 0) for k in ("critical", "high", "medium", "low"))
|
|
329
|
+
total = _normalize_scan(scan_data)["total"]
|
|
276
330
|
|
|
277
331
|
if status == "completed" and total == 0:
|
|
278
332
|
status_text = "Clean — no issues found"
|
|
@@ -23,7 +23,7 @@ def _generate_claude_config() -> str:
|
|
|
23
23
|
"hooks": [{"type": "command", "command": "rafter hook pretool"}],
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
|
-
"matcher": "Write|Edit",
|
|
26
|
+
"matcher": "Write|Edit|MultiEdit",
|
|
27
27
|
"hooks": [{"type": "command", "command": "rafter hook pretool"}],
|
|
28
28
|
},
|
|
29
29
|
],
|