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,302 @@
|
|
|
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
|
+
* Security Confidence Checker — Pre-response confidence assessment.
|
|
7
|
+
*
|
|
8
|
+
* Prevents wrong-direction responses by scoring confidence across
|
|
9
|
+
* 5 dimensions before generating security advice. Inspired by
|
|
10
|
+
* SuperClaude's ConfidenceChecker pattern, adapted for security domain.
|
|
11
|
+
*
|
|
12
|
+
* Confidence levels:
|
|
13
|
+
* HIGH (≥0.90): Proceed with full response
|
|
14
|
+
* MEDIUM (0.70–0.89): Respond with explicit caveats
|
|
15
|
+
* LOW (<0.70): Flag uncertainty, recommend verification
|
|
16
|
+
*
|
|
17
|
+
* @module gates/confidence
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Hedging language detection
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
const HEDGING_PATTERNS = [
|
|
25
|
+
/\bshould\s+(?:work|fix|resolve|detect|prevent|mitigate)\b/i,
|
|
26
|
+
/\bprobably\s+(?:works?|fixes?|covers?|detects?)\b/i,
|
|
27
|
+
/\blikely\s+sufficient\b/i,
|
|
28
|
+
/\bseems?\s+to\s+(?:work|fix|detect)\b/i,
|
|
29
|
+
/\bmight\s+(?:work|help|detect|prevent)\b/i,
|
|
30
|
+
/\bI\s+(?:think|believe|assume)\s+(?:this|it|that)\b/i,
|
|
31
|
+
/\bjust\s+this\s+once\b/i,
|
|
32
|
+
/\bshould\s+be\s+(?:fine|enough|sufficient|okay)\b/i,
|
|
33
|
+
/\bI'm\s+(?:fairly|pretty|quite)\s+(?:sure|confident)\b/i,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const RATIONALIZATION_PATTERNS = [
|
|
37
|
+
{ pattern: /\bshould\s+work\s+now\b/i, category: 'unverified-claim' },
|
|
38
|
+
{ pattern: /\bI'm\s+confident\b/i, category: 'false-confidence' },
|
|
39
|
+
{ pattern: /\bjust\s+this\s+once\b/i, category: 'exception-seeking' },
|
|
40
|
+
{ pattern: /\bpartial\s+check\s+is\s+enough\b/i, category: 'incomplete-verification' },
|
|
41
|
+
{ pattern: /\bno\s+need\s+to\s+(?:test|verify|check)\b/i, category: 'verification-avoidance' },
|
|
42
|
+
{ pattern: /\bobviously\s+(?:works?|correct|right)\b/i, category: 'assumed-correctness' },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// SecurityConfidenceChecker
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} AssessmentContext
|
|
51
|
+
* @property {string} query — The user's query
|
|
52
|
+
* @property {string} mode — CIPHER mode (RED, BLUE, etc.)
|
|
53
|
+
* @property {number} knowledgeHits — Number of knowledge base matches
|
|
54
|
+
* @property {number} knowledgeRelevance — Relevance score of top match (0–1)
|
|
55
|
+
* @property {'current'|'recent'|'outdated'|'unknown'} topicRecency
|
|
56
|
+
* @property {boolean} hasConflictingGuidance — Whether sources conflict
|
|
57
|
+
* @property {boolean} authoritativeSourceFound — Whether authoritative source exists
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @typedef {Object} ConfidenceResult
|
|
62
|
+
* @property {number} score — Overall confidence (0–1)
|
|
63
|
+
* @property {'HIGH'|'MEDIUM'|'LOW'} level — Confidence level
|
|
64
|
+
* @property {boolean} shouldProceed — Whether to generate full response
|
|
65
|
+
* @property {string[]} checks — Individual check results
|
|
66
|
+
* @property {string[]} concerns — Identified concerns
|
|
67
|
+
* @property {string} recommendation — Action recommendation
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
export class SecurityConfidenceChecker {
|
|
71
|
+
/**
|
|
72
|
+
* Assess confidence before generating a security response.
|
|
73
|
+
* @param {AssessmentContext} context
|
|
74
|
+
* @returns {ConfidenceResult}
|
|
75
|
+
*/
|
|
76
|
+
assess(context) {
|
|
77
|
+
let score = 0;
|
|
78
|
+
const checks = [];
|
|
79
|
+
const concerns = [];
|
|
80
|
+
|
|
81
|
+
// Check 1: Knowledge base has relevant content (30%)
|
|
82
|
+
if (context.knowledgeHits > 0 && context.knowledgeRelevance > 0.7) {
|
|
83
|
+
score += 0.30;
|
|
84
|
+
checks.push('✅ Knowledge base has relevant content');
|
|
85
|
+
} else if (context.knowledgeHits > 0) {
|
|
86
|
+
score += 0.15;
|
|
87
|
+
checks.push('⚠️ Knowledge base has partial matches');
|
|
88
|
+
concerns.push('Knowledge relevance below threshold');
|
|
89
|
+
} else {
|
|
90
|
+
checks.push('❌ No knowledge base matches');
|
|
91
|
+
concerns.push('No authoritative knowledge for this topic');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check 2: Information recency (25%)
|
|
95
|
+
if (context.topicRecency === 'current' || context.topicRecency === 'recent') {
|
|
96
|
+
score += 0.25;
|
|
97
|
+
checks.push('✅ Information is current');
|
|
98
|
+
} else if (context.topicRecency === 'outdated') {
|
|
99
|
+
score += 0.10;
|
|
100
|
+
checks.push('⚠️ Information may be outdated');
|
|
101
|
+
concerns.push('Knowledge may not reflect latest threats/mitigations');
|
|
102
|
+
} else {
|
|
103
|
+
checks.push('❌ Information recency unknown');
|
|
104
|
+
concerns.push('Cannot verify information currency');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check 3: Mode-appropriate response (20%)
|
|
108
|
+
if (this._isModeAligned(context)) {
|
|
109
|
+
score += 0.20;
|
|
110
|
+
checks.push('✅ Query aligns with active mode');
|
|
111
|
+
} else {
|
|
112
|
+
score += 0.10;
|
|
113
|
+
checks.push('⚠️ Query may not align with active mode');
|
|
114
|
+
concerns.push('Consider switching mode for better results');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check 4: No conflicting guidance (15%)
|
|
118
|
+
if (!context.hasConflictingGuidance) {
|
|
119
|
+
score += 0.15;
|
|
120
|
+
checks.push('✅ No conflicting guidance detected');
|
|
121
|
+
} else {
|
|
122
|
+
checks.push('❌ Conflicting guidance exists');
|
|
123
|
+
concerns.push('Multiple sources provide contradictory advice');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check 5: Authoritative source verified (10%)
|
|
127
|
+
if (context.authoritativeSourceFound) {
|
|
128
|
+
score += 0.10;
|
|
129
|
+
checks.push('✅ Authoritative source verified');
|
|
130
|
+
} else {
|
|
131
|
+
checks.push('⚠️ No authoritative source found');
|
|
132
|
+
concerns.push('Response based on general knowledge, not verified source');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const level = score >= 0.90 ? 'HIGH' : score >= 0.70 ? 'MEDIUM' : 'LOW';
|
|
136
|
+
const shouldProceed = score >= 0.70;
|
|
137
|
+
|
|
138
|
+
let recommendation;
|
|
139
|
+
if (level === 'HIGH') {
|
|
140
|
+
recommendation = 'Proceed with full response — high confidence';
|
|
141
|
+
} else if (level === 'MEDIUM') {
|
|
142
|
+
recommendation = 'Respond with explicit caveats — flag areas of uncertainty';
|
|
143
|
+
} else {
|
|
144
|
+
recommendation = 'Flag low confidence — recommend verification before acting on advice';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { score, level, shouldProceed, checks, concerns, recommendation };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if query aligns with the active CIPHER mode.
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
_isModeAligned(context) {
|
|
155
|
+
const modeTopics = {
|
|
156
|
+
RED: ['exploit', 'payload', 'attack', 'bypass', 'lateral', 'privesc', 'c2'],
|
|
157
|
+
BLUE: ['detect', 'detection', 'sigma', 'siem', 'hunting', 'hardening', 'edr', 'log'],
|
|
158
|
+
PURPLE: ['coverage', 'emulation', 'gap', 'detection engineering'],
|
|
159
|
+
PRIVACY: ['gdpr', 'ccpa', 'hipaa', 'dpia', 'anonymization', 'data flow'],
|
|
160
|
+
RECON: ['osint', 'reconnaissance', 'subdomain', 'footprinting'],
|
|
161
|
+
INCIDENT: ['triage', 'forensics', 'containment', 'eradication', 'timeline'],
|
|
162
|
+
ARCHITECT: ['design', 'architecture', 'threat model', 'zero trust'],
|
|
163
|
+
RESEARCHER: ['research', 'analysis', 'technique', 'methodology'],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const topics = modeTopics[context.mode] || [];
|
|
167
|
+
if (topics.length === 0) return true; // Unknown mode — don't penalize
|
|
168
|
+
|
|
169
|
+
const queryLower = context.query.toLowerCase();
|
|
170
|
+
return topics.some(topic => queryLower.includes(topic));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Hedging & Rationalization Detection
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Detect hedging language in security output.
|
|
180
|
+
* @param {string} text — Response text to check
|
|
181
|
+
* @returns {{ hedgingFound: boolean, matches: string[], count: number }}
|
|
182
|
+
*/
|
|
183
|
+
export function detectHedging(text) {
|
|
184
|
+
const matches = [];
|
|
185
|
+
for (const pattern of HEDGING_PATTERNS) {
|
|
186
|
+
const match = text.match(pattern);
|
|
187
|
+
if (match) {
|
|
188
|
+
matches.push(match[0]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return { hedgingFound: matches.length > 0, matches, count: matches.length };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Detect rationalization patterns in security output.
|
|
196
|
+
* @param {string} text
|
|
197
|
+
* @returns {{ found: boolean, rationalizations: Array<{ text: string, category: string }> }}
|
|
198
|
+
*/
|
|
199
|
+
export function detectRationalizations(text) {
|
|
200
|
+
const rationalizations = [];
|
|
201
|
+
for (const { pattern, category } of RATIONALIZATION_PATTERNS) {
|
|
202
|
+
const match = text.match(pattern);
|
|
203
|
+
if (match) {
|
|
204
|
+
rationalizations.push({ text: match[0], category });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { found: rationalizations.length > 0, rationalizations };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Verification Gate
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Evidence types that satisfy the verification gate.
|
|
216
|
+
*/
|
|
217
|
+
const EVIDENCE_TYPES = {
|
|
218
|
+
sigma_rule: {
|
|
219
|
+
validators: [
|
|
220
|
+
(text) => /^title:\s*.+/m.test(text),
|
|
221
|
+
(text) => /^logsource:/m.test(text),
|
|
222
|
+
(text) => /^detection:/m.test(text),
|
|
223
|
+
],
|
|
224
|
+
description: 'Sigma rule must have title, logsource, and detection fields',
|
|
225
|
+
},
|
|
226
|
+
kql_query: {
|
|
227
|
+
validators: [
|
|
228
|
+
(text) => /\b(?:where|project|summarize|extend|join|let)\b/i.test(text),
|
|
229
|
+
],
|
|
230
|
+
description: 'KQL query must contain valid operators',
|
|
231
|
+
},
|
|
232
|
+
spl_query: {
|
|
233
|
+
validators: [
|
|
234
|
+
(text) => /\b(?:index=|sourcetype=|search|stats|eval|table)\b/i.test(text),
|
|
235
|
+
],
|
|
236
|
+
description: 'SPL query must contain valid Splunk operators',
|
|
237
|
+
},
|
|
238
|
+
cve_reference: {
|
|
239
|
+
validators: [
|
|
240
|
+
(text) => /CVE-\d{4}-\d{4,}/.test(text),
|
|
241
|
+
],
|
|
242
|
+
description: 'CVE reference must be a valid CVE ID',
|
|
243
|
+
},
|
|
244
|
+
mitre_reference: {
|
|
245
|
+
validators: [
|
|
246
|
+
(text) => /T\d{4}(?:\.\d{3})?/.test(text),
|
|
247
|
+
],
|
|
248
|
+
description: 'MITRE reference must be a valid technique ID',
|
|
249
|
+
},
|
|
250
|
+
command_output: {
|
|
251
|
+
validators: [
|
|
252
|
+
(text) => text.includes('$') || text.includes('#') || text.includes('>>>'),
|
|
253
|
+
],
|
|
254
|
+
description: 'Command output must include shell prompt evidence',
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Verification gate — checks that security claims have supporting evidence.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} claim — The security claim being made
|
|
262
|
+
* @param {string} evidence — Supporting evidence
|
|
263
|
+
* @param {string} evidenceType — Type of evidence expected
|
|
264
|
+
* @returns {{ verified: boolean, reason: string, evidenceType: string }}
|
|
265
|
+
*/
|
|
266
|
+
export function verifyEvidence(claim, evidence, evidenceType) {
|
|
267
|
+
if (!evidence || evidence.trim().length === 0) {
|
|
268
|
+
return { verified: false, reason: 'No evidence provided', evidenceType };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const typeSpec = EVIDENCE_TYPES[evidenceType];
|
|
272
|
+
if (!typeSpec) {
|
|
273
|
+
// Unknown evidence type — accept if non-empty
|
|
274
|
+
return { verified: true, reason: 'Evidence provided (untyped)', evidenceType };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const failedValidators = typeSpec.validators.filter(v => !v(evidence));
|
|
278
|
+
if (failedValidators.length > 0) {
|
|
279
|
+
return {
|
|
280
|
+
verified: false,
|
|
281
|
+
reason: `Evidence does not satisfy ${evidenceType} requirements: ${typeSpec.description}`,
|
|
282
|
+
evidenceType,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { verified: true, reason: `Evidence verified as valid ${evidenceType}`, evidenceType };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Batch verify multiple claims against their evidence.
|
|
291
|
+
* @param {Array<{ claim: string, evidence: string, evidenceType: string }>} items
|
|
292
|
+
* @returns {{ allVerified: boolean, results: Array, unverifiedCount: number }}
|
|
293
|
+
*/
|
|
294
|
+
export function verifyBatch(items) {
|
|
295
|
+
const results = items.map(item => ({
|
|
296
|
+
claim: item.claim,
|
|
297
|
+
...verifyEvidence(item.claim, item.evidence, item.evidenceType),
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
const unverifiedCount = results.filter(r => !r.verified).length;
|
|
301
|
+
return { allVerified: unverifiedCount === 0, results, unverifiedCount };
|
|
302
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
* Reflexion Error Learning — corrections log for pattern-based error prevention.
|
|
7
|
+
*
|
|
8
|
+
* Stores error patterns and corrections in a JSONL file. Before generating
|
|
9
|
+
* structured output (Sigma rules, KQL queries, etc.), checks the corrections
|
|
10
|
+
* log and applies known fixes automatically.
|
|
11
|
+
*
|
|
12
|
+
* Inspired by SuperClaude's ReflectionEngine pattern.
|
|
13
|
+
*
|
|
14
|
+
* @module gates/corrections
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
18
|
+
import { join, dirname } from 'node:path';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Correction entry
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} CorrectionEntry
|
|
27
|
+
* @property {string} id — Unique correction ID
|
|
28
|
+
* @property {string} category — Error category (sigma, kql, spl, hardening, general)
|
|
29
|
+
* @property {string} pattern — Regex pattern string that matches the error
|
|
30
|
+
* @property {string} replacement — Replacement string or fix description
|
|
31
|
+
* @property {string} description — Human-readable description of the error
|
|
32
|
+
* @property {string} timestamp — ISO timestamp
|
|
33
|
+
* @property {number} applyCount — Times this correction has been applied
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// CorrectionsLog
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
const DEFAULT_DIR = join(homedir(), '.cipher', 'data');
|
|
41
|
+
const MAX_ENTRIES = 500;
|
|
42
|
+
|
|
43
|
+
export class CorrectionsLog {
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} [logPath] — Path to corrections.jsonl file
|
|
46
|
+
*/
|
|
47
|
+
constructor(logPath) {
|
|
48
|
+
this._path = logPath || join(DEFAULT_DIR, 'corrections.jsonl');
|
|
49
|
+
this._entries = null; // Lazy load
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Load entries from disk.
|
|
54
|
+
* @returns {CorrectionEntry[]}
|
|
55
|
+
*/
|
|
56
|
+
_load() {
|
|
57
|
+
if (this._entries !== null) return this._entries;
|
|
58
|
+
|
|
59
|
+
this._entries = [];
|
|
60
|
+
if (!existsSync(this._path)) return this._entries;
|
|
61
|
+
|
|
62
|
+
const lines = readFileSync(this._path, 'utf8').split('\n').filter(l => l.trim());
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
try {
|
|
65
|
+
this._entries.push(JSON.parse(line));
|
|
66
|
+
} catch { /* skip malformed lines */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this._entries;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Record a new correction.
|
|
74
|
+
* @param {{ category: string, pattern: string, replacement: string, description: string }} opts
|
|
75
|
+
* @returns {CorrectionEntry}
|
|
76
|
+
*/
|
|
77
|
+
record(opts) {
|
|
78
|
+
const entry = {
|
|
79
|
+
id: `COR-${Date.now().toString(36)}`,
|
|
80
|
+
category: opts.category || 'general',
|
|
81
|
+
pattern: opts.pattern,
|
|
82
|
+
replacement: opts.replacement || '',
|
|
83
|
+
description: opts.description || '',
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
applyCount: 0,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
mkdirSync(dirname(this._path), { recursive: true });
|
|
89
|
+
appendFileSync(this._path, JSON.stringify(entry) + '\n');
|
|
90
|
+
|
|
91
|
+
// Invalidate cache
|
|
92
|
+
this._entries = null;
|
|
93
|
+
|
|
94
|
+
return entry;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get all corrections, optionally filtered by category.
|
|
99
|
+
* @param {string} [category]
|
|
100
|
+
* @returns {CorrectionEntry[]}
|
|
101
|
+
*/
|
|
102
|
+
getAll(category) {
|
|
103
|
+
const entries = this._load();
|
|
104
|
+
if (category) {
|
|
105
|
+
return entries.filter(e => e.category === category);
|
|
106
|
+
}
|
|
107
|
+
return entries;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Apply known corrections to text.
|
|
112
|
+
* @param {string} text — Input text to correct
|
|
113
|
+
* @param {string} category — Category to filter corrections by
|
|
114
|
+
* @returns {{ corrected: string, applied: string[], count: number }}
|
|
115
|
+
*/
|
|
116
|
+
apply(text, category) {
|
|
117
|
+
const corrections = this.getAll(category);
|
|
118
|
+
let corrected = text;
|
|
119
|
+
const applied = [];
|
|
120
|
+
|
|
121
|
+
for (const entry of corrections) {
|
|
122
|
+
try {
|
|
123
|
+
const regex = new RegExp(entry.pattern, 'g');
|
|
124
|
+
if (regex.test(corrected)) {
|
|
125
|
+
corrected = corrected.replace(regex, entry.replacement);
|
|
126
|
+
applied.push(entry.id);
|
|
127
|
+
entry.applyCount++;
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// Invalid regex — skip
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { corrected, applied, count: applied.length };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get correction statistics.
|
|
139
|
+
* @returns {{ total: number, byCategory: Record<string, number>, topApplied: CorrectionEntry[] }}
|
|
140
|
+
*/
|
|
141
|
+
stats() {
|
|
142
|
+
const entries = this._load();
|
|
143
|
+
const byCategory = {};
|
|
144
|
+
for (const e of entries) {
|
|
145
|
+
byCategory[e.category] = (byCategory[e.category] || 0) + 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const topApplied = [...entries]
|
|
149
|
+
.sort((a, b) => (b.applyCount || 0) - (a.applyCount || 0))
|
|
150
|
+
.slice(0, 10);
|
|
151
|
+
|
|
152
|
+
return { total: entries.length, byCategory, topApplied };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Prune old entries, keeping only the most recent per category.
|
|
157
|
+
* @param {number} [maxPerCategory=100]
|
|
158
|
+
* @returns {number} — Number of entries pruned
|
|
159
|
+
*/
|
|
160
|
+
prune(maxPerCategory = 100) {
|
|
161
|
+
const entries = this._load();
|
|
162
|
+
if (entries.length <= maxPerCategory) return 0;
|
|
163
|
+
|
|
164
|
+
const byCategory = {};
|
|
165
|
+
for (const e of entries) {
|
|
166
|
+
if (!byCategory[e.category]) byCategory[e.category] = [];
|
|
167
|
+
byCategory[e.category].push(e);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const kept = [];
|
|
171
|
+
for (const [, catEntries] of Object.entries(byCategory)) {
|
|
172
|
+
catEntries.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
173
|
+
kept.push(...catEntries.slice(0, maxPerCategory));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const pruned = entries.length - kept.length;
|
|
177
|
+
if (pruned > 0) {
|
|
178
|
+
mkdirSync(dirname(this._path), { recursive: true });
|
|
179
|
+
const content = kept.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
180
|
+
// Atomic write
|
|
181
|
+
const { writeFileSync: wfs } = require('node:fs');
|
|
182
|
+
wfs(this._path, content);
|
|
183
|
+
this._entries = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return pruned;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Number of entries */
|
|
190
|
+
get size() {
|
|
191
|
+
return this._load().length;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Pre-built Sigma corrections
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
/** Common Sigma rule errors that LLMs make */
|
|
200
|
+
export const SIGMA_CORRECTIONS = [
|
|
201
|
+
{
|
|
202
|
+
category: 'sigma',
|
|
203
|
+
pattern: 'level:\\s*(?:critical|high|medium|low)\\s*\\n(?!tags:)',
|
|
204
|
+
replacement: '',
|
|
205
|
+
description: 'Sigma level must be one of: critical, high, medium, low, informational',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
category: 'sigma',
|
|
209
|
+
pattern: 'status:\\s*(?:new|draft)\\b',
|
|
210
|
+
replacement: 'status: experimental',
|
|
211
|
+
description: 'Sigma status "new" or "draft" should be "experimental"',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
category: 'sigma',
|
|
215
|
+
pattern: 'logsource:\\s*\\n\\s*service:\\s*',
|
|
216
|
+
replacement: 'logsource:\n category: ',
|
|
217
|
+
description: 'Sigma logsource should use "category" not "service" as primary field',
|
|
218
|
+
},
|
|
219
|
+
];
|