Fix dotted target parsing and network bookkeeping (#60)

Closes #17
This commit is contained in:
Dan
2026-08-01 22:09:33 +01:00
committed by GitHub
parent f72f838933
commit e5e258fa0b
5 changed files with 43 additions and 3 deletions
+1
View File
@@ -2,6 +2,7 @@
## 3.0.0
- Fixed target extraction for dotted identifiers and direct `/dev/tcp`/`/dev/udp` redirections, and prevented network-capable local-looking commands from bypassing execution accounting.
- Made interrupted skill preparation recoverable with expiring reservations and stale-owner protection; batch review now remains tied to the delivered execution receipt.
- Stabilized the core engagement workflow: domain/URL-only scopes now validate, runtime execution cannot substitute another scope file, PTT/review CLI contracts carry skill metadata, and review reuses the active delivered binding.
- Added receipt-backed skill routing, delivery, task binding, browser enforcement, Kali auto-backend selection, proof-based finding review, and semantic anti-stuck enforcement.
+18 -1
View File
@@ -14,6 +14,7 @@ class CommandSegment:
raw_text: str
words: list[str] = field(default_factory=list)
executable: str = ""
redirects: list[str] = field(default_factory=list)
class _CommandVisitor:
@@ -31,8 +32,19 @@ class _CommandVisitor:
segment_text = self.command[start:end]
words = self._collect_words(node)
executable = self._extract_executable(words)
redirects = [
str(getattr(getattr(part, "output", None), "word", ""))
for part in getattr(node, "parts", [])
if getattr(part, "kind", None) == "redirect"
and getattr(getattr(part, "output", None), "word", None)
]
self.segments.append(
CommandSegment(raw_text=segment_text, words=words, executable=executable)
CommandSegment(
raw_text=segment_text,
words=words,
executable=executable,
redirects=redirects,
)
)
if hasattr(node, "parts"):
@@ -52,6 +64,11 @@ class _CommandVisitor:
if kind == "word" and hasattr(n, "word"):
words.append(n.word)
self.words.append(n.word)
elif kind == "redirect":
output = getattr(n, "output", None)
if output is not None and hasattr(output, "word"):
words.append(output.word)
self.words.append(output.word)
if hasattr(n, "parts"):
for child in n.parts:
collect(child)
+10 -2
View File
@@ -170,8 +170,16 @@ def mutate_json(path: Path, mutation) -> Any:
def is_local_bookkeeping_command(command: str) -> bool:
"""Whether a command is a harmless local bookkeeping action."""
leading = command.strip().split(maxsplit=1)
return bool(leading) and leading[0] in LOCAL_TOOLS
from .bash_ast import parse_bash_segments
from .targets import extract_target_candidates
segments = parse_bash_segments(command)
if len(segments) != 1:
return False
segment = segments[0]
if segment.executable not in LOCAL_TOOLS or segment.redirects:
return False
return not extract_target_candidates(command)
# Sync credit / pending sync
@@ -140,3 +140,15 @@ def test_callback_hosts_are_secondary_only_and_exclusions_still_win(tmp_path: Pa
scope, "nc -l -v -s 10.10.10.99 4444", primary_target="10.10.10.10"
)
assert any("excluded target 10.10.10.99" in error for error in excluded.errors)
def test_direct_dev_tcp_redirection_is_checked_and_not_bookkeeping(tmp_path: Path) -> None:
scope = tmp_path / "scope.yaml"
_write_scope(scope, callback_hosts="10.10.10.10")
result = check_scope_targets(
scope,
"echo ready > /dev/tcp/10.10.10.99/4444",
primary_target="10.10.10.10",
)
assert any("10.10.10.99" in error for error in result.errors)
@@ -33,6 +33,8 @@ def test_network_clients_are_not_local_bookkeeping() -> None:
for command in ("curl https://10.10.10.10", "dig 10.10.10.10", "host 10.10.10.10"):
assert not state.is_local_bookkeeping_command(command)
assert state.is_local_bookkeeping_command("echo local-note")
assert not state.is_local_bookkeeping_command("echo ping > /dev/tcp/10.10.10.10/80")
assert not state.is_local_bookkeeping_command("cat < /dev/tcp/10.10.10.10/80")
def test_phase_window_runs_without_yolo_then_next_command_blocks(