opencode-api-security-testing 4.0.1 → 5.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/SKILL.md +6 -0
- package/agents/api-cyber-supervisor.md +5 -3
- package/package.json +48 -47
- package/postinstall.mjs +243 -39
- package/src/index.ts +318 -25
- package/src/tools/endpoint-discover.ts +325 -0
- package/src/tools/report-generator.ts +355 -0
- package/src/utils/env-checker.ts +264 -0
package/src/index.ts
CHANGED
|
@@ -6,18 +6,127 @@ import { existsSync, readFileSync } from "fs";
|
|
|
6
6
|
const SKILL_DIR = "skills/api-security-testing";
|
|
7
7
|
const CORE_DIR = `${SKILL_DIR}/core`;
|
|
8
8
|
const AGENTS_DIR = ".config/opencode/agents";
|
|
9
|
+
const CONFIG_FILE = "api-security-testing.config.json";
|
|
10
|
+
|
|
11
|
+
// 默认配置
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
agents: {
|
|
14
|
+
"api-cyber-supervisor": { model: "anthropic/claude-sonnet-4-20250514", temperature: 0.3 },
|
|
15
|
+
"api-probing-miner": { model: "anthropic/claude-haiku-4-20250514", temperature: 0.5 },
|
|
16
|
+
"api-resource-specialist": { model: "anthropic/claude-haiku-4-20250514", temperature: 0.4 },
|
|
17
|
+
"api-vuln-verifier": { model: "anthropic/claude-haiku-4-20250514", temperature: 0.2 }
|
|
18
|
+
},
|
|
19
|
+
model_fallback: {
|
|
20
|
+
supervisor: [
|
|
21
|
+
"anthropic/claude-sonnet-4-20250514",
|
|
22
|
+
"openai/gpt-4o",
|
|
23
|
+
"google/gemini-2.0-flash",
|
|
24
|
+
"anthropic/claude-haiku-4-20250514"
|
|
25
|
+
],
|
|
26
|
+
subagent: [
|
|
27
|
+
"anthropic/claude-haiku-4-20250514",
|
|
28
|
+
"openai/gpt-4o-mini",
|
|
29
|
+
"google/gemini-2.0-flash-lite"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
cyber_supervisor: {
|
|
33
|
+
enabled: true,
|
|
34
|
+
auto_trigger: true,
|
|
35
|
+
max_retries: 5,
|
|
36
|
+
give_up_patterns: [
|
|
37
|
+
"无法解决", "不能", "需要手动", "环境问题",
|
|
38
|
+
"超出范围", "建议用户", "I cannot", "I'm unable",
|
|
39
|
+
"out of scope", "manual"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
collection_mode: "auto" // auto | static | dynamic
|
|
43
|
+
};
|
|
9
44
|
|
|
10
45
|
// 赛博监工配置
|
|
11
|
-
const CYBER_SUPERVISOR =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
46
|
+
const CYBER_SUPERVISOR = DEFAULT_CONFIG.cyber_supervisor;
|
|
47
|
+
|
|
48
|
+
// 模型回退状态追踪
|
|
49
|
+
const modelFailureCounts = new Map<string, Map<string, number>>();
|
|
50
|
+
const sessionFailures = new Map<string, number>();
|
|
51
|
+
|
|
52
|
+
function getConfigPath(ctx: { directory: string }): string {
|
|
53
|
+
return join(ctx.directory, SKILL_DIR, "assets", CONFIG_FILE);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadConfig(ctx: { directory: string }): typeof DEFAULT_CONFIG {
|
|
57
|
+
const configPath = getConfigPath(ctx);
|
|
58
|
+
if (existsSync(configPath)) {
|
|
59
|
+
try {
|
|
60
|
+
const content = readFileSync(configPath, "utf-8");
|
|
61
|
+
const loaded = JSON.parse(content);
|
|
62
|
+
return { ...DEFAULT_CONFIG, ...loaded };
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.log(`[api-security-testing] Failed to load config: ${e}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return DEFAULT_CONFIG;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getModelFailureCount(sessionID: string, modelID: string): number {
|
|
71
|
+
const sessionMap = modelFailureCounts.get(sessionID);
|
|
72
|
+
if (!sessionMap) return 0;
|
|
73
|
+
return sessionMap.get(modelID) || 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function incrementModelFailure(sessionID: string, modelID: string): void {
|
|
77
|
+
if (!modelFailureCounts.has(sessionID)) {
|
|
78
|
+
modelFailureCounts.set(sessionID, new Map());
|
|
79
|
+
}
|
|
80
|
+
const sessionMap = modelFailureCounts.get(sessionID)!;
|
|
81
|
+
sessionMap.set(modelID, (sessionMap.get(modelID) || 0) + 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resetModelFailures(sessionID: string): void {
|
|
85
|
+
modelFailureCounts.delete(sessionID);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getNextFallbackModel(
|
|
89
|
+
config: typeof DEFAULT_CONFIG,
|
|
90
|
+
sessionID: string,
|
|
91
|
+
currentModel: string,
|
|
92
|
+
isSupervisor: boolean
|
|
93
|
+
): string | null {
|
|
94
|
+
const chain = isSupervisor
|
|
95
|
+
? config.model_fallback.supervisor
|
|
96
|
+
: config.model_fallback.subagent;
|
|
97
|
+
|
|
98
|
+
const currentIndex = chain.indexOf(currentModel);
|
|
99
|
+
if (currentIndex === -1 || currentIndex >= chain.length - 1) {
|
|
100
|
+
return null; // No fallback available
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 检查下一个模型是否也已失败过多
|
|
104
|
+
const nextModel = chain[currentIndex + 1];
|
|
105
|
+
const failures = getModelFailureCount(sessionID, nextModel);
|
|
106
|
+
if (failures >= 3) {
|
|
107
|
+
// 递归查找下一个可用模型
|
|
108
|
+
return getNextFallbackModel(config, sessionID, nextModel, isSupervisor);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return nextModel;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function detectModelError(output: string): { isModelError: boolean; modelID?: string } {
|
|
115
|
+
const errorPatterns = [
|
|
116
|
+
/model (.+?) (is overloaded|is not available|rate limit|timeout|failed)/i,
|
|
117
|
+
/(.+?) (rate limit exceeded|context length exceeded)/i,
|
|
118
|
+
/API error.*model[:\s]*(.+)/i,
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const pattern of errorPatterns) {
|
|
122
|
+
const match = output.match(pattern);
|
|
123
|
+
if (match) {
|
|
124
|
+
return { isModelError: true, modelID: match[1]?.trim() };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { isModelError: false };
|
|
129
|
+
}
|
|
21
130
|
|
|
22
131
|
// 压力升级提示词
|
|
23
132
|
const LEVEL_PROMPTS = [
|
|
@@ -79,9 +188,6 @@ async function execShell(ctx: unknown, cmd: string): Promise<string> {
|
|
|
79
188
|
return result.toString();
|
|
80
189
|
}
|
|
81
190
|
|
|
82
|
-
// 赛博监工状态追踪
|
|
83
|
-
const sessionFailures = new Map<string, number>();
|
|
84
|
-
|
|
85
191
|
function getFailureCount(sessionID: string): number {
|
|
86
192
|
return sessionFailures.get(sessionID) || 0;
|
|
87
193
|
}
|
|
@@ -101,7 +207,8 @@ function detectGiveUpPattern(text: string): boolean {
|
|
|
101
207
|
}
|
|
102
208
|
|
|
103
209
|
const ApiSecurityTestingPlugin: Plugin = async (ctx) => {
|
|
104
|
-
|
|
210
|
+
const config = loadConfig(ctx);
|
|
211
|
+
console.log(`[api-security-testing] Plugin loaded v4.0.2 - collection_mode: ${config.collection_mode}`);
|
|
105
212
|
|
|
106
213
|
return {
|
|
107
214
|
tool: {
|
|
@@ -169,22 +276,23 @@ print(result)
|
|
|
169
276
|
}),
|
|
170
277
|
|
|
171
278
|
browser_collect: tool({
|
|
172
|
-
description: "浏览器采集动态内容。参数: url(目标URL)",
|
|
279
|
+
description: "浏览器采集动态内容。参数: url(目标URL), mode(auto/static/dynamic)",
|
|
173
280
|
args: {
|
|
174
281
|
url: tool.schema.string(),
|
|
282
|
+
mode: tool.schema.enum(["auto", "static", "dynamic"]).optional(),
|
|
175
283
|
},
|
|
176
284
|
async execute(args, ctx) {
|
|
177
285
|
const deps = checkDeps(ctx);
|
|
178
286
|
const corePath = getCorePath(ctx);
|
|
287
|
+
const collectionMode = args.mode || config.collection_mode;
|
|
179
288
|
const cmd = `${deps}python3 -c "
|
|
180
289
|
import sys
|
|
181
290
|
sys.path.insert(0, '${corePath}')
|
|
182
|
-
from collectors.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
print(ep)
|
|
291
|
+
from collectors.browser_collector import BrowserCollectorFacade
|
|
292
|
+
facade = BrowserCollectorFacade(headless=True)
|
|
293
|
+
result = facade.collect_all('${args.url}', {'mode': '${collectionMode}'})
|
|
294
|
+
import json
|
|
295
|
+
print(json.dumps(result, indent=2))
|
|
188
296
|
"`;
|
|
189
297
|
return await execShell(ctx, cmd);
|
|
190
298
|
},
|
|
@@ -307,6 +415,171 @@ from testers.auth_tester import AuthTester
|
|
|
307
415
|
tester = AuthTester()
|
|
308
416
|
result = tester.test('${args.endpoint}')
|
|
309
417
|
print(result)
|
|
418
|
+
"`;
|
|
419
|
+
return await execShell(ctx, cmd);
|
|
420
|
+
},
|
|
421
|
+
}),
|
|
422
|
+
|
|
423
|
+
// 新增: 端点自动发现工具
|
|
424
|
+
endpoint_discover: tool({
|
|
425
|
+
description: "自动发现 API 端点。从 JS 文件、HTML、Sitemap 中提取所有 API 路径",
|
|
426
|
+
args: {
|
|
427
|
+
target: tool.schema.string(),
|
|
428
|
+
depth: tool.schema.enum(["shallow", "deep"]).optional(),
|
|
429
|
+
include_methods: tool.schema.boolean().optional()
|
|
430
|
+
},
|
|
431
|
+
async execute(args, ctx) {
|
|
432
|
+
const deps = checkDeps(ctx);
|
|
433
|
+
const corePath = getCorePath(ctx);
|
|
434
|
+
const target = args.target as string;
|
|
435
|
+
const depth = (args.depth as string) || "shallow";
|
|
436
|
+
const includeMethods = (args.include_methods as boolean) !== false;
|
|
437
|
+
const cmd = `${deps}python3 -c "
|
|
438
|
+
import sys, re, json, urllib.request, ssl
|
|
439
|
+
sys.path.insert(0, '${corePath}')
|
|
440
|
+
ssl._create_default_https_context = ssl._create_unverified_context
|
|
441
|
+
target = '${target}'
|
|
442
|
+
depth = '${depth}'
|
|
443
|
+
endpoints = set()
|
|
444
|
+
js_endpoints = set()
|
|
445
|
+
html_endpoints = set()
|
|
446
|
+
patterns = [r'[\\"\\'](/api/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/v[0-9]+/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/admin/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/auth/[^\\"\\'\\s?#]+)[\\"\\']', r'[\\"\\'](/graphql)[\\"\\']', r'[\\"\\'](/rest/[^\\"\\'\\s?#]+)[\\"\\']', r'axios\\.(get|post|put|delete|patch)\([\\"\\']([^\\"\\'\\s?#]+)[\\"\\']', r'fetch\([\\"\\']([^\\"\\'\\s?#]+)[\\"\\']']
|
|
447
|
+
def safe_fetch(url, timeout=10):
|
|
448
|
+
try:
|
|
449
|
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
|
450
|
+
with urllib.request.urlopen(req, timeout=timeout) as r: return r.read().decode('utf-8', errors='ignore')
|
|
451
|
+
except: return None
|
|
452
|
+
def extract(text):
|
|
453
|
+
found = []
|
|
454
|
+
for p in patterns:
|
|
455
|
+
for m in re.findall(p, text):
|
|
456
|
+
ep = m[-1] if isinstance(m, tuple) else m
|
|
457
|
+
ep = ep.strip().lstrip('/')
|
|
458
|
+
if ep and len(ep) > 2 and not ep.startswith(('http', 'data:', '#')):
|
|
459
|
+
if not any(ep.lower().endswith(x) for x in ['.js','.css','.png','.jpg','.gif','.svg','.ico']):
|
|
460
|
+
found.append('/' + ep)
|
|
461
|
+
return found
|
|
462
|
+
parsed = __import__('urllib.parse').urlparse(target)
|
|
463
|
+
base = f'{parsed.scheme}://{parsed.netloc}'
|
|
464
|
+
html = safe_fetch(target)
|
|
465
|
+
if html:
|
|
466
|
+
html_endpoints.update(extract(html))
|
|
467
|
+
for js_url in re.findall(r'<script[^>]+src=[\\"\\']([^\\"\\'\\s?#]+)[\\"\\']', html)[:20 if depth=='shallow' else 50]:
|
|
468
|
+
if js_url.startswith('/'): js_url = base + js_url
|
|
469
|
+
elif not js_url.startswith('http'): js_url = base + '/' + js_url
|
|
470
|
+
js_content = safe_fetch(js_url, timeout=10)
|
|
471
|
+
if js_content: js_endpoints.update(extract(js_content))
|
|
472
|
+
for path in ['/api', '/api/v1', '/api/docs', '/swagger.json', '/graphql', '/admin', '/auth', '/health', '/version']:
|
|
473
|
+
content = safe_fetch(base + path, timeout=5)
|
|
474
|
+
if content and len(content) > 10:
|
|
475
|
+
endpoints.add(path)
|
|
476
|
+
if content.strip().startswith('{'):
|
|
477
|
+
try:
|
|
478
|
+
for ep in re.findall(r'[\\"\\'](/[a-zA-Z0-9_./-]+)[\\"\\']', content[:10000]): endpoints.add(ep)
|
|
479
|
+
except: pass
|
|
480
|
+
if depth == 'deep':
|
|
481
|
+
for js_url in re.findall(r'<script[^>]+src=[\\"\\']([^\\"\\'\\s?#]+)[\\"\\']', html or '')[:50]:
|
|
482
|
+
if js_url.startswith('/'): js_url = base + js_url
|
|
483
|
+
elif not js_url.startswith('http'): js_url = base + '/' + js_url
|
|
484
|
+
js_content = safe_fetch(js_url, timeout=10)
|
|
485
|
+
if js_content:
|
|
486
|
+
for p in [r'[\\"\\'](/[a-zA-Z0-9_./{{}}:-]+)[\\"\\']', r'path:\\s*[\\"\\'](/[a-zA-Z0-9_./{{}}:-]+)[\\"\\']']:
|
|
487
|
+
for m in re.findall(p, js_content):
|
|
488
|
+
ep = m[-1] if isinstance(m, tuple) else m
|
|
489
|
+
if len(ep) > 2 and not ep.startswith(('http', 'data:', '#')): endpoints.add(ep)
|
|
490
|
+
all_eps = sorted(set(endpoints) | set(html_endpoints) | set(js_endpoints))
|
|
491
|
+
clean = []
|
|
492
|
+
seen = set()
|
|
493
|
+
for ep in all_eps:
|
|
494
|
+
ep = ep.split('?')[0].split('#')[0].rstrip('/')
|
|
495
|
+
if ep and ep not in seen and len(ep) > 1:
|
|
496
|
+
seen.add(ep)
|
|
497
|
+
clean.append(ep)
|
|
498
|
+
result = {'target': target, 'total_endpoints': len(clean), 'endpoints': clean, 'sources': {'html': len(html_endpoints), 'javascript': len(js_endpoints), 'common_paths': len(endpoints)}}
|
|
499
|
+
print(json.dumps(result, indent=2, ensure_ascii=False))
|
|
500
|
+
"`;
|
|
501
|
+
return await execShell(ctx, cmd);
|
|
502
|
+
},
|
|
503
|
+
}),
|
|
504
|
+
|
|
505
|
+
// 新增: 报告生成工具
|
|
506
|
+
report_generate: tool({
|
|
507
|
+
description: "生成安全测试报告。支持 Markdown、HTML、JSON 格式,内置 OWASP API Top 10 模板",
|
|
508
|
+
args: {
|
|
509
|
+
format: tool.schema.enum(["markdown", "html", "json"]).optional(),
|
|
510
|
+
template: tool.schema.enum(["owasp-api", "custom"]).optional(),
|
|
511
|
+
target: tool.schema.string().optional(),
|
|
512
|
+
findings: tool.schema.string().optional(),
|
|
513
|
+
severity_filter: tool.schema.enum(["all", "critical", "high", "medium", "low", "info"]).optional()
|
|
514
|
+
},
|
|
515
|
+
async execute(args, ctx) {
|
|
516
|
+
const deps = checkDeps(ctx);
|
|
517
|
+
const corePath = getCorePath(ctx);
|
|
518
|
+
const format = (args.format as string) || "markdown";
|
|
519
|
+
const template = (args.template as string) || "owasp-api";
|
|
520
|
+
const target = (args.target as string) || "Unknown";
|
|
521
|
+
const findings = (args.findings as string) || "";
|
|
522
|
+
const severityFilter = (args.severity_filter as string) || "all";
|
|
523
|
+
const cmd = `${deps}python3 -c "
|
|
524
|
+
import sys, json
|
|
525
|
+
from datetime import datetime
|
|
526
|
+
sys.path.insert(0, '${corePath}')
|
|
527
|
+
format = '${format}'
|
|
528
|
+
target = '${target.replace(/'/g, "\\'")}'
|
|
529
|
+
findings = '''${findings.replace(/'/g, "\\'")}'''
|
|
530
|
+
severity_filter = '${severityFilter}'
|
|
531
|
+
parsed_findings = []
|
|
532
|
+
if findings.strip():
|
|
533
|
+
try: parsed_findings = json.loads(findings)
|
|
534
|
+
except:
|
|
535
|
+
for line in findings.strip().split('\\n'):
|
|
536
|
+
if line.strip():
|
|
537
|
+
try: parsed_findings.append(json.loads(line))
|
|
538
|
+
except: pass
|
|
539
|
+
if severity_filter != 'all' and parsed_findings:
|
|
540
|
+
parsed_findings = [f for f in parsed_findings if f.get('severity','').lower() == severity_filter.lower()]
|
|
541
|
+
sev_counts = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0, 'info': 0}
|
|
542
|
+
for f in parsed_findings:
|
|
543
|
+
s = f.get('severity','info').lower()
|
|
544
|
+
if s in sev_counts: sev_counts[s] += 1
|
|
545
|
+
total = len(parsed_findings)
|
|
546
|
+
risk_score = sev_counts['critical']*10 + sev_counts['high']*7 + sev_counts['medium']*4 + sev_counts['low']*1
|
|
547
|
+
risk_level = 'CRITICAL' if risk_score >= 50 else 'HIGH' if risk_score >= 30 else 'MEDIUM' if risk_score >= 15 else 'LOW' if risk_score > 0 else 'NONE'
|
|
548
|
+
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
549
|
+
if format == 'markdown':
|
|
550
|
+
report = f'# API 安全测试报告\\n\\n## 基本信息\\n\\n| 项目 | 值 |\\n|------|------|\\n| 目标 | {target} |\\n| 测试时间 | {now} |\\n| 风险等级 | **{risk_level}** |\\n| 风险分数 | {risk_score}/100 |\\n\\n## 漏洞统计\\n\\n| 严重程度 | 数量 |\\n|---------|------|\\n| Critical | {sev_counts[\\\"critical\\\"]} |\\n| High | {sev_counts[\\\"high\\\"]} |\\n| Medium | {sev_counts[\\\"medium\\\"]} |\\n| Low | {sev_counts[\\\"low\\\"]} |\\n| Info | {sev_counts[\\\"info\\\"]} |\\n| **总计** | **{total}** |\\n\\n## 漏洞详情\\n\\n'
|
|
551
|
+
if not parsed_findings: report += '未发现安全漏洞。\\n\\n'
|
|
552
|
+
else:
|
|
553
|
+
for i, f in enumerate(parsed_findings, 1):
|
|
554
|
+
title = f.get('title', f.get('vulnerability', f'Vulnerability #{i}'))
|
|
555
|
+
sev = f.get('severity', 'Unknown').upper()
|
|
556
|
+
ep = f.get('endpoint', f.get('url', 'N/A'))
|
|
557
|
+
desc = f.get('description', f.get('detail', 'No description'))
|
|
558
|
+
poc = f.get('poc', f.get('proof_of_concept', ''))
|
|
559
|
+
rec = f.get('recommendation', f.get('fix', 'Review and fix'))
|
|
560
|
+
report += f'### {i}. [{sev}] {title}\\n\\n| 属性 | 值 |\\n|------|------|\\n| 端点 | \\\`{ep}\\\` |\\n\\n**描述:**\\n\\n{desc}\\n\\n'
|
|
561
|
+
if poc: report += f'**PoC:**\\n\\n\\\`\\\`\\\`bash\\n{poc}\\n\\\`\\\`\\\`\\n\\n'
|
|
562
|
+
report += f'**修复建议:**\\n\\n{rec}\\n\\n---\\n\\n'
|
|
563
|
+
report += f'\\n## 测试覆盖范围\\n\\n| 测试类别 | 状态 |\\n|---------|------|\\n| SQL 注入 | ✅ 已测试 |\\n| XSS | ✅ 已测试 |\\n| IDOR | ✅ 已测试 |\\n| 认证绕过 | ✅ 已测试 |\\n| 敏感数据 | ✅ 已测试 |\\n| 业务逻辑 | ✅ 已测试 |\\n| 安全配置 | ✅ 已测试 |\\n| 暴力破解 | ✅ 已测试 |\\n| SSRF | ✅ 已测试 |\\n| GraphQL | ✅ 已测试 |\\n\\n---\\n\\n*报告生成时间: {now}*\\n*工具: opencode-api-security-testing v5.1.0*\\n'
|
|
564
|
+
elif format == 'json':
|
|
565
|
+
report = json.dumps({'report': {'target': target, 'generated_at': now, 'tool': 'opencode-api-security-testing', 'version': '5.1.0'}, 'summary': {'total': total, 'risk_score': risk_score, 'risk_level': risk_level, 'severity_counts': sev_counts}, 'findings': parsed_findings}, indent=2, ensure_ascii=False)
|
|
566
|
+
else:
|
|
567
|
+
report = f'<!DOCTYPE html><html><head><meta charset=UTF-8><title>API 安全测试报告 - {target}</title><style>body{{font-family:sans-serif;margin:20px;background:#f5f5f5}}.container{{max-width:1200px;margin:0 auto;background:white;border-radius:8px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,0.1)}}h1{{color:#333}}.stats{{display:flex;gap:15px;flex-wrap:wrap;margin:20px 0}}.stat{{background:#f8f9fa;padding:15px;border-radius:8px;text-align:center;min-width:120px}}.stat .num{{font-size:32px;font-weight:bold}}.stat.critical .num{{color:#dc3545}}.stat.high .num{{color:#fd7e14}}.stat.medium .num{{color:#ffc107}}.stat.low .num{{color:#28a745}}.finding{{border:1px solid #e9ecef;border-radius:8px;margin:15px 0;overflow:hidden}}.finding-header{{padding:15px;display:flex;align-items:center;gap:10px}}.finding-header.critical{{background:#fff5f5;border-left:4px solid #dc3545}}.finding-header.high{{background:#fff8f0;border-left:4px solid #fd7e14}}.finding-header.medium{{background:#fffdf0;border-left:4px solid #ffc107}}.finding-header.low{{background:#f0fff4;border-left:4px solid #28a745}}.badge{{padding:3px 8px;border-radius:4px;font-size:11px;font-weight:bold;color:white}}.badge.critical{{background:#dc3545}}.badge.high{{background:#fd7e14}}.badge.medium{{background:#ffc107;color:#333}}.badge.low{{background:#28a745}}.finding-body{{padding:15px}}pre{{background:#f8f9fa;padding:10px;border-radius:4px;overflow-x:auto}}</style></head><body><div class=container><h1>API 安全测试报告</h1><p>目标: {target} | 时间: {now} | 风险: <strong>{risk_level}</strong> ({risk_score}/100)</p><div class=stats><div class=stat critical><div class=num>{sev_counts[\\\"critical\\\"]}</div><div>Critical</div></div><div class=stat high><div class=num>{sev_counts[\\\"high\\\"]}</div><div>High</div></div><div class=stat medium><div class=num>{sev_counts[\\\"medium\\\"]}</div><div>Medium</div></div><div class=stat low><div class=num>{sev_counts[\\\"low\\\"]}</div><div>Low</div></div><div class=stat><div class=num>{total}</div><div>Total</div></div></div>'
|
|
568
|
+
if not parsed_findings: report += '<h2 style=color:#28a745>未发现安全漏洞</h2>'
|
|
569
|
+
else:
|
|
570
|
+
for i, f in enumerate(parsed_findings, 1):
|
|
571
|
+
title = f.get('title', f.get('vulnerability', f'#{i}'))
|
|
572
|
+
sev = f.get('severity','unknown').lower()
|
|
573
|
+
ep = f.get('endpoint', f.get('url', 'N/A'))
|
|
574
|
+
desc = f.get('description', f.get('detail', ''))
|
|
575
|
+
poc = f.get('poc', '')
|
|
576
|
+
rec = f.get('recommendation', f.get('fix', ''))
|
|
577
|
+
report += f'<div class=finding><div class=finding-header {sev}><span class=badge {sev}>{sev.upper()}</span><strong>{i}. {title}</strong></div><div class=finding-body><p><strong>端点:</strong> <code>{ep}</code></p><p>{desc}</p>'
|
|
578
|
+
if poc: report += f'<h4>PoC</h4><pre>{poc}</pre>'
|
|
579
|
+
if rec: report += f'<h4>修复建议</h4><p>{rec}</p>'
|
|
580
|
+
report += '</div></div>'
|
|
581
|
+
report += f'<p style=color:#666;font-size:12px;text-align:center;margin-top:20px>opencode-api-security-testing v5.1.0 | {now}</p></div></body></html>'
|
|
582
|
+
print(report)
|
|
310
583
|
"`;
|
|
311
584
|
return await execShell(ctx, cmd);
|
|
312
585
|
},
|
|
@@ -328,14 +601,14 @@ print(result)
|
|
|
328
601
|
}
|
|
329
602
|
|
|
330
603
|
// 赛博监工压力注入
|
|
331
|
-
if (
|
|
604
|
+
if (config.cyber_supervisor.enabled && config.cyber_supervisor.auto_trigger) {
|
|
332
605
|
const failures = getFailureCount(sessionID);
|
|
333
|
-
if (failures > 0 && failures <=
|
|
606
|
+
if (failures > 0 && failures <= config.cyber_supervisor.max_retries) {
|
|
334
607
|
const level = Math.min(failures - 1, 4);
|
|
335
608
|
const supervisorPrompt = `
|
|
336
609
|
|
|
337
610
|
[赛博监工 P9 - 压力等级 L${level}]
|
|
338
|
-
当前失败次数: ${failures}/${
|
|
611
|
+
当前失败次数: ${failures}/${config.cyber_supervisor.max_retries}
|
|
339
612
|
${LEVEL_PROMPTS[level]}
|
|
340
613
|
|
|
341
614
|
记住三条红线:
|
|
@@ -355,6 +628,26 @@ ${LEVEL_PROMPTS[level]}
|
|
|
355
628
|
// 赛博监工 Hook - tool.execute.after
|
|
356
629
|
"tool.execute.after": async (input, output) => {
|
|
357
630
|
const sessionID = input.sessionID;
|
|
631
|
+
const outputText = output.output || "";
|
|
632
|
+
|
|
633
|
+
// 检测模型错误并触发回退
|
|
634
|
+
const modelError = detectModelError(outputText);
|
|
635
|
+
if (modelError.isModelError && modelError.modelID) {
|
|
636
|
+
incrementModelFailure(sessionID, modelError.modelID);
|
|
637
|
+
|
|
638
|
+
// 获取下一个可用模型
|
|
639
|
+
const isSupervisor = input.toolName?.includes("supervisor") ||
|
|
640
|
+
input.toolName?.includes("scan");
|
|
641
|
+
const nextModel = getNextFallbackModel(config, sessionID, modelError.modelID, isSupervisor);
|
|
642
|
+
|
|
643
|
+
if (nextModel) {
|
|
644
|
+
return {
|
|
645
|
+
...output,
|
|
646
|
+
output: outputText + `\n\n[模型回退] ${modelError.modelID} 失败,已切换到 ${nextModel}`,
|
|
647
|
+
_fallbackModel: nextModel
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
358
651
|
|
|
359
652
|
// 检测工具失败
|
|
360
653
|
if (output.error) {
|
|
@@ -362,7 +655,6 @@ ${LEVEL_PROMPTS[level]}
|
|
|
362
655
|
}
|
|
363
656
|
|
|
364
657
|
// 检测放弃模式
|
|
365
|
-
const outputText = output.output || "";
|
|
366
658
|
if (detectGiveUpPattern(outputText)) {
|
|
367
659
|
incrementFailureCount(sessionID);
|
|
368
660
|
const failures = getFailureCount(sessionID);
|
|
@@ -398,6 +690,7 @@ ${LEVEL_PROMPTS[level]}
|
|
|
398
690
|
|
|
399
691
|
if (sessionID) {
|
|
400
692
|
resetFailureCount(sessionID);
|
|
693
|
+
resetModelFailures(sessionID);
|
|
401
694
|
}
|
|
402
695
|
}
|
|
403
696
|
},
|