-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathyml-maker.py
More file actions
158 lines (131 loc) · 5.95 KB
/
yml-maker.py
File metadata and controls
158 lines (131 loc) · 5.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import re
import yaml
from pathlib import Path
# ===========================================
CHALLENGES_DIR = "./challenges" # Path to challenges directory
AUTHOR = 'CSSA'
FLAG_PREFIX = 'cssactf' # Flag format prefix
# Categories that appear in READMEs
CATEGORIES = [
'Binary Exploitation',
'Web Exploitation',
'Cryptography',
'Forensics',
'Reverse Engineering',
'Misc',
'OSINT',
'Pwn',
'SQL Injection',
'Steganography',
'General Skills'
]
# ===========================================
def parse_readme(readme_path):
"""Parse README and extract challenge metadata"""
with open(readme_path, 'r', encoding='utf-8') as f:
content = f.read()
lines = content.strip().split('\n')
name = lines[0].strip() # From first line of README
# Remove leading # if present
if name.startswith('# '):
name = name[2:]
# Category from README (using the categories list)
category_pattern = '|'.join(CATEGORIES)
category = re.search(rf'^\s*[-*]\s*({category_pattern})', content, re.MULTILINE)
# Difficulty from README
difficulty = re.search(r'^\s*[-*]\s*(Easy|Medium|Hard)', content, re.MULTILINE)
# Points from "XXX Points" in README
points = re.search(r'(\d+)\s+Points', content)
# Type from README (optional: standard or dynamic)
type_match = re.search(r'^\s*[-*]\s*Type:\s*(standard|dynamic)', content, re.MULTILINE | re.IGNORECASE)
challenge_type = type_match.group(1).lower() if type_match else 'standard'
# State from README (optional: visible or hidden)
state_match = re.search(r'^\s*[-*]\s*State:\s*(visible|hidden)', content, re.MULTILINE | re.IGNORECASE)
state = state_match.group(1).lower() if state_match else 'visible'
# Flag Type from README (optional: static, regex, case_insensitive)
flag_type_match = re.search(r'^\s*[-*]\s*Flag Type:\s*(static|regex|case_insensitive)', content, re.MULTILINE | re.IGNORECASE)
flag_type = flag_type_match.group(1).lower() if flag_type_match else 'static'
# Description section from README
desc_match = re.search(r'##\s*Description\s*\n(.*?)(?=\n##\s*(?:Hints?|Solution|Flag)|$)', content, re.DOTALL)
# Hints section from README
hints_match = re.search(r'##\s*Hints?\s*\n(.*?)(?=\n##\s*(?:Solution|Flag)|$)', content, re.DOTALL)
# Parse hints into list
hints = []
if hints_match:
hints_text = hints_match.group(1).strip()
# Parse each line that starts with - or * as a hint
for line in hints_text.split('\n'):
hint_line = re.match(r'^\s*[-*]\s*(.+)', line)
if hint_line:
hints.append(hint_line.group(1).strip())
# Flag from Flag section
flag_section_match = re.search(r'##\s*Flag\s*\n`([^`]+)`', content)
if flag_section_match:
flag = flag_section_match.group(1).strip()
else:
# Fallback to searching for flag pattern anywhere
flag_match = re.search(rf'{FLAG_PREFIX}\{{[^}}]+\}}', content)
flag = flag_match.group(0) if flag_match else ''
description = desc_match.group(1).strip() if desc_match else ''
return {
'name': name,
'category': category.group(1) if category else 'Misc',
'value': int(points.group(1)) if points else 100,
'description': description,
'flag': flag,
'type': challenge_type,
'state': state,
'flag_type': flag_type,
'tags': [difficulty.group(1) if difficulty else 'Medium'],
'hints': hints
}
def generate_challenge_yml(challenge_dir):
"""Generate challenge.yml for a challenge"""
readme_path = challenge_dir / 'README.md'
if not readme_path.exists():
print(f"Skipping {challenge_dir.name}: No README.md found")
return
data = parse_readme(readme_path)
# Find files in the files/subdirectory if it exists
files = []
files_dir = challenge_dir / 'files'
if files_dir.exists() and files_dir.is_dir():
for f in files_dir.iterdir():
if f.is_file():
files.append(f'files/{f.name}')
# ============= CTFd STANDARD STRUCTURE =============
challenge_yml = {
'name': data['name'], # Challenge name
'author': AUTHOR,
'category': data['category'], # Challenge category
'description': data['description'], # Challenge description
'value': data['value'], # Points value
'type': data['type'], # 'standard' or 'dynamic' (from README or default)
'state': data['state'], # 'visible' or 'hidden' (from README or default)
'flags': [data['flag']], # Simple array of flag strings
'tags': data['tags'], # Array of tags (we use difficulty)
'files': files, # The files to upload
'hints': data['hints'] # Array of hint strings
}
# ==================================================
yml_path = challenge_dir / 'challenge.yml'
with open(yml_path, 'w', encoding='utf-8') as f:
yaml.dump(challenge_yml, f, default_flow_style=False, sort_keys=False, allow_unicode=True, width=1000)
print(f"Generated: {challenge_dir.name}/challenge.yml")
def process_challenges(challenges_dir):
"""Process all challenge directories"""
challenges_path = Path(challenges_dir)
if not challenges_path.exists():
print(f"Error: Challenges directory '{challenges_dir}' not found")
return
challenge_count = 0
for challenge_dir in sorted(challenges_path.iterdir()):
if challenge_dir.is_dir() and not challenge_dir.name.startswith('.'):
try:
generate_challenge_yml(challenge_dir)
challenge_count += 1
except Exception as e:
print(f"Error in {challenge_dir.name}: {e}")
print(f"\nProcessed {challenge_count} challenges")
if __name__ == "__main__":
process_challenges(CHALLENGES_DIR)