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.
- package/bin/cipher.js +11 -1
- package/lib/agent-runtime/handlers/architect.js +199 -0
- package/lib/agent-runtime/handlers/base.js +240 -0
- package/lib/agent-runtime/handlers/blue.js +220 -0
- package/lib/agent-runtime/handlers/incident.js +161 -0
- package/lib/agent-runtime/handlers/privacy.js +190 -0
- package/lib/agent-runtime/handlers/purple.js +209 -0
- package/lib/agent-runtime/handlers/recon.js +174 -0
- package/lib/agent-runtime/handlers/red.js +246 -0
- package/lib/agent-runtime/handlers/researcher.js +170 -0
- package/lib/agent-runtime/handlers.js +35 -0
- package/lib/agent-runtime/index.js +196 -0
- package/lib/agent-runtime/parser.js +316 -0
- package/lib/analyze/consistency.js +566 -0
- package/lib/analyze/constitution.js +110 -0
- package/lib/analyze/sharding.js +251 -0
- package/lib/autonomous/agent-tool.js +165 -0
- package/lib/autonomous/feedback-loop.js +13 -6
- package/lib/autonomous/framework.js +17 -0
- package/lib/autonomous/handoff.js +506 -0
- package/lib/autonomous/modes/blue.js +26 -0
- package/lib/autonomous/modes/red.js +585 -0
- package/lib/autonomous/modes/researcher.js +322 -0
- package/lib/autonomous/researcher.js +12 -45
- package/lib/autonomous/runner.js +9 -537
- package/lib/benchmark/agent.js +88 -26
- package/lib/benchmark/baselines.js +3 -0
- package/lib/benchmark/claude-code-solver.js +254 -0
- package/lib/benchmark/cognitive.js +283 -0
- package/lib/benchmark/index.js +12 -2
- package/lib/benchmark/knowledge.js +281 -0
- package/lib/benchmark/llm.js +156 -15
- package/lib/benchmark/models.js +5 -2
- package/lib/benchmark/nyu-ctf.js +192 -0
- package/lib/benchmark/overthewire.js +347 -0
- package/lib/benchmark/picoctf.js +281 -0
- package/lib/benchmark/prompts.js +280 -0
- package/lib/benchmark/registry.js +219 -0
- package/lib/benchmark/remote-solver.js +356 -0
- package/lib/benchmark/remote-target.js +263 -0
- package/lib/benchmark/reporter.js +35 -0
- package/lib/benchmark/runner.js +174 -10
- package/lib/benchmark/sandbox.js +35 -0
- package/lib/benchmark/scorer.js +22 -4
- package/lib/benchmark/solver.js +34 -1
- package/lib/benchmark/tools.js +262 -16
- package/lib/commands.js +9 -0
- package/lib/execution/council.js +434 -0
- package/lib/execution/parallel.js +292 -0
- package/lib/gates/circuit-breaker.js +135 -0
- package/lib/gates/confidence.js +302 -0
- package/lib/gates/corrections.js +219 -0
- package/lib/gates/self-check.js +245 -0
- package/lib/gateway/commands.js +727 -0
- package/lib/guardrails/engine.js +364 -0
- package/lib/mcp/server.js +349 -3
- package/lib/memory/compressor.js +94 -7
- package/lib/pipeline/hooks.js +288 -0
- package/lib/pipeline/index.js +11 -0
- package/lib/review/budget.js +210 -0
- package/lib/review/engine.js +526 -0
- package/lib/review/layers/acceptance-auditor.js +279 -0
- package/lib/review/layers/blind-hunter.js +500 -0
- package/lib/review/layers/defense-in-depth.js +209 -0
- package/lib/review/layers/edge-case-hunter.js +266 -0
- package/lib/review/panel.js +519 -0
- package/lib/review/two-stage.js +244 -0
- package/lib/session/cost-tracker.js +203 -0
- package/lib/session/logger.js +349 -0
- 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
|
+
}
|