cipher-security 2.1.0 → 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 (54) hide show
  1. package/bin/cipher.js +10 -0
  2. package/lib/analyze/consistency.js +566 -0
  3. package/lib/analyze/constitution.js +110 -0
  4. package/lib/analyze/sharding.js +251 -0
  5. package/lib/autonomous/agent-tool.js +165 -0
  6. package/lib/autonomous/framework.js +17 -0
  7. package/lib/autonomous/handoff.js +506 -0
  8. package/lib/autonomous/modes/blue.js +26 -0
  9. package/lib/autonomous/modes/red.js +28 -0
  10. package/lib/benchmark/agent.js +88 -26
  11. package/lib/benchmark/baselines.js +3 -0
  12. package/lib/benchmark/claude-code-solver.js +254 -0
  13. package/lib/benchmark/cognitive.js +283 -0
  14. package/lib/benchmark/index.js +12 -2
  15. package/lib/benchmark/knowledge.js +281 -0
  16. package/lib/benchmark/llm.js +156 -15
  17. package/lib/benchmark/models.js +5 -2
  18. package/lib/benchmark/nyu-ctf.js +192 -0
  19. package/lib/benchmark/overthewire.js +347 -0
  20. package/lib/benchmark/picoctf.js +281 -0
  21. package/lib/benchmark/prompts.js +280 -0
  22. package/lib/benchmark/registry.js +219 -0
  23. package/lib/benchmark/remote-solver.js +356 -0
  24. package/lib/benchmark/remote-target.js +263 -0
  25. package/lib/benchmark/reporter.js +35 -0
  26. package/lib/benchmark/runner.js +174 -10
  27. package/lib/benchmark/sandbox.js +35 -0
  28. package/lib/benchmark/scorer.js +22 -4
  29. package/lib/benchmark/solver.js +34 -1
  30. package/lib/benchmark/tools.js +262 -16
  31. package/lib/commands.js +9 -0
  32. package/lib/execution/council.js +434 -0
  33. package/lib/execution/parallel.js +292 -0
  34. package/lib/gates/circuit-breaker.js +135 -0
  35. package/lib/gates/confidence.js +302 -0
  36. package/lib/gates/corrections.js +219 -0
  37. package/lib/gates/self-check.js +245 -0
  38. package/lib/gateway/commands.js +727 -0
  39. package/lib/guardrails/engine.js +364 -0
  40. package/lib/mcp/server.js +349 -3
  41. package/lib/memory/compressor.js +94 -7
  42. package/lib/pipeline/hooks.js +288 -0
  43. package/lib/pipeline/index.js +11 -0
  44. package/lib/review/budget.js +210 -0
  45. package/lib/review/engine.js +526 -0
  46. package/lib/review/layers/acceptance-auditor.js +279 -0
  47. package/lib/review/layers/blind-hunter.js +500 -0
  48. package/lib/review/layers/defense-in-depth.js +209 -0
  49. package/lib/review/layers/edge-case-hunter.js +266 -0
  50. package/lib/review/panel.js +519 -0
  51. package/lib/review/two-stage.js +244 -0
  52. package/lib/session/cost-tracker.js +203 -0
  53. package/lib/session/logger.js +349 -0
  54. package/package.json +1 -1
