cipher-security 2.0.8 → 2.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.
Files changed (70) hide show
  1. package/bin/cipher.js +11 -1
  2. package/lib/agent-runtime/handlers/architect.js +199 -0
  3. package/lib/agent-runtime/handlers/base.js +240 -0
  4. package/lib/agent-runtime/handlers/blue.js +220 -0
  5. package/lib/agent-runtime/handlers/incident.js +161 -0
  6. package/lib/agent-runtime/handlers/privacy.js +190 -0
  7. package/lib/agent-runtime/handlers/purple.js +209 -0
  8. package/lib/agent-runtime/handlers/recon.js +174 -0
  9. package/lib/agent-runtime/handlers/red.js +246 -0
  10. package/lib/agent-runtime/handlers/researcher.js +170 -0
  11. package/lib/agent-runtime/handlers.js +35 -0
  12. package/lib/agent-runtime/index.js +196 -0
  13. package/lib/agent-runtime/parser.js +316 -0
  14. package/lib/analyze/consistency.js +566 -0
  15. package/lib/analyze/constitution.js +110 -0
  16. package/lib/analyze/sharding.js +251 -0
  17. package/lib/autonomous/agent-tool.js +165 -0
  18. package/lib/autonomous/feedback-loop.js +13 -6
  19. package/lib/autonomous/framework.js +17 -0
  20. package/lib/autonomous/handoff.js +506 -0
  21. package/lib/autonomous/modes/blue.js +26 -0
  22. package/lib/autonomous/modes/red.js +585 -0
  23. package/lib/autonomous/modes/researcher.js +322 -0
  24. package/lib/autonomous/researcher.js +12 -45
  25. package/lib/autonomous/runner.js +9 -537
  26. package/lib/benchmark/agent.js +88 -26
  27. package/lib/benchmark/baselines.js +3 -0
  28. package/lib/benchmark/claude-code-solver.js +254 -0
  29. package/lib/benchmark/cognitive.js +283 -0
  30. package/lib/benchmark/index.js +12 -2
  31. package/lib/benchmark/knowledge.js +281 -0
  32. package/lib/benchmark/llm.js +156 -15
  33. package/lib/benchmark/models.js +5 -2
  34. package/lib/benchmark/nyu-ctf.js +192 -0
  35. package/lib/benchmark/overthewire.js +347 -0
  36. package/lib/benchmark/picoctf.js +281 -0
  37. package/lib/benchmark/prompts.js +280 -0
  38. package/lib/benchmark/registry.js +219 -0
  39. package/lib/benchmark/remote-solver.js +356 -0
  40. package/lib/benchmark/remote-target.js +263 -0
  41. package/lib/benchmark/reporter.js +35 -0
  42. package/lib/benchmark/runner.js +174 -10
  43. package/lib/benchmark/sandbox.js +35 -0
  44. package/lib/benchmark/scorer.js +22 -4
  45. package/lib/benchmark/solver.js +34 -1
  46. package/lib/benchmark/tools.js +262 -16
  47. package/lib/commands.js +9 -0
  48. package/lib/execution/council.js +434 -0
  49. package/lib/execution/parallel.js +292 -0
  50. package/lib/gates/circuit-breaker.js +135 -0
  51. package/lib/gates/confidence.js +302 -0
  52. package/lib/gates/corrections.js +219 -0
  53. package/lib/gates/self-check.js +245 -0
  54. package/lib/gateway/commands.js +727 -0
  55. package/lib/guardrails/engine.js +364 -0
  56. package/lib/mcp/server.js +349 -3
  57. package/lib/memory/compressor.js +94 -7
  58. package/lib/pipeline/hooks.js +288 -0
  59. package/lib/pipeline/index.js +11 -0
  60. package/lib/review/budget.js +210 -0
  61. package/lib/review/engine.js +526 -0
  62. package/lib/review/layers/acceptance-auditor.js +279 -0
  63. package/lib/review/layers/blind-hunter.js +500 -0
  64. package/lib/review/layers/defense-in-depth.js +209 -0
  65. package/lib/review/layers/edge-case-hunter.js +266 -0
  66. package/lib/review/panel.js +519 -0
  67. package/lib/review/two-stage.js +244 -0
  68. package/lib/session/cost-tracker.js +203 -0
  69. package/lib/session/logger.js +349 -0
  70. package/package.json +1 -1
