cipher-security 2.0.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 +566 -0
- package/lib/api/billing.js +321 -0
- package/lib/api/compliance.js +693 -0
- package/lib/api/controls.js +1401 -0
- package/lib/api/index.js +49 -0
- package/lib/api/marketplace.js +467 -0
- package/lib/api/openai-proxy.js +383 -0
- package/lib/api/server.js +685 -0
- package/lib/autonomous/feedback-loop.js +554 -0
- package/lib/autonomous/framework.js +512 -0
- package/lib/autonomous/index.js +97 -0
- package/lib/autonomous/leaderboard.js +594 -0
- package/lib/autonomous/modes/architect.js +412 -0
- package/lib/autonomous/modes/blue.js +386 -0
- package/lib/autonomous/modes/incident.js +684 -0
- package/lib/autonomous/modes/privacy.js +369 -0
- package/lib/autonomous/modes/purple.js +294 -0
- package/lib/autonomous/modes/recon.js +250 -0
- package/lib/autonomous/parallel.js +587 -0
- package/lib/autonomous/researcher.js +583 -0
- package/lib/autonomous/runner.js +955 -0
- package/lib/autonomous/scheduler.js +615 -0
- package/lib/autonomous/task-parser.js +127 -0
- package/lib/autonomous/validators/forensic.js +266 -0
- package/lib/autonomous/validators/osint.js +216 -0
- package/lib/autonomous/validators/privacy.js +296 -0
- package/lib/autonomous/validators/purple.js +298 -0
- package/lib/autonomous/validators/sigma.js +248 -0
- package/lib/autonomous/validators/threat-model.js +363 -0
- package/lib/benchmark/agent.js +119 -0
- package/lib/benchmark/baselines.js +43 -0
- package/lib/benchmark/builder.js +143 -0
- package/lib/benchmark/config.js +35 -0
- package/lib/benchmark/coordinator.js +91 -0
- package/lib/benchmark/index.js +20 -0
- package/lib/benchmark/llm.js +58 -0
- package/lib/benchmark/models.js +137 -0
- package/lib/benchmark/reporter.js +103 -0
- package/lib/benchmark/runner.js +103 -0
- package/lib/benchmark/sandbox.js +96 -0
- package/lib/benchmark/scorer.js +32 -0
- package/lib/benchmark/solver.js +166 -0
- package/lib/benchmark/tools.js +62 -0
- package/lib/bot/bot.js +238 -0
- package/lib/brand.js +105 -0
- package/lib/commands.js +100 -0
- package/lib/complexity.js +377 -0
- package/lib/config.js +213 -0
- package/lib/gateway/client.js +309 -0
- package/lib/gateway/commands.js +991 -0
- package/lib/gateway/config-validate.js +109 -0
- package/lib/gateway/gateway.js +367 -0
- package/lib/gateway/index.js +62 -0
- package/lib/gateway/mode.js +309 -0
- package/lib/gateway/plugins.js +222 -0
- package/lib/gateway/prompt.js +214 -0
- package/lib/mcp/server.js +262 -0
- package/lib/memory/compressor.js +425 -0
- package/lib/memory/engine.js +763 -0
- package/lib/memory/evolution.js +668 -0
- package/lib/memory/index.js +58 -0
- package/lib/memory/orchestrator.js +506 -0
- package/lib/memory/retriever.js +515 -0
- package/lib/memory/synthesizer.js +333 -0
- package/lib/pipeline/async-scanner.js +510 -0
- package/lib/pipeline/binary-analysis.js +1043 -0
- package/lib/pipeline/dom-xss-scanner.js +435 -0
- package/lib/pipeline/github-actions.js +792 -0
- package/lib/pipeline/index.js +124 -0
- package/lib/pipeline/osint.js +498 -0
- package/lib/pipeline/sarif.js +373 -0
- package/lib/pipeline/scanner.js +880 -0
- package/lib/pipeline/template-manager.js +525 -0
- package/lib/pipeline/xss-scanner.js +353 -0
- package/lib/setup-wizard.js +288 -0
- package/package.json +31 -0
|
@@ -0,0 +1,515 @@
|
|
|
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 Memory — Stage 3: Intent-Aware Retrieval Planning
|
|
7
|
+
*
|
|
8
|
+
* Intelligent retrieval that understands query intent and adapts strategy:
|
|
9
|
+
* - Query complexity analysis → determines retrieval depth
|
|
10
|
+
* - Multi-query decomposition → sub-queries for complex questions
|
|
11
|
+
* - Hybrid search fusion → lexical + symbolic via RRF
|
|
12
|
+
* - Reflection-based refinement → iterative retrieval for complex queries
|
|
13
|
+
*
|
|
14
|
+
* Ported from Python memory/core/retriever.py.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { extractSecurityEntities } from './compressor.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Query Intent Classification
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Security query intent categories with retrieval strategy mapping.
|
|
25
|
+
*/
|
|
26
|
+
const QueryIntent = {
|
|
27
|
+
// Intent → [description, searchDepth, requiresSymbolic]
|
|
28
|
+
INTENTS: {
|
|
29
|
+
finding_lookup: ['Looking up specific vulnerability or finding', 'shallow', true],
|
|
30
|
+
ioc_search: ['Searching for indicators of compromise', 'shallow', true],
|
|
31
|
+
ttp_mapping: ['Mapping techniques, tactics, procedures', 'medium', true],
|
|
32
|
+
engagement_context: ['Getting context for an engagement', 'deep', true],
|
|
33
|
+
temporal_query: ['Time-based query about when something happened', 'medium', true],
|
|
34
|
+
comparative: ['Comparing findings, hosts, or engagements', 'deep', false],
|
|
35
|
+
causal: ['Understanding why or how something happened', 'deep', false],
|
|
36
|
+
general: ['General security knowledge query', 'shallow', false],
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Keyword patterns for intent detection
|
|
40
|
+
PATTERNS: {
|
|
41
|
+
finding_lookup: [
|
|
42
|
+
'vulnerability', 'vuln', 'finding', 'cve-', 'exploit', 'weakness',
|
|
43
|
+
'injection', 'xss', 'sqli', 'rce', 'ssrf', 'csrf',
|
|
44
|
+
],
|
|
45
|
+
ioc_search: [
|
|
46
|
+
'ioc', 'indicator', 'hash', 'ip address', 'domain', 'c2', 'beacon',
|
|
47
|
+
'callback', 'malware', 'sample',
|
|
48
|
+
],
|
|
49
|
+
ttp_mapping: [
|
|
50
|
+
'technique', 'tactic', 'procedure', 'mitre', 'att&ck', 't1',
|
|
51
|
+
'lateral', 'persistence', 'privilege',
|
|
52
|
+
],
|
|
53
|
+
engagement_context: [
|
|
54
|
+
'engagement', 'pentest', 'assessment', 'audit', 'scope', 'client',
|
|
55
|
+
'target', 'progress', 'status',
|
|
56
|
+
],
|
|
57
|
+
temporal_query: [
|
|
58
|
+
'when', 'timeline', 'first', 'last', 'before', 'after', 'during',
|
|
59
|
+
'recently', 'latest', 'oldest',
|
|
60
|
+
],
|
|
61
|
+
comparative: [
|
|
62
|
+
'compare', 'difference', 'between', 'versus', 'vs', 'similar',
|
|
63
|
+
'different', 'common',
|
|
64
|
+
],
|
|
65
|
+
causal: [
|
|
66
|
+
'why', 'how', 'because', 'cause', 'root cause', 'reason', 'led to',
|
|
67
|
+
'resulted in', 'impact',
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// RetrievalPlan
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Structured retrieval plan for a query.
|
|
78
|
+
*/
|
|
79
|
+
class RetrievalPlan {
|
|
80
|
+
constructor(opts = {}) {
|
|
81
|
+
this.intent = opts.intent ?? 'general';
|
|
82
|
+
this.complexity = opts.complexity ?? 0.3;
|
|
83
|
+
this.subQueries = opts.subQueries ?? [];
|
|
84
|
+
this.requiredFilters = opts.requiredFilters ?? {};
|
|
85
|
+
this.searchDepth = opts.searchDepth ?? 'shallow';
|
|
86
|
+
this.needsReflection = opts.needsReflection ?? false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// IntentClassifier
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Classify security query intent using keyword matching.
|
|
96
|
+
*/
|
|
97
|
+
class IntentClassifier {
|
|
98
|
+
/**
|
|
99
|
+
* Return the most likely intent for a query.
|
|
100
|
+
* @param {string} query
|
|
101
|
+
* @returns {string}
|
|
102
|
+
*/
|
|
103
|
+
classify(query) {
|
|
104
|
+
const queryLower = query.toLowerCase();
|
|
105
|
+
const scores = {};
|
|
106
|
+
|
|
107
|
+
for (const [intent, keywords] of Object.entries(QueryIntent.PATTERNS)) {
|
|
108
|
+
let score = 0;
|
|
109
|
+
for (const kw of keywords) {
|
|
110
|
+
if (queryLower.includes(kw)) score++;
|
|
111
|
+
}
|
|
112
|
+
if (score > 0) scores[intent] = score;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (Object.keys(scores).length === 0) return 'general';
|
|
116
|
+
|
|
117
|
+
// Return intent with highest score
|
|
118
|
+
let maxIntent = 'general';
|
|
119
|
+
let maxScore = 0;
|
|
120
|
+
for (const [intent, score] of Object.entries(scores)) {
|
|
121
|
+
if (score > maxScore) {
|
|
122
|
+
maxScore = score;
|
|
123
|
+
maxIntent = intent;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return maxIntent;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Estimate query complexity (0.0 = trivial, 1.0 = very complex).
|
|
131
|
+
* @param {string} query
|
|
132
|
+
* @returns {number}
|
|
133
|
+
*/
|
|
134
|
+
estimateComplexity(query) {
|
|
135
|
+
let score = 0.0;
|
|
136
|
+
|
|
137
|
+
// Length-based
|
|
138
|
+
const words = query.split(/\s+/);
|
|
139
|
+
if (words.length > 20) {
|
|
140
|
+
score += 0.2;
|
|
141
|
+
} else if (words.length > 10) {
|
|
142
|
+
score += 0.1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Multi-hop indicators
|
|
146
|
+
const multiHop = ['and', 'also', 'then', 'after that', 'in addition'];
|
|
147
|
+
const queryLower = query.toLowerCase();
|
|
148
|
+
for (const kw of multiHop) {
|
|
149
|
+
if (queryLower.includes(kw)) score += 0.1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Comparative/causal is inherently complex
|
|
153
|
+
if (['compare', 'why', 'how did'].some((w) => queryLower.includes(w))) {
|
|
154
|
+
score += 0.3;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Multiple entity references
|
|
158
|
+
const entities = extractSecurityEntities(query);
|
|
159
|
+
const entityCount = Object.values(entities).reduce((sum, arr) => sum + arr.length, 0);
|
|
160
|
+
if (entityCount > 3) {
|
|
161
|
+
score += 0.2;
|
|
162
|
+
} else if (entityCount > 1) {
|
|
163
|
+
score += 0.1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return Math.min(score, 1.0);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// QueryDecomposer
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Decompose complex queries into targeted sub-queries.
|
|
176
|
+
*/
|
|
177
|
+
class QueryDecomposer {
|
|
178
|
+
/**
|
|
179
|
+
* Break a complex query into focused sub-queries.
|
|
180
|
+
* @param {string} query
|
|
181
|
+
* @param {string} intent
|
|
182
|
+
* @returns {string[]}
|
|
183
|
+
*/
|
|
184
|
+
decompose(query, intent) {
|
|
185
|
+
let subQueries = [query];
|
|
186
|
+
|
|
187
|
+
// Split on conjunctions
|
|
188
|
+
if (/\s+and\s+/i.test(query)) {
|
|
189
|
+
const parts = query.split(/\s+and\s+/i);
|
|
190
|
+
if (parts.length > 1 && parts.every((p) => p.split(/\s+/).length > 3)) {
|
|
191
|
+
subQueries = parts;
|
|
192
|
+
}
|
|
193
|
+
} else if (intent === 'comparative') {
|
|
194
|
+
// For comparative queries, search each entity separately
|
|
195
|
+
const entities = extractSecurityEntities(query);
|
|
196
|
+
const targets = [
|
|
197
|
+
...entities.ips,
|
|
198
|
+
...entities.domains,
|
|
199
|
+
...entities.cves,
|
|
200
|
+
];
|
|
201
|
+
if (targets.length >= 2) {
|
|
202
|
+
subQueries = targets.slice(0, 3).map((t) => `${query} ${t}`);
|
|
203
|
+
}
|
|
204
|
+
} else if (intent === 'causal') {
|
|
205
|
+
// For causal queries, search for both the event and the cause
|
|
206
|
+
subQueries = [query];
|
|
207
|
+
const stripped = query
|
|
208
|
+
.replace(/\b(why|how|because|cause)\b/gi, '')
|
|
209
|
+
.trim();
|
|
210
|
+
if (stripped !== query && stripped.length > 10) {
|
|
211
|
+
subQueries.push(stripped);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return subQueries.slice(0, 4); // Max 4 sub-queries
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// AdaptiveRetriever
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
/** Depth → top_k mapping */
|
|
224
|
+
const DEPTH_MAP = { shallow: 5, medium: 15, deep: 30 };
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Stage 3: Intent-Aware Adaptive Retrieval.
|
|
228
|
+
*
|
|
229
|
+
* Combines intent classification, query decomposition, and hybrid search
|
|
230
|
+
* with optional reflection-based iterative refinement.
|
|
231
|
+
*/
|
|
232
|
+
class AdaptiveRetriever {
|
|
233
|
+
/**
|
|
234
|
+
* @param {import('./engine.js').CipherMemory} memory
|
|
235
|
+
* @param {{ enableReflection?: boolean, maxReflectionRounds?: number }} opts
|
|
236
|
+
*/
|
|
237
|
+
constructor(memory, opts = {}) {
|
|
238
|
+
this.memory = memory;
|
|
239
|
+
this.classifier = new IntentClassifier();
|
|
240
|
+
this.decomposer = new QueryDecomposer();
|
|
241
|
+
this.enableReflection = opts.enableReflection ?? true;
|
|
242
|
+
this.maxReflectionRounds = opts.maxReflectionRounds ?? 2;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create a retrieval plan for the query.
|
|
247
|
+
* @param {string} query
|
|
248
|
+
* @returns {RetrievalPlan}
|
|
249
|
+
*/
|
|
250
|
+
plan(query) {
|
|
251
|
+
const intent = this.classifier.classify(query);
|
|
252
|
+
const complexity = this.classifier.estimate_complexity
|
|
253
|
+
? this.classifier.estimateComplexity(query)
|
|
254
|
+
: this.classifier.estimateComplexity(query);
|
|
255
|
+
|
|
256
|
+
const intentInfo = QueryIntent.INTENTS[intent] ?? ['', 'shallow', false];
|
|
257
|
+
const [, depth] = intentInfo;
|
|
258
|
+
|
|
259
|
+
const subQueries =
|
|
260
|
+
complexity > 0.5
|
|
261
|
+
? this.decomposer.decompose(query, intent)
|
|
262
|
+
: [query];
|
|
263
|
+
|
|
264
|
+
// Extract filter hints from query
|
|
265
|
+
const filters = {};
|
|
266
|
+
const entities = extractSecurityEntities(query);
|
|
267
|
+
if (entities.cves.length > 0) filters.cve_id = entities.cves[0];
|
|
268
|
+
if (entities.mitre.length > 0) filters.mitre_technique = entities.mitre[0];
|
|
269
|
+
|
|
270
|
+
// Engagement ID extraction (e.g., "engagement ENG-001")
|
|
271
|
+
const engMatch = query.match(/engagement\s+([A-Za-z0-9-]+)/i);
|
|
272
|
+
if (engMatch) filters.engagement_id = engMatch[1];
|
|
273
|
+
|
|
274
|
+
return new RetrievalPlan({
|
|
275
|
+
intent,
|
|
276
|
+
complexity,
|
|
277
|
+
subQueries,
|
|
278
|
+
requiredFilters: filters,
|
|
279
|
+
searchDepth: depth,
|
|
280
|
+
needsReflection: complexity > 0.6 && this.enableReflection,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Retrieve relevant memories with intent-aware planning.
|
|
286
|
+
* @param {string} query
|
|
287
|
+
* @param {string} engagementId
|
|
288
|
+
* @param {number} [limit]
|
|
289
|
+
* @returns {import('./engine.js').MemoryEntry[]}
|
|
290
|
+
*/
|
|
291
|
+
retrieve(query, engagementId = '', limit = null) {
|
|
292
|
+
const plan = this.plan(query);
|
|
293
|
+
|
|
294
|
+
if (engagementId) {
|
|
295
|
+
plan.requiredFilters.engagement_id = engagementId;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const topK = limit ?? DEPTH_MAP[plan.searchDepth] ?? 10;
|
|
299
|
+
|
|
300
|
+
// Execute sub-queries and merge results
|
|
301
|
+
const allResults = [];
|
|
302
|
+
const seenIds = new Set();
|
|
303
|
+
|
|
304
|
+
for (const subQuery of plan.subQueries) {
|
|
305
|
+
const results = this.memory.search(subQuery, {
|
|
306
|
+
engagementId: plan.requiredFilters.engagement_id ?? '',
|
|
307
|
+
memoryType: plan.requiredFilters.memory_type ?? '',
|
|
308
|
+
severity: plan.requiredFilters.severity ?? '',
|
|
309
|
+
mitreTechnique: plan.requiredFilters.mitre_technique ?? '',
|
|
310
|
+
}, topK);
|
|
311
|
+
|
|
312
|
+
for (const entry of results) {
|
|
313
|
+
if (!seenIds.has(entry.entryId)) {
|
|
314
|
+
seenIds.add(entry.entryId);
|
|
315
|
+
allResults.push(entry);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Reflection: check if we have enough relevant results
|
|
321
|
+
if (plan.needsReflection && allResults.length < 3) {
|
|
322
|
+
return this._reflectAndRetry(query, plan, allResults, seenIds, topK);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return allResults.slice(0, topK);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Iterative refinement when initial results are insufficient.
|
|
330
|
+
* @private
|
|
331
|
+
*/
|
|
332
|
+
_reflectAndRetry(query, plan, currentResults, seenIds, topK) {
|
|
333
|
+
for (let round = 0; round < this.maxReflectionRounds; round++) {
|
|
334
|
+
const altQueries = this._generateAlternativeQueries(query, currentResults);
|
|
335
|
+
|
|
336
|
+
for (const altQuery of altQueries) {
|
|
337
|
+
const results = this.memory.search(altQuery, {
|
|
338
|
+
engagementId: plan.requiredFilters.engagement_id ?? '',
|
|
339
|
+
}, topK);
|
|
340
|
+
|
|
341
|
+
for (const entry of results) {
|
|
342
|
+
if (!seenIds.has(entry.entryId)) {
|
|
343
|
+
seenIds.add(entry.entryId);
|
|
344
|
+
currentResults.push(entry);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (currentResults.length >= 3) break;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return currentResults;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Generate alternative search queries based on current results.
|
|
357
|
+
* @private
|
|
358
|
+
*/
|
|
359
|
+
_generateAlternativeQueries(originalQuery, currentResults) {
|
|
360
|
+
const alternatives = [];
|
|
361
|
+
|
|
362
|
+
// Try broader query (remove specifics)
|
|
363
|
+
let broader = originalQuery.replace(/CVE-\d+-\d+/g, '').trim();
|
|
364
|
+
broader = broader.replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '').trim();
|
|
365
|
+
if (broader && broader !== originalQuery) {
|
|
366
|
+
alternatives.push(broader);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Try extracting key terms
|
|
370
|
+
const words = originalQuery.split(/\s+/);
|
|
371
|
+
const skipWords = new Set([
|
|
372
|
+
'what', 'where', 'when', 'which', 'about', 'find', 'search', 'look',
|
|
373
|
+
'show', 'there',
|
|
374
|
+
]);
|
|
375
|
+
const contentWords = words.filter(
|
|
376
|
+
(w) => w.length > 4 && !skipWords.has(w.toLowerCase()),
|
|
377
|
+
);
|
|
378
|
+
if (contentWords.length > 0) {
|
|
379
|
+
alternatives.push(contentWords.slice(0, 5).join(' '));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Try using tags from current results
|
|
383
|
+
if (currentResults.length > 0) {
|
|
384
|
+
for (const entry of currentResults.slice(0, 2)) {
|
|
385
|
+
if (entry.tags && entry.tags.length > 0) {
|
|
386
|
+
alternatives.push(entry.tags.slice(0, 3).join(' '));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return alternatives.slice(0, 3);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// ContextBuilder — Token-budgeted context assembly
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Build token-budgeted context from retrieved memories.
|
|
401
|
+
*
|
|
402
|
+
* Assembles memories into a structured context block suitable for
|
|
403
|
+
* injection into LLM prompts, respecting token budget constraints.
|
|
404
|
+
*/
|
|
405
|
+
class ContextBuilder {
|
|
406
|
+
/**
|
|
407
|
+
* @param {{ maxTokens?: number, charsPerToken?: number }} opts
|
|
408
|
+
*/
|
|
409
|
+
constructor(opts = {}) {
|
|
410
|
+
this.maxTokens = opts.maxTokens ?? 4000;
|
|
411
|
+
this.charsPerToken = opts.charsPerToken ?? 4.0;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Build structured context from memory entries.
|
|
416
|
+
* Returns a formatted string suitable for system prompt injection.
|
|
417
|
+
* @param {import('./engine.js').MemoryEntry[]} entries
|
|
418
|
+
* @param {string} query
|
|
419
|
+
* @returns {string}
|
|
420
|
+
*/
|
|
421
|
+
build(entries, query = '') {
|
|
422
|
+
if (!entries || entries.length === 0) return '';
|
|
423
|
+
|
|
424
|
+
const maxChars = Math.floor(this.maxTokens * this.charsPerToken);
|
|
425
|
+
const sections = {
|
|
426
|
+
findings: [],
|
|
427
|
+
iocs: [],
|
|
428
|
+
ttps: [],
|
|
429
|
+
decisions: [],
|
|
430
|
+
context: [],
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
for (const entry of entries) {
|
|
434
|
+
let line = `- ${entry.content}`;
|
|
435
|
+
if (entry.severity) {
|
|
436
|
+
line = `- [${entry.severity.toUpperCase()}] ${entry.content}`;
|
|
437
|
+
}
|
|
438
|
+
if (entry.mitreAttack && entry.mitreAttack.length > 0) {
|
|
439
|
+
const attacks = entry.mitreAttack.slice(0, 3).join(', ');
|
|
440
|
+
line += ` (${attacks})`;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Route to section based on memoryType
|
|
444
|
+
// memoryType is a string value from MemoryType enum
|
|
445
|
+
const memType = typeof entry.memoryType === 'object' && entry.memoryType.value
|
|
446
|
+
? entry.memoryType.value
|
|
447
|
+
: entry.memoryType;
|
|
448
|
+
|
|
449
|
+
if (memType === 'finding') {
|
|
450
|
+
sections.findings.push(line);
|
|
451
|
+
} else if (memType === 'ioc') {
|
|
452
|
+
sections.iocs.push(line);
|
|
453
|
+
} else if (memType === 'ttp') {
|
|
454
|
+
sections.ttps.push(line);
|
|
455
|
+
} else if (memType === 'decision') {
|
|
456
|
+
sections.decisions.push(line);
|
|
457
|
+
} else {
|
|
458
|
+
sections.context.push(line);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Assemble within budget
|
|
463
|
+
const parts = ['## Engagement Memory'];
|
|
464
|
+
let currentChars = parts[0].length;
|
|
465
|
+
|
|
466
|
+
const sectionLabels = {
|
|
467
|
+
findings: '### Findings',
|
|
468
|
+
iocs: '### Indicators of Compromise',
|
|
469
|
+
ttps: '### Techniques & Procedures',
|
|
470
|
+
decisions: '### Decisions',
|
|
471
|
+
context: '### Context',
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
for (const [key, label] of Object.entries(sectionLabels)) {
|
|
475
|
+
const items = sections[key];
|
|
476
|
+
if (items.length === 0) continue;
|
|
477
|
+
|
|
478
|
+
const sectionText = `\n${label}\n${items.join('\n')}`;
|
|
479
|
+
if (currentChars + sectionText.length > maxChars) {
|
|
480
|
+
// Truncate items to fit
|
|
481
|
+
const remaining = maxChars - currentChars - (`\n${label}\n`).length;
|
|
482
|
+
if (remaining > 100) {
|
|
483
|
+
const truncated = [];
|
|
484
|
+
let charsUsed = 0;
|
|
485
|
+
for (const item of items) {
|
|
486
|
+
if (charsUsed + item.length + 1 > remaining) break;
|
|
487
|
+
truncated.push(item);
|
|
488
|
+
charsUsed += item.length + 1;
|
|
489
|
+
}
|
|
490
|
+
if (truncated.length > 0) {
|
|
491
|
+
parts.push(`\n${label}`);
|
|
492
|
+
parts.push(...truncated);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
parts.push(`\n${label}`);
|
|
499
|
+
parts.push(...items);
|
|
500
|
+
currentChars += sectionText.length;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return parts.join('\n');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export {
|
|
508
|
+
QueryIntent,
|
|
509
|
+
RetrievalPlan,
|
|
510
|
+
IntentClassifier,
|
|
511
|
+
QueryDecomposer,
|
|
512
|
+
AdaptiveRetriever,
|
|
513
|
+
ContextBuilder,
|
|
514
|
+
DEPTH_MAP,
|
|
515
|
+
};
|