mirror of
https://github.com/Strategic-Automation/violin.git
synced 2026-08-14 12:33:37 +02:00
fix(guard): review-block curl -s without status capture so receipts are decisive
This commit is contained in:
@@ -332,6 +332,60 @@ def check_local_artifact_paths(command: str) -> CheckResult:
|
||||
return result
|
||||
|
||||
|
||||
_HTTP_CLIENT_RE = re.compile(r"\b(?:curl|wget)\b", re.I)
|
||||
_HTTP_URL_RE = re.compile(r"https?://\S+", re.I)
|
||||
_HTTP_LONG_FLAG_RE = re.compile(
|
||||
r"-(?:include|verbose|head|dump-header|write-out|output|remote-name|output-document)\b",
|
||||
re.I,
|
||||
)
|
||||
# Offline captures (`-o file`, `-O`, `-e/--output`, `> file`) are not
|
||||
# interactive HTTP evidence; status probes via `-w` are separately exempted.
|
||||
_HTTP_OFFLINE_CAPTURE_RE = re.compile(
|
||||
r"-(?:o|O|output|remote-name|output-document)\b|\s>\s*[^\s|]+", re.I
|
||||
)
|
||||
|
||||
|
||||
def _has_short_flag(command: str, *flags: str) -> bool:
|
||||
"""True if any single-dash short-flag cluster contains one of `flags`.
|
||||
|
||||
Handles combined clusters (`-si`, `-sv`, `-Dk`) and separate tokens
|
||||
(`-s -i`). Case-sensitive on purpose: `-D` (dump-header) counts while
|
||||
`-d` (POST data) does not. Guarded to letters-only tokens to avoid
|
||||
matching data payloads or stray dashes.
|
||||
"""
|
||||
wanted = set(flags)
|
||||
return any(
|
||||
any(ch in wanted for ch in token)
|
||||
for token in re.findall(r"(?<!\S)-[A-Za-z]+", command)
|
||||
)
|
||||
|
||||
|
||||
def check_http_proof_flags(command: str) -> CheckResult:
|
||||
"""Review-level guard: HTTP probes must capture the response status/headers.
|
||||
|
||||
SKILL.md §4 mandates `-i` or `-sv` when testing HTTP endpoints so evidence
|
||||
files carry empirical status lines. Receipts from plain `curl -s` (no `-i`)
|
||||
make `has_decisive_proof` fail and burn real confirmations at scoring time
|
||||
(benchmark-run-20260811_175611: only 6/194 receipts carried HTTP/1.1).
|
||||
Exempted: status probes (`-w %{http_code}`), HEAD (`-I`), header dumps
|
||||
(`-D`), and offline captures (`-o`/`-O`/`> file`) that are not interactive
|
||||
HTTP evidence.
|
||||
"""
|
||||
result = CheckResult()
|
||||
if not _HTTP_CLIENT_RE.search(command) or not _HTTP_URL_RE.search(command):
|
||||
return result
|
||||
if _HTTP_LONG_FLAG_RE.search(command) or _HTTP_OFFLINE_CAPTURE_RE.search(command):
|
||||
return result
|
||||
if _has_short_flag(command, "i", "v", "I", "D", "w", "o", "O"):
|
||||
return result
|
||||
result.add_warning(
|
||||
"HTTP probe without status/headers capture: add `-i` (or `-sv`) to curl "
|
||||
"so the evidence file records the response status line — plain `-s` "
|
||||
"produces no HTTP/1.1 line and fails decisive-proof scoring"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def check_cross_engagement_paths(command: str, active_eng_dir: Path) -> CheckResult:
|
||||
"""Block commands that reference a foreign engagement directory under engagements/."""
|
||||
result = CheckResult()
|
||||
@@ -626,6 +680,11 @@ def check_command(args: CheckCommandArgs) -> CheckResult:
|
||||
destructive_result = check_destructive_patterns(args.command)
|
||||
result.errors.extend(destructive_result.errors)
|
||||
|
||||
# 2c2. HTTP proof flags (review): `-i`/`-sv` so receipts are decisive
|
||||
proof_result = check_http_proof_flags(args.command)
|
||||
result.warnings.extend(proof_result.warnings)
|
||||
result.infos.extend(proof_result.infos)
|
||||
|
||||
artifact_result = check_local_artifact_paths(args.command)
|
||||
result.infos.extend(artifact_result.infos)
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""HTTP proof-flag guard tests — decisiveness of curl receipts at scoring time.
|
||||
|
||||
Regression: benchmark-run-20260811_175611 wrote 194 receipts but only 6
|
||||
carried HTTP/1.1 status lines (plain `curl -s` without `-i`), so the
|
||||
decisive-proof scorer rejected most of them. SKILL.md §4 mandates `-i`/`-sv`;
|
||||
this guard makes the requirement a review gate.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from plugins.violin_guard.command import check_http_proof_flags
|
||||
|
||||
|
||||
def test_plain_silent_curl_warns_review() -> None:
|
||||
result = check_http_proof_flags(
|
||||
"curl -s https://duck-store.escape.tech/api/v1/products"
|
||||
)
|
||||
assert result.errors == []
|
||||
assert len(result.warnings) == 1
|
||||
assert "-i" in result.warnings[0]
|
||||
assert result.exit_code() == 2 # review, not hard block
|
||||
|
||||
|
||||
def test_curl_with_status_capture_is_clean() -> None:
|
||||
for command in (
|
||||
"curl -si https://duck-store.escape.tech/api/v1/products",
|
||||
"curl -i -X POST https://duck-store.escape.tech/api/v1/auth/login -d '{}'",
|
||||
"curl -sv https://duck-store.escape.tech/api/v1/orders/1",
|
||||
"curl -I https://duck-store.escape.tech/",
|
||||
"curl -D headers.txt https://duck-store.escape.tech/api/v1/products",
|
||||
):
|
||||
result = check_http_proof_flags(command)
|
||||
assert result.errors == [] and result.warnings == [], command
|
||||
assert result.exit_code() == 0, command
|
||||
|
||||
|
||||
def test_status_probe_and_offline_capture_exempt() -> None:
|
||||
for command in (
|
||||
"curl -s -o /dev/null -w '%{http_code}' https://duck-store.escape.tech/api/v1/auth/login",
|
||||
"curl -sO https://duck-store.escape.tech/bundle.js",
|
||||
"curl -s https://duck-store.escape.tech/api/v1/products > evidence/recon/products.json",
|
||||
):
|
||||
result = check_http_proof_flags(command)
|
||||
assert result.errors == [] and result.warnings == [], command
|
||||
|
||||
|
||||
def test_non_http_or_non_curl_commands_untouched() -> None:
|
||||
for command in (
|
||||
"nmap -sV 10.10.10.10",
|
||||
"grep -i admin evidence/recon/products.json",
|
||||
"python3 tools/burst.py --target https://duck-store.escape.tech",
|
||||
):
|
||||
result = check_http_proof_flags(command)
|
||||
assert result.errors == [] and result.warnings == [], command
|
||||
Reference in New Issue
Block a user