ctf-agent 1.2.0
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.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,1026 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CTF-Agent Preflight Environment Detector & Workspace Initializer (v1.0)
|
|
4
|
+
Analyzes host capabilities, scores execution backends (WSL vs Docker),
|
|
5
|
+
selects workload profiles, provisions workspaces, and verifies health.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import platform
|
|
11
|
+
import shutil
|
|
12
|
+
import argparse
|
|
13
|
+
import subprocess
|
|
14
|
+
import json
|
|
15
|
+
import socket
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
18
|
+
|
|
19
|
+
# Ensure safe UTF-8 output on Windows consoles
|
|
20
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
21
|
+
try:
|
|
22
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
23
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
28
|
+
|
|
29
|
+
# Import deployment logic from install_as_agent
|
|
30
|
+
sys.path.insert(0, str(REPO_ROOT / "scripts"))
|
|
31
|
+
try:
|
|
32
|
+
import install_as_agent
|
|
33
|
+
except ImportError:
|
|
34
|
+
install_as_agent = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Workload & Profile Definitions
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
PURPOSE_PROFILES = {
|
|
42
|
+
"live-ctf": {
|
|
43
|
+
"title": "Live CTF Competition (Blitz / Speedrun)",
|
|
44
|
+
"description": "Fastest time-to-flag, minimal documentation overhead, stop-on-flag workflow.",
|
|
45
|
+
"profiles": "core,pwn,web,crypto",
|
|
46
|
+
"agent_mode": "@ctf-speedrun (--blitz)",
|
|
47
|
+
},
|
|
48
|
+
"security-lab": {
|
|
49
|
+
"title": "Security Lab & Research (Deep Analysis / RCA)",
|
|
50
|
+
"description": "Exhaustive root-cause analysis, standardized writeup.md, resources/ preservation.",
|
|
51
|
+
"profiles": "core,pwn,rev,crypto,forensics,web,cloud,ad,kernel,web3",
|
|
52
|
+
"agent_mode": "@ctf-analyzer (--deep)",
|
|
53
|
+
},
|
|
54
|
+
"rev-pwn": {
|
|
55
|
+
"title": "Reverse Engineering & Binary Exploitation",
|
|
56
|
+
"description": "Dedicated disassemblers, decompilers, ROP, heap, and symbolic execution.",
|
|
57
|
+
"profiles": "core,pwn,rev,kernel,wasm",
|
|
58
|
+
"agent_mode": "@ctf-speedrun / @ctf-analyzer",
|
|
59
|
+
},
|
|
60
|
+
"full": {
|
|
61
|
+
"title": "Full Comprehensive CTF Workstation",
|
|
62
|
+
"description": "All 15 category profiles installed into the isolated backend.",
|
|
63
|
+
"profiles": "all",
|
|
64
|
+
"agent_mode": "@ctf-controller",
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Environment Preflight Detector
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
class EnvironmentDetector:
|
|
74
|
+
"""Detects host platform, resources, WSL distro state, Docker state, and network."""
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def detect_os() -> Dict[str, Any]:
|
|
78
|
+
os_type = platform.system()
|
|
79
|
+
return {
|
|
80
|
+
"system": os_type,
|
|
81
|
+
"platform": sys.platform,
|
|
82
|
+
"release": platform.release(),
|
|
83
|
+
"arch": platform.machine(),
|
|
84
|
+
"cpu_cores": os.cpu_count() or 1,
|
|
85
|
+
"is_windows": os_type == "Windows",
|
|
86
|
+
"is_linux": os_type == "Linux",
|
|
87
|
+
"is_macos": os_type == "Darwin",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def detect_memory() -> Dict[str, float]:
|
|
92
|
+
total_gb = 0.0
|
|
93
|
+
avail_gb = 0.0
|
|
94
|
+
|
|
95
|
+
if platform.system() == "Windows":
|
|
96
|
+
try:
|
|
97
|
+
import ctypes
|
|
98
|
+
class MEMORYSTATUSEX(ctypes.Structure):
|
|
99
|
+
_fields_ = [
|
|
100
|
+
("dwLength", ctypes.c_ulong),
|
|
101
|
+
("dwMemoryLoad", ctypes.c_ulong),
|
|
102
|
+
("ullTotalPhys", ctypes.c_ulonglong),
|
|
103
|
+
("ullAvailPhys", ctypes.c_ulonglong),
|
|
104
|
+
("ullTotalPageFile", ctypes.c_ulonglong),
|
|
105
|
+
("ullAvailPageFile", ctypes.c_ulonglong),
|
|
106
|
+
("ullTotalVirtual", ctypes.c_ulonglong),
|
|
107
|
+
("ullAvailVirtual", ctypes.c_ulonglong),
|
|
108
|
+
("sullAvailExtendedVirtual", ctypes.c_ulonglong),
|
|
109
|
+
]
|
|
110
|
+
stat = MEMORYSTATUSEX()
|
|
111
|
+
stat.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
|
|
112
|
+
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)):
|
|
113
|
+
total_gb = round(stat.ullTotalPhys / (1024 ** 3), 2)
|
|
114
|
+
avail_gb = round(stat.ullAvailPhys / (1024 ** 3), 2)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
elif platform.system() == "Linux":
|
|
118
|
+
try:
|
|
119
|
+
with open("/proc/meminfo", "r", encoding="utf-8") as f:
|
|
120
|
+
mem_data = {}
|
|
121
|
+
for line in f:
|
|
122
|
+
parts = line.split(":")
|
|
123
|
+
if len(parts) == 2:
|
|
124
|
+
mem_data[parts[0].strip()] = parts[1].strip()
|
|
125
|
+
if "MemTotal" in mem_data:
|
|
126
|
+
kb = float(mem_data["MemTotal"].split()[0])
|
|
127
|
+
total_gb = round(kb / (1024 ** 2), 2)
|
|
128
|
+
if "MemAvailable" in mem_data:
|
|
129
|
+
kb = float(mem_data["MemAvailable"].split()[0])
|
|
130
|
+
avail_gb = round(kb / (1024 ** 2), 2)
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
elif platform.system() == "Darwin":
|
|
134
|
+
try:
|
|
135
|
+
out = subprocess.check_output(["sysctl", "-n", "hw.memsize"], text=True).strip()
|
|
136
|
+
bytes_mem = float(out)
|
|
137
|
+
total_gb = round(bytes_mem / (1024 ** 3), 2)
|
|
138
|
+
avail_gb = total_gb * 0.5
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
return {"total_gb": total_gb, "avail_gb": avail_gb}
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def detect_disk(path: Path) -> Dict[str, float]:
|
|
146
|
+
target = path.resolve()
|
|
147
|
+
if not target.exists():
|
|
148
|
+
target = target.parent if target.parent.exists() else Path.cwd()
|
|
149
|
+
try:
|
|
150
|
+
total, used, free = shutil.disk_usage(str(target))
|
|
151
|
+
return {
|
|
152
|
+
"total_gb": round(total / (1024 ** 3), 2),
|
|
153
|
+
"free_gb": round(free / (1024 ** 3), 2),
|
|
154
|
+
"used_gb": round(used / (1024 ** 3), 2),
|
|
155
|
+
}
|
|
156
|
+
except Exception:
|
|
157
|
+
return {"total_gb": 0.0, "free_gb": 0.0, "used_gb": 0.0}
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def detect_wsl() -> Dict[str, Any]:
|
|
161
|
+
if platform.system() != "Windows":
|
|
162
|
+
return {
|
|
163
|
+
"available": False,
|
|
164
|
+
"reason": "WSL is only available on Windows",
|
|
165
|
+
"distros": [],
|
|
166
|
+
"kali_present": False,
|
|
167
|
+
"kali_running": False,
|
|
168
|
+
"wsl2_supported": False,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
wsl_bin = shutil.which("wsl.exe") or shutil.which("wsl")
|
|
172
|
+
if not wsl_bin:
|
|
173
|
+
return {
|
|
174
|
+
"available": False,
|
|
175
|
+
"reason": "wsl.exe executable not found in PATH",
|
|
176
|
+
"distros": [],
|
|
177
|
+
"kali_present": False,
|
|
178
|
+
"kali_running": False,
|
|
179
|
+
"wsl2_supported": False,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
res = subprocess.run(
|
|
184
|
+
[wsl_bin, "-l", "-v"],
|
|
185
|
+
capture_output=True,
|
|
186
|
+
check=False,
|
|
187
|
+
timeout=5,
|
|
188
|
+
)
|
|
189
|
+
raw = res.stdout
|
|
190
|
+
text = ""
|
|
191
|
+
try:
|
|
192
|
+
decoded_utf16 = raw.decode("utf-16le", errors="ignore")
|
|
193
|
+
if "NAME" in decoded_utf16 or "Name" in decoded_utf16:
|
|
194
|
+
text = decoded_utf16
|
|
195
|
+
else:
|
|
196
|
+
text = raw.decode("utf-8", errors="replace")
|
|
197
|
+
except Exception:
|
|
198
|
+
text = raw.decode("utf-8", errors="replace")
|
|
199
|
+
|
|
200
|
+
distros = []
|
|
201
|
+
kali_present = False
|
|
202
|
+
kali_running = False
|
|
203
|
+
wsl2_supported = False
|
|
204
|
+
|
|
205
|
+
for line in text.splitlines():
|
|
206
|
+
line = line.strip()
|
|
207
|
+
if not line or "NAME" in line or "---" in line:
|
|
208
|
+
continue
|
|
209
|
+
is_default = line.startswith("*")
|
|
210
|
+
clean = line.lstrip("*").strip()
|
|
211
|
+
parts = clean.split()
|
|
212
|
+
if len(parts) >= 2:
|
|
213
|
+
distro_name = parts[0]
|
|
214
|
+
state = parts[1]
|
|
215
|
+
version = parts[2] if len(parts) >= 3 else "Unknown"
|
|
216
|
+
if version == "2":
|
|
217
|
+
wsl2_supported = True
|
|
218
|
+
is_kali = "kali" in distro_name.lower()
|
|
219
|
+
if is_kali:
|
|
220
|
+
kali_present = True
|
|
221
|
+
if state.lower() == "running":
|
|
222
|
+
kali_running = True
|
|
223
|
+
distros.append({
|
|
224
|
+
"name": distro_name,
|
|
225
|
+
"state": state,
|
|
226
|
+
"version": version,
|
|
227
|
+
"is_default": is_default,
|
|
228
|
+
"is_kali": is_kali,
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
"available": True,
|
|
233
|
+
"reason": f"Found {len(distros)} registered distribution(s)",
|
|
234
|
+
"distros": distros,
|
|
235
|
+
"kali_present": kali_present,
|
|
236
|
+
"kali_running": kali_running,
|
|
237
|
+
"wsl2_supported": wsl2_supported,
|
|
238
|
+
}
|
|
239
|
+
except subprocess.TimeoutExpired:
|
|
240
|
+
return {
|
|
241
|
+
"available": False,
|
|
242
|
+
"reason": "WSL command timed out",
|
|
243
|
+
"distros": [],
|
|
244
|
+
"kali_present": False,
|
|
245
|
+
"kali_running": False,
|
|
246
|
+
"wsl2_supported": False,
|
|
247
|
+
}
|
|
248
|
+
except Exception as e:
|
|
249
|
+
return {
|
|
250
|
+
"available": False,
|
|
251
|
+
"reason": f"Error querying WSL: {e}",
|
|
252
|
+
"distros": [],
|
|
253
|
+
"kali_present": False,
|
|
254
|
+
"kali_running": False,
|
|
255
|
+
"wsl2_supported": False,
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def detect_docker() -> Dict[str, Any]:
|
|
260
|
+
docker_bin = shutil.which("docker")
|
|
261
|
+
if not docker_bin:
|
|
262
|
+
return {
|
|
263
|
+
"available": False,
|
|
264
|
+
"daemon_running": False,
|
|
265
|
+
"reason": "docker executable not found in PATH",
|
|
266
|
+
"version": "",
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
version_str = ""
|
|
270
|
+
daemon_running = False
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
res_ver = subprocess.run(
|
|
274
|
+
[docker_bin, "--version"],
|
|
275
|
+
capture_output=True,
|
|
276
|
+
text=True,
|
|
277
|
+
check=False,
|
|
278
|
+
timeout=4,
|
|
279
|
+
)
|
|
280
|
+
if res_ver.returncode == 0:
|
|
281
|
+
version_str = res_ver.stdout.strip()
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
res_info = subprocess.run(
|
|
287
|
+
[docker_bin, "info", "--format", "{{.ServerVersion}}"],
|
|
288
|
+
capture_output=True,
|
|
289
|
+
text=True,
|
|
290
|
+
check=False,
|
|
291
|
+
timeout=5,
|
|
292
|
+
)
|
|
293
|
+
if res_info.returncode == 0 and res_info.stdout.strip():
|
|
294
|
+
daemon_running = True
|
|
295
|
+
except Exception:
|
|
296
|
+
daemon_running = False
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
"available": True,
|
|
300
|
+
"daemon_running": daemon_running,
|
|
301
|
+
"reason": "Docker daemon running" if daemon_running else "Docker CLI present, but daemon is stopped",
|
|
302
|
+
"version": version_str,
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def detect_network() -> Dict[str, bool]:
|
|
307
|
+
connected = False
|
|
308
|
+
try:
|
|
309
|
+
socket.create_connection(("1.1.1.1", 53), timeout=1.5).close()
|
|
310
|
+
connected = True
|
|
311
|
+
except OSError:
|
|
312
|
+
try:
|
|
313
|
+
socket.create_connection(("8.8.8.8", 53), timeout=1.5).close()
|
|
314
|
+
connected = True
|
|
315
|
+
except OSError:
|
|
316
|
+
connected = False
|
|
317
|
+
return {"connected": connected}
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def run_preflight(cls, workspace_path: Path) -> Dict[str, Any]:
|
|
321
|
+
dot_agents = workspace_path / ".agents"
|
|
322
|
+
dot_agent_legacy = workspace_path / ".agent"
|
|
323
|
+
return {
|
|
324
|
+
"os": cls.detect_os(),
|
|
325
|
+
"memory": cls.detect_memory(),
|
|
326
|
+
"disk": cls.detect_disk(workspace_path),
|
|
327
|
+
"wsl": cls.detect_wsl(),
|
|
328
|
+
"docker": cls.detect_docker(),
|
|
329
|
+
"network": cls.detect_network(),
|
|
330
|
+
"workspace_path": str(workspace_path.resolve()),
|
|
331
|
+
"existing_agents": dot_agents.exists() or dot_agent_legacy.exists(),
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ---------------------------------------------------------------------------
|
|
336
|
+
# Backend Capability Scoring Engine
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
|
|
339
|
+
class CapabilityScoringEngine:
|
|
340
|
+
"""Calculates weighted scores for WSL vs Docker execution backends."""
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def calculate_scores(env: Dict[str, Any], workload: str = "live-ctf") -> Dict[str, Any]:
|
|
344
|
+
os_info = env["os"]
|
|
345
|
+
wsl_info = env["wsl"]
|
|
346
|
+
docker_info = env["docker"]
|
|
347
|
+
disk_info = env["disk"]
|
|
348
|
+
mem_info = env["memory"]
|
|
349
|
+
|
|
350
|
+
free_disk = disk_info.get("free_gb", 0.0)
|
|
351
|
+
total_ram = mem_info.get("total_gb", 0.0)
|
|
352
|
+
|
|
353
|
+
wsl_score = 0
|
|
354
|
+
wsl_reasons: List[str] = []
|
|
355
|
+
|
|
356
|
+
docker_score = 0
|
|
357
|
+
docker_reasons: List[str] = []
|
|
358
|
+
|
|
359
|
+
# WSL Evaluation
|
|
360
|
+
if os_info["is_windows"]:
|
|
361
|
+
wsl_score += 3
|
|
362
|
+
wsl_reasons.append("Windows native OS environment (+3)")
|
|
363
|
+
|
|
364
|
+
if wsl_info["available"]:
|
|
365
|
+
wsl_score += 3
|
|
366
|
+
wsl_reasons.append("WSL service active (+3)")
|
|
367
|
+
|
|
368
|
+
if wsl_info["kali_present"]:
|
|
369
|
+
wsl_score += 3
|
|
370
|
+
wsl_reasons.append("Kali Linux distribution installed (+3)")
|
|
371
|
+
if wsl_info["kali_running"]:
|
|
372
|
+
wsl_score += 1
|
|
373
|
+
wsl_reasons.append("Kali Linux instance currently running (+1)")
|
|
374
|
+
else:
|
|
375
|
+
wsl_reasons.append("Kali Linux distribution missing (0)")
|
|
376
|
+
|
|
377
|
+
if wsl_info["wsl2_supported"]:
|
|
378
|
+
wsl_score += 1
|
|
379
|
+
wsl_reasons.append("WSL2 virtualization architecture verified (+1)")
|
|
380
|
+
else:
|
|
381
|
+
wsl_reasons.append("WSL not available on host (0)")
|
|
382
|
+
|
|
383
|
+
if free_disk >= 25.0:
|
|
384
|
+
wsl_score += 2
|
|
385
|
+
wsl_reasons.append(f"Storage ample ({free_disk} GB free) (+2)")
|
|
386
|
+
elif free_disk >= 15.0:
|
|
387
|
+
wsl_score += 1
|
|
388
|
+
wsl_reasons.append(f"Storage sufficient ({free_disk} GB free) (+1)")
|
|
389
|
+
else:
|
|
390
|
+
wsl_score -= 2
|
|
391
|
+
wsl_reasons.append(f"Storage low ({free_disk} GB free) (-2)")
|
|
392
|
+
|
|
393
|
+
if total_ram >= 8.0:
|
|
394
|
+
wsl_score += 1
|
|
395
|
+
wsl_reasons.append(f"RAM sufficient ({total_ram} GB) (+1)")
|
|
396
|
+
else:
|
|
397
|
+
wsl_score = 0
|
|
398
|
+
wsl_reasons.append("Non-Windows OS: WSL not applicable (0)")
|
|
399
|
+
|
|
400
|
+
# Docker Evaluation
|
|
401
|
+
if docker_info["available"]:
|
|
402
|
+
docker_score += 2
|
|
403
|
+
docker_reasons.append("Docker CLI installed (+2)")
|
|
404
|
+
|
|
405
|
+
if docker_info["daemon_running"]:
|
|
406
|
+
docker_score += 3
|
|
407
|
+
docker_reasons.append("Docker daemon running and responsive (+3)")
|
|
408
|
+
|
|
409
|
+
if os_info["is_linux"]:
|
|
410
|
+
docker_score += 3
|
|
411
|
+
docker_reasons.append("Native Linux container execution (+3)")
|
|
412
|
+
elif os_info["is_macos"]:
|
|
413
|
+
docker_score += 2
|
|
414
|
+
docker_reasons.append("macOS container execution (+2)")
|
|
415
|
+
elif os_info["is_windows"]:
|
|
416
|
+
docker_score -= 1
|
|
417
|
+
docker_reasons.append("Windows Docker 9P filesystem interop penalty (-1)")
|
|
418
|
+
|
|
419
|
+
if free_disk >= 25.0:
|
|
420
|
+
docker_score += 2
|
|
421
|
+
docker_reasons.append(f"Storage ample ({free_disk} GB free) (+2)")
|
|
422
|
+
elif free_disk >= 15.0:
|
|
423
|
+
docker_score += 1
|
|
424
|
+
docker_reasons.append(f"Storage sufficient ({free_disk} GB free) (+1)")
|
|
425
|
+
else:
|
|
426
|
+
docker_score -= 2
|
|
427
|
+
docker_reasons.append(f"Storage low ({free_disk} GB free) (-2)")
|
|
428
|
+
|
|
429
|
+
if total_ram >= 8.0:
|
|
430
|
+
docker_score += 1
|
|
431
|
+
docker_reasons.append(f"RAM sufficient ({total_ram} GB) (+1)")
|
|
432
|
+
else:
|
|
433
|
+
docker_reasons.append("Docker daemon stopped (0)")
|
|
434
|
+
else:
|
|
435
|
+
docker_reasons.append("Docker CLI not installed (0)")
|
|
436
|
+
|
|
437
|
+
# Workload-Aware Capability Scoring adjustments
|
|
438
|
+
if workload == "rev-pwn":
|
|
439
|
+
if os_info["is_windows"] and wsl_info["available"]:
|
|
440
|
+
wsl_score += 2
|
|
441
|
+
wsl_reasons.append("Workload 'rev-pwn': native ELF debugging & GDB kernel syscalls (+2)")
|
|
442
|
+
elif os_info["is_linux"]:
|
|
443
|
+
docker_score += 1
|
|
444
|
+
docker_reasons.append("Workload 'rev-pwn': native Linux execution (+1)")
|
|
445
|
+
elif workload == "security-lab":
|
|
446
|
+
if docker_info["daemon_running"]:
|
|
447
|
+
docker_score += 2
|
|
448
|
+
docker_reasons.append("Workload 'security-lab': Docker containment sandbox for research (+2)")
|
|
449
|
+
elif workload == "live-ctf":
|
|
450
|
+
if wsl_info["available"] and wsl_info["kali_running"]:
|
|
451
|
+
wsl_score += 1
|
|
452
|
+
wsl_reasons.append("Workload 'live-ctf': Zero spin-up latency with active Kali instance (+1)")
|
|
453
|
+
elif workload == "full":
|
|
454
|
+
if free_disk < 30.0:
|
|
455
|
+
wsl_score -= 1
|
|
456
|
+
docker_score -= 1
|
|
457
|
+
wsl_reasons.append(f"Workload 'full': high storage footprint ({free_disk} GB free) (-1)")
|
|
458
|
+
docker_reasons.append(f"Workload 'full': high storage footprint ({free_disk} GB free) (-1)")
|
|
459
|
+
|
|
460
|
+
# Decision synthesis
|
|
461
|
+
if os_info["is_windows"]:
|
|
462
|
+
if wsl_score >= docker_score and wsl_info["available"]:
|
|
463
|
+
recommended = "wsl"
|
|
464
|
+
distro_target = "kali-linux" if wsl_info["kali_present"] else "kali-linux"
|
|
465
|
+
if wsl_info["kali_present"]:
|
|
466
|
+
reason = "Native Windows + Kali Linux distro verified in WSL2"
|
|
467
|
+
else:
|
|
468
|
+
reason = "Windows detected with WSL available; Kali Linux installation recommended"
|
|
469
|
+
elif docker_score > wsl_score and docker_info["daemon_running"]:
|
|
470
|
+
recommended = "docker"
|
|
471
|
+
distro_target = ""
|
|
472
|
+
reason = "Docker daemon is active and scored higher than WSL"
|
|
473
|
+
elif wsl_info["available"]:
|
|
474
|
+
recommended = "wsl"
|
|
475
|
+
distro_target = "kali-linux"
|
|
476
|
+
reason = "WSL is available and Docker daemon is stopped"
|
|
477
|
+
elif docker_info["available"]:
|
|
478
|
+
recommended = "docker"
|
|
479
|
+
distro_target = ""
|
|
480
|
+
reason = "Docker CLI is present; start daemon to proceed"
|
|
481
|
+
else:
|
|
482
|
+
recommended = "manual"
|
|
483
|
+
distro_target = ""
|
|
484
|
+
reason = "Neither WSL nor Docker is currently ready"
|
|
485
|
+
else:
|
|
486
|
+
if docker_info["daemon_running"]:
|
|
487
|
+
recommended = "docker"
|
|
488
|
+
distro_target = ""
|
|
489
|
+
reason = "Native container runtime active for Unix platform"
|
|
490
|
+
elif os_info["is_linux"]:
|
|
491
|
+
recommended = "host"
|
|
492
|
+
distro_target = ""
|
|
493
|
+
reason = "Native Linux host execution available directly"
|
|
494
|
+
else:
|
|
495
|
+
recommended = "docker"
|
|
496
|
+
distro_target = ""
|
|
497
|
+
reason = "Docker runtime recommended for macOS/Linux"
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
"wsl_score": max(0, wsl_score),
|
|
501
|
+
"docker_score": max(0, docker_score),
|
|
502
|
+
"recommended": recommended,
|
|
503
|
+
"distro_target": distro_target,
|
|
504
|
+
"reason": reason,
|
|
505
|
+
"wsl_factors": wsl_reasons,
|
|
506
|
+
"docker_factors": docker_reasons,
|
|
507
|
+
"workload": workload,
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# ---------------------------------------------------------------------------
|
|
512
|
+
# Workspace Provisioning & Initialization
|
|
513
|
+
# ---------------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
class WorkspaceProvisioner:
|
|
516
|
+
"""Configures workspace scaffolding, templates, and backend toolchain."""
|
|
517
|
+
|
|
518
|
+
@staticmethod
|
|
519
|
+
def setup_templates(workspace_path: Path):
|
|
520
|
+
"""Creates standard resources/, solve.py, and .env.example files."""
|
|
521
|
+
resources_dir = workspace_path / "resources"
|
|
522
|
+
resources_dir.mkdir(parents=True, exist_ok=True)
|
|
523
|
+
|
|
524
|
+
notes_dir = workspace_path / "notes"
|
|
525
|
+
notes_dir.mkdir(parents=True, exist_ok=True)
|
|
526
|
+
|
|
527
|
+
env_example = workspace_path / ".env.example"
|
|
528
|
+
if not env_example.exists():
|
|
529
|
+
env_content = (
|
|
530
|
+
"# CTF-Agent Target & Competition Credentials\n"
|
|
531
|
+
"CTFD_URL=https://ctf.example.com\n"
|
|
532
|
+
"CTFD_TOKEN=your_ctfd_token_here\n"
|
|
533
|
+
"TARGET_HOST=127.0.0.1\n"
|
|
534
|
+
"TARGET_PORT=1337\n"
|
|
535
|
+
"TARGET_URL=http://127.0.0.1:8000\n"
|
|
536
|
+
)
|
|
537
|
+
env_example.write_text(env_content, encoding="utf-8")
|
|
538
|
+
print(" [+] Created .env.example template")
|
|
539
|
+
|
|
540
|
+
solve_py = workspace_path / "solve.py"
|
|
541
|
+
if not solve_py.exists():
|
|
542
|
+
solve_content = (
|
|
543
|
+
"#!/usr/bin/env python3\n"
|
|
544
|
+
"# ===========================================================================\n"
|
|
545
|
+
"# CTF Educational PoC & Vulnerability Verification Script\n"
|
|
546
|
+
"# Scope: Authorized CTF sandbox challenge\n"
|
|
547
|
+
"# ===========================================================================\n"
|
|
548
|
+
"import sys\n"
|
|
549
|
+
"import os\n"
|
|
550
|
+
"\n"
|
|
551
|
+
"def solve():\n"
|
|
552
|
+
" print('[*] Initializing verification sequence...')\n"
|
|
553
|
+
" # Implementation here\n"
|
|
554
|
+
"\n"
|
|
555
|
+
"if __name__ == '__main__':\n"
|
|
556
|
+
" solve()\n"
|
|
557
|
+
)
|
|
558
|
+
solve_py.write_text(solve_content, encoding="utf-8")
|
|
559
|
+
print(" [+] Created solve.py template")
|
|
560
|
+
|
|
561
|
+
@classmethod
|
|
562
|
+
def provision(
|
|
563
|
+
cls,
|
|
564
|
+
workspace_path: Path,
|
|
565
|
+
backend: str,
|
|
566
|
+
distro: str,
|
|
567
|
+
profiles: str,
|
|
568
|
+
use_symlink: bool = False,
|
|
569
|
+
force: bool = False,
|
|
570
|
+
skip_toolchain: bool = False,
|
|
571
|
+
) -> bool:
|
|
572
|
+
print(f"\n[*] Initializing workspace at: {workspace_path.resolve()}")
|
|
573
|
+
|
|
574
|
+
# 1. Deploy .agents/ and configuration
|
|
575
|
+
if install_as_agent:
|
|
576
|
+
install_as_agent.deploy_to_workspace(
|
|
577
|
+
workspace_path,
|
|
578
|
+
use_symlink=use_symlink,
|
|
579
|
+
force=force,
|
|
580
|
+
setup_workspace=True,
|
|
581
|
+
install_wsl=(backend == "wsl" and not skip_toolchain),
|
|
582
|
+
wsl_profile=profiles,
|
|
583
|
+
wsl_distro=distro,
|
|
584
|
+
)
|
|
585
|
+
else:
|
|
586
|
+
print("[!] Error: install_as_agent module unavailable")
|
|
587
|
+
return False
|
|
588
|
+
|
|
589
|
+
# 2. Setup standard challenge templates
|
|
590
|
+
cls.setup_templates(workspace_path)
|
|
591
|
+
|
|
592
|
+
# 3. Handle Docker backend guidance if selected
|
|
593
|
+
if backend == "docker" and not skip_toolchain:
|
|
594
|
+
cls.provision_docker(workspace_path, profiles)
|
|
595
|
+
|
|
596
|
+
return True
|
|
597
|
+
|
|
598
|
+
@classmethod
|
|
599
|
+
def provision_docker(cls, workspace_path: Path, profiles: str, dry_run: bool = False):
|
|
600
|
+
print(f"\n[*] Provisioning Docker backend for workspace...")
|
|
601
|
+
compose_file = REPO_ROOT / "docker-compose.yml"
|
|
602
|
+
dockerfile = REPO_ROOT / "Dockerfile"
|
|
603
|
+
target_compose = workspace_path / "docker-compose.yml"
|
|
604
|
+
|
|
605
|
+
if compose_file.exists() and not target_compose.exists():
|
|
606
|
+
shutil.copy2(compose_file, target_compose)
|
|
607
|
+
print(f" [+] Copied docker-compose.yml -> {target_compose}")
|
|
608
|
+
|
|
609
|
+
if dockerfile.exists() and not (workspace_path / "Dockerfile").exists():
|
|
610
|
+
shutil.copy2(dockerfile, workspace_path / "Dockerfile")
|
|
611
|
+
print(f" [+] Copied Dockerfile -> {workspace_path / 'Dockerfile'}")
|
|
612
|
+
|
|
613
|
+
if dry_run:
|
|
614
|
+
print(" [*] Dry-run enabled: skipping container execution.")
|
|
615
|
+
return
|
|
616
|
+
|
|
617
|
+
docker_bin = shutil.which("docker")
|
|
618
|
+
if not docker_bin:
|
|
619
|
+
print(" [!] Docker CLI not detected on system PATH.")
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
# 1. Validate compose configuration
|
|
623
|
+
try:
|
|
624
|
+
res_cfg = subprocess.run(
|
|
625
|
+
[docker_bin, "compose", "-f", str(target_compose), "config"],
|
|
626
|
+
capture_output=True,
|
|
627
|
+
text=True,
|
|
628
|
+
check=False,
|
|
629
|
+
timeout=6,
|
|
630
|
+
)
|
|
631
|
+
if res_cfg.returncode == 0:
|
|
632
|
+
print(" [+] Validated docker compose configuration syntax")
|
|
633
|
+
else:
|
|
634
|
+
print(f" [!] Compose config warning: {res_cfg.stderr.strip()[:120]}")
|
|
635
|
+
except Exception:
|
|
636
|
+
pass
|
|
637
|
+
|
|
638
|
+
# 2. Check docker daemon and test container readiness
|
|
639
|
+
try:
|
|
640
|
+
res_info = subprocess.run(
|
|
641
|
+
[docker_bin, "info"],
|
|
642
|
+
capture_output=True,
|
|
643
|
+
check=False,
|
|
644
|
+
timeout=4,
|
|
645
|
+
)
|
|
646
|
+
if res_info.returncode == 0:
|
|
647
|
+
print(" [*] Docker daemon is active. Verifying container readiness...")
|
|
648
|
+
test_cmd = [
|
|
649
|
+
docker_bin, "compose", "-f", str(target_compose),
|
|
650
|
+
"run", "--rm", "ctf-agent", "python3", "--version"
|
|
651
|
+
]
|
|
652
|
+
test_run = subprocess.run(test_cmd, capture_output=True, text=True, check=False, timeout=12)
|
|
653
|
+
if test_run.returncode == 0:
|
|
654
|
+
print(f" [OK] Docker CTF container ready: {test_run.stdout.strip()}")
|
|
655
|
+
else:
|
|
656
|
+
print(" [*] Container image not yet built. To build:")
|
|
657
|
+
print(f" docker compose -f '{target_compose}' build")
|
|
658
|
+
else:
|
|
659
|
+
print(" [!] Docker daemon is not running. Please start Docker to activate container sandbox.")
|
|
660
|
+
except Exception as e:
|
|
661
|
+
print(f" [!] Docker readiness check skipped: {e}")
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
# ---------------------------------------------------------------------------
|
|
665
|
+
# Health Check Runner
|
|
666
|
+
# ---------------------------------------------------------------------------
|
|
667
|
+
|
|
668
|
+
class HealthCheckRunner:
|
|
669
|
+
"""Verifies workspace file integrity, agent components, and backend availability."""
|
|
670
|
+
|
|
671
|
+
@staticmethod
|
|
672
|
+
def check_workspace(workspace_path: Path, backend: str, distro: str) -> Dict[str, Any]:
|
|
673
|
+
results: Dict[str, Any] = {
|
|
674
|
+
"workspace": str(workspace_path.resolve()),
|
|
675
|
+
"backend": backend,
|
|
676
|
+
"checks": [],
|
|
677
|
+
"all_passed": True,
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
def record(check_name: str, passed: bool, detail: str):
|
|
681
|
+
results["checks"].append({
|
|
682
|
+
"name": check_name,
|
|
683
|
+
"passed": passed,
|
|
684
|
+
"detail": detail,
|
|
685
|
+
})
|
|
686
|
+
if not passed:
|
|
687
|
+
results["all_passed"] = False
|
|
688
|
+
|
|
689
|
+
dot_agents = workspace_path / ".agents"
|
|
690
|
+
record("dot_agents_directory", dot_agents.is_dir(), f"Checked {dot_agents}")
|
|
691
|
+
|
|
692
|
+
agents_md = workspace_path / "AGENTS.md"
|
|
693
|
+
record("workspace_agents_md", agents_md.is_file(), f"Checked {agents_md}")
|
|
694
|
+
|
|
695
|
+
skills_lock = workspace_path / "skills-lock.json"
|
|
696
|
+
record("skills_lock_file", skills_lock.is_file(), f"Checked {skills_lock}")
|
|
697
|
+
|
|
698
|
+
controller_md = dot_agents / "agents" / "ctf-controller.md"
|
|
699
|
+
record("agent_ctf_controller", controller_md.is_file(), f"Checked {controller_md}")
|
|
700
|
+
|
|
701
|
+
speedrun_md = dot_agents / "agents" / "ctf-speedrun.md"
|
|
702
|
+
record("agent_ctf_speedrun", speedrun_md.is_file(), f"Checked {speedrun_md}")
|
|
703
|
+
|
|
704
|
+
analyzer_md = dot_agents / "agents" / "ctf-analyzer.md"
|
|
705
|
+
record("agent_ctf_analyzer", analyzer_md.is_file(), f"Checked {analyzer_md}")
|
|
706
|
+
|
|
707
|
+
scripts_dir = dot_agents / "scripts"
|
|
708
|
+
record("scripts_present", scripts_dir.is_dir() and any(scripts_dir.iterdir()), f"Checked {scripts_dir}")
|
|
709
|
+
|
|
710
|
+
if backend == "wsl":
|
|
711
|
+
try:
|
|
712
|
+
test_cmd = ["wsl", "-d", distro, "bash", "-c", "python3 --version"]
|
|
713
|
+
res = subprocess.run(test_cmd, capture_output=True, text=True, check=False, timeout=5)
|
|
714
|
+
wsl_ok = res.returncode == 0
|
|
715
|
+
record("wsl_kali_python", wsl_ok, f"Output: {res.stdout.strip() if wsl_ok else res.stderr.strip()}")
|
|
716
|
+
except Exception as e:
|
|
717
|
+
record("wsl_kali_python", False, f"WSL execution error: {e}")
|
|
718
|
+
|
|
719
|
+
elif backend == "docker":
|
|
720
|
+
docker_bin = shutil.which("docker")
|
|
721
|
+
if docker_bin:
|
|
722
|
+
try:
|
|
723
|
+
res = subprocess.run([docker_bin, "info"], capture_output=True, text=True, check=False, timeout=5)
|
|
724
|
+
record("docker_daemon_active", res.returncode == 0, "Docker daemon connection verified" if res.returncode == 0 else "Daemon stopped")
|
|
725
|
+
except Exception as e:
|
|
726
|
+
record("docker_daemon_active", False, f"Docker query error: {e}")
|
|
727
|
+
else:
|
|
728
|
+
record("docker_daemon_active", False, "docker CLI not found in PATH")
|
|
729
|
+
|
|
730
|
+
return results
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
# ---------------------------------------------------------------------------
|
|
734
|
+
# Presentation & CLI Formatting Helpers
|
|
735
|
+
# ---------------------------------------------------------------------------
|
|
736
|
+
|
|
737
|
+
def print_banner(env: Dict[str, Any], scoring: Dict[str, Any]):
|
|
738
|
+
os_info = env["os"]
|
|
739
|
+
mem_info = env["memory"]
|
|
740
|
+
disk_info = env["disk"]
|
|
741
|
+
wsl_info = env["wsl"]
|
|
742
|
+
docker_info = env["docker"]
|
|
743
|
+
|
|
744
|
+
print("=================================================================")
|
|
745
|
+
print("CTF-AGENT ENVIRONMENT PREFLIGHT & WORKSPACE INITIALIZER")
|
|
746
|
+
print("=================================================================")
|
|
747
|
+
print(f"Platform : {os_info['system']} {os_info['release']} ({os_info['arch']})")
|
|
748
|
+
print(f"Hardware : {os_info['cpu_cores']} CPU cores | {mem_info['total_gb']} GB RAM")
|
|
749
|
+
print(f"Storage : {disk_info['free_gb']} GB Free / {disk_info['total_gb']} GB Total")
|
|
750
|
+
ws_status = "Existing .agents/ configuration found" if env.get("existing_agents", False) else "Fresh directory (clean)"
|
|
751
|
+
print(f"Workspace: {ws_status}")
|
|
752
|
+
print("-----------------------------------------------------------------")
|
|
753
|
+
print("Detected Backends:")
|
|
754
|
+
|
|
755
|
+
if os_info["is_windows"]:
|
|
756
|
+
if wsl_info["available"]:
|
|
757
|
+
kali_status = "READY & RUNNING" if wsl_info["kali_running"] else ("INSTALLED" if wsl_info["kali_present"] else "MISSING")
|
|
758
|
+
print(f" [+] WSL2 : Available ({len(wsl_info['distros'])} distros)")
|
|
759
|
+
print(f" [{'+' if wsl_info['kali_present'] else '!'}] Kali Linux : {kali_status}")
|
|
760
|
+
else:
|
|
761
|
+
print(f" [-] WSL2 : Unavailable ({wsl_info.get('reason', 'N/A')})")
|
|
762
|
+
else:
|
|
763
|
+
print(f" [*] Host Native : {os_info['system']} Linux/Unix Environment")
|
|
764
|
+
|
|
765
|
+
if docker_info["available"]:
|
|
766
|
+
doc_status = "RUNNING" if docker_info["daemon_running"] else "STOPPED"
|
|
767
|
+
print(f" [{'+' if docker_info['daemon_running'] else '!'}] Docker : {doc_status} ({docker_info.get('version', 'CLI found')})")
|
|
768
|
+
else:
|
|
769
|
+
print(" [-] Docker : Not Installed")
|
|
770
|
+
|
|
771
|
+
print("-----------------------------------------------------------------")
|
|
772
|
+
print("Backend Capability Evaluation:")
|
|
773
|
+
print(f" WSL Score : {scoring['wsl_score']:2d} / 12 (Factors: {len(scoring['wsl_factors'])})")
|
|
774
|
+
print(f" Docker Score : {scoring['docker_score']:2d} / 12 (Factors: {len(scoring['docker_factors'])})")
|
|
775
|
+
print(f" Recommended : {scoring['recommended'].upper()} {'(' + scoring['distro_target'] + ')' if scoring['distro_target'] else ''}")
|
|
776
|
+
print(f" Rationale : {scoring['reason']}")
|
|
777
|
+
print("=================================================================")
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def print_health_report(report: Dict[str, Any]):
|
|
781
|
+
print("\n=================================================================")
|
|
782
|
+
print("WORKSPACE HEALTH VERIFICATION REPORT")
|
|
783
|
+
print("=================================================================")
|
|
784
|
+
for c in report["checks"]:
|
|
785
|
+
status = "[PASS]" if c["passed"] else "[FAIL]"
|
|
786
|
+
print(f" {status} {c['name']:<25} : {c['detail']}")
|
|
787
|
+
print("-----------------------------------------------------------------")
|
|
788
|
+
if report["all_passed"]:
|
|
789
|
+
print("[OK] Workspace initialized successfully! All components verified.")
|
|
790
|
+
else:
|
|
791
|
+
print("[!] Workspace initialized with warnings or missing components.")
|
|
792
|
+
print("=================================================================")
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
# ---------------------------------------------------------------------------
|
|
796
|
+
# Main CLI Entrypoint
|
|
797
|
+
# ---------------------------------------------------------------------------
|
|
798
|
+
|
|
799
|
+
def main():
|
|
800
|
+
parser = argparse.ArgumentParser(
|
|
801
|
+
description="CTF-Agent Preflight Environment Detector & Workspace Initializer"
|
|
802
|
+
)
|
|
803
|
+
parser.add_argument(
|
|
804
|
+
"workspace",
|
|
805
|
+
nargs="?",
|
|
806
|
+
default=".",
|
|
807
|
+
help="Target workspace directory (default: current directory .)",
|
|
808
|
+
)
|
|
809
|
+
parser.add_argument(
|
|
810
|
+
"--auto",
|
|
811
|
+
action="store_true",
|
|
812
|
+
help="Run non-interactively, automatically accepting recommended backend and profile",
|
|
813
|
+
)
|
|
814
|
+
parser.add_argument(
|
|
815
|
+
"--backend",
|
|
816
|
+
choices=["wsl", "docker", "host", "none"],
|
|
817
|
+
help="Explicitly override execution backend",
|
|
818
|
+
)
|
|
819
|
+
parser.add_argument(
|
|
820
|
+
"--distro",
|
|
821
|
+
default="kali-linux",
|
|
822
|
+
help="WSL distribution target (default: kali-linux)",
|
|
823
|
+
)
|
|
824
|
+
parser.add_argument(
|
|
825
|
+
"--purpose",
|
|
826
|
+
choices=list(PURPOSE_PROFILES.keys()),
|
|
827
|
+
default="live-ctf",
|
|
828
|
+
help="Workload purpose: live-ctf, security-lab, rev-pwn, full (default: live-ctf)",
|
|
829
|
+
)
|
|
830
|
+
parser.add_argument(
|
|
831
|
+
"--profile",
|
|
832
|
+
help="Explicit comma-separated toolchain profiles (overrides purpose preset)",
|
|
833
|
+
)
|
|
834
|
+
parser.add_argument(
|
|
835
|
+
"--symlink",
|
|
836
|
+
action="store_true",
|
|
837
|
+
help="Use symlinks/junctions for .agents/ deployment",
|
|
838
|
+
)
|
|
839
|
+
parser.add_argument(
|
|
840
|
+
"--force",
|
|
841
|
+
"-f",
|
|
842
|
+
action="store_true",
|
|
843
|
+
help="Overwrite existing workspace configuration",
|
|
844
|
+
)
|
|
845
|
+
parser.add_argument(
|
|
846
|
+
"--skip-toolchain",
|
|
847
|
+
action="store_true",
|
|
848
|
+
help="Deploy agent workspace without executing backend toolchain installations",
|
|
849
|
+
)
|
|
850
|
+
parser.add_argument(
|
|
851
|
+
"--dry-run",
|
|
852
|
+
action="store_true",
|
|
853
|
+
help="Perform preflight checks and output recommendation without modifying workspace",
|
|
854
|
+
)
|
|
855
|
+
parser.add_argument(
|
|
856
|
+
"--json",
|
|
857
|
+
action="store_true",
|
|
858
|
+
help="Output preflight detection, scoring, and plan as JSON",
|
|
859
|
+
)
|
|
860
|
+
parser.add_argument(
|
|
861
|
+
"--check-only",
|
|
862
|
+
action="store_true",
|
|
863
|
+
help="Run health check on existing workspace without modifying it",
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
args = parser.parse_args()
|
|
867
|
+
ws_path = Path(args.workspace).resolve()
|
|
868
|
+
|
|
869
|
+
# Preflight Detection
|
|
870
|
+
env = EnvironmentDetector.run_preflight(ws_path)
|
|
871
|
+
scoring = CapabilityScoringEngine.calculate_scores(env, workload=args.purpose)
|
|
872
|
+
|
|
873
|
+
# Health check only mode
|
|
874
|
+
if args.check_only:
|
|
875
|
+
backend_target = args.backend or scoring["recommended"]
|
|
876
|
+
distro_target = args.distro or scoring["distro_target"] or "kali-linux"
|
|
877
|
+
report = HealthCheckRunner.check_workspace(ws_path, backend_target, distro_target)
|
|
878
|
+
if args.json:
|
|
879
|
+
print(json.dumps(report, indent=2))
|
|
880
|
+
else:
|
|
881
|
+
print_health_report(report)
|
|
882
|
+
sys.exit(0 if report["all_passed"] else 1)
|
|
883
|
+
|
|
884
|
+
# JSON output mode
|
|
885
|
+
if args.json:
|
|
886
|
+
payload = {
|
|
887
|
+
"environment": env,
|
|
888
|
+
"scoring": scoring,
|
|
889
|
+
"purpose_profiles": PURPOSE_PROFILES,
|
|
890
|
+
"selected_purpose": args.purpose,
|
|
891
|
+
"selected_profiles": args.profile or PURPOSE_PROFILES[args.purpose]["profiles"],
|
|
892
|
+
}
|
|
893
|
+
print(json.dumps(payload, indent=2))
|
|
894
|
+
sys.exit(0)
|
|
895
|
+
|
|
896
|
+
# Print summary banner
|
|
897
|
+
print_banner(env, scoring)
|
|
898
|
+
|
|
899
|
+
# Dry-run mode
|
|
900
|
+
if args.dry_run:
|
|
901
|
+
target_backend = args.backend or scoring["recommended"]
|
|
902
|
+
target_distro = args.distro if target_backend == "wsl" else ""
|
|
903
|
+
selected_prof = args.profile or PURPOSE_PROFILES[args.purpose]["profiles"]
|
|
904
|
+
print("\n[*] Dry-run mode enabled. Planned actions:")
|
|
905
|
+
print(f" Target Workspace : {ws_path}")
|
|
906
|
+
print(f" Selected Backend : {target_backend}")
|
|
907
|
+
if target_distro:
|
|
908
|
+
print(f" WSL Distro : {target_distro}")
|
|
909
|
+
print(f" Workload Purpose : {args.purpose} ({PURPOSE_PROFILES[args.purpose]['title']})")
|
|
910
|
+
print(f" Tool Profiles : {selected_prof}")
|
|
911
|
+
print(f" Skip Toolchain : {args.skip_toolchain}")
|
|
912
|
+
if args.auto or not sys.stdin.isatty():
|
|
913
|
+
mode_desc = "Automated flag (--auto)" if args.auto else "Non-interactive environment (headless/non-TTY)"
|
|
914
|
+
print(f" Execution Mode : {mode_desc}")
|
|
915
|
+
print("\n[OK] Preflight checks passed without modifications.")
|
|
916
|
+
sys.exit(0)
|
|
917
|
+
|
|
918
|
+
# Interactive vs Non-interactive selection
|
|
919
|
+
chosen_backend = args.backend or scoring["recommended"]
|
|
920
|
+
chosen_distro = args.distro or scoring["distro_target"] or "kali-linux"
|
|
921
|
+
chosen_purpose = args.purpose
|
|
922
|
+
chosen_profiles = args.profile or PURPOSE_PROFILES[chosen_purpose]["profiles"]
|
|
923
|
+
chosen_skip_toolchain = args.skip_toolchain
|
|
924
|
+
|
|
925
|
+
if not args.auto and sys.stdin.isatty():
|
|
926
|
+
while True:
|
|
927
|
+
print("\nAvailable Actions:")
|
|
928
|
+
rec_label = f"Recommended ({scoring['recommended'].upper()})"
|
|
929
|
+
tool_status = "DISABLED (Agent workspace only)" if chosen_skip_toolchain else f"ENABLED ({chosen_profiles})"
|
|
930
|
+
print(f" [1] Continue with {rec_label} [{chosen_backend.upper()}]")
|
|
931
|
+
print(" [2] Switch backend (WSL <-> Docker)")
|
|
932
|
+
print(" [3] Select workload purpose (Live CTF, Lab, Rev/Pwn, Full)")
|
|
933
|
+
print(f" [4] Toggle backend toolchain installation (Currently: {tool_status})")
|
|
934
|
+
print(" [5] Exit")
|
|
935
|
+
|
|
936
|
+
try:
|
|
937
|
+
choice = input("\nSelect option [1-5] (default: 1): ").strip()
|
|
938
|
+
except (KeyboardInterrupt, EOFError):
|
|
939
|
+
print("\n[!] Operation cancelled by user.")
|
|
940
|
+
sys.exit(0)
|
|
941
|
+
|
|
942
|
+
if not choice or choice == "1":
|
|
943
|
+
break
|
|
944
|
+
elif choice == "2":
|
|
945
|
+
chosen_backend = "docker" if chosen_backend == "wsl" else "wsl"
|
|
946
|
+
print(f"[*] Backend switched to: {chosen_backend.upper()}")
|
|
947
|
+
elif choice == "3":
|
|
948
|
+
print("\nSelect Workload Purpose:")
|
|
949
|
+
purpose_keys = list(PURPOSE_PROFILES.keys())
|
|
950
|
+
for idx, k in enumerate(purpose_keys, 1):
|
|
951
|
+
p = PURPOSE_PROFILES[k]
|
|
952
|
+
print(f" [{idx}] {p['title']}")
|
|
953
|
+
print(f" Profiles: {p['profiles']}")
|
|
954
|
+
try:
|
|
955
|
+
p_choice = input(f"\nSelect purpose [1-{len(purpose_keys)}] (default: 1): ").strip()
|
|
956
|
+
if p_choice.isdigit() and 1 <= int(p_choice) <= len(purpose_keys):
|
|
957
|
+
chosen_purpose = purpose_keys[int(p_choice) - 1]
|
|
958
|
+
chosen_profiles = PURPOSE_PROFILES[chosen_purpose]["profiles"]
|
|
959
|
+
print(f"[*] Selected purpose: {PURPOSE_PROFILES[chosen_purpose]['title']}")
|
|
960
|
+
except (KeyboardInterrupt, EOFError):
|
|
961
|
+
pass
|
|
962
|
+
elif choice == "4":
|
|
963
|
+
chosen_skip_toolchain = not chosen_skip_toolchain
|
|
964
|
+
new_state = "DISABLED (Agent workspace only)" if chosen_skip_toolchain else "ENABLED"
|
|
965
|
+
print(f"[*] Toolchain installation is now: {new_state}")
|
|
966
|
+
elif choice == "5":
|
|
967
|
+
print("[*] Exiting without changes.")
|
|
968
|
+
sys.exit(0)
|
|
969
|
+
else:
|
|
970
|
+
env_mode = "Automated flag (--auto)" if args.auto else "Non-interactive environment detected (headless/non-TTY)"
|
|
971
|
+
print(f"\n[*] {env_mode}.")
|
|
972
|
+
print(f" Auto-selected backend : {chosen_backend.upper()} ({chosen_distro})")
|
|
973
|
+
print(f" Workload purpose : {chosen_purpose} ({PURPOSE_PROFILES[chosen_purpose]['title']})")
|
|
974
|
+
print(f" Toolchain installation : {'DISABLED' if chosen_skip_toolchain else 'ENABLED (' + chosen_profiles + ')'}")
|
|
975
|
+
|
|
976
|
+
# Check for existing workspace directory (.agents or legacy .agent)
|
|
977
|
+
dot_agents = ws_path / ".agents"
|
|
978
|
+
dot_agent_legacy = ws_path / ".agent"
|
|
979
|
+
existing_agents_found = dot_agents.exists() or dot_agent_legacy.exists()
|
|
980
|
+
effective_force = args.force
|
|
981
|
+
|
|
982
|
+
if existing_agents_found and not args.force:
|
|
983
|
+
if not args.auto and sys.stdin.isatty():
|
|
984
|
+
print("\n[!] CONFLICT / UPDATE DETECTED:")
|
|
985
|
+
target_name = ".agents/" if dot_agents.exists() else ".agent/ (legacy)"
|
|
986
|
+
print(f" Found existing {target_name} in target workspace: {ws_path}")
|
|
987
|
+
print(" Overwriting will refresh agent skills, rules, and scripts, while")
|
|
988
|
+
print(" preserving your existing challenge files (resources/, notes/, solve.py).")
|
|
989
|
+
try:
|
|
990
|
+
ov_choice = input(" Proceed to update/overwrite agent framework? [y/N]: ").strip().lower()
|
|
991
|
+
if ov_choice in ("y", "yes"):
|
|
992
|
+
effective_force = True
|
|
993
|
+
print(" [+] Overwrite confirmed. Updating agent framework...")
|
|
994
|
+
else:
|
|
995
|
+
print(" [*] Operation cancelled by user. Existing files left intact.")
|
|
996
|
+
sys.exit(0)
|
|
997
|
+
except (KeyboardInterrupt, EOFError):
|
|
998
|
+
print("\n[!] Operation cancelled by user.")
|
|
999
|
+
sys.exit(0)
|
|
1000
|
+
else:
|
|
1001
|
+
print(f"\n[!] Error: Target workspace already contains .agents/: {ws_path}")
|
|
1002
|
+
print(" Re-run with --force (-f) to overwrite or update agent configuration.")
|
|
1003
|
+
sys.exit(1)
|
|
1004
|
+
|
|
1005
|
+
# Execute Provisioning
|
|
1006
|
+
success = WorkspaceProvisioner.provision(
|
|
1007
|
+
workspace_path=ws_path,
|
|
1008
|
+
backend=chosen_backend,
|
|
1009
|
+
distro=chosen_distro,
|
|
1010
|
+
profiles=chosen_profiles,
|
|
1011
|
+
use_symlink=args.symlink,
|
|
1012
|
+
force=effective_force,
|
|
1013
|
+
skip_toolchain=chosen_skip_toolchain,
|
|
1014
|
+
)
|
|
1015
|
+
|
|
1016
|
+
if not success:
|
|
1017
|
+
print("[!] Workspace provisioning failed.")
|
|
1018
|
+
sys.exit(1)
|
|
1019
|
+
|
|
1020
|
+
# Post-provision Health Check
|
|
1021
|
+
health_report = HealthCheckRunner.check_workspace(ws_path, chosen_backend, chosen_distro)
|
|
1022
|
+
print_health_report(health_report)
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
if __name__ == "__main__":
|
|
1026
|
+
main()
|