agentshield-sdk 7.0.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/CHANGELOG.md +191 -0
- package/LICENSE +21 -0
- package/README.md +975 -0
- package/bin/agent-shield.js +680 -0
- package/package.json +118 -0
- package/src/adaptive.js +330 -0
- package/src/agent-protocol.js +998 -0
- package/src/alert-tuning.js +480 -0
- package/src/allowlist.js +603 -0
- package/src/audit-immutable.js +914 -0
- package/src/audit-streaming.js +469 -0
- package/src/badges.js +196 -0
- package/src/behavior-profiling.js +289 -0
- package/src/benchmark-harness.js +804 -0
- package/src/canary.js +271 -0
- package/src/certification.js +563 -0
- package/src/circuit-breaker.js +321 -0
- package/src/compliance.js +617 -0
- package/src/confidence-tuning.js +324 -0
- package/src/confused-deputy.js +624 -0
- package/src/context-scoring.js +360 -0
- package/src/conversation.js +494 -0
- package/src/cost-optimizer.js +1024 -0
- package/src/ctf.js +462 -0
- package/src/detector-core.js +1999 -0
- package/src/distributed.js +359 -0
- package/src/document-scanner.js +795 -0
- package/src/embedding.js +307 -0
- package/src/encoding.js +429 -0
- package/src/enterprise.js +405 -0
- package/src/errors.js +100 -0
- package/src/eu-ai-act.js +523 -0
- package/src/fuzzer.js +764 -0
- package/src/honeypot.js +328 -0
- package/src/i18n-patterns.js +523 -0
- package/src/index.js +430 -0
- package/src/integrations.js +528 -0
- package/src/llm-redteam.js +670 -0
- package/src/main.js +741 -0
- package/src/main.mjs +38 -0
- package/src/mcp-bridge.js +542 -0
- package/src/mcp-certification.js +846 -0
- package/src/mcp-sdk-integration.js +355 -0
- package/src/mcp-security-runtime.js +741 -0
- package/src/mcp-server.js +740 -0
- package/src/middleware.js +208 -0
- package/src/model-finetuning.js +884 -0
- package/src/model-fingerprint.js +1042 -0
- package/src/multi-agent-trust.js +453 -0
- package/src/multi-agent.js +404 -0
- package/src/multimodal.js +296 -0
- package/src/nist-mapping.js +505 -0
- package/src/observability.js +330 -0
- package/src/openclaw.js +450 -0
- package/src/otel.js +544 -0
- package/src/owasp-2025.js +483 -0
- package/src/pii.js +390 -0
- package/src/plugin-marketplace.js +628 -0
- package/src/plugin-system.js +349 -0
- package/src/policy-dsl.js +775 -0
- package/src/policy-extended.js +635 -0
- package/src/policy.js +443 -0
- package/src/presets.js +409 -0
- package/src/production.js +557 -0
- package/src/prompt-leakage.js +321 -0
- package/src/rag-vulnerability.js +579 -0
- package/src/redteam.js +475 -0
- package/src/response-handler.js +429 -0
- package/src/scanners.js +357 -0
- package/src/self-healing.js +363 -0
- package/src/semantic.js +339 -0
- package/src/shield-score.js +250 -0
- package/src/sso-saml.js +897 -0
- package/src/stream-scanner.js +806 -0
- package/src/testing.js +505 -0
- package/src/threat-encyclopedia.js +629 -0
- package/src/threat-intel-network.js +1017 -0
- package/src/token-analysis.js +467 -0
- package/src/tool-guard.js +412 -0
- package/src/tool-output-validator.js +354 -0
- package/src/utils.js +83 -0
- package/src/watermark.js +235 -0
- package/src/worker-scanner.js +601 -0
- package/types/index.d.ts +2088 -0
package/src/ctf.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Shield — Capture-the-Flag Challenge System
|
|
5
|
+
*
|
|
6
|
+
* A CTF engine for security researchers to test their ability to bypass
|
|
7
|
+
* Agent Shield's detection. Each challenge defines a goal (bypass detection,
|
|
8
|
+
* exfiltrate data, hijack role) and participants craft payloads that attempt
|
|
9
|
+
* to evade the scanner.
|
|
10
|
+
*
|
|
11
|
+
* All detection runs locally — no data ever leaves your environment.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { CTFEngine } = require('./ctf');
|
|
15
|
+
* const engine = new CTFEngine({ difficulty: 'medium' });
|
|
16
|
+
* const challenges = engine.getChallenges();
|
|
17
|
+
* const result = engine.submitAttempt('easy_01', 'some sneaky payload');
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { scanText } = require('./detector-core');
|
|
21
|
+
const { formatHeader, makeBar, generateId } = require('./utils');
|
|
22
|
+
|
|
23
|
+
// =========================================================================
|
|
24
|
+
// CHALLENGE DEFINITIONS
|
|
25
|
+
// =========================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} Challenge
|
|
29
|
+
* @property {string} id - Unique challenge identifier.
|
|
30
|
+
* @property {string} title - Human-readable title.
|
|
31
|
+
* @property {'easy'|'medium'|'hard'|'expert'} difficulty - Difficulty tier.
|
|
32
|
+
* @property {string} description - What the challenger must accomplish.
|
|
33
|
+
* @property {string} hint - A nudge toward a viable approach.
|
|
34
|
+
* @property {'bypass_detection'|'exfiltrate_data'|'hijack_role'} goal - The attack goal.
|
|
35
|
+
* @property {Function} validateFn - Receives a scan result, returns true if challenge is solved.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const CHALLENGES = [
|
|
39
|
+
// ---- Easy ----
|
|
40
|
+
{
|
|
41
|
+
id: 'easy_01',
|
|
42
|
+
title: 'Basic Injection Bypass',
|
|
43
|
+
difficulty: 'easy',
|
|
44
|
+
description: 'Craft a prompt injection that tells the AI to ignore its instructions without being detected.',
|
|
45
|
+
hint: 'The detector looks for common phrases like "ignore previous instructions". Can you say the same thing differently?',
|
|
46
|
+
goal: 'bypass_detection',
|
|
47
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'easy_02',
|
|
51
|
+
title: 'Base64 Smuggling',
|
|
52
|
+
difficulty: 'easy',
|
|
53
|
+
description: 'Hide an injection payload inside base64 encoding so the scanner misses it.',
|
|
54
|
+
hint: 'Wrap your attack payload in base64. The scanner may or may not decode it.',
|
|
55
|
+
goal: 'bypass_detection',
|
|
56
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'easy_03',
|
|
60
|
+
title: 'Role Reassignment',
|
|
61
|
+
difficulty: 'easy',
|
|
62
|
+
description: 'Convince the AI it is a different persona without triggering role_hijack detection.',
|
|
63
|
+
hint: 'Direct role reassignment phrases are caught. Try an indirect approach.',
|
|
64
|
+
goal: 'hijack_role',
|
|
65
|
+
validateFn: (scanResult) => {
|
|
66
|
+
return !scanResult.threats.some(t => t.category === 'role_hijack');
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'easy_04',
|
|
71
|
+
title: 'Simple Exfiltration',
|
|
72
|
+
difficulty: 'easy',
|
|
73
|
+
description: 'Request the AI to send data to an external URL without detection.',
|
|
74
|
+
hint: 'Direct send-to-URL patterns are caught. Think about indirect phrasing.',
|
|
75
|
+
goal: 'exfiltrate_data',
|
|
76
|
+
validateFn: (scanResult) => {
|
|
77
|
+
return !scanResult.threats.some(t => t.category === 'data_exfiltration');
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// ---- Medium ----
|
|
82
|
+
{
|
|
83
|
+
id: 'med_01',
|
|
84
|
+
title: 'Homoglyph Bypass',
|
|
85
|
+
difficulty: 'medium',
|
|
86
|
+
description: 'Use visually similar Unicode characters (homoglyphs) to bypass pattern matching.',
|
|
87
|
+
hint: 'Replace Latin characters with Cyrillic or Greek lookalikes.',
|
|
88
|
+
goal: 'bypass_detection',
|
|
89
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'med_02',
|
|
93
|
+
title: 'Multi-Language Injection',
|
|
94
|
+
difficulty: 'medium',
|
|
95
|
+
description: 'Write an injection in a language other than English, Spanish, French, German, Portuguese, or Chinese.',
|
|
96
|
+
hint: 'The detector covers several languages. Try one it might not cover.',
|
|
97
|
+
goal: 'bypass_detection',
|
|
98
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'med_03',
|
|
102
|
+
title: 'Base64 + Instruction Wrapping',
|
|
103
|
+
difficulty: 'medium',
|
|
104
|
+
description: 'Combine base64 encoding with a legitimate-sounding instruction wrapper to evade detection.',
|
|
105
|
+
hint: 'Encode the dangerous part and wrap it in something that looks like a normal request.',
|
|
106
|
+
goal: 'bypass_detection',
|
|
107
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'med_04',
|
|
111
|
+
title: 'Markdown Image Exfil',
|
|
112
|
+
difficulty: 'medium',
|
|
113
|
+
description: 'Exfiltrate data via a markdown image tag without triggering exfiltration detection.',
|
|
114
|
+
hint: 'The detector checks for query parameters with suspicious names. Can you use a different encoding?',
|
|
115
|
+
goal: 'exfiltrate_data',
|
|
116
|
+
validateFn: (scanResult) => {
|
|
117
|
+
return !scanResult.threats.some(t => t.category === 'data_exfiltration');
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// ---- Hard ----
|
|
122
|
+
{
|
|
123
|
+
id: 'hard_01',
|
|
124
|
+
title: 'Multi-Step Attack Chain',
|
|
125
|
+
difficulty: 'hard',
|
|
126
|
+
description: 'Build a multi-step attack: first establish trust, then inject instructions, then exfiltrate.',
|
|
127
|
+
hint: 'Each step individually may look safe. The challenge is making the full chain undetectable.',
|
|
128
|
+
goal: 'bypass_detection',
|
|
129
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'hard_02',
|
|
133
|
+
title: 'Semantic Injection',
|
|
134
|
+
difficulty: 'hard',
|
|
135
|
+
description: 'Write an injection that conveys the meaning of "ignore your instructions" without using any known trigger phrases.',
|
|
136
|
+
hint: 'Pattern matching is keyword-based. Rephrase using metaphors or analogies.',
|
|
137
|
+
goal: 'bypass_detection',
|
|
138
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'hard_03',
|
|
142
|
+
title: 'Nested Encoding',
|
|
143
|
+
difficulty: 'hard',
|
|
144
|
+
description: 'Layer multiple encodings (base64 inside URL encoding inside hex) to smuggle an injection.',
|
|
145
|
+
hint: 'The detector may decode one layer but not multiple nested layers.',
|
|
146
|
+
goal: 'bypass_detection',
|
|
147
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'hard_04',
|
|
151
|
+
title: 'Context Window Manipulation',
|
|
152
|
+
difficulty: 'hard',
|
|
153
|
+
description: 'Use a very long benign prefix to push the injection outside the scanning window.',
|
|
154
|
+
hint: 'Some scanners have time or length budgets. Exploit that.',
|
|
155
|
+
goal: 'bypass_detection',
|
|
156
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// ---- Expert ----
|
|
160
|
+
{
|
|
161
|
+
id: 'expert_01',
|
|
162
|
+
title: 'Zero-Day Novel Attack',
|
|
163
|
+
difficulty: 'expert',
|
|
164
|
+
description: 'Invent a completely novel injection technique that uses no known attack patterns.',
|
|
165
|
+
hint: 'Think about what the detector cannot see: timing, structure, metadata, or side channels.',
|
|
166
|
+
goal: 'bypass_detection',
|
|
167
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'expert_02',
|
|
171
|
+
title: 'Composite Multi-Vector',
|
|
172
|
+
difficulty: 'expert',
|
|
173
|
+
description: 'Combine at least 3 different attack vectors (injection, encoding, social engineering, exfiltration) in a single payload that evades all detection.',
|
|
174
|
+
hint: 'Each vector adds risk of detection. The art is combining them subtly.',
|
|
175
|
+
goal: 'bypass_detection',
|
|
176
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'expert_03',
|
|
180
|
+
title: 'Steganographic Payload',
|
|
181
|
+
difficulty: 'expert',
|
|
182
|
+
description: 'Hide an injection payload using invisible Unicode characters or zero-width joiners that the scanner misses.',
|
|
183
|
+
hint: 'The steganography detector checks for known invisible chars. Find ones it does not cover.',
|
|
184
|
+
goal: 'bypass_detection',
|
|
185
|
+
validateFn: (scanResult) => scanResult.status === 'safe'
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'expert_04',
|
|
189
|
+
title: 'Full System Compromise',
|
|
190
|
+
difficulty: 'expert',
|
|
191
|
+
description: 'Achieve role hijack, data exfiltration, and instruction override simultaneously without any detection.',
|
|
192
|
+
hint: 'This requires mastery of all bypass techniques. Good luck.',
|
|
193
|
+
goal: 'bypass_detection',
|
|
194
|
+
validateFn: (scanResult) => scanResult.status === 'safe' && scanResult.threats.length === 0
|
|
195
|
+
}
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
// =========================================================================
|
|
199
|
+
// DIFFICULTY CONFIG
|
|
200
|
+
// =========================================================================
|
|
201
|
+
|
|
202
|
+
const DIFFICULTY_CONFIG = {
|
|
203
|
+
easy: { sensitivity: 'low', label: 'Easy' },
|
|
204
|
+
medium: { sensitivity: 'medium', label: 'Medium' },
|
|
205
|
+
hard: { sensitivity: 'high', label: 'Hard' },
|
|
206
|
+
expert: { sensitivity: 'high', label: 'Expert' }
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// =========================================================================
|
|
210
|
+
// CTF ENGINE
|
|
211
|
+
// =========================================================================
|
|
212
|
+
|
|
213
|
+
class CTFEngine {
|
|
214
|
+
/**
|
|
215
|
+
* Creates a new CTF engine instance.
|
|
216
|
+
*
|
|
217
|
+
* @param {object} [options] - Configuration options.
|
|
218
|
+
* @param {object} [options.shieldConfig] - AgentShield configuration overrides.
|
|
219
|
+
* @param {'easy'|'medium'|'hard'|'expert'} [options.difficulty='medium'] - Default difficulty tier.
|
|
220
|
+
*/
|
|
221
|
+
constructor(options = {}) {
|
|
222
|
+
this.shieldConfig = options.shieldConfig || {};
|
|
223
|
+
this.difficulty = options.difficulty || 'medium';
|
|
224
|
+
this.attempts = {}; // challengeId -> { count, solved, payloads }
|
|
225
|
+
this.startTime = Date.now();
|
|
226
|
+
|
|
227
|
+
console.log(`[Agent Shield] CTF engine initialized (difficulty: ${this.difficulty})`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Returns a single challenge by ID.
|
|
232
|
+
*
|
|
233
|
+
* @param {string} id - Challenge identifier.
|
|
234
|
+
* @returns {object|null} The challenge object or null if not found.
|
|
235
|
+
*/
|
|
236
|
+
getChallenge(id) {
|
|
237
|
+
const challenge = CHALLENGES.find(c => c.id === id);
|
|
238
|
+
if (!challenge) {
|
|
239
|
+
console.log(`[Agent Shield] CTF challenge not found: ${id}`);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
id: challenge.id,
|
|
244
|
+
title: challenge.title,
|
|
245
|
+
difficulty: challenge.difficulty,
|
|
246
|
+
description: challenge.description,
|
|
247
|
+
hint: challenge.hint,
|
|
248
|
+
goal: challenge.goal
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Submit an attack payload for a specific challenge.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} challengeId - The challenge to attempt.
|
|
256
|
+
* @param {string} payload - The attack payload text.
|
|
257
|
+
* @returns {object} Result: { success, detected, feedback, attempts }.
|
|
258
|
+
*/
|
|
259
|
+
submitAttempt(challengeId, payload) {
|
|
260
|
+
const challenge = CHALLENGES.find(c => c.id === challengeId);
|
|
261
|
+
if (!challenge) {
|
|
262
|
+
return { success: false, detected: false, feedback: 'Challenge not found.', attempts: 0 };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Initialize attempt tracking
|
|
266
|
+
if (!this.attempts[challengeId]) {
|
|
267
|
+
this.attempts[challengeId] = { count: 0, solved: false, payloads: [] };
|
|
268
|
+
}
|
|
269
|
+
const tracker = this.attempts[challengeId];
|
|
270
|
+
tracker.count++;
|
|
271
|
+
tracker.payloads.push({ payload: payload.substring(0, 500), timestamp: Date.now() });
|
|
272
|
+
|
|
273
|
+
// Determine scanner sensitivity based on challenge difficulty
|
|
274
|
+
const diffConfig = DIFFICULTY_CONFIG[challenge.difficulty] || DIFFICULTY_CONFIG.medium;
|
|
275
|
+
const sensitivity = this.shieldConfig.sensitivity || diffConfig.sensitivity;
|
|
276
|
+
|
|
277
|
+
// Scan the payload
|
|
278
|
+
const scanResult = scanText(payload, { source: 'ctf_attempt', sensitivity });
|
|
279
|
+
|
|
280
|
+
// Validate against challenge criteria
|
|
281
|
+
const success = challenge.validateFn(scanResult);
|
|
282
|
+
|
|
283
|
+
if (success && !tracker.solved) {
|
|
284
|
+
tracker.solved = true;
|
|
285
|
+
console.log(`[Agent Shield] CTF challenge solved: ${challenge.title} (${tracker.count} attempts)`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Build feedback
|
|
289
|
+
let feedback;
|
|
290
|
+
if (success) {
|
|
291
|
+
feedback = `Challenge "${challenge.title}" solved! Your payload evaded detection.`;
|
|
292
|
+
} else if (scanResult.threats.length > 0) {
|
|
293
|
+
const categories = [...new Set(scanResult.threats.map(t => t.category))];
|
|
294
|
+
feedback = `Detected ${scanResult.threats.length} threat(s) in categories: ${categories.join(', ')}. Try a different approach.`;
|
|
295
|
+
} else {
|
|
296
|
+
feedback = 'Payload was not detected but did not meet the challenge goal. Check the goal requirements.';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
success,
|
|
301
|
+
detected: scanResult.status !== 'safe',
|
|
302
|
+
feedback,
|
|
303
|
+
attempts: tracker.count,
|
|
304
|
+
scanResult: {
|
|
305
|
+
status: scanResult.status,
|
|
306
|
+
threatCount: scanResult.threats.length,
|
|
307
|
+
categories: [...new Set(scanResult.threats.map(t => t.category))]
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Returns the scoreboard of all challenges and their completion status.
|
|
314
|
+
*
|
|
315
|
+
* @returns {object} Scoreboard with per-challenge stats and summary.
|
|
316
|
+
*/
|
|
317
|
+
getScoreboard() {
|
|
318
|
+
const board = CHALLENGES.map(c => {
|
|
319
|
+
const tracker = this.attempts[c.id] || { count: 0, solved: false };
|
|
320
|
+
return {
|
|
321
|
+
id: c.id,
|
|
322
|
+
title: c.title,
|
|
323
|
+
difficulty: c.difficulty,
|
|
324
|
+
goal: c.goal,
|
|
325
|
+
solved: tracker.solved,
|
|
326
|
+
attempts: tracker.count
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const solved = board.filter(c => c.solved).length;
|
|
331
|
+
const total = board.length;
|
|
332
|
+
const byDifficulty = {};
|
|
333
|
+
for (const entry of board) {
|
|
334
|
+
if (!byDifficulty[entry.difficulty]) {
|
|
335
|
+
byDifficulty[entry.difficulty] = { total: 0, solved: 0 };
|
|
336
|
+
}
|
|
337
|
+
byDifficulty[entry.difficulty].total++;
|
|
338
|
+
if (entry.solved) byDifficulty[entry.difficulty].solved++;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
challenges: board,
|
|
343
|
+
summary: {
|
|
344
|
+
total,
|
|
345
|
+
solved,
|
|
346
|
+
remaining: total - solved,
|
|
347
|
+
completionRate: total > 0 ? Math.round((solved / total) * 100) : 0,
|
|
348
|
+
byDifficulty,
|
|
349
|
+
elapsedMs: Date.now() - this.startTime
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Lists all available challenges, optionally filtered by difficulty.
|
|
356
|
+
*
|
|
357
|
+
* @param {object} [options] - Filter options.
|
|
358
|
+
* @param {'easy'|'medium'|'hard'|'expert'} [options.difficulty] - Filter by difficulty.
|
|
359
|
+
* @returns {Array} Array of challenge summaries.
|
|
360
|
+
*/
|
|
361
|
+
getChallenges(options = {}) {
|
|
362
|
+
let challenges = CHALLENGES;
|
|
363
|
+
if (options.difficulty) {
|
|
364
|
+
challenges = challenges.filter(c => c.difficulty === options.difficulty);
|
|
365
|
+
}
|
|
366
|
+
return challenges.map(c => ({
|
|
367
|
+
id: c.id,
|
|
368
|
+
title: c.title,
|
|
369
|
+
difficulty: c.difficulty,
|
|
370
|
+
description: c.description,
|
|
371
|
+
hint: c.hint,
|
|
372
|
+
goal: c.goal,
|
|
373
|
+
solved: !!(this.attempts[c.id] && this.attempts[c.id].solved),
|
|
374
|
+
attempts: (this.attempts[c.id] || {}).count || 0
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// =========================================================================
|
|
380
|
+
// CTF REPORTER
|
|
381
|
+
// =========================================================================
|
|
382
|
+
|
|
383
|
+
class CTFReporter {
|
|
384
|
+
/**
|
|
385
|
+
* Creates a new CTF reporter.
|
|
386
|
+
*/
|
|
387
|
+
constructor() {
|
|
388
|
+
this.prefix = '[Agent Shield]';
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Formats a full scoreboard report.
|
|
393
|
+
*
|
|
394
|
+
* @param {object} scoreboard - Scoreboard from CTFEngine.getScoreboard().
|
|
395
|
+
* @returns {string} Formatted text report.
|
|
396
|
+
*/
|
|
397
|
+
formatReport(scoreboard) {
|
|
398
|
+
const lines = [];
|
|
399
|
+
lines.push(formatHeader('Agent Shield CTF Scoreboard'));
|
|
400
|
+
lines.push('');
|
|
401
|
+
|
|
402
|
+
const { summary } = scoreboard;
|
|
403
|
+
lines.push(` Challenges: ${summary.solved}/${summary.total} solved (${summary.completionRate}%)`);
|
|
404
|
+
lines.push(` Elapsed: ${Math.round(summary.elapsedMs / 1000)}s`);
|
|
405
|
+
lines.push('');
|
|
406
|
+
|
|
407
|
+
// Per-difficulty breakdown
|
|
408
|
+
const diffOrder = ['easy', 'medium', 'hard', 'expert'];
|
|
409
|
+
for (const diff of diffOrder) {
|
|
410
|
+
const stats = summary.byDifficulty[diff];
|
|
411
|
+
if (!stats) continue;
|
|
412
|
+
const bar = makeBar(stats.solved, stats.total, 16);
|
|
413
|
+
const pct = stats.total > 0 ? Math.round((stats.solved / stats.total) * 100) : 0;
|
|
414
|
+
const label = (DIFFICULTY_CONFIG[diff] || {}).label || diff;
|
|
415
|
+
lines.push(` ${label.padEnd(8)} ${bar} ${stats.solved}/${stats.total} (${pct}%)`);
|
|
416
|
+
}
|
|
417
|
+
lines.push('');
|
|
418
|
+
|
|
419
|
+
// Challenge list
|
|
420
|
+
lines.push(' Challenges:');
|
|
421
|
+
lines.push(' ' + '-'.repeat(52));
|
|
422
|
+
for (const c of scoreboard.challenges) {
|
|
423
|
+
const status = c.solved ? '[SOLVED]' : '[ ]';
|
|
424
|
+
const attempts = c.attempts > 0 ? ` (${c.attempts} attempts)` : '';
|
|
425
|
+
lines.push(` ${status} ${c.difficulty.padEnd(7)} ${c.title}${attempts}`);
|
|
426
|
+
}
|
|
427
|
+
lines.push('');
|
|
428
|
+
|
|
429
|
+
return lines.join('\n');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Formats a single challenge for display.
|
|
434
|
+
*
|
|
435
|
+
* @param {object} challenge - Challenge object from getChallenge() or getChallenges().
|
|
436
|
+
* @returns {string} Formatted challenge display.
|
|
437
|
+
*/
|
|
438
|
+
formatChallenge(challenge) {
|
|
439
|
+
const lines = [];
|
|
440
|
+
lines.push(formatHeader(`CTF: ${challenge.title}`));
|
|
441
|
+
lines.push('');
|
|
442
|
+
lines.push(` ID: ${challenge.id}`);
|
|
443
|
+
lines.push(` Difficulty: ${challenge.difficulty}`);
|
|
444
|
+
lines.push(` Goal: ${challenge.goal}`);
|
|
445
|
+
lines.push(` Status: ${challenge.solved ? 'SOLVED' : 'Unsolved'}`);
|
|
446
|
+
if (challenge.attempts > 0) {
|
|
447
|
+
lines.push(` Attempts: ${challenge.attempts}`);
|
|
448
|
+
}
|
|
449
|
+
lines.push('');
|
|
450
|
+
lines.push(` ${challenge.description}`);
|
|
451
|
+
lines.push('');
|
|
452
|
+
lines.push(` Hint: ${challenge.hint}`);
|
|
453
|
+
lines.push('');
|
|
454
|
+
return lines.join('\n');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// =========================================================================
|
|
459
|
+
// EXPORTS
|
|
460
|
+
// =========================================================================
|
|
461
|
+
|
|
462
|
+
module.exports = { CTFEngine, CTFReporter, CHALLENGES };
|