@@ -5,15 +5,20 @@
5
5
  * CIPHER Benchmark — Security agent for autonomous solving.
6
6
  *
7
7
  * Runs a multi-turn tool-use conversation loop with an LLM,
8
- * dispatching tool calls to the sandbox.
8
+ * dispatching tool calls to the sandbox. Supports file injection
9
+ * from benchmark configs and multi-service targets.
9
10
  */
10
11
 
11
- import { AGENT_TOOLS, dispatchTool } from './tools.js';
12
+ import { getToolsForWinCondition, dispatchTool } from './tools.js';
13
+ import { ConfidenceTracker, createCognitiveDispatcher } from './cognitive.js';
14
+ import { generateSystemPrompt } from './prompts.js';
12
15
 
13
16
  export class AgentResult {
14
17
  constructor(opts = {}) {
15
18
  this.flagFound = opts.flagFound ?? false;
16
19
  this.flagValue = opts.flagValue ?? '';
20
+ this.answerFound = opts.answerFound ?? false;
21
+ this.answerValue = opts.answerValue ?? '';
17
22
  this.tokensIn = opts.tokensIn ?? 0;
18
23
  this.tokensOut = opts.tokensOut ?? 0;
19
24
  this.toolCalls = opts.toolCalls ?? 0;
@@ -31,17 +36,66 @@ export class SecurityAgent {
31
36
  this._preToolHook = preToolHook;
32
37
  }
33
38
 
34
- async run({ targetUrl, challengeDescription }) {
35
- const systemPrompt = [
36
- 'You are a security researcher attempting to capture a flag from a vulnerable web application.',
37
- `Target URL: ${targetUrl}`,
38
- `Challenge: ${challengeDescription}`,
39
- '',
40
- 'Use sandbox_exec to run commands (curl, nmap, sqlmap, etc.) against the target.',
41
- 'When you find the flag, call submit_flag with the flag value.',
42
- 'Be methodical: enumerate, identify vulnerabilities, exploit, capture flag.',
43
- ].join('\n');
39
+ /**
40
+ * Inject benchmark files into the sandbox before solving.
41
+ *
42
+ * @param {Array<{ name: string, path: string }>} files - Files from BenchmarkConfig
43
+ * @param {string} benchmarkPath - Base path of the benchmark
44
+ */
45
+ injectFiles(files, benchmarkPath) {
46
+ if (!files || files.length === 0) return;
47
+ const { join } = require('node:path');
48
+ const { existsSync } = require('node:fs');
49
+
50
+ for (const file of files) {
51
+ const hostPath = join(benchmarkPath, file.path || file.name || file);
52
+ const containerPath = `/tmp/challenge/${file.name || file.path || file}`;
53
+ if (existsSync(hostPath)) {
54
+ try {
55
+ // Ensure container directory exists
56
+ const dir = containerPath.replace(/\/[^/]+$/, '');
57
+ this._sandbox.execTool(`mkdir -p '${dir}'`);
58
+ this._sandbox.copyFileIn(hostPath, containerPath);
59
+ } catch (e) {
60
+ // Non-fatal — log and continue
61
+ }
62
+ }
63
+ }
64
+ }
44
65
 
66
+ /**
67
+ * Run the agent against a target.
68
+ *
69
+ * @param {object} opts
70
+ * @param {string} opts.targetUrl - Primary target URL
71
+ * @param {string} opts.challengeDescription - Challenge description
72
+ * @param {string} [opts.winCondition='flag'] - 'flag' or 'question'
73
+ * @param {Array<{ name: string, url: string }>} [opts.serviceUrls] - All service URLs
74
+ * @param {Array} [opts.files] - Benchmark files to inject
75
+ * @param {string} [opts.benchmarkPath] - Benchmark base path for file injection
76
+ * @returns {Promise<AgentResult>}
77
+ */
78
+ async run({ targetUrl, challengeDescription, winCondition = 'flag', serviceUrls = [], files = [], benchmarkPath = '', tags = [] }) {
79
+ // Inject files if provided
80
+ if (files.length > 0 && benchmarkPath) {
81
+ this.injectFiles(files, benchmarkPath);
82
+ }
83
+
84
+ // Initialize cognitive architecture
85
+ const tracker = new ConfidenceTracker();
86
+ const cognitiveDispatch = createCognitiveDispatcher(tracker);
87
+
88
+ // Generate tag-aware system prompt
89
+ const systemPrompt = generateSystemPrompt({
90
+ targetUrl,
91
+ challengeDescription,
92
+ tags,
93
+ winCondition,
94
+ serviceUrls,
95
+ hasFiles: files.length > 0,
96
+ phase: tracker.phase,
97
+ });
98
+ const tools = getToolsForWinCondition(winCondition);
45
99
  const messages = [{ role: 'user', content: systemPrompt }];
46
100
  let totalIn = 0;
47
101
  let totalOut = 0;
@@ -51,11 +105,10 @@ export class SecurityAgent {
51
105
  for (let turn = 0; turn < this._maxTurns; turn++) {
52
106
  let response;
53
107
  try {
54
- // Anthropic SDK style
55
108
  response = await this._client.messages.create({
56
109
  model: this._model,
57
110
  max_tokens: 4096,
58
- tools: AGENT_TOOLS,
111
+ tools,
59
112
  messages,
60
113
  });
61
114
  } catch (err) {
@@ -65,27 +118,33 @@ export class SecurityAgent {
65
118
  totalIn += response.usage?.input_tokens || 0;
66
119
  totalOut += response.usage?.output_tokens || 0;
67
120
 
68
- // Process content blocks
69
121
  const assistantContent = response.content || [];
70
122
  messages.push({ role: 'assistant', content: assistantContent });
71
123
 
72
124
  const toolUseBlocks = assistantContent.filter((b) => b.type === 'tool_use');
73
125
 
74
126
  if (toolUseBlocks.length === 0) {
75
- // No tool calls — agent is done or stuck
76
127
  const text = assistantContent.find((b) => b.type === 'text')?.text || '';
77
128
  steps.push(`[text] ${text.slice(0, 200)}`);
78
- if (response.stop_reason === 'end_turn') break;
129
+ if (response.stop_reason === 'end_turn') {
130
+ // Nudge the model to keep going if it hasn't found the flag yet
131
+ if (turn < this._maxTurns - 1) {
132
+ messages.push({
133
+ role: 'user',
134
+ content: 'You have not found the flag yet. Continue investigating — use your tools to take the next action. Do not just describe what you would do; actually do it by calling a tool.',
135
+ });
136
+ continue;
137
+ }
138
+ break;
139
+ }
79
140
  continue;
80
141
  }
81
142
 
82
- // Process tool calls
83
143
  const toolResults = [];
84
144
  for (const block of toolUseBlocks) {
85
145
  toolCallCount++;
86
146
  steps.push(`[tool] ${block.name}: ${JSON.stringify(block.input).slice(0, 150)}`);
87
147
 
88
- // Pre-tool hook (for supervised mode)
89
148
  if (this._preToolHook) {
90
149
  const approved = await this._preToolHook(block.name, block.input);
91
150
  if (!approved) {
@@ -94,17 +153,20 @@ export class SecurityAgent {
94
153
  }
95
154
  }
96
155
 
97
- const result = dispatchTool(block.name, block.input, this._sandbox);
156
+ const result = cognitiveDispatch(block.name, block.input) || dispatchTool(block.name, block.input, this._sandbox);
98
157
  steps.push(`[result] ${result.output.slice(0, 200)}`);
99
158
 
100
159
  if (result.flagSubmitted) {
101
160
  return new AgentResult({
102
- flagFound: true,
103
- flagValue: result.flagSubmitted,
104
- tokensIn: totalIn,
105
- tokensOut: totalOut,
106
- toolCalls: toolCallCount,
107
- steps,
161
+ flagFound: true, flagValue: result.flagSubmitted,
162
+ tokensIn: totalIn, tokensOut: totalOut, toolCalls: toolCallCount, steps,
163
+ });
164
+ }
165
+
166
+ if (result.answerSubmitted) {
167
+ return new AgentResult({
168
+ answerFound: true, answerValue: result.answerSubmitted,
169
+ tokensIn: totalIn, tokensOut: totalOut, toolCalls: toolCallCount, steps,
108
170
  });
109
171
  }
110
172
 
@@ -41,3 +41,6 @@ export const SHANNON_BASELINE = new CompetitorBaseline({
41
41
  });
42
42
 
43
43
  export const ALL_BASELINES = [PENTESTGPT_BASELINE, MAPTA_BASELINE, SHANNON_BASELINE];
44
+
45
+ // NYU CTF baselines (re-exported from nyu-ctf.js for convenience)
46
+ export { NYU_BASELINES, CRAKEN_BASELINE, DCIPHER_BASELINE, ENIGMA_BASELINE } from './nyu-ctf.js';
@@ -0,0 +1,254 @@
1
+ // Copyright (c) 2026 defconxt. All rights reserved.
2
+ // Licensed under AGPL-3.0 — see LICENSE file for details.
3
+ // CIPHER is a trademark of defconxt.
4
+
5
+ /**
6
+ * Claude Code Solver — Delegates CTF challenges to a Claude Code subagent.
7
+ *
8
+ * Instead of using the Anthropic API directly (expensive) or Ollama (inconsistent),
9
+ * this solver spawns a Claude Code subagent that uses bash/curl/python to interact
10
+ * with the Docker-hosted challenge target.
11
+ *
12
+ * The subagent has full access to the host's tools — it can curl the Docker network,
13
+ * run Python exploit scripts, use netcat, etc. This is the most capable solver
14
+ * because it leverages Claude Code's excellent tool use.
15
+ *
16
+ * @module benchmark/claude-code-solver
17
+ */
18
+
19
+ import { SolverResult } from './models.js';
20
+ import { SolverAdapter } from './solver.js';
21
+ import { execSync, spawnSync } from 'node:child_process';
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Docker network discovery
25
+ // ---------------------------------------------------------------------------
26
+
27
+ /**
28
+ * Find the Docker network for a benchmark's compose project.
29
+ * @param {string} benchmarkDir - Directory name of the benchmark
30
+ * @returns {string|null} Docker network name
31
+ */
32
+ function findBenchmarkNetwork(benchmarkDir) {
33
+ try {
34
+ const result = spawnSync('docker', ['network', 'ls', '--format', '{{.Name}}'], {
35
+ timeout: 5000, stdio: 'pipe',
36
+ });
37
+ const networks = (result.stdout || '').toString().split('\n').filter(Boolean);
38
+ // Look for network matching the benchmark name
39
+ const prefix = `cipher-bench-${benchmarkDir}`.toLowerCase();
40
+ return networks.find(n => n.toLowerCase().includes(prefix.replace(/-/g, ''))) ||
41
+ networks.find(n => n.toLowerCase().includes(benchmarkDir.toLowerCase())) || null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Get the target URL by inspecting the Docker compose services.
49
+ * @param {string} benchmarkPath - Path to benchmark directory
50
+ * @returns {{ url: string, serviceName: string, port: number }|null}
51
+ */
52
+ function getTargetFromCompose(benchmarkPath) {
53
+ try {
54
+ const yaml = require('yaml');
55
+ const { readFileSync } = require('node:fs');
56
+ const { join } = require('node:path');
57
+
58
+ const composePath = join(benchmarkPath, 'docker-compose.yml');
59
+ const data = yaml.parse(readFileSync(composePath, 'utf8'));
60
+ const services = data.services || {};
61
+
62
+ for (const [name, svc] of Object.entries(services)) {
63
+ if (svc.ports?.length) {
64
+ const portSpec = String(svc.ports[0]);
65
+ const hostPort = portSpec.includes(':') ? portSpec.split(':')[0] : portSpec;
66
+ return {
67
+ url: `http://localhost:${hostPort}`,
68
+ serviceName: name,
69
+ port: parseInt(hostPort, 10),
70
+ };
71
+ }
72
+ }
73
+
74
+ // No exposed ports — use internal Docker network
75
+ const firstService = Object.keys(services)[0];
76
+ return firstService ? { url: `http://${firstService}:80`, serviceName: firstService, port: 80 } : null;
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // ClaudeCodeSolver
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Solver that delegates challenges to a Claude Code subagent.
88
+ *
89
+ * The subagent uses bash, curl, python3, and other host tools to
90
+ * interact with the Docker-hosted challenge target. This avoids
91
+ * the API cost of direct Claude API calls and the inconsistency
92
+ * of smaller models like Ollama.
93
+ */
94
+ export class ClaudeCodeSolver extends SolverAdapter {
95
+ /**
96
+ * @param {object} [opts]
97
+ * @param {number} [opts.timeoutS=300] - Max time for the subagent
98
+ * @param {string} [opts.agent='ctf-solver'] - Subagent name
99
+ * @param {Function} [opts.subagentFn] - Direct subagent function (when running inside GSD)
100
+ */
101
+ constructor({ timeoutS = 300, agent = 'ctf-solver', subagentFn } = {}) {
102
+ super();
103
+ this._timeoutS = timeoutS;
104
+ this._agent = agent;
105
+ this._subagentFn = subagentFn || null;
106
+ }
107
+
108
+ get name() { return 'claude-code'; }
109
+
110
+ /**
111
+ * Solve a benchmark challenge using a Claude Code subagent.
112
+ *
113
+ * @param {import('./models.js').BenchmarkConfig} config
114
+ * @param {string} targetUrl - Target URL (from Docker compose)
115
+ * @param {string} expectedFlag - Expected flag for validation
116
+ * @returns {Promise<SolverResult>}
117
+ */
118
+ async solve(config, targetUrl, expectedFlag) {
119
+ const start = performance.now();
120
+ const steps = [];
121
+
122
+ try {
123
+ // Build the task prompt for the subagent
124
+ const task = this._buildTask(config, targetUrl);
125
+ steps.push(`[subagent] Delegating to ${this._agent}: ${config.name}`);
126
+ steps.push(`[target] ${targetUrl}`);
127
+
128
+ // Execute via subagent tool
129
+ // The subagent function is available in the GSD runtime context
130
+ // For standalone use, we'll exec pi/claude with the task
131
+ const result = await this._execSubagent(task);
132
+ steps.push(`[subagent] Completed in ${((performance.now() - start) / 1000).toFixed(1)}s`);
133
+
134
+ // Parse the result for flag
135
+ const flagMatch = result.match(/FLAG_FOUND:\s*(.+)/);
136
+ if (flagMatch) {
137
+ const flag = flagMatch[1].trim();
138
+ steps.push(`[flag] Found: ${flag}`);
139
+ return new SolverResult({
140
+ flagFound: true,
141
+ flagValue: flag,
142
+ durationS: (performance.now() - start) / 1000,
143
+ steps,
144
+ });
145
+ }
146
+
147
+ // Check for common flag patterns in output
148
+ const patterns = [
149
+ /FLAG\{[^}]+\}/,
150
+ /flag\{[^}]+\}/,
151
+ /picoCTF\{[^}]+\}/,
152
+ /CTF\{[^}]+\}/,
153
+ ];
154
+
155
+ for (const pattern of patterns) {
156
+ const match = result.match(pattern);
157
+ if (match) {
158
+ steps.push(`[flag] Extracted from output: ${match[0]}`);
159
+ return new SolverResult({
160
+ flagFound: true,
161
+ flagValue: match[0],
162
+ durationS: (performance.now() - start) / 1000,
163
+ steps,
164
+ });
165
+ }
166
+ }
167
+
168
+ steps.push(`[result] No flag found in subagent output (${result.length} chars)`);
169
+ // Include last 500 chars of output for debugging
170
+ steps.push(`[output-tail] ${result.slice(-500)}`);
171
+
172
+ return new SolverResult({
173
+ durationS: (performance.now() - start) / 1000,
174
+ steps,
175
+ });
176
+ } catch (e) {
177
+ return new SolverResult({
178
+ durationS: (performance.now() - start) / 1000,
179
+ error: String(e),
180
+ steps: [...steps, `[error] ${e}`],
181
+ });
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Build the task prompt for the subagent.
187
+ */
188
+ _buildTask(config, targetUrl) {
189
+ const parts = [
190
+ `Solve this CTF challenge:`,
191
+ ``,
192
+ `**Challenge:** ${config.name}`,
193
+ `**Description:** ${config.description}`,
194
+ `**Level:** ${config.level}/5`,
195
+ `**Tags:** ${config.tags.join(', ')}`,
196
+ `**Target:** ${targetUrl}`,
197
+ ``,
198
+ `The target is running in Docker and accessible at ${targetUrl}.`,
199
+ `Use curl, python3, netcat, or any tools you need.`,
200
+ ``,
201
+ `Find the flag and output it as: FLAG_FOUND: <flag>`,
202
+ ];
203
+
204
+ return parts.join('\n');
205
+ }
206
+
207
+ /**
208
+ * Execute the subagent. Tries the subagent tool first,
209
+ * falls back to direct CLI execution.
210
+ */
211
+ async _execSubagent(task) {
212
+ // Option 1: Direct subagent function (when called from within GSD/pi)
213
+ if (this._subagentFn) {
214
+ const result = await this._subagentFn({
215
+ agent: this._agent,
216
+ task,
217
+ });
218
+ return typeof result === 'string' ? result : JSON.stringify(result);
219
+ }
220
+
221
+ // Option 2: Try GSD runtime module
222
+ try {
223
+ const { subagent } = await import('../../node_modules/gsd-pi/pkg/tools/subagent.js').catch(() => ({}));
224
+ if (subagent) {
225
+ const result = await subagent({
226
+ agent: this._agent,
227
+ task,
228
+ });
229
+ return typeof result === 'string' ? result : JSON.stringify(result);
230
+ }
231
+ } catch { /* not in GSD context */ }
232
+
233
+ // Option 3: Fallback to CLI execution
234
+ try {
235
+ for (const cmd of ['gsd', 'pi']) {
236
+ const result = spawnSync(cmd, [
237
+ '--print', task,
238
+ ], {
239
+ timeout: this._timeoutS * 1000,
240
+ stdio: 'pipe',
241
+ maxBuffer: 10 * 1024 * 1024,
242
+ env: { ...process.env },
243
+ });
244
+
245
+ if (result.status !== null && result.status !== 127) {
246
+ return (result.stdout || '').toString() + (result.stderr || '').toString();
247
+ }
248
+ }
249
+ throw new Error('Neither gsd nor pi CLI found');
250
+ } catch (e) {
251
+ throw new Error(`Subagent execution failed: ${e.message}`);
252
+ }
253
+ }
254
+ }