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.
Files changed (152) hide show
  1. package/AGENTS.md +131 -0
  2. package/Dockerfile +192 -0
  3. package/README.md +522 -0
  4. package/agents/ctf-analyzer.md +58 -0
  5. package/agents/ctf-controller.md +151 -0
  6. package/agents/ctf-speedrun.md +74 -0
  7. package/bin/ctf-agent.js +119 -0
  8. package/docker-compose.yml +13 -0
  9. package/mcp_config.json +19 -0
  10. package/package.json +44 -0
  11. package/references/ctf-triage-ladder.md +68 -0
  12. package/references/exploit-databases.md +575 -0
  13. package/references/llm-safety-and-policy-compliance.md +173 -0
  14. package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
  15. package/references/security-events-and-intelligence.md +566 -0
  16. package/references/version-matrix.md +59 -0
  17. package/rules/ctf-execution-rules.md +44 -0
  18. package/rules/ctf-safety-framing-rules.md +140 -0
  19. package/rules/flag-validation-rules.md +25 -0
  20. package/scripts/__init__.py +1 -0
  21. package/scripts/ctf-tools.lock +205 -0
  22. package/scripts/ctf_init.py +1026 -0
  23. package/scripts/ctf_update.py +583 -0
  24. package/scripts/ctfd_client.py +81 -0
  25. package/scripts/cve_lookup.py +453 -0
  26. package/scripts/extract_flags.py +155 -0
  27. package/scripts/install_as_agent.py +278 -0
  28. package/scripts/install_ctf_tools.sh +603 -0
  29. package/scripts/parallel_triage.py +395 -0
  30. package/scripts/prompt_policy_sanitizer.py +283 -0
  31. package/scripts/scope_guard.py +444 -0
  32. package/scripts/skill_validator.py +504 -0
  33. package/scripts/workspace_cleaner.py +141 -0
  34. package/skills/ctf-ai-ml/SKILL.md +120 -0
  35. package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
  36. package/skills/ctf-ai-ml/llm-attacks.md +487 -0
  37. package/skills/ctf-ai-ml/model-attacks.md +422 -0
  38. package/skills/ctf-crypto/SKILL.md +320 -0
  39. package/skills/ctf-crypto/advanced-math.md +798 -0
  40. package/skills/ctf-crypto/classic-ciphers.md +651 -0
  41. package/skills/ctf-crypto/ecc-attacks.md +347 -0
  42. package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
  43. package/skills/ctf-crypto/exotic-crypto.md +528 -0
  44. package/skills/ctf-crypto/historical.md +113 -0
  45. package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
  46. package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
  47. package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
  48. package/skills/ctf-crypto/modern-ciphers.md +649 -0
  49. package/skills/ctf-crypto/prng-attacks.md +257 -0
  50. package/skills/ctf-crypto/prng.md +664 -0
  51. package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
  52. package/skills/ctf-crypto/rsa-attacks.md +487 -0
  53. package/skills/ctf-crypto/stream-ciphers.md +390 -0
  54. package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
  55. package/skills/ctf-forensics/3d-printing.md +121 -0
  56. package/skills/ctf-forensics/SKILL.md +379 -0
  57. package/skills/ctf-forensics/disk-advanced.md +497 -0
  58. package/skills/ctf-forensics/disk-and-memory.md +491 -0
  59. package/skills/ctf-forensics/disk-recovery.md +699 -0
  60. package/skills/ctf-forensics/linux-forensics.md +511 -0
  61. package/skills/ctf-forensics/network-advanced.md +583 -0
  62. package/skills/ctf-forensics/network.md +645 -0
  63. package/skills/ctf-forensics/peripheral-capture.md +287 -0
  64. package/skills/ctf-forensics/signals-and-hardware.md +713 -0
  65. package/skills/ctf-forensics/steganography.md +694 -0
  66. package/skills/ctf-forensics/stego-advanced-2.md +475 -0
  67. package/skills/ctf-forensics/stego-advanced.md +481 -0
  68. package/skills/ctf-forensics/stego-image.md +691 -0
  69. package/skills/ctf-forensics/windows.md +625 -0
  70. package/skills/ctf-malware/SKILL.md +181 -0
  71. package/skills/ctf-malware/c2-and-protocols.md +274 -0
  72. package/skills/ctf-malware/pe-and-dotnet.md +108 -0
  73. package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
  74. package/skills/ctf-misc/SKILL.md +498 -0
  75. package/skills/ctf-misc/bashjails.md +323 -0
  76. package/skills/ctf-misc/ctfd-navigation.md +465 -0
  77. package/skills/ctf-misc/dns.md +255 -0
  78. package/skills/ctf-misc/encodings-advanced.md +504 -0
  79. package/skills/ctf-misc/encodings.md +431 -0
  80. package/skills/ctf-misc/games-and-vms-2.md +254 -0
  81. package/skills/ctf-misc/games-and-vms-3.md +690 -0
  82. package/skills/ctf-misc/games-and-vms-4.md +229 -0
  83. package/skills/ctf-misc/games-and-vms.md +529 -0
  84. package/skills/ctf-misc/linux-privesc.md +333 -0
  85. package/skills/ctf-misc/pyjails.md +671 -0
  86. package/skills/ctf-misc/rf-sdr.md +91 -0
  87. package/skills/ctf-osint/SKILL.md +198 -0
  88. package/skills/ctf-osint/geolocation-and-media.md +464 -0
  89. package/skills/ctf-osint/social-media.md +312 -0
  90. package/skills/ctf-osint/web-and-dns.md +341 -0
  91. package/skills/ctf-pwn/SKILL.md +214 -0
  92. package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
  93. package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
  94. package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
  95. package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
  96. package/skills/ctf-pwn/advanced-exploits.md +773 -0
  97. package/skills/ctf-pwn/advanced.md +326 -0
  98. package/skills/ctf-pwn/field-notes.md +245 -0
  99. package/skills/ctf-pwn/format-string.md +694 -0
  100. package/skills/ctf-pwn/heap-fsop.md +285 -0
  101. package/skills/ctf-pwn/heap-techniques-2.md +333 -0
  102. package/skills/ctf-pwn/heap-techniques.md +513 -0
  103. package/skills/ctf-pwn/kernel-bypass.md +421 -0
  104. package/skills/ctf-pwn/kernel-techniques.md +366 -0
  105. package/skills/ctf-pwn/kernel.md +636 -0
  106. package/skills/ctf-pwn/overflow-basics.md +611 -0
  107. package/skills/ctf-pwn/rop-advanced.md +725 -0
  108. package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
  109. package/skills/ctf-pwn/sandbox-escape.md +313 -0
  110. package/skills/ctf-reverse/SKILL.md +163 -0
  111. package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
  112. package/skills/ctf-reverse/anti-analysis.md +693 -0
  113. package/skills/ctf-reverse/field-notes.md +376 -0
  114. package/skills/ctf-reverse/languages-compiled.md +666 -0
  115. package/skills/ctf-reverse/languages-platforms.md +592 -0
  116. package/skills/ctf-reverse/languages.md +553 -0
  117. package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
  118. package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
  119. package/skills/ctf-reverse/patterns-ctf.md +670 -0
  120. package/skills/ctf-reverse/patterns-runtime.md +274 -0
  121. package/skills/ctf-reverse/patterns.md +572 -0
  122. package/skills/ctf-reverse/platforms-hardware.md +387 -0
  123. package/skills/ctf-reverse/platforms.md +664 -0
  124. package/skills/ctf-reverse/tools-advanced-2.md +421 -0
  125. package/skills/ctf-reverse/tools-advanced.md +407 -0
  126. package/skills/ctf-reverse/tools-dynamic.md +679 -0
  127. package/skills/ctf-reverse/tools-emulation.md +319 -0
  128. package/skills/ctf-reverse/tools.md +573 -0
  129. package/skills/ctf-web/SKILL.md +153 -0
  130. package/skills/ctf-web/auth-and-access-2.md +82 -0
  131. package/skills/ctf-web/auth-and-access.md +783 -0
  132. package/skills/ctf-web/auth-infra.md +321 -0
  133. package/skills/ctf-web/auth-jwt.md +186 -0
  134. package/skills/ctf-web/client-side-advanced.md +739 -0
  135. package/skills/ctf-web/client-side.md +529 -0
  136. package/skills/ctf-web/cves.md +373 -0
  137. package/skills/ctf-web/field-notes.md +482 -0
  138. package/skills/ctf-web/node-and-prototype.md +200 -0
  139. package/skills/ctf-web/server-side-2.md +337 -0
  140. package/skills/ctf-web/server-side-advanced-2.md +559 -0
  141. package/skills/ctf-web/server-side-advanced-3.md +125 -0
  142. package/skills/ctf-web/server-side-advanced-4.md +480 -0
  143. package/skills/ctf-web/server-side-advanced.md +378 -0
  144. package/skills/ctf-web/server-side-deser.md +443 -0
  145. package/skills/ctf-web/server-side-exec-2.md +799 -0
  146. package/skills/ctf-web/server-side-exec.md +457 -0
  147. package/skills/ctf-web/server-side.md +629 -0
  148. package/skills/ctf-web/sql-injection.md +790 -0
  149. package/skills/ctf-web/web3.md +374 -0
  150. package/skills/ctf-writeup/SKILL.md +90 -0
  151. package/skills/solve-challenge/SKILL.md +269 -0
  152. package/skills.json +16 -0
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CTF-Agent Custom Skill & Prompt Envelope Compatibility Validator
4
+ =================================================================
5
+ Validates external custom skills added to `.agents/skills/` or `skills/`
6
+ to prevent prompt envelope conflicts, recursive orchestration rejections,
7
+ and LLM backend safety policy violations.
8
+
9
+ Zero Host Dependencies: Pure Python Standard Library.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import os
17
+ import re
18
+ import sys
19
+ from dataclasses import asdict, dataclass, field
20
+ from enum import Enum
21
+ from pathlib import Path
22
+ from typing import Any, Dict, List, Optional, Set, Tuple
23
+
24
+ REPO_ROOT = Path(__file__).resolve().parent.parent
25
+
26
+ # Reserved built-in skills defined by CTF-Agent framework
27
+ RESERVED_CORE_SKILLS: Set[str] = {
28
+ "solve-challenge",
29
+ "ctf-web",
30
+ "ctf-pwn",
31
+ "ctf-crypto",
32
+ "ctf-reverse",
33
+ "ctf-forensics",
34
+ "ctf-ai-ml",
35
+ "ctf-osint",
36
+ "ctf-misc",
37
+ "ctf-malware",
38
+ "ctf-writeup",
39
+ }
40
+
41
+ # High-risk lexical triggers that trigger upstream LLM safety policy refusals
42
+ POLICY_TRIGGER_DICTIONARY: Dict[str, str] = {
43
+ "evil_payload": "test_payload / proof-of-concept input",
44
+ "malicious_payload": "diagnostic test vector",
45
+ "weaponize": "reproduce vulnerability / construct PoC",
46
+ "jailbreak": "evaluate prompt robustness",
47
+ "steal credentials": "retrieve challenge token",
48
+ "exfiltrate": "retrieve / verify challenge flag",
49
+ "c2 beacon": "simulated command channel",
50
+ "redteam attack": "diagnostic security assessment",
51
+ "bypass safety": "evaluate boundary controls",
52
+ "bypass policy": "test mitigation boundaries",
53
+ }
54
+
55
+ # Dangerous / destructive command patterns
56
+ DESTRUCTIVE_PATTERNS: List[Tuple[re.Pattern, str]] = [
57
+ (re.compile(r"\brm\s+-[a-zA-Z]*rf?\s+/(?:\s|$|\*)", re.I), "Root filesystem deletion (rm -rf /)"),
58
+ (re.compile(r"\bmkfs(?:\.[a-z0-9]+)?\s+", re.I), "Filesystem format (mkfs)"),
59
+ (re.compile(r"\bdd\s+if=/dev/(?:zero|urandom)\s+of=/dev/[a-z0-9]+", re.I), "Raw block device wipe (dd)"),
60
+ (re.compile(r"\bcrontab\s+-[eir]", re.I), "Persistent crontab installation"),
61
+ (re.compile(r"\.ssh/authorized_keys", re.I), "SSH key injection persistence"),
62
+ ]
63
+
64
+ # Recursive multi-agent chaining indicators
65
+ RECURSIVE_CHAIN_PATTERNS: List[Tuple[re.Pattern, str]] = [
66
+ (re.compile(r"invoke_subagent\s*\([^)]*invoke_subagent", re.I), "Recursive subagent dispatch within subagent"),
67
+ (re.compile(r"\bdepth\s*:\s*[2-9]\b", re.I), "Orchestration depth > 1 requested"),
68
+ (re.compile(r"(?:chain|delegate)\s+to\s+(?:another|nested|child)\s+subagent", re.I), "Multi-tier nested subagent delegation"),
69
+ ]
70
+
71
+
72
+ class ValidationLevel(str, Enum):
73
+ INFO = "INFO"
74
+ WARNING = "WARNING"
75
+ ERROR = "ERROR"
76
+
77
+
78
+ @dataclass
79
+ class ValidationIssue:
80
+ rule: str
81
+ level: ValidationLevel
82
+ message: str
83
+ line_number: Optional[int] = None
84
+ suggestion: Optional[str] = None
85
+
86
+ def to_dict(self) -> Dict[str, Any]:
87
+ return {
88
+ "rule": self.rule,
89
+ "level": self.level.value,
90
+ "message": self.message,
91
+ "line_number": self.line_number,
92
+ "suggestion": self.suggestion,
93
+ }
94
+
95
+
96
+ @dataclass
97
+ class SkillValidationReport:
98
+ skill_name: str
99
+ skill_path: str
100
+ is_valid: bool
101
+ errors: List[ValidationIssue] = field(default_factory=list)
102
+ warnings: List[ValidationIssue] = field(default_factory=list)
103
+ info: List[ValidationIssue] = field(default_factory=list)
104
+
105
+ @property
106
+ def error_count(self) -> int:
107
+ return len(self.errors)
108
+
109
+ @property
110
+ def warning_count(self) -> int:
111
+ return len(self.warnings)
112
+
113
+ def to_dict(self) -> Dict[str, Any]:
114
+ return {
115
+ "skill_name": self.skill_name,
116
+ "skill_path": self.skill_path,
117
+ "is_valid": self.is_valid,
118
+ "error_count": len(self.errors),
119
+ "warning_count": len(self.warnings),
120
+ "info_count": len(self.info),
121
+ "errors": [e.to_dict() for e in self.errors],
122
+ "warnings": [w.to_dict() for w in self.warnings],
123
+ "info": [i.to_dict() for i in self.info],
124
+ }
125
+
126
+
127
+ def parse_simple_yaml_frontmatter(content: str) -> Tuple[Optional[Dict[str, Any]], Optional[str], Optional[int]]:
128
+ """
129
+ Parses YAML frontmatter between opening and closing '---' markers without external libraries.
130
+ Returns: (parsed_dict, error_message, closing_line_number)
131
+ """
132
+ lines = content.splitlines()
133
+ if not lines or lines[0].strip() != "---":
134
+ return None, "File does not begin with frontmatter marker '---'", None
135
+
136
+ closing_idx = -1
137
+ for i in range(1, len(lines)):
138
+ if lines[i].strip() == "---":
139
+ closing_idx = i
140
+ break
141
+
142
+ if closing_idx == -1:
143
+ return None, "Unterminated frontmatter (missing closing '---')", None
144
+
145
+ yaml_lines = lines[1:closing_idx]
146
+ data: Dict[str, Any] = {}
147
+ current_key: Optional[str] = None
148
+
149
+ for line_no, raw_line in enumerate(yaml_lines, start=2):
150
+ line = raw_line.strip()
151
+ if not line or line.startswith("#"):
152
+ continue
153
+
154
+ if ":" in line:
155
+ key, val = line.split(":", 1)
156
+ key = key.strip()
157
+ val = val.strip()
158
+
159
+ # Handle quotes
160
+ if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
161
+ val = val[1:-1]
162
+
163
+ data[key] = val
164
+ current_key = key
165
+ elif current_key and line.startswith("- "):
166
+ # Simple list item
167
+ item = line[2:].strip()
168
+ if not isinstance(data[current_key], list):
169
+ data[current_key] = []
170
+ data[current_key].append(item)
171
+
172
+ return data, None, closing_idx + 1
173
+
174
+
175
+ class SkillEnvelopeValidator:
176
+ """Validates custom CTF skills against architecture, security envelope, and policy rules."""
177
+
178
+ @classmethod
179
+ def validate_skill_dir(cls, skill_dir: Path, is_core: bool = False) -> SkillValidationReport:
180
+ skill_path_str = str(skill_dir.resolve())
181
+ skill_name = skill_dir.name
182
+
183
+ errors: List[ValidationIssue] = []
184
+ warnings: List[ValidationIssue] = []
185
+ info: List[ValidationIssue] = []
186
+
187
+ # 1. Structural Integrity Check
188
+ if not skill_dir.exists() or not skill_dir.is_dir():
189
+ errors.append(
190
+ ValidationIssue(
191
+ rule="STRUCTURAL_INTEGRITY",
192
+ level=ValidationLevel.ERROR,
193
+ message=f"Skill path is not an existing directory: {skill_dir}",
194
+ )
195
+ )
196
+ return SkillValidationReport(
197
+ skill_name=skill_name,
198
+ skill_path=skill_path_str,
199
+ is_valid=False,
200
+ errors=errors,
201
+ )
202
+
203
+ skill_md = skill_dir / "SKILL.md"
204
+ if not skill_md.is_file():
205
+ errors.append(
206
+ ValidationIssue(
207
+ rule="STRUCTURAL_INTEGRITY",
208
+ level=ValidationLevel.ERROR,
209
+ message="Missing mandatory 'SKILL.md' entrypoint file in skill directory.",
210
+ suggestion="Create a 'SKILL.md' file with valid YAML frontmatter and operational instructions.",
211
+ )
212
+ )
213
+ return SkillValidationReport(
214
+ skill_name=skill_name,
215
+ skill_path=skill_path_str,
216
+ is_valid=False,
217
+ errors=errors,
218
+ )
219
+
220
+ try:
221
+ content = skill_md.read_text(encoding="utf-8")
222
+ except UnicodeDecodeError:
223
+ errors.append(
224
+ ValidationIssue(
225
+ rule="STRUCTURAL_INTEGRITY",
226
+ level=ValidationLevel.ERROR,
227
+ message="SKILL.md is not valid UTF-8 encoded text.",
228
+ suggestion="Convert SKILL.md to standard UTF-8 encoding.",
229
+ )
230
+ )
231
+ return SkillValidationReport(
232
+ skill_name=skill_name,
233
+ skill_path=skill_path_str,
234
+ is_valid=False,
235
+ errors=errors,
236
+ )
237
+
238
+ # 2. YAML Frontmatter Schema
239
+ fm_data, fm_err, end_line = parse_simple_yaml_frontmatter(content)
240
+ if fm_err:
241
+ errors.append(
242
+ ValidationIssue(
243
+ rule="FRONTMATTER_SCHEMA",
244
+ level=ValidationLevel.ERROR,
245
+ message=f"Invalid frontmatter syntax: {fm_err}",
246
+ suggestion="Ensure SKILL.md starts with '---', contains key-value pairs, and closes with '---'.",
247
+ )
248
+ )
249
+ else:
250
+ assert fm_data is not None
251
+ # Check name field
252
+ declared_name = fm_data.get("name")
253
+ if not declared_name:
254
+ errors.append(
255
+ ValidationIssue(
256
+ rule="FRONTMATTER_SCHEMA",
257
+ level=ValidationLevel.ERROR,
258
+ message="Frontmatter is missing required 'name' attribute.",
259
+ suggestion=f"Add 'name: {skill_name}' inside frontmatter.",
260
+ )
261
+ )
262
+ else:
263
+ declared_name_str = str(declared_name).strip()
264
+ if not re.match(r"^[a-z0-9-_]+$", declared_name_str):
265
+ errors.append(
266
+ ValidationIssue(
267
+ rule="FRONTMATTER_SCHEMA",
268
+ level=ValidationLevel.ERROR,
269
+ message=f"Declared name '{declared_name_str}' contains invalid characters.",
270
+ suggestion="Use lowercase alphanumeric characters, dashes, and underscores only.",
271
+ )
272
+ )
273
+ elif declared_name_str != skill_name:
274
+ warnings.append(
275
+ ValidationIssue(
276
+ rule="FRONTMATTER_SCHEMA",
277
+ level=ValidationLevel.WARNING,
278
+ message=f"Declared name '{declared_name_str}' does not match directory name '{skill_name}'.",
279
+ suggestion=f"Rename directory to '{declared_name_str}' or update frontmatter name.",
280
+ )
281
+ )
282
+
283
+ # Check description field
284
+ description = fm_data.get("description")
285
+ if not description or len(str(description).strip()) < 20:
286
+ errors.append(
287
+ ValidationIssue(
288
+ rule="FRONTMATTER_SCHEMA",
289
+ level=ValidationLevel.ERROR,
290
+ message="Frontmatter 'description' is missing or too short (< 20 characters).",
291
+ suggestion="Provide a clear, detailed description explaining when and how agents should invoke this skill.",
292
+ )
293
+ )
294
+
295
+ # 3. Collision / Namespace Guard
296
+ if not is_core and declared_name in RESERVED_CORE_SKILLS:
297
+ warnings.append(
298
+ ValidationIssue(
299
+ rule="NAME_COLLISION",
300
+ level=ValidationLevel.WARNING,
301
+ message=f"Skill name '{declared_name}' collides with built-in framework skill.",
302
+ suggestion="If this is a custom tool, rename to a unique identifier (e.g. 'custom-pwn-heap') to avoid overriding core functionality.",
303
+ )
304
+ )
305
+
306
+ # 4. Prompt Envelope & Shallow Orchestration Checks
307
+ body_content = content[content.find("---", 3) + 3 :] if "---" in content[3:] else content
308
+
309
+ for pat, desc in RECURSIVE_CHAIN_PATTERNS:
310
+ match = pat.search(body_content)
311
+ if match:
312
+ errors.append(
313
+ ValidationIssue(
314
+ rule="ENVELOPE_SHALLOW_DEPTH",
315
+ level=ValidationLevel.ERROR,
316
+ message=f"Violation of Shallow Orchestration Mandate (Depth <= 1): {desc}.",
317
+ suggestion="CTF-Agent enforces Depth=1 execution. Custom skills must operate as direct specialist workers or leaf tools without recursive agent spawning.",
318
+ )
319
+ )
320
+
321
+ # Mode Awareness & Stop Conditions
322
+ has_blitz = bool(re.search(r"\b(?:--blitz|--fast|blitz mode|stop-on-flag)\b", body_content, re.I))
323
+ has_deep = bool(re.search(r"\b(?:--deep|--lab|deep analysis|rca|writeup)\b", body_content, re.I))
324
+
325
+ if not (has_blitz or has_deep):
326
+ warnings.append(
327
+ ValidationIssue(
328
+ rule="MODE_AWARENESS",
329
+ level=ValidationLevel.WARNING,
330
+ message="Skill does not document operational mode handling (--blitz or --deep).",
331
+ suggestion="Add instructions specifying fast flag retrieval for Blitz mode and deep RCA/writeup for Lab mode.",
332
+ )
333
+ )
334
+ else:
335
+ info.append(
336
+ ValidationIssue(
337
+ rule="MODE_AWARENESS",
338
+ level=ValidationLevel.INFO,
339
+ message=f"Operational mode awareness verified (Blitz: {has_blitz}, Deep: {has_deep}).",
340
+ )
341
+ )
342
+
343
+ # 5. LLM Safety & Policy Lexicon Audit
344
+ for trigger, replacement in POLICY_TRIGGER_DICTIONARY.items():
345
+ pattern = re.compile(rf"\b{re.escape(trigger)}\b", re.I)
346
+ matches = list(pattern.finditer(body_content))
347
+ if matches:
348
+ # Find line number of first match
349
+ first_match = matches[0]
350
+ line_no = body_content[: first_match.start()].count("\n") + (end_line or 1)
351
+ warnings.append(
352
+ ValidationIssue(
353
+ rule="POLICY_TRIGGER_LEXICON",
354
+ level=ValidationLevel.WARNING,
355
+ line_number=line_no,
356
+ message=f"Detected high-risk policy trigger jargon: '{trigger}'.",
357
+ suggestion=f"Replace '{trigger}' with policy-safe academic equivalent: '{replacement}'.",
358
+ )
359
+ )
360
+
361
+ # 6. Prohibited Destructive Command Inspection
362
+ for pat, desc in DESTRUCTIVE_PATTERNS:
363
+ match = pat.search(body_content)
364
+ if match:
365
+ line_no = body_content[: match.start()].count("\n") + (end_line or 1)
366
+ errors.append(
367
+ ValidationIssue(
368
+ rule="PROHIBITED_COMMANDS",
369
+ level=ValidationLevel.ERROR,
370
+ line_number=line_no,
371
+ message=f"Detected prohibited dangerous command pattern: {desc}.",
372
+ suggestion="Remove dangerous host destruction or backdoor persistence commands from skill instructions.",
373
+ )
374
+ )
375
+
376
+ is_valid = len(errors) == 0
377
+ return SkillValidationReport(
378
+ skill_name=skill_name,
379
+ skill_path=skill_path_str,
380
+ is_valid=is_valid,
381
+ errors=errors,
382
+ warnings=warnings,
383
+ info=info,
384
+ )
385
+
386
+ @classmethod
387
+ def validate_all_skills(cls, parent_dir: Path) -> List[SkillValidationReport]:
388
+ """Scans and validates all skills within a directory."""
389
+ reports: List[SkillValidationReport] = []
390
+ if not parent_dir.exists() or not parent_dir.is_dir():
391
+ return reports
392
+
393
+ for item in sorted(parent_dir.iterdir()):
394
+ if item.is_dir() and (item / "SKILL.md").exists():
395
+ is_core = (parent_dir.resolve() == (REPO_ROOT / "skills").resolve())
396
+ report = cls.validate_skill_dir(item, is_core=is_core)
397
+ reports.append(report)
398
+
399
+ return reports
400
+
401
+
402
+ def print_validation_report(reports: List[SkillValidationReport]) -> int:
403
+ """Prints user-friendly terminal report. Returns 0 if all valid, 1 otherwise."""
404
+ print("\n=================================================================")
405
+ print("CTF-AGENT SKILL & PROMPT ENVELOPE COMPATIBILITY AUDIT")
406
+ print("=================================================================")
407
+
408
+ total = len(reports)
409
+ passed = sum(1 for r in reports if r.is_valid)
410
+ failed = total - passed
411
+ total_warnings = sum(len(r.warnings) for r in reports)
412
+
413
+ for r in reports:
414
+ status_tag = "[PASS]" if r.is_valid else "[FAIL]"
415
+ print(f"\n{status_tag} Skill: {r.skill_name}")
416
+ print(f" Path : {r.skill_path}")
417
+
418
+ if r.errors:
419
+ print(" Errors:")
420
+ for err in r.errors:
421
+ loc = f" (line {err.line_number})" if err.line_number else ""
422
+ print(f" - [{err.rule}]{loc}: {err.message}")
423
+ if err.suggestion:
424
+ print(f" Tip: {err.suggestion}")
425
+
426
+ if r.warnings:
427
+ print(" Warnings:")
428
+ for warn in r.warnings:
429
+ loc = f" (line {warn.line_number})" if warn.line_number else ""
430
+ print(f" - [{warn.rule}]{loc}: {warn.message}")
431
+ if warn.suggestion:
432
+ print(f" Tip: {warn.suggestion}")
433
+
434
+ print("\n-----------------------------------------------------------------")
435
+ print(f"Summary: {passed}/{total} skills valid | {failed} errors | {total_warnings} warnings")
436
+ print("=================================================================\n")
437
+
438
+ return 0 if failed == 0 else 1
439
+
440
+
441
+ def main() -> int:
442
+ parser = argparse.ArgumentParser(
443
+ description="CTF-Agent Custom Skill & Prompt Envelope Compatibility Validator"
444
+ )
445
+ parser.add_argument(
446
+ "target",
447
+ nargs="?",
448
+ default=None,
449
+ help="Path to specific skill directory or parent directory containing skills.",
450
+ )
451
+ parser.add_argument(
452
+ "--all",
453
+ action="store_true",
454
+ help="Validate all skills found in target directory (or current workspace .agents/skills).",
455
+ )
456
+ parser.add_argument(
457
+ "--json",
458
+ action="store_true",
459
+ help="Output machine-readable JSON format.",
460
+ )
461
+
462
+ args = parser.parse_args()
463
+
464
+ # Determine target path
465
+ if args.target:
466
+ target_path = Path(args.target).resolve()
467
+ else:
468
+ # Default to local workspace .agents/skills or repo skills/
469
+ dot_skills = Path.cwd() / ".agents" / "skills"
470
+ if dot_skills.exists():
471
+ target_path = dot_skills
472
+ args.all = True
473
+ else:
474
+ target_path = REPO_ROOT / "skills"
475
+ args.all = True
476
+
477
+ reports: List[SkillValidationReport] = []
478
+
479
+ if args.all or (target_path.is_dir() and not (target_path / "SKILL.md").exists()):
480
+ reports = SkillEnvelopeValidator.validate_all_skills(target_path)
481
+ if not reports:
482
+ print(f"No valid skill directories containing 'SKILL.md' found in {target_path}", file=sys.stderr)
483
+ return 1
484
+ else:
485
+ # Single skill directory
486
+ is_core = (target_path.parent.resolve() == (REPO_ROOT / "skills").resolve())
487
+ report = SkillEnvelopeValidator.validate_skill_dir(target_path, is_core=is_core)
488
+ reports = [report]
489
+
490
+ if args.json:
491
+ payload = {
492
+ "total_skills": len(reports),
493
+ "valid_skills": sum(1 for r in reports if r.is_valid),
494
+ "has_errors": any(not r.is_valid for r in reports),
495
+ "reports": [r.to_dict() for r in reports],
496
+ }
497
+ print(json.dumps(payload, indent=2))
498
+ return 0 if payload["valid_skills"] == payload["total_skills"] else 1
499
+
500
+ return print_validation_report(reports)
501
+
502
+
503
+ if __name__ == "__main__":
504
+ sys.exit(main())
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CTF Workspace Cleaner & Environment Sanitizer (v1.0)
4
+ Cleans up temporary scratch scripts, ephemeral payloads, core dumps, and compiles artifacts.
5
+ Supports two modes:
6
+ --fast / --blitz: Quick sweep of temporary scratch/test files while preserving the winning solve script and original challenge assets.
7
+ --deep / --organize: Enforces the ctf-writeup standard directory structure (writeup.md, solve.py, resources/).
8
+ """
9
+
10
+ import os
11
+ import sys
12
+ import shutil
13
+ import argparse
14
+ from pathlib import Path
15
+ from typing import List, Set
16
+
17
+ # Ensure safe UTF-8 output on Windows consoles
18
+ if hasattr(sys.stdout, "reconfigure"):
19
+ try:
20
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
21
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
22
+ except Exception:
23
+ pass
24
+
25
+ # Ephemeral file patterns generated during rapid exploitation
26
+ EPHEMERAL_PATTERNS = [
27
+ "test*.py",
28
+ "test_*.py",
29
+ "tmp*",
30
+ "temp*",
31
+ "*_tmp.py",
32
+ "scratch*",
33
+ "fuzz*.py",
34
+ "fuzz*.txt",
35
+ "payload*.bin",
36
+ "payload*.txt",
37
+ "exploit_tmp*.py",
38
+ "core",
39
+ "core.*",
40
+ "vgcore.*",
41
+ "gmon.out",
42
+ "*.pyc",
43
+ "*.swp",
44
+ "*~",
45
+ "*.o",
46
+ "*.out",
47
+ "peda-session-*",
48
+ ".gdb_history",
49
+ ]
50
+
51
+ # Essential files that must NEVER be deleted in fast mode
52
+ PROTECTED_FILES = {
53
+ "solve.py",
54
+ "writeup.md",
55
+ "Dockerfile",
56
+ "docker-compose.yml",
57
+ "requirements.txt",
58
+ }
59
+
60
+ def clean_ephemeral_files(target_dir: Path, dry_run: bool = False) -> List[Path]:
61
+ """Identify and remove scratch/temporary files from target directory."""
62
+ deleted_files = []
63
+
64
+ for pattern in EPHEMERAL_PATTERNS:
65
+ for p in target_dir.glob(pattern):
66
+ if p.is_file() and p.name not in PROTECTED_FILES:
67
+ deleted_files.append(p)
68
+ if not dry_run:
69
+ try:
70
+ p.unlink()
71
+ except Exception as e:
72
+ print(f"[!] Warning: Could not delete {p.name}: {e}")
73
+ elif p.is_dir() and p.name == "__pycache__":
74
+ deleted_files.append(p)
75
+ if not dry_run:
76
+ try:
77
+ shutil.rmtree(p)
78
+ except Exception as e:
79
+ print(f"[!] Warning: Could not delete {p.name}: {e}")
80
+
81
+ return deleted_files
82
+
83
+ def organize_deep_directory(target_dir: Path, dry_run: bool = False) -> List[Path]:
84
+ """Organize workspace into writeup.md, solve.py, and resources/ folder."""
85
+ resources_dir = target_dir / "resources"
86
+ moved_files = []
87
+
88
+ if not dry_run:
89
+ resources_dir.mkdir(exist_ok=True)
90
+
91
+ # First clean ephemeral debris
92
+ clean_ephemeral_files(target_dir, dry_run=dry_run)
93
+
94
+ for item in target_dir.iterdir():
95
+ if item == resources_dir or item.name in PROTECTED_FILES or item.name.startswith("."):
96
+ continue
97
+
98
+ moved_files.append(item)
99
+ if not dry_run:
100
+ dest = resources_dir / item.name
101
+ try:
102
+ if dest.exists():
103
+ if item.is_dir():
104
+ shutil.rmtree(dest)
105
+ else:
106
+ dest.unlink()
107
+ shutil.move(str(item), str(dest))
108
+ except Exception as e:
109
+ print(f"[!] Warning: Could not move {item.name} to resources/: {e}")
110
+
111
+ return moved_files
112
+
113
+ def main():
114
+ parser = argparse.ArgumentParser(description="CTF Workspace Sanitizer & Cleaner")
115
+ parser.add_argument("directory", nargs="?", default=".", help="Target workspace directory (default: current directory)")
116
+ parser.add_argument("--fast", "--blitz", action="store_true", help="Fast mode: wipe scratch/debris files, preserve challenge & solve.py")
117
+ parser.add_argument("--deep", "--organize", action="store_true", help="Deep mode: enforce resources/ directory structure")
118
+ parser.add_argument("--dry-run", action="store_true", help="List files that would be cleaned without deleting")
119
+ parser.add_argument("--quiet", "-q", action="store_true", help="Quiet output")
120
+
121
+ args = parser.parse_args()
122
+ target = Path(args.directory).resolve()
123
+
124
+ if not target.is_dir():
125
+ print(f"[-] Error: {target} is not a valid directory.")
126
+ sys.exit(1)
127
+
128
+ if args.deep:
129
+ moved = organize_deep_directory(target, dry_run=args.dry_run)
130
+ if not args.quiet:
131
+ prefix = "[DRY-RUN] Would organize" if args.dry_run else "[+] Deep organized"
132
+ print(f"{prefix} {len(moved)} item(s) into resources/ inside {target.name}")
133
+ else:
134
+ # Default is fast cleanup
135
+ removed = clean_ephemeral_files(target, dry_run=args.dry_run)
136
+ if not args.quiet:
137
+ prefix = "[DRY-RUN] Would remove" if args.dry_run else "[+] Cleaned"
138
+ print(f"{prefix} {len(removed)} temporary scratch/debris file(s) in {target.name}")
139
+
140
+ if __name__ == "__main__":
141
+ main()