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,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()
|