rtexit-method 0.1.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/RTEXIT.md +127 -0
- package/_rtexit/config.toml +103 -0
- package/_rtexit/config.user.toml +28 -0
- package/_rtexit/custom/config.toml +12 -0
- package/_rtexit/scripts/autodoc_engine.py +203 -0
- package/_rtexit/scripts/finding_tracker.py +251 -0
- package/_rtexit/scripts/resolve_config.py +127 -0
- package/_rtexit/scripts/resolve_customization.py +154 -0
- package/package.json +53 -0
- package/resources/certifications.md +21 -0
- package/resources/payloads.md +21 -0
- package/resources/tools.md +53 -0
- package/resources/wordlists.md +15 -0
- package/templates/attack-chain-template.md +33 -0
- package/templates/executive-report-template.md +64 -0
- package/templates/executive-report.md +27 -0
- package/templates/finding-template.md +74 -0
- package/templates/remediation-roadmap.md +31 -0
- package/templates/sead-template.md +73 -0
- package/templates/technical-report.md +63 -0
- package/tools/installer/commands/install.js +40 -0
- package/tools/installer/lib/asset-manifest.js +11 -0
- package/tools/installer/lib/banner.js +12 -0
- package/tools/installer/lib/config-template.js +29 -0
- package/tools/installer/lib/copy-assets.js +39 -0
- package/tools/installer/lib/paths.js +11 -0
- package/tools/installer/lib/prompts.js +43 -0
- package/tools/installer/lib/write-config.js +32 -0
- package/tools/installer/rt-cli.js +20 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
RTExit Finding Tracker
|
|
4
|
+
Manages findings across a Red Team engagement.
|
|
5
|
+
Usage:
|
|
6
|
+
python3 finding_tracker.py add "SQL Injection" CRITICAL 9.8 "target.com/login"
|
|
7
|
+
python3 finding_tracker.py list [--severity CRITICAL]
|
|
8
|
+
python3 finding_tracker.py show F-001
|
|
9
|
+
python3 finding_tracker.py export [--format html|md|csv]
|
|
10
|
+
python3 finding_tracker.py stats
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import csv
|
|
15
|
+
import datetime
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
FINDINGS_CSV = os.path.join(
|
|
22
|
+
os.environ.get('RTEXIT_OUTPUT', '_rtexit-output'),
|
|
23
|
+
'docs', 'findings', 'findings-master.csv'
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
SEVERITY_ORDER = {'CRITICAL': 0, 'HIGH': 1, 'MEDIUM': 2, 'LOW': 3, 'INFO': 4}
|
|
27
|
+
SEVERITY_ICONS = {'CRITICAL': '🔴', 'HIGH': '🟠', 'MEDIUM': '🟡', 'LOW': '🔵', 'INFO': '⚪'}
|
|
28
|
+
|
|
29
|
+
FIELDNAMES = [
|
|
30
|
+
'id', 'title', 'severity', 'cvss', 'status', 'asset',
|
|
31
|
+
'cwe', 'cve', 'mitre', 'phase', 'date', 'operator', 'notes'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def ensure_csv():
|
|
36
|
+
os.makedirs(os.path.dirname(FINDINGS_CSV), exist_ok=True)
|
|
37
|
+
if not os.path.exists(FINDINGS_CSV):
|
|
38
|
+
with open(FINDINGS_CSV, 'w', newline='', encoding='utf-8') as f:
|
|
39
|
+
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
|
|
40
|
+
writer.writeheader()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_findings() -> list:
|
|
44
|
+
ensure_csv()
|
|
45
|
+
with open(FINDINGS_CSV, 'r', encoding='utf-8') as f:
|
|
46
|
+
return list(csv.DictReader(f))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def save_findings(findings: list):
|
|
50
|
+
ensure_csv()
|
|
51
|
+
with open(FINDINGS_CSV, 'w', newline='', encoding='utf-8') as f:
|
|
52
|
+
writer = csv.DictWriter(f, fieldnames=FIELDNAMES)
|
|
53
|
+
writer.writeheader()
|
|
54
|
+
writer.writerows(findings)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def next_id(findings: list) -> str:
|
|
58
|
+
if not findings:
|
|
59
|
+
return 'F-001'
|
|
60
|
+
last = sorted([f['id'] for f in findings if f['id'].startswith('F-')])[-1]
|
|
61
|
+
num = int(last.split('-')[1]) + 1
|
|
62
|
+
return f'F-{num:03d}'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_add(args):
|
|
66
|
+
findings = load_findings()
|
|
67
|
+
fid = next_id(findings)
|
|
68
|
+
finding = {
|
|
69
|
+
'id': fid,
|
|
70
|
+
'title': args.title,
|
|
71
|
+
'severity': args.severity.upper(),
|
|
72
|
+
'cvss': args.cvss,
|
|
73
|
+
'status': args.status or 'CONFIRMED',
|
|
74
|
+
'asset': args.asset or '',
|
|
75
|
+
'cwe': args.cwe or '',
|
|
76
|
+
'cve': args.cve or '',
|
|
77
|
+
'mitre': args.mitre or '',
|
|
78
|
+
'phase': args.phase or '',
|
|
79
|
+
'date': datetime.date.today().isoformat(),
|
|
80
|
+
'operator': args.operator or '',
|
|
81
|
+
'notes': args.notes or '',
|
|
82
|
+
}
|
|
83
|
+
findings.append(finding)
|
|
84
|
+
save_findings(findings)
|
|
85
|
+
|
|
86
|
+
# Create individual finding MD file
|
|
87
|
+
md_path = os.path.join(os.path.dirname(FINDINGS_CSV), f'{fid}.md')
|
|
88
|
+
icon = SEVERITY_ICONS.get(finding['severity'], '⚪')
|
|
89
|
+
with open(md_path, 'w', encoding='utf-8') as f:
|
|
90
|
+
f.write(f"""---
|
|
91
|
+
id: {fid}
|
|
92
|
+
title: "{finding['title']}"
|
|
93
|
+
severity: {finding['severity']}
|
|
94
|
+
cvss: {finding['cvss']}
|
|
95
|
+
status: {finding['status']}
|
|
96
|
+
asset: {finding['asset']}
|
|
97
|
+
cwe: {finding['cwe']}
|
|
98
|
+
cve: {finding['cve']}
|
|
99
|
+
mitre: {finding['mitre']}
|
|
100
|
+
date: {finding['date']}
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
# {icon} {fid} — {finding['title']}
|
|
104
|
+
|
|
105
|
+
## Summary
|
|
106
|
+
> **Severity:** {finding['severity']} | **CVSS:** {finding['cvss']} | **Asset:** {finding['asset']}
|
|
107
|
+
|
|
108
|
+
## Description
|
|
109
|
+
<!-- Describe the vulnerability in detail -->
|
|
110
|
+
|
|
111
|
+
## Technical Evidence
|
|
112
|
+
```
|
|
113
|
+
<!-- Paste command output, HTTP requests/responses, screenshots references -->
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Impact
|
|
117
|
+
<!-- Business and technical impact -->
|
|
118
|
+
|
|
119
|
+
## Reproduction Steps
|
|
120
|
+
1.
|
|
121
|
+
2.
|
|
122
|
+
3.
|
|
123
|
+
|
|
124
|
+
## Remediation
|
|
125
|
+
### Immediate (0-24 hours)
|
|
126
|
+
-
|
|
127
|
+
|
|
128
|
+
### Short-term (1-30 days)
|
|
129
|
+
-
|
|
130
|
+
|
|
131
|
+
### Long-term
|
|
132
|
+
-
|
|
133
|
+
|
|
134
|
+
## References
|
|
135
|
+
- CWE: https://cwe.mitre.org/data/definitions/{finding['cwe'].replace('CWE-', '')}.html
|
|
136
|
+
- MITRE ATT&CK: https://attack.mitre.org/techniques/{finding['mitre']}/
|
|
137
|
+
""")
|
|
138
|
+
|
|
139
|
+
print(f"✅ Added: {fid} — {finding['title']} [{finding['severity']}]")
|
|
140
|
+
print(f" File: {md_path}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def cmd_list(args):
|
|
144
|
+
findings = load_findings()
|
|
145
|
+
if args.severity:
|
|
146
|
+
findings = [f for f in findings if f['severity'] == args.severity.upper()]
|
|
147
|
+
if args.status:
|
|
148
|
+
findings = [f for f in findings if f['status'] == args.status.upper()]
|
|
149
|
+
|
|
150
|
+
findings.sort(key=lambda f: SEVERITY_ORDER.get(f['severity'], 99))
|
|
151
|
+
|
|
152
|
+
if not findings:
|
|
153
|
+
print("No findings found.")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
print(f"\n{'ID':<8} {'SEVERITY':<10} {'CVSS':<6} {'STATUS':<12} {'TITLE'}")
|
|
157
|
+
print("-" * 80)
|
|
158
|
+
for f in findings:
|
|
159
|
+
icon = SEVERITY_ICONS.get(f['severity'], '⚪')
|
|
160
|
+
print(f"{f['id']:<8} {icon} {f['severity']:<8} {f['cvss']:<6} {f['status']:<12} {f['title']}")
|
|
161
|
+
print(f"\nTotal: {len(findings)} findings")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def cmd_stats(args):
|
|
165
|
+
findings = load_findings()
|
|
166
|
+
counts = {}
|
|
167
|
+
for f in findings:
|
|
168
|
+
counts[f['severity']] = counts.get(f['severity'], 0) + 1
|
|
169
|
+
|
|
170
|
+
print("\n=== Finding Statistics ===")
|
|
171
|
+
for sev in ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO']:
|
|
172
|
+
icon = SEVERITY_ICONS.get(sev, '⚪')
|
|
173
|
+
count = counts.get(sev, 0)
|
|
174
|
+
bar = 'â–ˆ' * count
|
|
175
|
+
print(f"{icon} {sev:<10}: {count:>3} {bar}")
|
|
176
|
+
print(f"\n TOTAL : {len(findings)}")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def cmd_export(args):
|
|
180
|
+
findings = load_findings()
|
|
181
|
+
fmt = args.format or 'md'
|
|
182
|
+
|
|
183
|
+
if fmt == 'csv':
|
|
184
|
+
print(open(FINDINGS_CSV).read())
|
|
185
|
+
|
|
186
|
+
elif fmt == 'json':
|
|
187
|
+
print(json.dumps(findings, indent=2))
|
|
188
|
+
|
|
189
|
+
elif fmt == 'md':
|
|
190
|
+
print("# Findings Summary\n")
|
|
191
|
+
print(f"| ID | Severity | CVSS | Title | Asset | Status |")
|
|
192
|
+
print(f"|----|---------:|-----:|-------|-------|--------|")
|
|
193
|
+
for f in sorted(findings, key=lambda x: SEVERITY_ORDER.get(x['severity'], 99)):
|
|
194
|
+
icon = SEVERITY_ICONS.get(f['severity'], '⚪')
|
|
195
|
+
print(f"| {f['id']} | {icon} {f['severity']} | {f['cvss']} | {f['title']} | {f['asset']} | {f['status']} |")
|
|
196
|
+
|
|
197
|
+
elif fmt == 'html':
|
|
198
|
+
print("<table>")
|
|
199
|
+
print("<tr><th>ID</th><th>Severity</th><th>CVSS</th><th>Title</th><th>Asset</th></tr>")
|
|
200
|
+
for f in findings:
|
|
201
|
+
print(f"<tr><td>{f['id']}</td><td>{f['severity']}</td><td>{f['cvss']}</td>"
|
|
202
|
+
f"<td>{f['title']}</td><td>{f['asset']}</td></tr>")
|
|
203
|
+
print("</table>")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def main():
|
|
207
|
+
parser = argparse.ArgumentParser(description='RTExit Finding Tracker')
|
|
208
|
+
sub = parser.add_subparsers(dest='command')
|
|
209
|
+
|
|
210
|
+
# add
|
|
211
|
+
p_add = sub.add_parser('add', help='Add a new finding')
|
|
212
|
+
p_add.add_argument('title', help='Finding title')
|
|
213
|
+
p_add.add_argument('severity', help='CRITICAL/HIGH/MEDIUM/LOW/INFO')
|
|
214
|
+
p_add.add_argument('cvss', help='CVSS score (e.g. 9.8)')
|
|
215
|
+
p_add.add_argument('asset', nargs='?', help='Affected asset URL/IP')
|
|
216
|
+
p_add.add_argument('--status', default='CONFIRMED')
|
|
217
|
+
p_add.add_argument('--cwe', default='')
|
|
218
|
+
p_add.add_argument('--cve', default='')
|
|
219
|
+
p_add.add_argument('--mitre', default='')
|
|
220
|
+
p_add.add_argument('--phase', default='')
|
|
221
|
+
p_add.add_argument('--operator', default='')
|
|
222
|
+
p_add.add_argument('--notes', default='')
|
|
223
|
+
|
|
224
|
+
# list
|
|
225
|
+
p_list = sub.add_parser('list', help='List findings')
|
|
226
|
+
p_list.add_argument('--severity', help='Filter by severity')
|
|
227
|
+
p_list.add_argument('--status', help='Filter by status')
|
|
228
|
+
|
|
229
|
+
# stats
|
|
230
|
+
sub.add_parser('stats', help='Show finding statistics')
|
|
231
|
+
|
|
232
|
+
# export
|
|
233
|
+
p_exp = sub.add_parser('export', help='Export findings')
|
|
234
|
+
p_exp.add_argument('--format', choices=['md', 'csv', 'json', 'html'], default='md')
|
|
235
|
+
|
|
236
|
+
args = parser.parse_args()
|
|
237
|
+
|
|
238
|
+
if args.command == 'add':
|
|
239
|
+
cmd_add(args)
|
|
240
|
+
elif args.command == 'list':
|
|
241
|
+
cmd_list(args)
|
|
242
|
+
elif args.command == 'stats':
|
|
243
|
+
cmd_stats(args)
|
|
244
|
+
elif args.command == 'export':
|
|
245
|
+
cmd_export(args)
|
|
246
|
+
else:
|
|
247
|
+
parser.print_help()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == '__main__':
|
|
251
|
+
main()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
RTExit Configuration Resolver
|
|
4
|
+
Merges 4 config layers (base → user → team → personal) into final config.
|
|
5
|
+
Usage: python3 resolve_config.py --project-root /path [--key section]
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import tomllib
|
|
15
|
+
except ImportError:
|
|
16
|
+
try:
|
|
17
|
+
import tomli as tomllib
|
|
18
|
+
except ImportError:
|
|
19
|
+
# Fallback: simple TOML parser for basic key=value
|
|
20
|
+
tomllib = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_simple_toml(content: str) -> dict:
|
|
24
|
+
"""Simple TOML parser for basic key=value pairs (no dependencies)."""
|
|
25
|
+
result = {}
|
|
26
|
+
current_section = result
|
|
27
|
+
|
|
28
|
+
for line in content.splitlines():
|
|
29
|
+
line = line.strip()
|
|
30
|
+
if not line or line.startswith('#'):
|
|
31
|
+
continue
|
|
32
|
+
if line.startswith('[') and line.endswith(']'):
|
|
33
|
+
section_path = line[1:-1].split('.')
|
|
34
|
+
current_section = result
|
|
35
|
+
for part in section_path:
|
|
36
|
+
if part not in current_section:
|
|
37
|
+
current_section[part] = {}
|
|
38
|
+
current_section = current_section[part]
|
|
39
|
+
elif '=' in line:
|
|
40
|
+
key, _, value = line.partition('=')
|
|
41
|
+
key = key.strip()
|
|
42
|
+
value = value.strip().strip('"').strip("'")
|
|
43
|
+
if value.lower() == 'true':
|
|
44
|
+
value = True
|
|
45
|
+
elif value.lower() == 'false':
|
|
46
|
+
value = False
|
|
47
|
+
current_section[key] = value
|
|
48
|
+
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_toml(path: str) -> dict:
|
|
53
|
+
if not os.path.exists(path):
|
|
54
|
+
return {}
|
|
55
|
+
with open(path, 'rb') as f:
|
|
56
|
+
content = f.read()
|
|
57
|
+
if tomllib:
|
|
58
|
+
return tomllib.loads(content.decode('utf-8'))
|
|
59
|
+
return parse_simple_toml(content.decode('utf-8'))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def deep_merge(base: dict, override: dict) -> dict:
|
|
63
|
+
"""Deep merge two dicts. Override values win."""
|
|
64
|
+
result = dict(base)
|
|
65
|
+
for key, value in override.items():
|
|
66
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
67
|
+
result[key] = deep_merge(result[key], value)
|
|
68
|
+
elif key in result and isinstance(result[key], list) and isinstance(value, list):
|
|
69
|
+
result[key] = result[key] + value
|
|
70
|
+
elif value != '' and value is not None:
|
|
71
|
+
result[key] = value
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve_config(project_root: str) -> dict:
|
|
76
|
+
"""Merge 4 config layers."""
|
|
77
|
+
layers = [
|
|
78
|
+
os.path.join(project_root, '_rtexit', 'config.toml'),
|
|
79
|
+
os.path.join(project_root, '_rtexit', 'config.user.toml'),
|
|
80
|
+
os.path.join(project_root, '_rtexit', 'custom', 'config.toml'),
|
|
81
|
+
os.path.join(project_root, '_rtexit', 'custom', 'config.user.toml'),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
config = {}
|
|
85
|
+
for layer in layers:
|
|
86
|
+
layer_config = load_toml(layer)
|
|
87
|
+
config = deep_merge(config, layer_config)
|
|
88
|
+
|
|
89
|
+
# Resolve path templates
|
|
90
|
+
config = resolve_templates(config, project_root)
|
|
91
|
+
return config
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def resolve_templates(obj, project_root: str):
|
|
95
|
+
"""Replace {project-root} and other templates in string values."""
|
|
96
|
+
if isinstance(obj, dict):
|
|
97
|
+
return {k: resolve_templates(v, project_root) for k, v in obj.items()}
|
|
98
|
+
elif isinstance(obj, list):
|
|
99
|
+
return [resolve_templates(i, project_root) for i in obj]
|
|
100
|
+
elif isinstance(obj, str):
|
|
101
|
+
dir_name = os.path.basename(os.path.abspath(project_root))
|
|
102
|
+
return obj.replace('{project-root}', project_root).replace('{dir-name}', dir_name)
|
|
103
|
+
return obj
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main():
|
|
107
|
+
parser = argparse.ArgumentParser(description='RTExit Config Resolver')
|
|
108
|
+
parser.add_argument('--project-root', required=True, help='Project root directory')
|
|
109
|
+
parser.add_argument('--key', help='Specific config section to return')
|
|
110
|
+
args = parser.parse_args()
|
|
111
|
+
|
|
112
|
+
config = resolve_config(args.project_root)
|
|
113
|
+
|
|
114
|
+
if args.key:
|
|
115
|
+
# Navigate nested keys like "agents.commander"
|
|
116
|
+
parts = args.key.split('.')
|
|
117
|
+
result = config
|
|
118
|
+
for part in parts:
|
|
119
|
+
result = result.get(part, {})
|
|
120
|
+
else:
|
|
121
|
+
result = config
|
|
122
|
+
|
|
123
|
+
print(json.dumps(result, indent=2))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == '__main__':
|
|
127
|
+
main()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
RTExit Skill Customization Resolver
|
|
4
|
+
Merges skill's customize.toml with team/user overrides.
|
|
5
|
+
Usage: python3 resolve_customization.py --skill /path/to/skill --key agent
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_simple_toml(content: str) -> dict:
|
|
15
|
+
"""Simple TOML parser."""
|
|
16
|
+
result = {}
|
|
17
|
+
current_section = result
|
|
18
|
+
current_section_path = []
|
|
19
|
+
|
|
20
|
+
for line in content.splitlines():
|
|
21
|
+
line = line.strip()
|
|
22
|
+
if not line or line.startswith('#'):
|
|
23
|
+
continue
|
|
24
|
+
|
|
25
|
+
if line.startswith('[[') and line.endswith(']]'):
|
|
26
|
+
# Array of tables
|
|
27
|
+
section_path = line[2:-2].split('.')
|
|
28
|
+
current_section_path = section_path
|
|
29
|
+
current_obj = result
|
|
30
|
+
for part in section_path[:-1]:
|
|
31
|
+
if part not in current_obj:
|
|
32
|
+
current_obj[part] = {}
|
|
33
|
+
current_obj = current_obj[part]
|
|
34
|
+
last_key = section_path[-1]
|
|
35
|
+
if last_key not in current_obj:
|
|
36
|
+
current_obj[last_key] = []
|
|
37
|
+
new_item = {}
|
|
38
|
+
current_obj[last_key].append(new_item)
|
|
39
|
+
current_section = new_item
|
|
40
|
+
|
|
41
|
+
elif line.startswith('[') and line.endswith(']'):
|
|
42
|
+
section_path = line[1:-1].split('.')
|
|
43
|
+
current_section_path = section_path
|
|
44
|
+
current_section = result
|
|
45
|
+
for part in section_path:
|
|
46
|
+
if part not in current_section:
|
|
47
|
+
current_section[part] = {}
|
|
48
|
+
current_section = current_section[part]
|
|
49
|
+
|
|
50
|
+
elif '=' in line:
|
|
51
|
+
key, _, value = line.partition('=')
|
|
52
|
+
key = key.strip()
|
|
53
|
+
value = value.strip()
|
|
54
|
+
if value.startswith('"') or value.startswith("'"):
|
|
55
|
+
value = value.strip('"').strip("'")
|
|
56
|
+
elif value.startswith('['):
|
|
57
|
+
# Array: parse inline
|
|
58
|
+
value = [v.strip().strip('"').strip("'")
|
|
59
|
+
for v in value.strip('[]').split(',') if v.strip()]
|
|
60
|
+
elif value.lower() == 'true':
|
|
61
|
+
value = True
|
|
62
|
+
elif value.lower() == 'false':
|
|
63
|
+
value = False
|
|
64
|
+
current_section[key] = value
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_toml(path: str) -> dict:
|
|
70
|
+
if not os.path.exists(path):
|
|
71
|
+
return {}
|
|
72
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
73
|
+
content = f.read()
|
|
74
|
+
try:
|
|
75
|
+
import tomllib
|
|
76
|
+
with open(path, 'rb') as fb:
|
|
77
|
+
return tomllib.load(fb)
|
|
78
|
+
except ImportError:
|
|
79
|
+
pass
|
|
80
|
+
try:
|
|
81
|
+
import tomli
|
|
82
|
+
with open(path, 'rb') as fb:
|
|
83
|
+
return tomli.load(fb)
|
|
84
|
+
except ImportError:
|
|
85
|
+
pass
|
|
86
|
+
return parse_simple_toml(content)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def deep_merge(base: dict, override: dict) -> dict:
|
|
90
|
+
result = dict(base)
|
|
91
|
+
for key, value in override.items():
|
|
92
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
93
|
+
result[key] = deep_merge(result[key], value)
|
|
94
|
+
elif key in result and isinstance(result[key], list) and isinstance(value, list):
|
|
95
|
+
# Merge arrays of tables by 'code' key
|
|
96
|
+
merged = list(result[key])
|
|
97
|
+
for new_item in value:
|
|
98
|
+
if isinstance(new_item, dict) and 'code' in new_item:
|
|
99
|
+
existing_idx = next(
|
|
100
|
+
(i for i, x in enumerate(merged)
|
|
101
|
+
if isinstance(x, dict) and x.get('code') == new_item['code']),
|
|
102
|
+
None
|
|
103
|
+
)
|
|
104
|
+
if existing_idx is not None:
|
|
105
|
+
merged[existing_idx] = deep_merge(merged[existing_idx], new_item)
|
|
106
|
+
else:
|
|
107
|
+
merged.append(new_item)
|
|
108
|
+
else:
|
|
109
|
+
merged.append(new_item)
|
|
110
|
+
result[key] = merged
|
|
111
|
+
elif value not in ('', None, []):
|
|
112
|
+
result[key] = value
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def resolve_customization(skill_root: str, project_root: str = None) -> dict:
|
|
117
|
+
"""Load and merge customize.toml layers for a skill."""
|
|
118
|
+
skill_name = os.path.basename(os.path.abspath(skill_root))
|
|
119
|
+
|
|
120
|
+
layers = [os.path.join(skill_root, 'customize.toml')]
|
|
121
|
+
|
|
122
|
+
if project_root:
|
|
123
|
+
layers += [
|
|
124
|
+
os.path.join(project_root, '_rtexit', 'custom', f'{skill_name}.toml'),
|
|
125
|
+
os.path.join(project_root, '_rtexit', 'custom', f'{skill_name}.user.toml'),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
config = {}
|
|
129
|
+
for layer in layers:
|
|
130
|
+
layer_config = load_toml(layer)
|
|
131
|
+
config = deep_merge(config, layer_config)
|
|
132
|
+
|
|
133
|
+
return config
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main():
|
|
137
|
+
parser = argparse.ArgumentParser(description='RTExit Customization Resolver')
|
|
138
|
+
parser.add_argument('--skill', required=True, help='Skill root directory')
|
|
139
|
+
parser.add_argument('--project-root', help='Project root (for custom overrides)')
|
|
140
|
+
parser.add_argument('--key', help='Specific section to return (e.g. agent)')
|
|
141
|
+
args = parser.parse_args()
|
|
142
|
+
|
|
143
|
+
config = resolve_customization(args.skill, args.project_root)
|
|
144
|
+
|
|
145
|
+
if args.key:
|
|
146
|
+
result = config.get(args.key, {})
|
|
147
|
+
else:
|
|
148
|
+
result = config
|
|
149
|
+
|
|
150
|
+
print(json.dumps(result, indent=2))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == '__main__':
|
|
154
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rtexit-method",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "RTExit - AI-assisted Red Team methodology installer",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Exit Code",
|
|
7
|
+
"bin": {
|
|
8
|
+
"rt": "tools/installer/rt-cli.js",
|
|
9
|
+
"rtexit": "tools/installer/rt-cli.js"
|
|
10
|
+
},
|
|
11
|
+
"main": "tools/installer/rt-cli.js",
|
|
12
|
+
"files": [
|
|
13
|
+
".agents/skills/rt-*",
|
|
14
|
+
"_rtexit",
|
|
15
|
+
"resources",
|
|
16
|
+
"templates",
|
|
17
|
+
"tools/installer",
|
|
18
|
+
"RTEXIT.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test test/*.test.js",
|
|
22
|
+
"test:cli": "node --test test/cli-help.test.js",
|
|
23
|
+
"test:install": "node --test test/install-command.test.js test/copy-assets.test.js test/write-config.test.js"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@clack/prompts": "^1.4.0",
|
|
27
|
+
"commander": "^14.0.0"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.12.0"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/exit-code-eg/RTExit.git"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://www.exitcode.me/",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/exit-code-eg/RTExit/issues"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"red-team",
|
|
42
|
+
"red-teaming",
|
|
43
|
+
"security",
|
|
44
|
+
"pentest",
|
|
45
|
+
"cli",
|
|
46
|
+
"ai",
|
|
47
|
+
"methodology",
|
|
48
|
+
"rtexit"
|
|
49
|
+
],
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# RTExit Learning Path and Certifications
|
|
2
|
+
|
|
3
|
+
## Foundations
|
|
4
|
+
|
|
5
|
+
| Area | Suggested Learning |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Web security | OWASP WSTG, PortSwigger Web Security Academy |
|
|
8
|
+
| Mobile security | OWASP MASVS/MSTG |
|
|
9
|
+
| Cloud security | AWS/Azure/GCP security fundamentals |
|
|
10
|
+
| Active Directory | Windows security fundamentals and AD lab practice |
|
|
11
|
+
| Reporting | CVSS 4.0, MITRE ATT&CK, executive writing |
|
|
12
|
+
|
|
13
|
+
## Certification Map
|
|
14
|
+
|
|
15
|
+
| Level | Examples |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Beginner | Security+, eJPT |
|
|
18
|
+
| Intermediate | PNPT, eCPPT, Burp Suite Certified Practitioner |
|
|
19
|
+
| Advanced | OSCP, CRTO, CARTP |
|
|
20
|
+
| Expert | OSEP, OSED, cloud specialty certifications |
|
|
21
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# RTExit Payload Reference
|
|
2
|
+
|
|
3
|
+
This repository intentionally keeps payload references defensive and low-risk.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
- Prefer harmless markers over exploit payloads.
|
|
8
|
+
- Use lab-only payloads outside production.
|
|
9
|
+
- Do not store real credentials, web shells, malware, or client data in this repository.
|
|
10
|
+
- Every payload class used in a report must include remediation guidance.
|
|
11
|
+
|
|
12
|
+
## Safe Marker Examples
|
|
13
|
+
|
|
14
|
+
| Class | Marker |
|
|
15
|
+
|---|---|
|
|
16
|
+
| XSS | `RTEXIT_XSS_MARKER` |
|
|
17
|
+
| SSTI | `RTEXIT_TEMPLATE_MARKER` |
|
|
18
|
+
| SQLi | Boolean behavior probe against test data |
|
|
19
|
+
| SSRF | Controlled callback URL |
|
|
20
|
+
| File upload | Text file with known hash |
|
|
21
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# RTExit Tool Reference
|
|
2
|
+
|
|
3
|
+
Use tools only inside the authorized scope and record commands through the autodoc workflow.
|
|
4
|
+
|
|
5
|
+
## Planning and Reporting
|
|
6
|
+
|
|
7
|
+
| Tool | Purpose | Notes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| Markdown | Engagement docs and reports | Keep evidence links relative to `_rtexit-output/docs/` |
|
|
10
|
+
| Python 3 | Automation scripts | Used by config resolver, tracker, autodoc |
|
|
11
|
+
| Pandoc | Report conversion | Optional for PDF/DOCX export |
|
|
12
|
+
|
|
13
|
+
## Reconnaissance
|
|
14
|
+
|
|
15
|
+
| Tool | Purpose | Notes |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Amass | Passive/active asset discovery | Respect active mode authorization |
|
|
18
|
+
| Subfinder | Passive subdomain discovery | Good first-pass inventory |
|
|
19
|
+
| httpx | HTTP probing | Capture title, status, tech, TLS |
|
|
20
|
+
| Nmap | Network and service discovery | Stage scans and obey rate limits |
|
|
21
|
+
| Nuclei | Template-based validation | Review templates before running |
|
|
22
|
+
| Shodan/Censys | Internet exposure search | Passive external visibility |
|
|
23
|
+
|
|
24
|
+
## Web and API
|
|
25
|
+
|
|
26
|
+
| Tool | Purpose | Notes |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| Burp Suite | Proxy, repeater, scanner, evidence | Save project files per engagement |
|
|
29
|
+
| OWASP ZAP | Proxy/scanner alternative | Useful for baseline scans |
|
|
30
|
+
| ffuf | Content and parameter discovery | Use scoped wordlists and rate limits |
|
|
31
|
+
| sqlmap | SQL injection validation | Use only with explicit approval |
|
|
32
|
+
| jwt-cli/jq | Token and JSON inspection | Do not forge production tokens |
|
|
33
|
+
|
|
34
|
+
## Mobile and Desktop
|
|
35
|
+
|
|
36
|
+
| Tool | Purpose | Notes |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| apktool | Android package inspection | Static analysis |
|
|
39
|
+
| jadx | Android decompilation | Source review |
|
|
40
|
+
| Frida | Dynamic instrumentation | Requires explicit authorization |
|
|
41
|
+
| MobSF | Mobile assessment platform | Good triage and reporting |
|
|
42
|
+
| dnSpy/ILSpy | .NET inspection | Desktop/.NET review |
|
|
43
|
+
|
|
44
|
+
## Cloud and Infrastructure
|
|
45
|
+
|
|
46
|
+
| Tool | Purpose | Notes |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| AWS CLI | AWS inventory and validation | Use read-only roles where possible |
|
|
49
|
+
| Azure CLI | Azure inventory and validation | Respect tenant boundaries |
|
|
50
|
+
| gcloud | GCP inventory and validation | Use scoped projects |
|
|
51
|
+
| ScoutSuite/Prowler | Cloud posture review | Validate findings manually |
|
|
52
|
+
| kubectl | Kubernetes review | Use scoped kubeconfig |
|
|
53
|
+
|