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,526 @@
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 Multi-Layer Code Review Engine
7
+ *
8
+ * Orchestrates parallel review layers (Blind Hunter, Edge Case Hunter,
9
+ * Acceptance Auditor) and triages/deduplicates findings into a unified
10
+ * report. Each layer runs independently to avoid anchoring bias.
11
+ *
12
+ * @module review/engine
13
+ */
14
+
15
+ import { readFile, readdir, stat } from 'node:fs/promises';
16
+ import { join, extname, relative } from 'node:path';
17
+ import { randomUUID } from 'node:crypto';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Language detection
21
+ // ---------------------------------------------------------------------------
22
+
23
+ const LANG_MAP = Object.freeze({
24
+ '.js': 'javascript',
25
+ '.mjs': 'javascript',
26
+ '.cjs': 'javascript',
27
+ '.jsx': 'javascript',
28
+ '.ts': 'typescript',
29
+ '.tsx': 'typescript',
30
+ '.py': 'python',
31
+ '.sh': 'shell',
32
+ '.bash': 'shell',
33
+ '.zsh': 'shell',
34
+ '.rb': 'ruby',
35
+ '.go': 'go',
36
+ '.rs': 'rust',
37
+ '.java': 'java',
38
+ '.c': 'c',
39
+ '.h': 'c',
40
+ '.cpp': 'cpp',
41
+ '.cc': 'cpp',
42
+ '.hpp': 'cpp',
43
+ '.cs': 'csharp',
44
+ '.php': 'php',
45
+ '.sql': 'sql',
46
+ '.yml': 'yaml',
47
+ '.yaml': 'yaml',
48
+ '.json': 'json',
49
+ '.xml': 'xml',
50
+ '.html': 'html',
51
+ '.htm': 'html',
52
+ '.css': 'css',
53
+ '.md': 'markdown',
54
+ '.dockerfile': 'dockerfile',
55
+ '.tf': 'terraform',
56
+ '.hcl': 'terraform',
57
+ });
58
+
59
+ /**
60
+ * Detect language from file extension.
61
+ * @param {string} filePath
62
+ * @returns {string}
63
+ */
64
+ export function detectLanguage(filePath) {
65
+ const ext = extname(filePath).toLowerCase();
66
+ if (LANG_MAP[ext]) return LANG_MAP[ext];
67
+ // Handle Dockerfile (no extension)
68
+ const base = filePath.split('/').pop()?.toLowerCase() ?? '';
69
+ if (base === 'dockerfile' || base.startsWith('dockerfile.')) return 'dockerfile';
70
+ if (base === 'makefile' || base === 'gnumakefile') return 'makefile';
71
+ return 'unknown';
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // ReviewFinding
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /** Severity levels ordered by impact. */
79
+ export const Severity = Object.freeze({
80
+ CRITICAL: 'critical',
81
+ HIGH: 'high',
82
+ MEDIUM: 'medium',
83
+ LOW: 'low',
84
+ INFO: 'info',
85
+ });
86
+
87
+ const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
88
+
89
+ /**
90
+ * A single code review finding from any layer.
91
+ */
92
+ export class ReviewFinding {
93
+ /**
94
+ * @param {object} opts
95
+ * @param {string} [opts.id] - Unique finding ID (auto-generated)
96
+ * @param {string} opts.title - Short title
97
+ * @param {string} opts.severity - critical|high|medium|low|info
98
+ * @param {string} opts.layer - Which review layer produced this
99
+ * @param {string} [opts.file] - File path
100
+ * @param {number} [opts.line] - Line number (1-indexed)
101
+ * @param {number} [opts.column] - Column number (1-indexed)
102
+ * @param {string} [opts.description] - Detailed explanation
103
+ * @param {string} [opts.proof] - Code snippet or evidence
104
+ * @param {string} [opts.remediation] - How to fix
105
+ * @param {string[]} [opts.cweIds] - CWE identifiers
106
+ * @param {string[]} [opts.tags] - MITRE ATT&CK, OWASP, etc.
107
+ * @param {string} [opts.language] - Source language
108
+ * @param {object} [opts.meta] - Layer-specific metadata
109
+ */
110
+ constructor(opts = {}) {
111
+ this.id = opts.id ?? `RF-${randomUUID().slice(0, 8)}`;
112
+ this.title = opts.title ?? '';
113
+ this.severity = opts.severity ?? Severity.INFO;
114
+ this.layer = opts.layer ?? '';
115
+ this.file = opts.file ?? '';
116
+ this.line = opts.line ?? 0;
117
+ this.column = opts.column ?? 0;
118
+ this.description = opts.description ?? '';
119
+ this.proof = opts.proof ?? '';
120
+ this.remediation = opts.remediation ?? '';
121
+ this.cweIds = opts.cweIds ?? [];
122
+ this.tags = opts.tags ?? [];
123
+ this.language = opts.language ?? '';
124
+ this.meta = opts.meta ?? {};
125
+ }
126
+
127
+ /** Numeric severity rank for sorting (higher = more severe). */
128
+ get rank() {
129
+ return SEVERITY_RANK[this.severity] ?? 0;
130
+ }
131
+
132
+ /** Format as CIPHER finding report. */
133
+ toReport() {
134
+ const lines = [
135
+ `[${this.id}]`,
136
+ `Severity : ${this.severity.toUpperCase()}`,
137
+ ];
138
+ if (this.cweIds.length) lines.push(`CWE : ${this.cweIds.join(', ')}`);
139
+ if (this.tags.length) lines.push(`Tags : ${this.tags.join(', ')}`);
140
+ if (this.file) {
141
+ const loc = this.line ? `${this.file}:${this.line}` : this.file;
142
+ lines.push(`Location : ${loc}`);
143
+ }
144
+ lines.push(`Layer : ${this.layer}`);
145
+ if (this.description) lines.push(`Description: ${this.description}`);
146
+ if (this.proof) lines.push(`Proof : ${this.proof}`);
147
+ if (this.remediation) lines.push(`Remediation: ${this.remediation}`);
148
+ return lines.join('\n');
149
+ }
150
+ }
151
+
152
+ // ---------------------------------------------------------------------------
153
+ // Source input types
154
+ // ---------------------------------------------------------------------------
155
+
156
+ /**
157
+ * A single source file prepared for review.
158
+ * @typedef {object} SourceFile
159
+ * @property {string} path - Relative or absolute file path
160
+ * @property {string} content - File content
161
+ * @property {string} language - Detected language
162
+ */
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Input normalization
166
+ // ---------------------------------------------------------------------------
167
+
168
+ const SKIP_DIRS = new Set([
169
+ 'node_modules', '.git', 'dist', 'build', 'coverage',
170
+ '__pycache__', '.next', '.nuxt', 'vendor', '.venv', 'venv',
171
+ ]);
172
+
173
+ const MAX_FILE_SIZE = 512 * 1024; // 512 KB — skip huge files
174
+
175
+ /**
176
+ * Resolve input to an array of SourceFile objects.
177
+ *
178
+ * @param {string} input - File path, directory path, or raw code string
179
+ * @param {object} [options]
180
+ * @param {string} [options.language] - Override language detection
181
+ * @param {string[]} [options.extensions] - Limit to these extensions (e.g. ['.js', '.ts'])
182
+ * @returns {Promise<SourceFile[]>}
183
+ */
184
+ export async function resolveInput(input, options = {}) {
185
+ // Try as file/directory path first
186
+ try {
187
+ const st = await stat(input);
188
+ if (st.isFile()) {
189
+ const content = await readFile(input, 'utf-8');
190
+ const language = options.language ?? detectLanguage(input);
191
+ return [{ path: input, content, language }];
192
+ }
193
+ if (st.isDirectory()) {
194
+ return collectDir(input, options);
195
+ }
196
+ } catch {
197
+ // Not a path — treat as raw code string
198
+ }
199
+
200
+ // Raw code string
201
+ const language = options.language ?? 'unknown';
202
+ return [{ path: '<inline>', content: input, language }];
203
+ }
204
+
205
+ /**
206
+ * Recursively collect source files from a directory.
207
+ * @param {string} dir
208
+ * @param {object} options
209
+ * @returns {Promise<SourceFile[]>}
210
+ */
211
+ async function collectDir(dir, options) {
212
+ const files = [];
213
+ const entries = await readdir(dir, { withFileTypes: true });
214
+
215
+ for (const entry of entries) {
216
+ if (SKIP_DIRS.has(entry.name)) continue;
217
+ const full = join(dir, entry.name);
218
+
219
+ if (entry.isDirectory()) {
220
+ const sub = await collectDir(full, options);
221
+ files.push(...sub);
222
+ } else if (entry.isFile()) {
223
+ const ext = extname(entry.name).toLowerCase();
224
+ if (options.extensions && !options.extensions.includes(ext)) continue;
225
+ const language = options.language ?? detectLanguage(entry.name);
226
+ if (language === 'unknown') continue; // skip unrecognized files
227
+
228
+ try {
229
+ const st = await stat(full);
230
+ if (st.size > MAX_FILE_SIZE) continue;
231
+ const content = await readFile(full, 'utf-8');
232
+ files.push({ path: full, content, language });
233
+ } catch {
234
+ // Skip unreadable files
235
+ }
236
+ }
237
+ }
238
+ return files;
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // CodeReviewEngine
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /**
246
+ * Multi-layer code review engine.
247
+ *
248
+ * Runs registered review layers in parallel, collects findings,
249
+ * deduplicates, and produces a unified report.
250
+ */
251
+ export class CodeReviewEngine {
252
+ constructor() {
253
+ /** @type {Array<{name: string, review: function}>} */
254
+ this._layers = [];
255
+ }
256
+
257
+ /**
258
+ * Register a review layer.
259
+ * @param {string} name - Layer identifier (e.g. 'blind-hunter')
260
+ * @param {function} reviewFn - async (sources: SourceFile[], options) => ReviewFinding[]
261
+ */
262
+ addLayer(name, reviewFn) {
263
+ this._layers.push({ name, review: reviewFn });
264
+ }
265
+
266
+ /**
267
+ * Run all layers against the input and return unified results.
268
+ *
269
+ * @param {string} input - File path, directory, or raw code
270
+ * @param {object} [options]
271
+ * @param {string} [options.language] - Override language
272
+ * @param {string[]} [options.extensions] - Limit file extensions
273
+ * @param {string} [options.minSeverity] - Filter findings at or above this level
274
+ * @returns {Promise<ReviewResult>}
275
+ */
276
+ async review(input, options = {}) {
277
+ const t0 = Date.now();
278
+
279
+ // 1. Resolve input to source files
280
+ const sources = await resolveInput(input, options);
281
+ if (!sources.length) {
282
+ return new ReviewResult({
283
+ findings: [],
284
+ filesReviewed: 0,
285
+ layerTimings: {},
286
+ totalTime: Date.now() - t0,
287
+ });
288
+ }
289
+
290
+ // 2. Run all layers in parallel
291
+ const layerTimings = {};
292
+ const layerResults = await Promise.allSettled(
293
+ this._layers.map(async (layer) => {
294
+ const lt0 = Date.now();
295
+ try {
296
+ const findings = await layer.review(sources, options);
297
+ layerTimings[layer.name] = Date.now() - lt0;
298
+ return { name: layer.name, findings };
299
+ } catch (err) {
300
+ layerTimings[layer.name] = Date.now() - lt0;
301
+ // Layer failure is non-fatal — report as info finding
302
+ return {
303
+ name: layer.name,
304
+ findings: [
305
+ new ReviewFinding({
306
+ title: `Review layer "${layer.name}" failed`,
307
+ severity: Severity.INFO,
308
+ layer: layer.name,
309
+ description: err.message,
310
+ tags: ['engine-error'],
311
+ }),
312
+ ],
313
+ };
314
+ }
315
+ }),
316
+ );
317
+
318
+ // 3. Collect all findings
319
+ const allFindings = [];
320
+ for (const result of layerResults) {
321
+ if (result.status === 'fulfilled') {
322
+ allFindings.push(...result.value.findings);
323
+ }
324
+ // 'rejected' shouldn't happen since we catch above, but guard anyway
325
+ }
326
+
327
+ // 4. Deduplicate
328
+ const deduped = this._deduplicate(allFindings);
329
+
330
+ // 5. Filter by severity if requested
331
+ const minRank = options.minSeverity
332
+ ? (SEVERITY_RANK[options.minSeverity] ?? 0)
333
+ : 0;
334
+ const filtered = deduped.filter((f) => f.rank >= minRank);
335
+
336
+ // 6. Sort by severity (highest first), then by file+line
337
+ filtered.sort((a, b) => {
338
+ if (b.rank !== a.rank) return b.rank - a.rank;
339
+ if (a.file !== b.file) return a.file.localeCompare(b.file);
340
+ return a.line - b.line;
341
+ });
342
+
343
+ return new ReviewResult({
344
+ findings: filtered,
345
+ filesReviewed: sources.length,
346
+ layerTimings,
347
+ totalTime: Date.now() - t0,
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Deduplicate findings that overlap in location and pattern.
353
+ * When two findings cover the same file:line and similar CWE/title,
354
+ * keep the one with higher severity and merge tags.
355
+ *
356
+ * @param {ReviewFinding[]} findings
357
+ * @returns {ReviewFinding[]}
358
+ */
359
+ _deduplicate(findings) {
360
+ /** @type {Map<string, ReviewFinding>} */
361
+ const seen = new Map();
362
+
363
+ for (const f of findings) {
364
+ // Key: file + line + normalized title stem
365
+ const titleStem = f.title.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 30);
366
+ const key = `${f.file}:${f.line}:${titleStem}`;
367
+
368
+ const existing = seen.get(key);
369
+ if (!existing) {
370
+ seen.set(key, f);
371
+ continue;
372
+ }
373
+
374
+ // Keep higher severity, merge metadata
375
+ if (f.rank > existing.rank) {
376
+ // Merge tags and CWEs from existing into the new winner
377
+ f.tags = [...new Set([...f.tags, ...existing.tags])];
378
+ f.cweIds = [...new Set([...f.cweIds, ...existing.cweIds])];
379
+ f.layer = `${f.layer}+${existing.layer}`;
380
+ seen.set(key, f);
381
+ } else {
382
+ existing.tags = [...new Set([...existing.tags, ...f.tags])];
383
+ existing.cweIds = [...new Set([...existing.cweIds, ...f.cweIds])];
384
+ if (!existing.layer.includes(f.layer)) {
385
+ existing.layer = `${existing.layer}+${f.layer}`;
386
+ }
387
+ }
388
+ }
389
+
390
+ return [...seen.values()];
391
+ }
392
+ }
393
+
394
+ // ---------------------------------------------------------------------------
395
+ // ReviewResult
396
+ // ---------------------------------------------------------------------------
397
+
398
+ /**
399
+ * Unified review result with findings and metadata.
400
+ */
401
+ export class ReviewResult {
402
+ /**
403
+ * @param {object} opts
404
+ * @param {ReviewFinding[]} opts.findings
405
+ * @param {number} opts.filesReviewed
406
+ * @param {object} opts.layerTimings
407
+ * @param {number} opts.totalTime
408
+ */
409
+ constructor({ findings = [], filesReviewed = 0, layerTimings = {}, totalTime = 0 } = {}) {
410
+ this.findings = findings;
411
+ this.filesReviewed = filesReviewed;
412
+ this.layerTimings = layerTimings;
413
+ this.totalTime = totalTime;
414
+ }
415
+
416
+ /** Count of findings by severity. */
417
+ get severityCounts() {
418
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
419
+ for (const f of this.findings) {
420
+ counts[f.severity] = (counts[f.severity] ?? 0) + 1;
421
+ }
422
+ return counts;
423
+ }
424
+
425
+ /** Human-readable summary line. */
426
+ get summary() {
427
+ const c = this.severityCounts;
428
+ const parts = [];
429
+ if (c.critical) parts.push(`${c.critical} critical`);
430
+ if (c.high) parts.push(`${c.high} high`);
431
+ if (c.medium) parts.push(`${c.medium} medium`);
432
+ if (c.low) parts.push(`${c.low} low`);
433
+ if (c.info) parts.push(`${c.info} info`);
434
+ const total = this.findings.length;
435
+ const detail = parts.length ? ` (${parts.join(', ')})` : '';
436
+ return `${total} finding${total !== 1 ? 's' : ''}${detail} across ${this.filesReviewed} file${this.filesReviewed !== 1 ? 's' : ''} in ${this.totalTime}ms`;
437
+ }
438
+
439
+ /** Full report as formatted text. */
440
+ toReport() {
441
+ const lines = [
442
+ '═══════════════════════════════════════════════════════',
443
+ ' CIPHER Code Review Report',
444
+ '═══════════════════════════════════════════════════════',
445
+ '',
446
+ `Summary: ${this.summary}`,
447
+ '',
448
+ ];
449
+
450
+ if (this.findings.length === 0) {
451
+ lines.push('No findings.');
452
+ } else {
453
+ for (const f of this.findings) {
454
+ lines.push(f.toReport());
455
+ lines.push('');
456
+ }
457
+ }
458
+
459
+ // Layer timing
460
+ lines.push('───────────────────────────────────────────────────────');
461
+ lines.push('Layer Timings:');
462
+ for (const [name, ms] of Object.entries(this.layerTimings)) {
463
+ lines.push(` ${name}: ${ms}ms`);
464
+ }
465
+
466
+ return lines.join('\n');
467
+ }
468
+
469
+ /** Structured JSON output. */
470
+ toJSON() {
471
+ return {
472
+ summary: this.summary,
473
+ severityCounts: this.severityCounts,
474
+ filesReviewed: this.filesReviewed,
475
+ totalTime: this.totalTime,
476
+ layerTimings: this.layerTimings,
477
+ findings: this.findings.map((f) => ({
478
+ id: f.id,
479
+ title: f.title,
480
+ severity: f.severity,
481
+ layer: f.layer,
482
+ file: f.file,
483
+ line: f.line,
484
+ column: f.column,
485
+ description: f.description,
486
+ proof: f.proof,
487
+ remediation: f.remediation,
488
+ cweIds: f.cweIds,
489
+ tags: f.tags,
490
+ language: f.language,
491
+ })),
492
+ };
493
+ }
494
+ }
495
+
496
+ // ---------------------------------------------------------------------------
497
+ // Factory — create engine with all standard layers
498
+ // ---------------------------------------------------------------------------
499
+
500
+ /**
501
+ * Create a CodeReviewEngine with all standard review layers loaded.
502
+ * Layers are imported lazily to keep the module lightweight.
503
+ *
504
+ * @returns {Promise<CodeReviewEngine>}
505
+ */
506
+ export async function createReviewEngine() {
507
+ const engine = new CodeReviewEngine();
508
+
509
+ // Layer 1: Blind Hunter — pattern-based vulnerability detection
510
+ const { blindHunterReview } = await import('./layers/blind-hunter.js');
511
+ engine.addLayer('blind-hunter', blindHunterReview);
512
+
513
+ // Layer 2: Edge Case Hunter — boundary condition analysis
514
+ const { edgeCaseReview } = await import('./layers/edge-case-hunter.js');
515
+ engine.addLayer('edge-case-hunter', edgeCaseReview);
516
+
517
+ // Layer 3: Acceptance Auditor — security architecture review
518
+ const { acceptanceAuditReview } = await import('./layers/acceptance-auditor.js');
519
+ engine.addLayer('acceptance-auditor', acceptanceAuditReview);
520
+
521
+ // Layer 4: Defense-in-Depth — single-layer validation gaps
522
+ const { defenseInDepthReview } = await import('./layers/defense-in-depth.js');
523
+ engine.addLayer('defense-in-depth', defenseInDepthReview);
524
+
525
+ return engine;
526
+ }