From eae934bd5b4c2b84af12d177258d8a78f4d85e9c Mon Sep 17 00:00:00 2001 From: Violin Date: Sun, 12 Jul 2026 17:09:00 +0100 Subject: [PATCH] Add CTF bootstrap and repair guard CLI docs --- plugins/violin_guard/core/bootstrap.py | 59 ++++++++++++++++++++++++-- scripts/violin_guard.py | 25 +++++++++-- skills/pentest/SKILL.md | 2 +- skills/pentest/playbooks/scoping.md | 16 +++++++ 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/plugins/violin_guard/core/bootstrap.py b/plugins/violin_guard/core/bootstrap.py index 46a83ae..9171d2b 100644 --- a/plugins/violin_guard/core/bootstrap.py +++ b/plugins/violin_guard/core/bootstrap.py @@ -85,6 +85,8 @@ def _create_artifact( rel: Path, template_rel: str | None, placeholder: str | None, + host: str | None = None, + ctf: bool = False, ) -> None: target = eng_dir / rel target.parent.mkdir(parents=True, exist_ok=True) @@ -96,7 +98,7 @@ def _create_artifact( content = src.read_text(encoding="utf-8") if rel == Path("scope/scope.yaml"): data = yaml.safe_load(content) - data["targets"]["ip_addresses"] = [_derive_host(eng_dir)] + data["targets"]["ip_addresses"] = [host or _derive_host(eng_dir)] data["engagement"]["date"] = date.today().isoformat() content = yaml.safe_dump(data, sort_keys=False, default_flow_style=False) if rel == Path("state/ptt.md"): @@ -105,10 +107,50 @@ def _create_artifact( f"*Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M')}*", content, ) + if ctf: + content = _ctf_ptt(host or _derive_host(eng_dir)) target.write_text(content, encoding="utf-8") -def init_engagement(eng_dir: str | Path, host: str | None = None) -> int: +def _ctf_ptt(host: str) -> str: + today = date.today().isoformat() + return f"""# CTF Task Tree — {host} {today} + +*Last updated: {datetime.now().strftime('%Y-%m-%d %H:%M')}* + +## Phase: RECON +| ID | Status | Task | Evidence / Notes | +|----|--------|------|------------------| +| PT-CTF-001 | [~] | Enumerate services and attack surface | evidence/recon/ | + +## Phase: EXPLOITATION +| ID | Status | Task | Evidence / Notes | +|----|--------|------|------------------| +| PT-CTF-002 | [ ] | Validate an in-scope foothold | evidence/exploitation/ | + +## Phase: PRIVESC +| ID | Status | Task | Evidence / Notes | +|----|--------|------|------------------| +| PT-CTF-003 | [ ] | Enumerate and validate privilege escalation | evidence/exploitation/ | + +## Phase: FLAGS +| ID | Status | Task | Evidence / Notes | +|----|--------|------|------------------| +| PT-CTF-004 | [ ] | Capture user.txt and root.txt | evidence/flags/ | +""" + + +def _ctf_scope(host: str) -> dict: + return { + "targets": {"ip_addresses": [host], "in_scope_urls": []}, + "authorized_parties": ["lab owner (user)"], + "rules_of_engagement": {"allowed_actions": ["host/port discovery", "banner grabbing", "version detection", "vulnerability scanning", "exploit validation (in-scope, non-destructive)", "privilege escalation", "flag capture (user.txt, root.txt)"], "forbidden_actions": []}, + "authorisation": {"confirmed": True, "confirmed_by": "user (HTB lab owner)"}, + "engagement": {"name": f"CTF {host}", "date": date.today().isoformat(), "type": "ctf", "mode": "standard-pentest", "depth": "black-box", "focus_areas": ["recon", "exploitation", "privilege-escalation", "flag-capture"]}, + } + + +def init_engagement(eng_dir: str | Path, host: str | None = None, *, ctf: bool = False, session_id: str = "") -> int: """Create a complete, guard-clean engagement directory from templates.""" eng_dir = Path(eng_dir) result = BootstrapResult() @@ -119,9 +161,18 @@ def init_engagement(eng_dir: str | Path, host: str | None = None) -> int: target = eng_dir / rel if target.exists(): continue - _create_artifact(eng_dir, rel, template_rel, placeholder) + _create_artifact(eng_dir, rel, template_rel, placeholder, host, ctf) result.add_info(f"created {rel}") + if ctf: + scope_path = eng_dir / "scope" / "scope.yaml" + scope_path.write_text(yaml.safe_dump(_ctf_scope(host), sort_keys=False), encoding="utf-8") + result.add_info("wrote CTF scope") + if session_id: + marker = eng_dir / "state" / f".skill-loaded-{session_id}" + marker.write_text("skill-loaded: ctf bootstrap\n", encoding="utf-8") + result.add_info(f"marked skill loaded for session {session_id}") + if result.errors or result.warnings: result.add_error("init-engagement produced an incomplete or non-compliant engagement") result.print() @@ -267,4 +318,4 @@ def _auto_repair_corrupt_artifacts( for w in result.warnings: new_warnings.append(w) - return BootstrapResult(errors=new_errors, warnings=new_warnings, infos=new_infos) \ No newline at end of file + return BootstrapResult(errors=new_errors, warnings=new_warnings, infos=new_infos) diff --git a/scripts/violin_guard.py b/scripts/violin_guard.py index 1cff030..7cc6672 100644 --- a/scripts/violin_guard.py +++ b/scripts/violin_guard.py @@ -42,7 +42,16 @@ def cmd_check_bootstrap(args: argparse.Namespace) -> int: def cmd_init_engagement(args: argparse.Namespace) -> int: - return bootstrap.init_engagement(args.eng_dir, host=args.host) + return bootstrap.init_engagement(args.eng_dir, host=args.host, ctf=args.ctf, session_id=args.session_id) + + +def cmd_validate_scope(args: argparse.Namespace) -> int: + result = command.validate_scope(Path(args.scope)) + code = result.exit_code() + label = "OK" if code == 0 else "REVIEW" if code == 2 else "BLOCK" + messages = result.errors or result.warnings or result.infos or ["scope valid"] + print(f"{label}: {messages[0]}") + return code def cmd_check_skill_loaded(args: argparse.Namespace) -> int: @@ -114,10 +123,11 @@ def cmd_message_tick(args: argparse.Namespace) -> int: def cmd_eng_root(args: argparse.Namespace) -> int: from plugins.violin_guard.core import state - eng_root = state._eng_dir(args.eng_dir) if args.eng_dir else state._eng_dir("") + eng_dir = args.eng_dir_option or args.eng_dir + eng_root = state._eng_dir(eng_dir) if eng_dir else state._eng_dir("") print(f"ENG_ROOT={eng_root}") - if args.eng_dir: - resolved = state._eng_dir(args.eng_dir) + if eng_dir: + resolved = state._eng_dir(eng_dir) print(f"resolved={resolved}") return 0 @@ -154,6 +164,8 @@ def main() -> int: p = sub.add_parser("init-engagement", help="Create guard-clean engagement") p.add_argument("eng_dir") p.add_argument("--host", default="") + p.add_argument("--ctf", action="store_true", help="Create an HTB/CTF-ready scope and PTT") + p.add_argument("--session-id", default="", help="Mark this session skill-loaded for CTF bootstrap") p.set_defaults(func=cmd_init_engagement) # check-skill-loaded @@ -197,8 +209,13 @@ def main() -> int: # eng-root p = sub.add_parser("eng-root", help="Print canonical engagement root") p.add_argument("eng_dir", nargs="?") + p.add_argument("--eng-dir", dest="eng_dir_option") p.set_defaults(func=cmd_eng_root) + p = sub.add_parser("validate-scope", help="Validate scope.yaml") + p.add_argument("--scope", required=True) + p.set_defaults(func=cmd_validate_scope) + p = sub.add_parser("check-release", help="Run release checks") p.set_defaults(func=cmd_check_release) diff --git a/skills/pentest/SKILL.md b/skills/pentest/SKILL.md index 09f23bc..f6e0f55 100644 --- a/skills/pentest/SKILL.md +++ b/skills/pentest/SKILL.md @@ -110,12 +110,12 @@ The phase workflow is mandatory for the entire session, including long, compress - `violin_record_ptt` changes task lifecycle state; `violin_record_hypothesis` records semantic hypothesis changes. - `violin_sync_done` verifies the explicit post-batch PTT update and unlocks the next target batch. - `violin_heartbeat_done` clears the periodic review lock after re-reading this skill and reviewing engagement files. -- `sync-clear` is for session bootstrap/manual reconciliation of prior-session locks only; never use it to skip documenting a command that just ran. 0. **Bootstrap gate** — at session start, after `/goal set`, or after context compression that loses track of state, verify the engagement is bootstrapped: `python $HOME/.hermes/profiles/violin/scripts/violin_guard.py check-bootstrap --eng-dir "$ENG_DIR"`. Exit `0` = proceed. Exit `1` = **STOP and run `playbooks/scoping.md §0`** (creates `$ENG_DIR/`, `scope/scope.yaml`, `state/ptt.md`, `hypotheses.md`, `state/history.md`). Exit `2` = fix the warning, then proceed. This gate is non-negotiable: no `curl`, `nmap`, `browser_navigate`, or other target-touching tool call is allowed until exit 0. 0.1. **Skill-load gate** — before any target interaction at session start, after `/goal set`, or after context compression, create the skill-load marker: `python $HOME/.hermes/profiles/violin/scripts/violin_guard.py check-skill-loaded --eng-dir "$ENG_DIR" --session-id ""`. Then pass `--skill-loaded-file "$ENG_DIR/state/.skill-loaded-"` to every subsequent `check-command` invocation. Missing or stale marker = **BLOCK**; reload SKILL.md §2 and recreate the marker. 1. Check/update `todo` with a single active `phase-gate` item named for the current phase. 2. Confirm an approved `$ENG_DIR/scope/scope.yaml` exists before touching any target. If it does not, remain in SCOPING and ask via `clarify`. Verify with `python $HOME/.hermes/profiles/violin/scripts/violin_guard.py validate-scope --scope $ENG_DIR/scope/scope.yaml` (exit 0 required). + For an authorized HTB/CTF lab, `init-engagement --ctf --host --session-id "$ENG_DIR"` creates a ready-to-test scope, active RECON PTT row, and skill marker. 3. **Read and activate the PTT task** — `read_file path="$ENG_DIR/state/ptt.md"` — and select the next open `[ ]` task for the current phase. Before any target command, mark exactly one task `[~]`: ```bash python $HOME/.hermes/profiles/violin/scripts/violin_guard.py record-ptt --eng-dir "$ENG_DIR" --id PT-XXX --status "[~]" --note "starting task" diff --git a/skills/pentest/playbooks/scoping.md b/skills/pentest/playbooks/scoping.md index 53cede9..6fa9e5b 100644 --- a/skills/pentest/playbooks/scoping.md +++ b/skills/pentest/playbooks/scoping.md @@ -22,9 +22,24 @@ ### 0. Bootstrap: Create the Engagement Project +Use the guard CLI rather than manually copying templates. It creates the +scope, PTT, hypothesis board, and command history: + +```bash +ENG_DIR="engagements/-$(date +%F)" +python3 scripts/violin_guard.py init-engagement --host "$ENG_DIR" + +# Authorized HTB/CTF lab: ready-to-test scope, active RECON task, and skill marker. +# python3 scripts/violin_guard.py init-engagement --ctf --session-id htb1 --host "$ENG_DIR" +``` + +The legacy manual bootstrap below is retained only as a recovery reference; +use `init-engagement` for normal work. + Run this before capturing any data — it creates the directory structure, PTT, and hypothesis board for this engagement: ```bash + ```