rafter-cli 0.8.7__tar.gz → 0.8.9__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.8.7 → rafter_cli-0.8.9}/PKG-INFO +1 -1
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/pyproject.toml +1 -1
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/agent.py +333 -71
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/hook.py +31 -15
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/mcp_server.py +20 -8
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/scan.py +28 -72
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/skill.py +154 -7
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/config_manager.py +41 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/rafter-security-skill.md +1 -1
- rafter_cli-0.8.9/rafter_cli/scanners/git_diff_scan.py +25 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/scanners/regex_scanner.py +14 -0
- rafter_cli-0.8.9/rafter_cli/scanners/skill_scanner.py +429 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/utils/api.py +20 -2
- rafter_cli-0.8.9/rafter_cli/utils/git_diff.py +95 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/README.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/__main__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/agent_components.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/backend.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/brief.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/ci.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/docs.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/issues/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/issues/dedup.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/issues/github_client.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/issues/issue_builder.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/issues/issues_app.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/notify.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/policy.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/report.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/commands/skill_remote.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/audit_logger.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/command_interceptor.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/config_schema.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/custom_patterns.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/docs_loader.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/hook_control.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/pattern_engine.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/policy_loader.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/risk_rules.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/core/suppression_writer.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/agents/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/agents/rafter.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/continue-rules/rafter-code-review.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/continue-rules/rafter-secure-design.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/continue-rules/rafter-skill-review.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/continue-rules/rafter.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/cursor-rules/rafter-code-review.mdc +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/cursor-rules/rafter-secure-design.mdc +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/cursor-rules/rafter-skill-review.mdc +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/cursor-rules/rafter.mdc +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/pre-commit-hook.sh +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/pre-push-hook.sh +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter/SKILL.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter/docs/backend.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter/docs/cli-reference.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter/docs/finding-triage.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter/docs/guardrails.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter/docs/shift-left.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-code-review/SKILL.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-code-review/docs/api.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-code-review/docs/asvs.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-code-review/docs/cwe-top25.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-code-review/docs/investigation-playbook.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-code-review/docs/llm.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-code-review/docs/web-app.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-secure-design/SKILL.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-secure-design/docs/api-design.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-secure-design/docs/auth.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-secure-design/docs/data-storage.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-secure-design/docs/dependencies.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-secure-design/docs/deployment.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-secure-design/docs/ingestion.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-secure-design/docs/standards-pointers.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-secure-design/docs/threat-modeling.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-skill-review/SKILL.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-skill-review/docs/authorship-provenance.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-skill-review/docs/changelog-review.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-skill-review/docs/data-practices.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-skill-review/docs/malware-indicators.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-skill-review/docs/prompt-injection.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/skills/rafter-skill-review/docs/telemetry.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/windsurf-rules/rafter-code-review.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/windsurf-rules/rafter-secure-design.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/windsurf-rules/rafter-skill-review.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/resources/windsurf-rules/rafter.md +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/scanners/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/scanners/betterleaks.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/scanners/secret_patterns.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/scanners/union.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/utils/__init__.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/utils/binary_manager.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/utils/formatter.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/utils/git.py +0 -0
- {rafter_cli-0.8.7 → rafter_cli-0.8.9}/rafter_cli/utils/skill_manager.py +0 -0
|
@@ -25,7 +25,12 @@ from rich import print as rprint
|
|
|
25
25
|
from .. import __version__
|
|
26
26
|
from ..core.audit_logger import AuditLogger
|
|
27
27
|
from ..core.command_interceptor import CommandInterceptor
|
|
28
|
-
from ..core.config_manager import
|
|
28
|
+
from ..core.config_manager import (
|
|
29
|
+
ConfigManager,
|
|
30
|
+
is_secret_config_key,
|
|
31
|
+
mask_secret_value,
|
|
32
|
+
redact_config_secrets,
|
|
33
|
+
)
|
|
29
34
|
from ..core.pattern_engine import PatternEngine
|
|
30
35
|
from ..scanners.betterleaks import BetterleaksScanner
|
|
31
36
|
from ..scanners.regex_scanner import RegexScanner, ScanResult
|
|
@@ -47,7 +52,7 @@ def config_show():
|
|
|
47
52
|
from dataclasses import asdict
|
|
48
53
|
|
|
49
54
|
manager = ConfigManager()
|
|
50
|
-
print(json.dumps(asdict(manager.load()), indent=2))
|
|
55
|
+
print(json.dumps(redact_config_secrets(asdict(manager.load())), indent=2))
|
|
51
56
|
|
|
52
57
|
|
|
53
58
|
@config_app.command("get")
|
|
@@ -58,8 +63,11 @@ def config_get(key: str = typer.Argument(..., help="Config key (e.g. agent.risk_
|
|
|
58
63
|
if value is None:
|
|
59
64
|
print(f"Key not found: {key}", file=sys.stderr)
|
|
60
65
|
raise typer.Exit(code=1)
|
|
66
|
+
leaf = key.split(".")[-1]
|
|
61
67
|
if isinstance(value, dict):
|
|
62
|
-
print(json.dumps(value, indent=2))
|
|
68
|
+
print(json.dumps(redact_config_secrets(value), indent=2))
|
|
69
|
+
elif is_secret_config_key(leaf) and isinstance(value, str):
|
|
70
|
+
print(mask_secret_value(value))
|
|
63
71
|
else:
|
|
64
72
|
print(value)
|
|
65
73
|
|
|
@@ -76,7 +84,13 @@ def config_set(
|
|
|
76
84
|
except (json.JSONDecodeError, ValueError):
|
|
77
85
|
parsed = value
|
|
78
86
|
manager.set(key, parsed)
|
|
79
|
-
|
|
87
|
+
leaf = key.split(".")[-1]
|
|
88
|
+
echo = (
|
|
89
|
+
json.dumps(mask_secret_value(parsed))
|
|
90
|
+
if is_secret_config_key(leaf) and isinstance(parsed, str)
|
|
91
|
+
else json.dumps(parsed)
|
|
92
|
+
)
|
|
93
|
+
rprint(fmt.success(f"Set {key} = {echo}"))
|
|
80
94
|
|
|
81
95
|
|
|
82
96
|
agent_app.add_typer(config_app)
|
|
@@ -251,6 +265,7 @@ def _print_dry_run_plan(
|
|
|
251
265
|
want_aider: bool,
|
|
252
266
|
want_hermes: bool,
|
|
253
267
|
want_betterleaks: bool,
|
|
268
|
+
want_skill_scanner: bool,
|
|
254
269
|
risk_level: str,
|
|
255
270
|
) -> None:
|
|
256
271
|
"""Print every file path the install would touch — without writing anything (rf-hrtd).
|
|
@@ -295,6 +310,11 @@ def _print_dry_run_plan(
|
|
|
295
310
|
print("Betterleaks (--with-betterleaks / --all):")
|
|
296
311
|
D(home / ".rafter" / "bin" / "betterleaks", "binary, ~12MB from GitHub releases")
|
|
297
312
|
|
|
313
|
+
if want_skill_scanner:
|
|
314
|
+
print()
|
|
315
|
+
print("skill-scanner deep engine (--with-skill-scanner):")
|
|
316
|
+
D(Path("skill-scanner"), "heavy PyPI package, isolated install via uv tool / pip --user")
|
|
317
|
+
|
|
298
318
|
if want_claude_code:
|
|
299
319
|
print()
|
|
300
320
|
print("Claude Code (--with-claude-code):")
|
|
@@ -1036,6 +1056,7 @@ def _install_hermes_mcp(root: Path) -> bool:
|
|
|
1036
1056
|
def init(
|
|
1037
1057
|
risk_level: str = typer.Option("moderate", "--risk-level", help="minimal, moderate, or aggressive"),
|
|
1038
1058
|
with_betterleaks: bool = typer.Option(False, "--with-betterleaks", help="Download and install Betterleaks binary"),
|
|
1059
|
+
with_skill_scanner: bool = typer.Option(False, "--with-skill-scanner", help="Install the optional skill-scanner deep engine (heavy; audit-skill --deep)"),
|
|
1039
1060
|
with_openclaw: bool = typer.Option(False, "--with-openclaw", help="Install OpenClaw integration"),
|
|
1040
1061
|
with_claude_code: bool = typer.Option(False, "--with-claude-code", help="Install Claude Code integration"),
|
|
1041
1062
|
with_codex: bool = typer.Option(False, "--with-codex", help="Install Codex CLI integration"),
|
|
@@ -1109,6 +1130,8 @@ def init(
|
|
|
1109
1130
|
# established. Excluded from --all in --local for the same reason (sable-gyw).
|
|
1110
1131
|
want_hermes = with_hermes or (all_integrations and not local)
|
|
1111
1132
|
want_betterleaks = with_betterleaks or (all_integrations and not local)
|
|
1133
|
+
# skill-scanner is heavy and opt-in only — deliberately NOT folded into --all.
|
|
1134
|
+
want_skill_scanner = with_skill_scanner
|
|
1112
1135
|
|
|
1113
1136
|
# Show detected environments
|
|
1114
1137
|
detected = []
|
|
@@ -1175,6 +1198,7 @@ def init(
|
|
|
1175
1198
|
want_aider=want_aider and (has_aider or local),
|
|
1176
1199
|
want_hermes=want_hermes and has_hermes,
|
|
1177
1200
|
want_betterleaks=want_betterleaks,
|
|
1201
|
+
want_skill_scanner=want_skill_scanner,
|
|
1178
1202
|
risk_level=risk_level,
|
|
1179
1203
|
)
|
|
1180
1204
|
return
|
|
@@ -1229,6 +1253,29 @@ def init(
|
|
|
1229
1253
|
"and ensure it is on PATH, then re-run 'rafter agent init'."
|
|
1230
1254
|
))
|
|
1231
1255
|
|
|
1256
|
+
if want_skill_scanner:
|
|
1257
|
+
_ss_on_path = None if update else shutil.which("skill-scanner")
|
|
1258
|
+
if _ss_on_path:
|
|
1259
|
+
rprint(fmt.success(f"skill-scanner available on PATH ({_ss_on_path})"))
|
|
1260
|
+
else:
|
|
1261
|
+
from ..scanners.skill_scanner import SkillScannerInstaller
|
|
1262
|
+
|
|
1263
|
+
rprint(fmt.info(
|
|
1264
|
+
"Installing optional skill-scanner deep engine (heavy "
|
|
1265
|
+
"third-party package; isolated install)..."
|
|
1266
|
+
))
|
|
1267
|
+
_result = SkillScannerInstaller().install(on_progress=typer.echo)
|
|
1268
|
+
if _result.ok:
|
|
1269
|
+
rprint(fmt.success(
|
|
1270
|
+
f"skill-scanner installed (via {_result.via}): {_result.message}"
|
|
1271
|
+
))
|
|
1272
|
+
else:
|
|
1273
|
+
rprint(fmt.warning(f"skill-scanner install failed: {_result.message}"))
|
|
1274
|
+
rprint(fmt.info(
|
|
1275
|
+
"To fix: run 'rafter agent update-skill-scanner' or install "
|
|
1276
|
+
"manually with 'uv tool install cisco-ai-skill-scanner'."
|
|
1277
|
+
))
|
|
1278
|
+
|
|
1232
1279
|
# Install OpenClaw skill if opted in
|
|
1233
1280
|
openclaw_ok = False
|
|
1234
1281
|
if has_openclaw and want_openclaw:
|
|
@@ -1575,6 +1622,76 @@ def _scan_file(file_path: str, engine: str, custom_patterns=None) -> list[ScanRe
|
|
|
1575
1622
|
return run_patterns()
|
|
1576
1623
|
|
|
1577
1624
|
|
|
1625
|
+
def _run_git_added_line_scan(
|
|
1626
|
+
git_args: list[str],
|
|
1627
|
+
git_cwd: str | None,
|
|
1628
|
+
custom_patterns,
|
|
1629
|
+
scan_cfg,
|
|
1630
|
+
baseline_entries: list,
|
|
1631
|
+
suppressions,
|
|
1632
|
+
context_label: str,
|
|
1633
|
+
empty_message: str,
|
|
1634
|
+
*,
|
|
1635
|
+
json_output: bool,
|
|
1636
|
+
quiet: bool,
|
|
1637
|
+
format: str,
|
|
1638
|
+
not_repo_message: str = "Error: Not in a git repository or invalid ref",
|
|
1639
|
+
) -> None:
|
|
1640
|
+
"""Parse a unified diff for + lines and scan with the patterns engine."""
|
|
1641
|
+
from ..utils.git_diff import parse_unified_diff_added_lines
|
|
1642
|
+
from ..scanners.git_diff_scan import scan_added_diff_lines
|
|
1643
|
+
|
|
1644
|
+
try:
|
|
1645
|
+
patch = subprocess.run(
|
|
1646
|
+
["git", *git_args],
|
|
1647
|
+
capture_output=True,
|
|
1648
|
+
text=True,
|
|
1649
|
+
check=True,
|
|
1650
|
+
cwd=git_cwd,
|
|
1651
|
+
).stdout
|
|
1652
|
+
except subprocess.CalledProcessError:
|
|
1653
|
+
print(not_repo_message, file=sys.stderr)
|
|
1654
|
+
raise typer.Exit(code=2)
|
|
1655
|
+
|
|
1656
|
+
if not patch.strip():
|
|
1657
|
+
if not quiet:
|
|
1658
|
+
rprint(fmt.success(empty_message))
|
|
1659
|
+
raise typer.Exit(code=0)
|
|
1660
|
+
|
|
1661
|
+
added = parse_unified_diff_added_lines(patch)
|
|
1662
|
+
if not added:
|
|
1663
|
+
if not quiet:
|
|
1664
|
+
rprint(fmt.success(empty_message))
|
|
1665
|
+
raise typer.Exit(code=0)
|
|
1666
|
+
|
|
1667
|
+
try:
|
|
1668
|
+
repo_root = subprocess.run(
|
|
1669
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
1670
|
+
capture_output=True,
|
|
1671
|
+
text=True,
|
|
1672
|
+
check=True,
|
|
1673
|
+
cwd=git_cwd,
|
|
1674
|
+
).stdout.strip()
|
|
1675
|
+
except subprocess.CalledProcessError:
|
|
1676
|
+
print(not_repo_message, file=sys.stderr)
|
|
1677
|
+
raise typer.Exit(code=2)
|
|
1678
|
+
|
|
1679
|
+
file_count = len({line.file for line in added})
|
|
1680
|
+
if not quiet:
|
|
1681
|
+
print(
|
|
1682
|
+
f"Scanning {len(added)} added line(s) in {file_count} file(s) ({context_label})...",
|
|
1683
|
+
file=sys.stderr,
|
|
1684
|
+
)
|
|
1685
|
+
|
|
1686
|
+
all_results = scan_added_diff_lines(added, repo_root, custom_patterns)
|
|
1687
|
+
exclude = scan_cfg.exclude_paths if scan_cfg else None
|
|
1688
|
+
all_results = _apply_exclude_paths(all_results, exclude, repo_root)
|
|
1689
|
+
filtered = _apply_baseline(all_results, baseline_entries)
|
|
1690
|
+
_output_scan_results(
|
|
1691
|
+
filtered, json_output, quiet, context_label, format=format, suppressions=suppressions
|
|
1692
|
+
)
|
|
1693
|
+
|
|
1694
|
+
|
|
1578
1695
|
def _path_matches_exclude_pattern(rel_path: str, pattern: str) -> bool:
|
|
1579
1696
|
"""Mirror of Node ``pathMatchesExcludePattern`` (sable-yz0).
|
|
1580
1697
|
|
|
@@ -1938,64 +2055,42 @@ def scan(
|
|
|
1938
2055
|
|
|
1939
2056
|
baseline_entries = _load_baseline_entries() if baseline else []
|
|
1940
2057
|
|
|
2058
|
+
resolved_scan_path = os.path.abspath(path)
|
|
2059
|
+
git_cwd = resolved_scan_path if os.path.isdir(resolved_scan_path) else None
|
|
2060
|
+
|
|
1941
2061
|
# --diff
|
|
1942
2062
|
if diff:
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
changed = [f.strip() for f in diff_output.split("\n") if f.strip()]
|
|
1958
|
-
if not quiet:
|
|
1959
|
-
print(f"Scanning {len(changed)} file(s) changed since {diff}...", file=sys.stderr)
|
|
1960
|
-
|
|
1961
|
-
eng = _select_engine(engine, quiet, auto_update_enabled)
|
|
1962
|
-
all_results: list[ScanResult] = []
|
|
1963
|
-
for f in changed:
|
|
1964
|
-
resolved = os.path.abspath(f)
|
|
1965
|
-
if os.path.isfile(resolved):
|
|
1966
|
-
all_results.extend(_scan_file(resolved, eng, custom_patterns))
|
|
1967
|
-
filtered = _apply_baseline(all_results, baseline_entries)
|
|
1968
|
-
_output_scan_results(filtered, json_output, quiet, f"files changed since {diff}", format=format, suppressions=suppressions)
|
|
2063
|
+
_run_git_added_line_scan(
|
|
2064
|
+
["diff", "-U0", "--no-color", "--diff-filter=ACM", diff],
|
|
2065
|
+
git_cwd,
|
|
2066
|
+
custom_patterns,
|
|
2067
|
+
scan_cfg,
|
|
2068
|
+
baseline_entries,
|
|
2069
|
+
suppressions,
|
|
2070
|
+
f"files changed since {diff}",
|
|
2071
|
+
f"No files changed since {diff}",
|
|
2072
|
+
json_output=json_output,
|
|
2073
|
+
quiet=quiet,
|
|
2074
|
+
format=format,
|
|
2075
|
+
)
|
|
1969
2076
|
return
|
|
1970
2077
|
|
|
1971
2078
|
# --staged
|
|
1972
2079
|
if staged:
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
staged_files = [f.strip() for f in staged_output.split("\n") if f.strip()]
|
|
1988
|
-
if not quiet:
|
|
1989
|
-
print(f"Scanning {len(staged_files)} staged file(s)...", file=sys.stderr)
|
|
1990
|
-
|
|
1991
|
-
eng = _select_engine(engine, quiet, auto_update_enabled)
|
|
1992
|
-
all_results = []
|
|
1993
|
-
for f in staged_files:
|
|
1994
|
-
resolved = os.path.abspath(f)
|
|
1995
|
-
if os.path.isfile(resolved):
|
|
1996
|
-
all_results.extend(_scan_file(resolved, eng, custom_patterns))
|
|
1997
|
-
filtered = _apply_baseline(all_results, baseline_entries)
|
|
1998
|
-
_output_scan_results(filtered, json_output, quiet, "staged files", format=format, suppressions=suppressions)
|
|
2080
|
+
_run_git_added_line_scan(
|
|
2081
|
+
["diff", "-U0", "--no-color", "--cached", "--diff-filter=ACM"],
|
|
2082
|
+
git_cwd,
|
|
2083
|
+
custom_patterns,
|
|
2084
|
+
scan_cfg,
|
|
2085
|
+
baseline_entries,
|
|
2086
|
+
suppressions,
|
|
2087
|
+
"staged files",
|
|
2088
|
+
"No files staged for commit",
|
|
2089
|
+
json_output=json_output,
|
|
2090
|
+
quiet=quiet,
|
|
2091
|
+
format=format,
|
|
2092
|
+
not_repo_message="Error: Not in a git repository",
|
|
2093
|
+
)
|
|
1999
2094
|
return
|
|
2000
2095
|
|
|
2001
2096
|
# Default: scan path
|
|
@@ -2232,17 +2327,26 @@ def exec_cmd(
|
|
|
2232
2327
|
interceptor.log_evaluation(evaluation, "blocked")
|
|
2233
2328
|
raise typer.Exit(code=1)
|
|
2234
2329
|
|
|
2235
|
-
# Pre-exec scan for git commands
|
|
2330
|
+
# Pre-exec scan for git commands (+ lines in staged diff only)
|
|
2236
2331
|
if not skip_scan and command.strip().startswith(("git commit", "git push")):
|
|
2237
2332
|
try:
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2333
|
+
from ..utils.git_diff import parse_unified_diff_added_lines
|
|
2334
|
+
from ..scanners.git_diff_scan import scan_added_diff_lines
|
|
2335
|
+
|
|
2336
|
+
patch = subprocess.run(
|
|
2337
|
+
["git", "diff", "-U0", "--no-color", "--cached", "--diff-filter=ACM"],
|
|
2338
|
+
capture_output=True,
|
|
2339
|
+
text=True,
|
|
2340
|
+
).stdout
|
|
2341
|
+
if patch.strip():
|
|
2342
|
+
repo_root = subprocess.run(
|
|
2343
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
2344
|
+
capture_output=True,
|
|
2345
|
+
text=True,
|
|
2346
|
+
check=True,
|
|
2347
|
+
).stdout.strip()
|
|
2348
|
+
added = parse_unified_diff_added_lines(patch)
|
|
2349
|
+
results = scan_added_diff_lines(added, repo_root)
|
|
2246
2350
|
total = sum(len(r.matches) for r in results)
|
|
2247
2351
|
if results:
|
|
2248
2352
|
rprint(f"\n{fmt.warning('Secrets detected in staged files!')}\n")
|
|
@@ -2997,11 +3101,50 @@ def _generate_manual_review_prompt(
|
|
|
2997
3101
|
Provide a clear risk rating (LOW/MEDIUM/HIGH/CRITICAL) and actionable recommendations."""
|
|
2998
3102
|
|
|
2999
3103
|
|
|
3104
|
+
def _display_deep_scan(deep, skill_name: str) -> None:
|
|
3105
|
+
"""Render human-readable skill-scanner (deep engine) results."""
|
|
3106
|
+
print("\n\U0001f50e Deep Scan Results (skill-scanner)")
|
|
3107
|
+
print("═" * 60)
|
|
3108
|
+
if not deep.available:
|
|
3109
|
+
print("⚠️ skill-scanner not available")
|
|
3110
|
+
return
|
|
3111
|
+
if deep.error:
|
|
3112
|
+
print(f"⚠️ Deep scan error: {deep.error}")
|
|
3113
|
+
return
|
|
3114
|
+
actionable = [f for f in deep.findings if f.severity in ("critical", "high", "medium")]
|
|
3115
|
+
if not actionable:
|
|
3116
|
+
print("✓ No critical/high/medium findings")
|
|
3117
|
+
else:
|
|
3118
|
+
print(f"⚠️ {len(actionable)} finding(s) (max severity: {deep.max_severity})")
|
|
3119
|
+
for f in actionable[:10]:
|
|
3120
|
+
loc = f" (line {f.line})" if f.line else ""
|
|
3121
|
+
print(f" • [{f.severity.upper()}] {f.category}: {f.title}{loc}")
|
|
3122
|
+
if len(actionable) > 10:
|
|
3123
|
+
print(f" ... and {len(actionable) - 10} more")
|
|
3124
|
+
if deep.analyzers_used:
|
|
3125
|
+
print(f" analyzers: {', '.join(deep.analyzers_used)} (offline only)")
|
|
3126
|
+
print()
|
|
3127
|
+
|
|
3128
|
+
|
|
3000
3129
|
@agent_app.command("audit-skill")
|
|
3001
3130
|
def audit_skill(
|
|
3002
3131
|
skill_path: str = typer.Argument(..., help="Path to skill file to audit"),
|
|
3003
3132
|
skip_openclaw: bool = typer.Option(False, "--skip-openclaw", help="Skip OpenClaw integration, show manual review prompt"),
|
|
3004
3133
|
json_output: bool = typer.Option(False, "--json", help="Output results as JSON"),
|
|
3134
|
+
deep: bool = typer.Option(
|
|
3135
|
+
False,
|
|
3136
|
+
"--deep",
|
|
3137
|
+
help=(
|
|
3138
|
+
"Run the optional DEEP engine (Cisco AI Defense skill-scanner) in "
|
|
3139
|
+
"addition to the quick scan. Offline analyzers only — no LLM/cloud/"
|
|
3140
|
+
"network. Requires `cisco-ai-skill-scanner` to be installed."
|
|
3141
|
+
),
|
|
3142
|
+
),
|
|
3143
|
+
engine: str = typer.Option(
|
|
3144
|
+
None,
|
|
3145
|
+
"--engine",
|
|
3146
|
+
help="Deep engine selector. 'skill-scanner' is equivalent to --deep.",
|
|
3147
|
+
),
|
|
3005
3148
|
) -> None:
|
|
3006
3149
|
"""[deprecated] Security audit of a Claude Code skill file — use `rafter skill review` instead."""
|
|
3007
3150
|
if not json_output:
|
|
@@ -3009,14 +3152,27 @@ def audit_skill(
|
|
|
3009
3152
|
"[deprecated] `rafter agent audit-skill` is deprecated; use `rafter skill review <path-or-url>` instead.",
|
|
3010
3153
|
file=sys.stderr,
|
|
3011
3154
|
)
|
|
3012
|
-
# Validate skill file
|
|
3155
|
+
# Validate target exists. Accept either a skill *file* (.md) or a skill
|
|
3156
|
+
# *directory*. The deep engine (--deep) is most thorough on a directory,
|
|
3157
|
+
# where it can also see bundled scripts / .pyc; the quick scan reads the
|
|
3158
|
+
# directory's SKILL.md (or the file itself).
|
|
3013
3159
|
resolved = Path(skill_path).resolve()
|
|
3014
3160
|
if not resolved.exists():
|
|
3015
|
-
print(f"Error: Skill
|
|
3161
|
+
print(f"Error: Skill path not found: {skill_path}", file=sys.stderr)
|
|
3016
3162
|
raise typer.Exit(code=2)
|
|
3017
3163
|
|
|
3018
|
-
|
|
3019
|
-
|
|
3164
|
+
if resolved.is_dir():
|
|
3165
|
+
skill_md = resolved / "SKILL.md"
|
|
3166
|
+
if skill_md.is_file():
|
|
3167
|
+
skill_content = skill_md.read_text(encoding="utf-8")
|
|
3168
|
+
else:
|
|
3169
|
+
# No SKILL.md to quick-scan; the deep engine can still scan the
|
|
3170
|
+
# directory's contents. Quick scan simply finds nothing.
|
|
3171
|
+
skill_content = ""
|
|
3172
|
+
skill_name = resolved.name
|
|
3173
|
+
else:
|
|
3174
|
+
skill_content = resolved.read_text(encoding="utf-8")
|
|
3175
|
+
skill_name = resolved.name
|
|
3020
3176
|
|
|
3021
3177
|
# Run deterministic analysis
|
|
3022
3178
|
if not json_output:
|
|
@@ -3030,11 +3186,44 @@ def audit_skill(
|
|
|
3030
3186
|
if not json_output:
|
|
3031
3187
|
_display_quick_scan(quick_scan, skill_name)
|
|
3032
3188
|
|
|
3189
|
+
# Optional DEEP engine (skill-scanner) — opt-in via --deep or
|
|
3190
|
+
# --engine skill-scanner. Offline analyzers only; preserves our
|
|
3191
|
+
# no-telemetry default (sable-7g7).
|
|
3192
|
+
want_deep = deep or (engine == "skill-scanner")
|
|
3193
|
+
if engine is not None and engine != "skill-scanner":
|
|
3194
|
+
print(
|
|
3195
|
+
f"Error: unknown --engine '{engine}' (supported: skill-scanner)",
|
|
3196
|
+
file=sys.stderr,
|
|
3197
|
+
)
|
|
3198
|
+
raise typer.Exit(code=2)
|
|
3199
|
+
|
|
3200
|
+
deep_result = None
|
|
3201
|
+
if want_deep:
|
|
3202
|
+
from ..scanners.skill_scanner import SkillScanner
|
|
3203
|
+
|
|
3204
|
+
scanner = SkillScanner()
|
|
3205
|
+
if not scanner.is_available():
|
|
3206
|
+
# --deep requested but tool missing: clear hint, non-zero exit,
|
|
3207
|
+
# don't crash.
|
|
3208
|
+
from ..scanners.skill_scanner import INSTALL_HINT
|
|
3209
|
+
|
|
3210
|
+
print(INSTALL_HINT, file=sys.stderr)
|
|
3211
|
+
raise typer.Exit(code=2)
|
|
3212
|
+
deep_result = scanner.scan_path(str(resolved))
|
|
3213
|
+
if deep_result.error:
|
|
3214
|
+
print(f"Error: deep scan failed: {deep_result.error}", file=sys.stderr)
|
|
3215
|
+
raise typer.Exit(code=2)
|
|
3216
|
+
if not json_output:
|
|
3217
|
+
_display_deep_scan(deep_result, skill_name)
|
|
3218
|
+
|
|
3033
3219
|
# Check OpenClaw availability
|
|
3034
3220
|
skill_manager = SkillManager()
|
|
3035
3221
|
openclaw_available = skill_manager.is_openclaw_installed()
|
|
3036
3222
|
rafter_skill_installed = skill_manager.is_rafter_skill_installed()
|
|
3037
3223
|
|
|
3224
|
+
quick_has_findings = quick_scan.secrets > 0 or len(quick_scan.high_risk_commands) > 0
|
|
3225
|
+
deep_has_findings = deep_result.has_findings if deep_result else False
|
|
3226
|
+
|
|
3038
3227
|
if json_output:
|
|
3039
3228
|
result = {
|
|
3040
3229
|
"skill": skill_name,
|
|
@@ -3047,7 +3236,14 @@ def audit_skill(
|
|
|
3047
3236
|
"openClawAvailable": openclaw_available,
|
|
3048
3237
|
"rafterSkillInstalled": rafter_skill_installed,
|
|
3049
3238
|
}
|
|
3050
|
-
|
|
3239
|
+
if deep_result is not None:
|
|
3240
|
+
result["deepScan"] = {
|
|
3241
|
+
"engine": "skill-scanner",
|
|
3242
|
+
"maxSeverity": deep_result.max_severity,
|
|
3243
|
+
"analyzersUsed": deep_result.analyzers_used,
|
|
3244
|
+
"findings": [f.to_dict() for f in deep_result.findings],
|
|
3245
|
+
}
|
|
3246
|
+
has_findings = quick_has_findings or deep_has_findings
|
|
3051
3247
|
print(json.dumps(result, indent=2))
|
|
3052
3248
|
raise typer.Exit(code=1 if has_findings else 0)
|
|
3053
3249
|
|
|
@@ -3084,7 +3280,7 @@ def audit_skill(
|
|
|
3084
3280
|
|
|
3085
3281
|
print()
|
|
3086
3282
|
|
|
3087
|
-
if
|
|
3283
|
+
if quick_has_findings or deep_has_findings:
|
|
3088
3284
|
raise typer.Exit(code=1)
|
|
3089
3285
|
|
|
3090
3286
|
|
|
@@ -3141,6 +3337,72 @@ def update_betterleaks(
|
|
|
3141
3337
|
raise typer.Exit(code=1)
|
|
3142
3338
|
|
|
3143
3339
|
|
|
3340
|
+
@agent_app.command("update-skill-scanner")
|
|
3341
|
+
def update_skill_scanner(
|
|
3342
|
+
version: str = typer.Option(
|
|
3343
|
+
None,
|
|
3344
|
+
"--version",
|
|
3345
|
+
help="skill-scanner version to install (default: pinned version)",
|
|
3346
|
+
),
|
|
3347
|
+
):
|
|
3348
|
+
"""Install or update the optional `skill-scanner` deep engine (audit-skill --deep).
|
|
3349
|
+
|
|
3350
|
+
Installs Cisco AI Defense's skill-scanner in an isolated environment (uv tool,
|
|
3351
|
+
or pip --user fallback). This is a heavy third-party package and its
|
|
3352
|
+
transitive dependencies; it is NOT bundled with Rafter and is only used when
|
|
3353
|
+
you pass --deep. The deep engine still runs OFFLINE analyzers only.
|
|
3354
|
+
"""
|
|
3355
|
+
from ..scanners.skill_scanner import (
|
|
3356
|
+
SkillScannerInstaller,
|
|
3357
|
+
SKILL_SCANNER_VERSION,
|
|
3358
|
+
)
|
|
3359
|
+
|
|
3360
|
+
target_version = version or SKILL_SCANNER_VERSION
|
|
3361
|
+
installer = SkillScannerInstaller()
|
|
3362
|
+
|
|
3363
|
+
existing = shutil.which("skill-scanner")
|
|
3364
|
+
if existing:
|
|
3365
|
+
rprint(fmt.info(f"Current skill-scanner: {existing}"))
|
|
3366
|
+
else:
|
|
3367
|
+
rprint(fmt.info("skill-scanner not currently on PATH"))
|
|
3368
|
+
|
|
3369
|
+
rprint(fmt.warning(
|
|
3370
|
+
"skill-scanner is a heavy third-party package (pulls litellm, fastapi, "
|
|
3371
|
+
"yara-x, …). Installing it in an isolated environment."
|
|
3372
|
+
))
|
|
3373
|
+
rprint(fmt.info(f"Installing skill-scanner v{target_version}..."))
|
|
3374
|
+
rprint()
|
|
3375
|
+
|
|
3376
|
+
result = installer.install(version=target_version, on_progress=typer.echo)
|
|
3377
|
+
rprint()
|
|
3378
|
+
if not result.ok:
|
|
3379
|
+
rprint(fmt.error(f"Install failed: {result.message}"))
|
|
3380
|
+
rprint(fmt.info(
|
|
3381
|
+
"To fix: install manually with `uv tool install "
|
|
3382
|
+
"cisco-ai-skill-scanner` (or `pip install --user "
|
|
3383
|
+
"cisco-ai-skill-scanner`) and ensure `skill-scanner` is on PATH."
|
|
3384
|
+
))
|
|
3385
|
+
raise typer.Exit(code=1)
|
|
3386
|
+
|
|
3387
|
+
rprint(fmt.success(f"skill-scanner installed (via {result.via}): {result.message}"))
|
|
3388
|
+
rprint(fmt.info("Run `rafter skill review <path> --deep` to use it."))
|
|
3389
|
+
|
|
3390
|
+
|
|
3391
|
+
@agent_app.command("remove-skill-scanner")
|
|
3392
|
+
def remove_skill_scanner():
|
|
3393
|
+
"""Uninstall the optional `skill-scanner` deep engine (inverse of update-skill-scanner).
|
|
3394
|
+
|
|
3395
|
+
Removes the managed install (uv tool, or pip fallback). Safe to run when it
|
|
3396
|
+
isn't installed. Your skills and Rafter's own dependencies are untouched.
|
|
3397
|
+
"""
|
|
3398
|
+
from ..scanners.skill_scanner import SkillScannerInstaller
|
|
3399
|
+
|
|
3400
|
+
result = SkillScannerInstaller().uninstall(on_progress=typer.echo)
|
|
3401
|
+
rprint()
|
|
3402
|
+
if not result.ok:
|
|
3403
|
+
rprint(fmt.error(f"Uninstall failed: {result.message}"))
|
|
3404
|
+
raise typer.Exit(code=1)
|
|
3405
|
+
rprint(fmt.success(f"skill-scanner removed: {result.message}"))
|
|
3144
3406
|
|
|
3145
3407
|
|
|
3146
3408
|
# ── agent status ─────────────────────────────────────────────────────────
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
|
+
import math
|
|
5
6
|
import os
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
@@ -52,6 +53,23 @@ def _format_approval_message(command: str, evaluation) -> str:
|
|
|
52
53
|
|
|
53
54
|
_STDIN_TIMEOUT_S = 5
|
|
54
55
|
|
|
56
|
+
def _stdin_timeout_s() -> float:
|
|
57
|
+
# Bound the stdin read so a hung/never-closing stdin can't wedge the hook.
|
|
58
|
+
# Overridable via env (milliseconds, parity with the Node hook) as an
|
|
59
|
+
# operator safety valve / for tests.
|
|
60
|
+
raw = os.environ.get("RAFTER_HOOK_STDIN_TIMEOUT_MS")
|
|
61
|
+
if raw:
|
|
62
|
+
try:
|
|
63
|
+
ms = float(raw)
|
|
64
|
+
# Require finite and positive (parity with the Node `Number.isFinite`
|
|
65
|
+
# check). `inf`/`nan` must NOT pass — `join(timeout=inf)` would
|
|
66
|
+
# reintroduce the exact unbounded hang this bound exists to prevent.
|
|
67
|
+
if math.isfinite(ms) and ms > 0:
|
|
68
|
+
return ms / 1000.0
|
|
69
|
+
except ValueError:
|
|
70
|
+
pass
|
|
71
|
+
return _STDIN_TIMEOUT_S
|
|
72
|
+
|
|
55
73
|
def _read_stdin() -> str:
|
|
56
74
|
import threading
|
|
57
75
|
result: list[str] = [""]
|
|
@@ -60,9 +78,11 @@ def _read_stdin() -> str:
|
|
|
60
78
|
result[0] = sys.stdin.read()
|
|
61
79
|
except Exception:
|
|
62
80
|
pass
|
|
81
|
+
# daemon=True: if stdin never closes, the abandoned reader thread does not
|
|
82
|
+
# block interpreter exit, so the process exits after the join timeout.
|
|
63
83
|
t = threading.Thread(target=_reader, daemon=True)
|
|
64
84
|
t.start()
|
|
65
|
-
t.join(timeout=
|
|
85
|
+
t.join(timeout=_stdin_timeout_s())
|
|
66
86
|
return result[0]
|
|
67
87
|
|
|
68
88
|
|
|
@@ -256,27 +276,23 @@ def _scan_staged_files() -> dict:
|
|
|
256
276
|
).stdout.strip() or os.getcwd()
|
|
257
277
|
|
|
258
278
|
output = subprocess.run(
|
|
259
|
-
["git", "diff", "
|
|
279
|
+
["git", "diff", "-U0", "--no-color", "--cached", "--diff-filter=ACM"],
|
|
260
280
|
capture_output=True, text=True,
|
|
261
|
-
).stdout
|
|
262
|
-
if not output:
|
|
281
|
+
).stdout
|
|
282
|
+
if not output.strip():
|
|
263
283
|
return {**empty, "repo_root": repo_root}
|
|
264
|
-
staged = [f for f in output.split("\n") if f.strip()]
|
|
265
284
|
|
|
285
|
+
from ..utils.git_diff import parse_unified_diff_added_lines
|
|
286
|
+
from ..scanners.git_diff_scan import scan_added_diff_lines
|
|
266
287
|
from ..core.custom_patterns import apply_suppressions
|
|
267
288
|
from .agent import _apply_exclude_paths
|
|
268
289
|
|
|
290
|
+
added = parse_unified_diff_added_lines(output)
|
|
291
|
+
if not added:
|
|
292
|
+
return {**empty, "repo_root": repo_root}
|
|
293
|
+
|
|
269
294
|
scan_cfg, suppressions, custom_patterns = _load_scan_config()
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
raw = []
|
|
273
|
-
for f in staged:
|
|
274
|
-
resolved = os.path.join(repo_root, f)
|
|
275
|
-
if not os.path.isfile(resolved):
|
|
276
|
-
continue
|
|
277
|
-
r = scanner.scan_file(resolved)
|
|
278
|
-
if r.matches:
|
|
279
|
-
raw.append(r)
|
|
295
|
+
raw = scan_added_diff_lines(added, repo_root, custom_patterns)
|
|
280
296
|
|
|
281
297
|
exclude = scan_cfg.exclude_paths if scan_cfg else None
|
|
282
298
|
after_exclude = _apply_exclude_paths(raw, exclude, repo_root)
|