Add CTF bootstrap and repair guard CLI docs

This commit is contained in:
Violin
2026-07-12 17:09:00 +01:00
parent 68a3db3a89
commit eae934bd5b
4 changed files with 93 additions and 9 deletions
+55 -4
View File
@@ -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)
return BootstrapResult(errors=new_errors, warnings=new_warnings, infos=new_infos)
+21 -4
View File
@@ -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)
+1 -1
View File
@@ -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 "<session label>"`. Then pass `--skill-loaded-file "$ENG_DIR/state/.skill-loaded-<session-id>"` 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 <ip> --session-id <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"
+16
View File
@@ -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/<target-name>-$(date +%F)"
python3 scripts/violin_guard.py init-engagement --host <target-ip> "$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 <target-ip> "$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
<!-- Legacy manual recovery reference.
# Resolve an ABSOLUTE ENG_DIR under the canonical engagement root so the skill
# tree and the violin-guard plugin tree never diverge (root-cause fix). The
# `eng-root` subcommand strips any leading "engagements/" and resolves the path
@@ -79,6 +94,7 @@ data.update({
})
path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
PY
-->
```