@@ -0,0 +1,288 @@
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
+ * CIPHER Pipeline Hook System
7
+ *
8
+ * EventEmitter-based hooks for pipeline stages. Supports before/after
9
+ * callbacks with context passing, finding transformation, and abort.
10
+ *
11
+ * Zero overhead when no hooks are registered.
12
+ *
13
+ * @module pipeline/hooks
14
+ */
15
+
16
+ import { EventEmitter } from 'node:events';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Hook Events
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** @enum {string} */
23
+ export const HookEvent = Object.freeze({
24
+ PIPELINE_START: 'pipeline:start',
25
+ PIPELINE_COMPLETE: 'pipeline:complete',
26
+ PIPELINE_ERROR: 'pipeline:error',
27
+ BEFORE_CRAWL: 'before:crawl',
28
+ AFTER_CRAWL: 'after:crawl',
29
+ BEFORE_SCAN: 'before:scan',
30
+ AFTER_SCAN: 'after:scan',
31
+ BEFORE_REVIEW: 'before:review',
32
+ AFTER_REVIEW: 'after:review',
33
+ BEFORE_ANALYZE: 'before:analyze',
34
+ AFTER_ANALYZE: 'after:analyze',
35
+ });
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Hook Context
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Context object passed to hook callbacks.
43
+ * Mutable — hooks can add metadata or transform findings.
44
+ */
45
+ export class HookContext {
46
+ /**
47
+ * @param {object} opts
48
+ * @param {string} opts.stage - Current pipeline stage
49
+ * @param {string} [opts.target] - Target being processed
50
+ * @param {object} [opts.options] - Stage options
51
+ * @param {any[]} [opts.findings] - Findings accumulated so far
52
+ * @param {object} [opts.result] - Stage result (for after hooks)
53
+ * @param {number} [opts.startTime] - Stage start timestamp
54
+ * @param {number} [opts.duration] - Stage duration ms (for after hooks)
55
+ * @param {object} [opts.meta] - Arbitrary metadata
56
+ */
57
+ constructor(opts = {}) {
58
+ this.stage = opts.stage ?? '';
59
+ this.target = opts.target ?? '';
60
+ this.options = opts.options ?? {};
61
+ this.findings = opts.findings ?? [];
62
+ this.result = opts.result ?? null;
63
+ this.startTime = opts.startTime ?? Date.now();
64
+ this.duration = opts.duration ?? 0;
65
+ this.meta = opts.meta ?? {};
66
+ this._aborted = false;
67
+ this._abortReason = '';
68
+ }
69
+
70
+ /**
71
+ * Signal that the pipeline should abort after this hook.
72
+ * @param {string} [reason]
73
+ */
74
+ abort(reason = '') {
75
+ this._aborted = true;
76
+ this._abortReason = reason;
77
+ }
78
+
79
+ get aborted() { return this._aborted; }
80
+ get abortReason() { return this._abortReason; }
81
+ }
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // PipelineHooks
85
+ // ---------------------------------------------------------------------------
86
+
87
+ /**
88
+ * Pipeline hook manager. Wraps EventEmitter with typed hook registration
89
+ * and context-aware execution.
90
+ */
91
+ export class PipelineHooks extends EventEmitter {
92
+ constructor() {
93
+ super();
94
+ this.setMaxListeners(50); // Allow many hooks
95
+ }
96
+
97
+ /**
98
+ * Register a hook callback.
99
+ * @param {string} event - HookEvent value
100
+ * @param {function} fn - async (ctx: HookContext) => void
101
+ * @param {object} [opts]
102
+ * @param {string} [opts.name] - Hook name for logging
103
+ * @param {number} [opts.priority] - Lower runs first (default: 100)
104
+ * @returns {PipelineHooks} this (chainable)
105
+ */
106
+ hook(event, fn, opts = {}) {
107
+ fn._hookName = opts.name ?? fn.name ?? 'anonymous';
108
+ fn._hookPriority = opts.priority ?? 100;
109
+ this.on(event, fn);
110
+ return this;
111
+ }
112
+
113
+ /**
114
+ * Execute all hooks for an event, in priority order.
115
+ * Returns the context (possibly mutated by hooks).
116
+ *
117
+ * @param {string} event
118
+ * @param {HookContext} ctx
119
+ * @returns {Promise<HookContext>}
120
+ */
121
+ async run(event, ctx) {
122
+ const listeners = this.listeners(event);
123
+ if (listeners.length === 0) return ctx;
124
+
125
+ // Sort by priority (stable sort)
126
+ const sorted = [...listeners].sort(
127
+ (a, b) => (a._hookPriority ?? 100) - (b._hookPriority ?? 100),
128
+ );
129
+
130
+ for (const fn of sorted) {
131
+ try {
132
+ await fn(ctx);
133
+ } catch (err) {
134
+ ctx.abort(`Hook "${fn._hookName}" threw: ${err.message}`);
135
+ }
136
+ if (ctx.aborted) break;
137
+ }
138
+
139
+ return ctx;
140
+ }
141
+
142
+ /**
143
+ * Check if any hooks are registered for an event.
144
+ * @param {string} event
145
+ * @returns {boolean}
146
+ */
147
+ hasHooks(event) {
148
+ return this.listenerCount(event) > 0;
149
+ }
150
+
151
+ /**
152
+ * Get all registered hook names grouped by event.
153
+ * @returns {object}
154
+ */
155
+ listHooks() {
156
+ const result = {};
157
+ for (const event of Object.values(HookEvent)) {
158
+ const listeners = this.listeners(event);
159
+ if (listeners.length > 0) {
160
+ result[event] = listeners.map((fn) => fn._hookName ?? 'anonymous');
161
+ }
162
+ }
163
+ return result;
164
+ }
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // HookablePipeline — wraps any stage function with hooks
169
+ // ---------------------------------------------------------------------------
170
+
171
+ /**
172
+ * Create a hookable wrapper around a pipeline stage function.
173
+ *
174
+ * @param {PipelineHooks} hooks - Hook manager
175
+ * @param {string} stageName - Stage name (e.g. 'crawl', 'scan', 'review')
176
+ * @param {function} stageFn - async (target, options) => result
177
+ * @returns {function} - async (target, options) => result (with hooks)
178
+ */
179
+ export function hookableStage(hooks, stageName, stageFn) {
180
+ const beforeEvent = `before:${stageName}`;
181
+ const afterEvent = `after:${stageName}`;
182
+
183
+ return async function hookedStage(target, options = {}) {
184
+ // Skip hook overhead if none registered
185
+ if (!hooks.hasHooks(beforeEvent) && !hooks.hasHooks(afterEvent)) {
186
+ return stageFn(target, options);
187
+ }
188
+
189
+ const t0 = Date.now();
190
+
191
+ // Before hook
192
+ const beforeCtx = new HookContext({
193
+ stage: stageName,
194
+ target,
195
+ options,
196
+ startTime: t0,
197
+ });
198
+ await hooks.run(beforeEvent, beforeCtx);
199
+
200
+ if (beforeCtx.aborted) {
201
+ throw new Error(`Pipeline aborted before ${stageName}: ${beforeCtx.abortReason}`);
202
+ }
203
+
204
+ // Execute stage
205
+ const result = await stageFn(target, beforeCtx.options);
206
+ const duration = Date.now() - t0;
207
+
208
+ // After hook
209
+ const afterCtx = new HookContext({
210
+ stage: stageName,
211
+ target,
212
+ options: beforeCtx.options,
213
+ result,
214
+ startTime: t0,
215
+ duration,
216
+ meta: beforeCtx.meta, // Pass through metadata from before hook
217
+ });
218
+ await hooks.run(afterEvent, afterCtx);
219
+
220
+ if (afterCtx.aborted) {
221
+ throw new Error(`Pipeline aborted after ${stageName}: ${afterCtx.abortReason}`);
222
+ }
223
+
224
+ return afterCtx.result ?? result;
225
+ };
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Built-in hooks
230
+ // ---------------------------------------------------------------------------
231
+
232
+ /**
233
+ * Timing hook — logs stage duration. Attach to after:* events.
234
+ * @param {function} logger - (message: string) => void
235
+ * @returns {function}
236
+ */
237
+ export function timingHook(logger = console.log) {
238
+ const fn = async (ctx) => {
239
+ logger(`[hook:timing] ${ctx.stage} completed in ${ctx.duration}ms`);
240
+ };
241
+ fn._hookName = 'timing';
242
+ fn._hookPriority = 999; // Run last
243
+ return fn;
244
+ }
245
+
246
+ /**
247
+ * Finding filter hook — removes findings below a severity threshold.
248
+ * Attach to after:review or after:scan events.
249
+ *
250
+ * @param {string} minSeverity - Minimum severity to keep
251
+ * @returns {function}
252
+ */
253
+ export function severityFilterHook(minSeverity) {
254
+ const RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
255
+ const minRank = RANK[minSeverity] ?? 0;
256
+
257
+ const fn = async (ctx) => {
258
+ if (ctx.result && ctx.result.findings) {
259
+ ctx.result.findings = ctx.result.findings.filter(
260
+ (f) => (RANK[f.severity] ?? 0) >= minRank,
261
+ );
262
+ }
263
+ };
264
+ fn._hookName = `severity-filter:${minSeverity}`;
265
+ fn._hookPriority = 50;
266
+ return fn;
267
+ }
268
+
269
+ /**
270
+ * Abort hook — aborts pipeline if findings exceed a threshold.
271
+ * Attach to after:review or after:scan events.
272
+ *
273
+ * @param {string} severity - Severity level to count
274
+ * @param {number} max - Maximum allowed findings of that severity
275
+ * @returns {function}
276
+ */
277
+ export function thresholdAbortHook(severity, max) {
278
+ const fn = async (ctx) => {
279
+ if (!ctx.result?.findings) return;
280
+ const count = ctx.result.findings.filter((f) => f.severity === severity).length;
281
+ if (count > max) {
282
+ ctx.abort(`${count} ${severity} findings exceed threshold of ${max}`);
283
+ }
284
+ };
285
+ fn._hookName = `threshold-abort:${severity}>${max}`;
286
+ fn._hookPriority = 80;
287
+ return fn;
288
+ }
@@ -124,3 +124,14 @@ export {
124
124
  TemplateCollection,
125
125
  NucleiTemplateManager,
126
126
  } from './template-manager.js';
127
+
128
+ // Pipeline hooks — before/after callbacks for pipeline stages
129
+ export {
130
+ HookEvent,
131
+ HookContext,
132
+ PipelineHooks,
133
+ hookableStage,
134
+ timingHook,
135
+ severityFilterHook,
136
+ thresholdAbortHook,
137
+ } from './hooks.js';
@@ -0,0 +1,210 @@
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
+ * CIPHER Token Budget Manager
7
+ *
8
+ * Analyzes input complexity and allocates context tokens across
9
+ * phases (research, planning, execution, review) based on
10
+ * file count, LOC, language diversity, and max file size.
11
+ *
12
+ * @module review/budget
13
+ */
14
+
15
+ import { resolveInput, detectLanguage } from './engine.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Complexity levels
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /** @enum {string} */
22
+ export const Complexity = Object.freeze({
23
+ SIMPLE: 'simple',
24
+ MEDIUM: 'medium',
25
+ COMPLEX: 'complex',
26
+ EXTREME: 'extreme',
27
+ });
28
+
29
+ /**
30
+ * Thresholds for complexity classification.
31
+ */
32
+ const THRESHOLDS = {
33
+ simple: { maxFiles: 3, maxLoc: 500, maxLanguages: 1 },
34
+ medium: { maxFiles: 15, maxLoc: 3000, maxLanguages: 3 },
35
+ complex: { maxFiles: 50, maxLoc: 15000, maxLanguages: 5 },
36
+ // anything above complex is extreme
37
+ };
38
+
39
+ /**
40
+ * Default token budgets per complexity level.
41
+ */
42
+ const DEFAULT_BUDGETS = {
43
+ simple: { total: 4000, weights: { research: 0.15, planning: 0.15, execution: 0.50, review: 0.20 } },
44
+ medium: { total: 8000, weights: { research: 0.20, planning: 0.20, execution: 0.40, review: 0.20 } },
45
+ complex: { total: 12000, weights: { research: 0.25, planning: 0.20, execution: 0.35, review: 0.20 } },
46
+ extreme: { total: 16000, weights: { research: 0.25, planning: 0.25, execution: 0.30, review: 0.20 } },
47
+ };
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Complexity Analysis
51
+ // ---------------------------------------------------------------------------
52
+
53
+ /**
54
+ * Complexity metrics for a set of source files.
55
+ */
56
+ export class ComplexityMetrics {
57
+ constructor(opts = {}) {
58
+ this.fileCount = opts.fileCount ?? 0;
59
+ this.totalLoc = opts.totalLoc ?? 0;
60
+ this.languages = opts.languages ?? [];
61
+ this.languageCount = opts.languageCount ?? 0;
62
+ this.maxFileSize = opts.maxFileSize ?? 0;
63
+ this.avgFileSize = opts.avgFileSize ?? 0;
64
+ this.complexity = opts.complexity ?? Complexity.SIMPLE;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Analyze complexity of resolved source files.
70
+ *
71
+ * @param {import('./engine.js').SourceFile[]} sources
72
+ * @returns {ComplexityMetrics}
73
+ */
74
+ export function analyzeComplexity(sources) {
75
+ if (!sources.length) {
76
+ return new ComplexityMetrics({ complexity: Complexity.SIMPLE });
77
+ }
78
+
79
+ const langSet = new Set();
80
+ let totalLoc = 0;
81
+ let maxFileSize = 0;
82
+
83
+ for (const src of sources) {
84
+ const loc = src.content.split('\n').length;
85
+ totalLoc += loc;
86
+ if (loc > maxFileSize) maxFileSize = loc;
87
+ langSet.add(src.language);
88
+ }
89
+
90
+ const fileCount = sources.length;
91
+ const languageCount = langSet.size;
92
+ const avgFileSize = Math.round(totalLoc / fileCount);
93
+ const languages = [...langSet].sort();
94
+
95
+ // Classify complexity
96
+ let complexity;
97
+ if (fileCount <= THRESHOLDS.simple.maxFiles &&
98
+ totalLoc <= THRESHOLDS.simple.maxLoc &&
99
+ languageCount <= THRESHOLDS.simple.maxLanguages) {
100
+ complexity = Complexity.SIMPLE;
101
+ } else if (fileCount <= THRESHOLDS.medium.maxFiles &&
102
+ totalLoc <= THRESHOLDS.medium.maxLoc &&
103
+ languageCount <= THRESHOLDS.medium.maxLanguages) {
104
+ complexity = Complexity.MEDIUM;
105
+ } else if (fileCount <= THRESHOLDS.complex.maxFiles &&
106
+ totalLoc <= THRESHOLDS.complex.maxLoc &&
107
+ languageCount <= THRESHOLDS.complex.maxLanguages) {
108
+ complexity = Complexity.COMPLEX;
109
+ } else {
110
+ complexity = Complexity.EXTREME;
111
+ }
112
+
113
+ return new ComplexityMetrics({
114
+ fileCount,
115
+ totalLoc,
116
+ languages,
117
+ languageCount,
118
+ maxFileSize,
119
+ avgFileSize,
120
+ complexity,
121
+ });
122
+ }
123
+
124
+ // ---------------------------------------------------------------------------
125
+ // Token Budget
126
+ // ---------------------------------------------------------------------------
127
+
128
+ /**
129
+ * Structured token budget with per-phase allocations.
130
+ */
131
+ export class TokenBudget {
132
+ /**
133
+ * @param {object} opts
134
+ * @param {number} opts.total
135
+ * @param {object} opts.phases - { research, planning, execution, review }
136
+ * @param {ComplexityMetrics} opts.metrics
137
+ */
138
+ constructor(opts = {}) {
139
+ this.total = opts.total ?? 0;
140
+ this.phases = opts.phases ?? {};
141
+ this.metrics = opts.metrics ?? new ComplexityMetrics();
142
+ }
143
+
144
+ toReport() {
145
+ const lines = [
146
+ `Token Budget (${this.metrics.complexity})`,
147
+ ` Total: ${this.total} tokens`,
148
+ ` Research: ${this.phases.research} tokens`,
149
+ ` Planning: ${this.phases.planning} tokens`,
150
+ ` Execution: ${this.phases.execution} tokens`,
151
+ ` Review: ${this.phases.review} tokens`,
152
+ '',
153
+ `Complexity: ${this.metrics.complexity}`,
154
+ ` Files: ${this.metrics.fileCount}`,
155
+ ` LOC: ${this.metrics.totalLoc}`,
156
+ ` Languages: ${this.metrics.languages.join(', ')} (${this.metrics.languageCount})`,
157
+ ` Max file: ${this.metrics.maxFileSize} lines`,
158
+ ` Avg file: ${this.metrics.avgFileSize} lines`,
159
+ ];
160
+ return lines.join('\n');
161
+ }
162
+
163
+ toJSON() {
164
+ return {
165
+ total: this.total,
166
+ phases: this.phases,
167
+ complexity: this.metrics.complexity,
168
+ metrics: {
169
+ fileCount: this.metrics.fileCount,
170
+ totalLoc: this.metrics.totalLoc,
171
+ languages: this.metrics.languages,
172
+ languageCount: this.metrics.languageCount,
173
+ maxFileSize: this.metrics.maxFileSize,
174
+ avgFileSize: this.metrics.avgFileSize,
175
+ },
176
+ };
177
+ }
178
+ }
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Factory
182
+ // ---------------------------------------------------------------------------
183
+
184
+ /**
185
+ * Create a token budget for the given input.
186
+ *
187
+ * @param {string} input - File path, directory, or code string
188
+ * @param {object} [options]
189
+ * @param {string} [options.language] - Override language detection
190
+ * @param {number} [options.total] - Override total token budget
191
+ * @param {object} [options.weights] - Override phase weights { research, planning, execution, review }
192
+ * @returns {Promise<TokenBudget>}
193
+ */
194
+ export async function createTokenBudget(input, options = {}) {
195
+ const sources = await resolveInput(input, { language: options.language });
196
+ const metrics = analyzeComplexity(sources);
197
+
198
+ const budgetConfig = DEFAULT_BUDGETS[metrics.complexity];
199
+ const total = options.total ?? budgetConfig.total;
200
+ const weights = { ...budgetConfig.weights, ...options.weights };
201
+
202
+ // Normalize weights
203
+ const weightSum = Object.values(weights).reduce((a, b) => a + b, 0);
204
+ const phases = {};
205
+ for (const [phase, weight] of Object.entries(weights)) {
206
+ phases[phase] = Math.round((weight / weightSum) * total);
207
+ }
208
+
209
+ return new TokenBudget({ total, phases, metrics });
210
+ }