cipher-security 5.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 +465 -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 +130 -0
- package/lib/commands.js +99 -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 +830 -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 +229 -0
- package/package.json +30 -0
package/lib/bot/bot.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Copyright (c) 2026 defconxt. All rights reserved.
|
|
2
|
+
// Licensed under AGPL-3.0 — see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CIPHER Signal Bot — format utilities and session manager.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Prefix mapping
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const PREFIX_RE = /^(RED|BLUE|PURPLE|PRIVACY|RECON|INCIDENT|ARCHITECT):\s*/i;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Map 'MODE: query' short-prefix format to '[MODE: MODE] query'.
|
|
16
|
+
* @param {string} text
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function mapPrefix(text) {
|
|
20
|
+
const m = text.match(PREFIX_RE);
|
|
21
|
+
if (m) {
|
|
22
|
+
const mode = m[1].toUpperCase();
|
|
23
|
+
const rest = text.slice(m[0].length);
|
|
24
|
+
return `[MODE: ${mode}] ${rest}`;
|
|
25
|
+
}
|
|
26
|
+
return text;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Markdown stripping
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const MD_PATTERNS = [
|
|
34
|
+
[/```[^\n]*\n([\s\S]*?)```/g, '$1'], // Fenced code blocks
|
|
35
|
+
[/^#{1,6}\s+/gm, ''], // Headers
|
|
36
|
+
[/\*{2}([^*\n]+)\*{2}/g, '$1'], // Bold
|
|
37
|
+
[/\*([^*\n]+)\*/g, '$1'], // Italic
|
|
38
|
+
[/`([^`\n]+)`/g, '$1'], // Inline code
|
|
39
|
+
[/\[([^\]]+)\]\([^)]+\)/g, '$1'], // Links
|
|
40
|
+
[/^(\s*)[*-]\s+/gm, '$1• '], // Bullets
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Strip markdown formatting for plaintext Signal delivery.
|
|
45
|
+
* @param {string} text
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function stripMarkdown(text) {
|
|
49
|
+
for (const [pattern, replacement] of MD_PATTERNS) {
|
|
50
|
+
text = text.replace(pattern, replacement);
|
|
51
|
+
}
|
|
52
|
+
return text.trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Session manager
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* In-memory conversation history manager keyed by sender.
|
|
61
|
+
*/
|
|
62
|
+
export class SessionManager {
|
|
63
|
+
/**
|
|
64
|
+
* @param {object} [opts]
|
|
65
|
+
* @param {number} [opts.timeoutSeconds=3600]
|
|
66
|
+
* @param {number} [opts.maxPairs=20]
|
|
67
|
+
*/
|
|
68
|
+
constructor({ timeoutSeconds = 3600, maxPairs = 20 } = {}) {
|
|
69
|
+
this._sessions = new Map();
|
|
70
|
+
this._timeout = timeoutSeconds * 1000;
|
|
71
|
+
this._maxPairs = maxPairs;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
get(sender) {
|
|
75
|
+
this.cleanup();
|
|
76
|
+
const session = this._sessions.get(sender);
|
|
77
|
+
return session ? [...session.history] : [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
update(sender, userMessage, assistantMessage) {
|
|
81
|
+
if (!this._sessions.has(sender)) {
|
|
82
|
+
this._sessions.set(sender, { history: [], lastActive: Date.now() });
|
|
83
|
+
}
|
|
84
|
+
const session = this._sessions.get(sender);
|
|
85
|
+
session.history.push({ role: 'user', content: userMessage });
|
|
86
|
+
session.history.push({ role: 'assistant', content: assistantMessage });
|
|
87
|
+
session.lastActive = Date.now();
|
|
88
|
+
|
|
89
|
+
const maxItems = this._maxPairs * 2;
|
|
90
|
+
if (session.history.length > maxItems) {
|
|
91
|
+
session.history = session.history.slice(-maxItems);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
reset(sender) {
|
|
96
|
+
this._sessions.delete(sender);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
cleanup() {
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
for (const [sender, session] of this._sessions) {
|
|
102
|
+
if (now - session.lastActive > this._timeout) {
|
|
103
|
+
this._sessions.delete(sender);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get size() { return this._sessions.size; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Bot runner (placeholder — requires signal-cli subprocess)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Start the CIPHER Signal bot.
|
|
117
|
+
*
|
|
118
|
+
* In the full implementation, this would:
|
|
119
|
+
* 1. Load config from config.yaml
|
|
120
|
+
* 2. Start signal-cli subprocess for receiving messages
|
|
121
|
+
* 3. Register CipherCommand handler
|
|
122
|
+
* 4. Run with exponential-backoff watchdog
|
|
123
|
+
*
|
|
124
|
+
* @param {object} config
|
|
125
|
+
*/
|
|
126
|
+
export function runBot(config) {
|
|
127
|
+
console.log(`CIPHER Bot starting (service: ${config.signalService}, phone: ${config.phoneNumber})`);
|
|
128
|
+
console.log('Bot requires signal-cli — see docs for setup.');
|
|
129
|
+
// The actual bot would use signal-cli subprocess here
|
|
130
|
+
}
|
package/lib/commands.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
* commands.js — Command routing table for the CIPHER CLI.
|
|
7
|
+
*
|
|
8
|
+
* Maps all 29 CLI commands to one of three dispatch modes:
|
|
9
|
+
* - native: Dispatched directly through Node.js handler functions (no Python)
|
|
10
|
+
* - passthrough: Spawned directly as `python -m gateway.app <cmd>` with stdio: 'inherit'
|
|
11
|
+
* (full terminal access for Rich panels, Textual TUIs, long-running services)
|
|
12
|
+
* - bridge: (Legacy) Dispatched via JSON-RPC through python-bridge.js — no commands
|
|
13
|
+
* use this mode as of M007/S03, retained for backward compatibility
|
|
14
|
+
*
|
|
15
|
+
* @module commands
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Dispatch mode for each CLI command.
|
|
20
|
+
*
|
|
21
|
+
* @type {Record<string, { mode: 'native' | 'bridge' | 'passthrough', description: string }>}
|
|
22
|
+
*/
|
|
23
|
+
export const COMMAND_MODES = {
|
|
24
|
+
// ── Native commands (Node.js handler dispatch, no Python) ──────────────
|
|
25
|
+
query: { mode: 'native', description: 'Run a security query (smart routing + streaming)' },
|
|
26
|
+
ingest: { mode: 'native', description: 'Ingest security data' },
|
|
27
|
+
status: { mode: 'native', description: 'Show system status' },
|
|
28
|
+
doctor: { mode: 'native', description: 'Diagnose installation health' },
|
|
29
|
+
version: { mode: 'native', description: 'Print version information' },
|
|
30
|
+
plugin: { mode: 'native', description: 'Manage plugins' },
|
|
31
|
+
search: { mode: 'native', description: 'Search security data' },
|
|
32
|
+
store: { mode: 'native', description: 'Manage data stores' },
|
|
33
|
+
diff: { mode: 'native', description: 'Compare security states' },
|
|
34
|
+
workflow: { mode: 'native', description: 'Manage workflows' },
|
|
35
|
+
stats: { mode: 'native', description: 'Show statistics' },
|
|
36
|
+
domains: { mode: 'native', description: 'Manage domains' },
|
|
37
|
+
skills: { mode: 'native', description: 'Manage skills' },
|
|
38
|
+
score: { mode: 'native', description: 'Show security score' },
|
|
39
|
+
marketplace: { mode: 'native', description: 'Browse marketplace' },
|
|
40
|
+
compliance: { mode: 'native', description: 'Run compliance checks' },
|
|
41
|
+
leaderboard: { mode: 'native', description: 'Show leaderboard' },
|
|
42
|
+
feedback: { mode: 'native', description: 'Submit feedback' },
|
|
43
|
+
'memory-export': { mode: 'native', description: 'Export memory' },
|
|
44
|
+
'memory-import': { mode: 'native', description: 'Import memory' },
|
|
45
|
+
sarif: { mode: 'native', description: 'SARIF report tools' },
|
|
46
|
+
osint: { mode: 'native', description: 'OSINT intelligence tools' },
|
|
47
|
+
|
|
48
|
+
// ── Passthrough commands (direct Python spawn, full terminal) ──────────
|
|
49
|
+
// These were Python-dependent — now ported to Node.js.
|
|
50
|
+
scan: { mode: 'native', description: 'Run a security scan' },
|
|
51
|
+
dashboard: { mode: 'native', description: 'System dashboard' },
|
|
52
|
+
web: { mode: 'native', description: 'Web interface (use `cipher api` instead)' },
|
|
53
|
+
bot: { mode: 'native', description: 'Manage bot integrations (long-running service)' },
|
|
54
|
+
mcp: { mode: 'native', description: 'MCP server tools (long-running service)' },
|
|
55
|
+
api: { mode: 'native', description: 'API management (long-running server)' },
|
|
56
|
+
'setup-signal': { mode: 'native', description: 'Configure Signal integration' },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set of command names dispatched via passthrough (direct Python spawn).
|
|
61
|
+
* @type {Set<string>}
|
|
62
|
+
*/
|
|
63
|
+
export const PASSTHROUGH_COMMANDS = new Set(
|
|
64
|
+
Object.entries(COMMAND_MODES)
|
|
65
|
+
.filter(([, v]) => v.mode === 'passthrough')
|
|
66
|
+
.map(([k]) => k)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set of command names dispatched via Node.js native handlers.
|
|
71
|
+
* @type {Set<string>}
|
|
72
|
+
*/
|
|
73
|
+
export const NATIVE_COMMANDS = new Set(
|
|
74
|
+
Object.entries(COMMAND_MODES)
|
|
75
|
+
.filter(([, v]) => v.mode === 'native')
|
|
76
|
+
.map(([k]) => k)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Set of command names dispatched via the JSON-RPC bridge.
|
|
81
|
+
* As of M007/S03, no commands use bridge mode — retained for backward compatibility.
|
|
82
|
+
* @type {Set<string>}
|
|
83
|
+
*/
|
|
84
|
+
export const BRIDGE_COMMANDS = new Set(
|
|
85
|
+
Object.entries(COMMAND_MODES)
|
|
86
|
+
.filter(([, v]) => v.mode === 'bridge')
|
|
87
|
+
.map(([k]) => k)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Look up the dispatch mode for a command.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} name — command name (e.g., 'scan', 'status')
|
|
94
|
+
* @returns {'native' | 'bridge' | 'passthrough' | null} — mode string, or null if unknown
|
|
95
|
+
*/
|
|
96
|
+
export function getCommandMode(name) {
|
|
97
|
+
const entry = COMMAND_MODES[name];
|
|
98
|
+
return entry ? entry.mode : null;
|
|
99
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
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
|
+
* Smart routing complexity classifier for CIPHER CLI.
|
|
7
|
+
*
|
|
8
|
+
* Evaluates multiple heuristic signals to determine query complexity and
|
|
9
|
+
* route to the appropriate backend (Ollama for simple/moderate, Claude for
|
|
10
|
+
* complex/expert).
|
|
11
|
+
*
|
|
12
|
+
* Ported from src/gateway/complexity.py — all keyword dictionaries, regex
|
|
13
|
+
* patterns, score thresholds, and scoring logic are identical.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* import { classify, routeBackend, classifyAndRoute, COMPLEXITY } from './complexity.js';
|
|
17
|
+
*
|
|
18
|
+
* const level = classify("design a zero trust architecture for AWS");
|
|
19
|
+
* const backend = routeBackend(level); // "claude"
|
|
20
|
+
*
|
|
21
|
+
* const [lvl, be] = classifyAndRoute("what port does SSH use");
|
|
22
|
+
* // lvl === 1 (SIMPLE), be === "ollama"
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Complexity enum
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
/** Query complexity levels, ordered by increasing difficulty. */
|
|
30
|
+
export const COMPLEXITY = Object.freeze({
|
|
31
|
+
SIMPLE: 1, // Factual lookups, single-concept questions → Ollama
|
|
32
|
+
MODERATE: 2, // Multi-part but bounded questions → Ollama
|
|
33
|
+
COMPLEX: 3, // Deep reasoning, threat models, architecture → Claude
|
|
34
|
+
EXPERT: 4, // Cross-domain, multi-framework, advanced analysis → Claude
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const _NAMES = Object.freeze({
|
|
38
|
+
[COMPLEXITY.SIMPLE]: 'SIMPLE',
|
|
39
|
+
[COMPLEXITY.MODERATE]: 'MODERATE',
|
|
40
|
+
[COMPLEXITY.COMPLEX]: 'COMPLEX',
|
|
41
|
+
[COMPLEXITY.EXPERT]: 'EXPERT',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Reverse lookup: complexity level integer → human-readable name.
|
|
46
|
+
* @param {number} level
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function nameOf(level) {
|
|
50
|
+
return _NAMES[level] ?? 'UNKNOWN';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Tunable weights — adjust these to change routing sensitivity
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/** Points thresholds for each complexity level. */
|
|
58
|
+
export const THRESHOLDS = Object.freeze({
|
|
59
|
+
moderate: 3, // >= 3 points → MODERATE
|
|
60
|
+
complex: 6, // >= 6 points → COMPLEX
|
|
61
|
+
expert: 10, // >= 10 points → EXPERT
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** Message length breakpoints (characters) and their point values. */
|
|
65
|
+
export const LENGTH_SCORES = Object.freeze([
|
|
66
|
+
[500, 4], // Very long queries are likely complex
|
|
67
|
+
[300, 3], // Long queries
|
|
68
|
+
[150, 2], // Medium queries
|
|
69
|
+
[80, 1], // Short but not trivial
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Keyword sets — each match adds the specified points
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/** Simple indicators (negative points — pull toward Ollama). */
|
|
77
|
+
export const SIMPLE_KEYWORDS = Object.freeze({
|
|
78
|
+
'what is': -2,
|
|
79
|
+
'what are': -1,
|
|
80
|
+
'what does': -2,
|
|
81
|
+
'what port': -3,
|
|
82
|
+
'which port': -3,
|
|
83
|
+
'default port': -3,
|
|
84
|
+
'how to install': -2,
|
|
85
|
+
'how to start': -2,
|
|
86
|
+
'how to run': -2,
|
|
87
|
+
'syntax for': -2,
|
|
88
|
+
'command for': -2,
|
|
89
|
+
'define ': -2,
|
|
90
|
+
'definition of': -2,
|
|
91
|
+
'meaning of': -2,
|
|
92
|
+
'example of': -1,
|
|
93
|
+
'list of': -1,
|
|
94
|
+
'difference between': -1,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/** Domain complexity indicators (positive points — push toward Claude). */
|
|
98
|
+
export const COMPLEX_KEYWORDS = Object.freeze({
|
|
99
|
+
// Architecture & design
|
|
100
|
+
'threat model': 4,
|
|
101
|
+
'architecture': 3,
|
|
102
|
+
'design': 2,
|
|
103
|
+
'zero trust': 3,
|
|
104
|
+
'blast radius': 3,
|
|
105
|
+
'compensating control': 3,
|
|
106
|
+
'defense in depth': 2,
|
|
107
|
+
'security architecture': 4,
|
|
108
|
+
'data flow diagram': 3,
|
|
109
|
+
'trust boundary': 3,
|
|
110
|
+
// Risk & compliance
|
|
111
|
+
'dpia': 4,
|
|
112
|
+
'risk assessment': 4,
|
|
113
|
+
'risk analysis': 3,
|
|
114
|
+
'compliance mapping': 4,
|
|
115
|
+
'gap analysis': 3,
|
|
116
|
+
'maturity assessment': 3,
|
|
117
|
+
'audit finding': 3,
|
|
118
|
+
'control mapping': 3,
|
|
119
|
+
// Analysis & reasoning
|
|
120
|
+
'analyze': 2,
|
|
121
|
+
'analyse': 2,
|
|
122
|
+
'evaluate': 2,
|
|
123
|
+
'compare and contrast': 3,
|
|
124
|
+
'trade-off': 2,
|
|
125
|
+
'tradeoff': 2,
|
|
126
|
+
'pros and cons': 2,
|
|
127
|
+
'impact analysis': 3,
|
|
128
|
+
'root cause': 3,
|
|
129
|
+
// Offensive / red team
|
|
130
|
+
'attack chain': 4,
|
|
131
|
+
'attack path': 3,
|
|
132
|
+
'kill chain': 3,
|
|
133
|
+
'exploitation chain': 4,
|
|
134
|
+
'lateral movement': 2,
|
|
135
|
+
'privilege escalation': 2,
|
|
136
|
+
'privesc': 2,
|
|
137
|
+
'post-exploitation': 3,
|
|
138
|
+
'evasion technique': 3,
|
|
139
|
+
'bypass': 1,
|
|
140
|
+
// Incident response
|
|
141
|
+
'incident analysis': 4,
|
|
142
|
+
'incident': 2,
|
|
143
|
+
'forensic analysis': 4,
|
|
144
|
+
'forensic': 2,
|
|
145
|
+
'timeline reconstruction': 4,
|
|
146
|
+
'indicator of compromise': 2,
|
|
147
|
+
'ioc': 1,
|
|
148
|
+
'beacon': 2,
|
|
149
|
+
'cobalt strike': 3,
|
|
150
|
+
'mimikatz': 2,
|
|
151
|
+
'dcsync': 3,
|
|
152
|
+
'triage': 2,
|
|
153
|
+
'containment strategy': 3,
|
|
154
|
+
'eradication plan': 3,
|
|
155
|
+
'breach': 2,
|
|
156
|
+
'malware analysis': 3,
|
|
157
|
+
'reverse engineer': 3,
|
|
158
|
+
// Detection engineering
|
|
159
|
+
'detection logic': 3,
|
|
160
|
+
'sigma rule': 2,
|
|
161
|
+
'detection coverage': 3,
|
|
162
|
+
'false positive': 2,
|
|
163
|
+
'detection gap': 3,
|
|
164
|
+
'correlation rule': 3,
|
|
165
|
+
'hunting hypothesis': 3,
|
|
166
|
+
// Multi-step / planning
|
|
167
|
+
'strategy': 2,
|
|
168
|
+
'roadmap': 3,
|
|
169
|
+
'implementation plan': 3,
|
|
170
|
+
'step by step': 1,
|
|
171
|
+
'runbook': 2,
|
|
172
|
+
'playbook': 2,
|
|
173
|
+
'workflow': 1,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
/** Framework references — signal expert-level queries. */
|
|
177
|
+
export const FRAMEWORK_KEYWORDS = Object.freeze({
|
|
178
|
+
'nist': 2,
|
|
179
|
+
'nist 800-53': 3,
|
|
180
|
+
'nist 800-171': 3,
|
|
181
|
+
'nist csf': 3,
|
|
182
|
+
'mitre att&ck': 3,
|
|
183
|
+
'mitre attack': 3,
|
|
184
|
+
'stride': 3,
|
|
185
|
+
'pasta': 3,
|
|
186
|
+
'dread': 3,
|
|
187
|
+
'owasp': 2,
|
|
188
|
+
'owasp top 10': 2,
|
|
189
|
+
'cis controls': 2,
|
|
190
|
+
'cis benchmark': 2,
|
|
191
|
+
'iso 27001': 3,
|
|
192
|
+
'iso 27002': 3,
|
|
193
|
+
'soc 2': 2,
|
|
194
|
+
'pci dss': 3,
|
|
195
|
+
'hipaa': 2,
|
|
196
|
+
'gdpr': 2,
|
|
197
|
+
'ccpa': 2,
|
|
198
|
+
'fedramp': 3,
|
|
199
|
+
'cmmc': 3,
|
|
200
|
+
'cve-': 1,
|
|
201
|
+
'cwe-': 1,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Structural patterns — regex-based signals
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
/** Multi-part query indicators (each match adds points). */
|
|
209
|
+
export const STRUCTURE_PATTERNS = Object.freeze([
|
|
210
|
+
// Conjunctions joining distinct requests
|
|
211
|
+
[/\b(?:and also|and then|additionally|furthermore|moreover)\b/, 2],
|
|
212
|
+
// Numbered lists in the query
|
|
213
|
+
[/(?:^|\n)\s*\d+[.)]\s/, 2],
|
|
214
|
+
// Conditional logic
|
|
215
|
+
[/\bif\s+.+\bthen\b/, 2],
|
|
216
|
+
[/\bwhat if\b/, 2],
|
|
217
|
+
[/\bassuming\b/, 1],
|
|
218
|
+
[/\bgiven that\b/, 1],
|
|
219
|
+
// Comparison requests
|
|
220
|
+
[/\bvs\.?\b/, 1],
|
|
221
|
+
[/\bversus\b/, 1],
|
|
222
|
+
[/\bcompare\b/, 2],
|
|
223
|
+
// Scope amplifiers
|
|
224
|
+
[/\b(?:comprehensive|exhaustive|thorough|detailed|in-depth|end-to-end)\b/, 2],
|
|
225
|
+
[/\b(?:enterprise|organization-wide|company-wide|across all)\b/, 2],
|
|
226
|
+
// Multiple question marks (multiple questions)
|
|
227
|
+
[/\?.*\?/, 2],
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
/** Count of "and" conjunctions — many ands suggest multi-part queries. */
|
|
231
|
+
export const AND_CONJUNCTION_THRESHOLD = 3;
|
|
232
|
+
export const AND_CONJUNCTION_POINTS = 2;
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Scoring functions
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Score based on message length.
|
|
240
|
+
* @param {string} message
|
|
241
|
+
* @returns {number}
|
|
242
|
+
*/
|
|
243
|
+
export function scoreLength(message) {
|
|
244
|
+
const length = message.length;
|
|
245
|
+
for (const [threshold, points] of LENGTH_SCORES) {
|
|
246
|
+
if (length >= threshold) {
|
|
247
|
+
return points;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Score based on keyword presence. Returns sum of matched keyword points.
|
|
255
|
+
* @param {string} lower — lowercased message
|
|
256
|
+
* @param {Record<string, number>} keywordSet
|
|
257
|
+
* @returns {number}
|
|
258
|
+
*/
|
|
259
|
+
export function scoreKeywords(lower, keywordSet) {
|
|
260
|
+
let total = 0;
|
|
261
|
+
for (const [keyword, points] of Object.entries(keywordSet)) {
|
|
262
|
+
if (lower.includes(keyword)) {
|
|
263
|
+
total += points;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return total;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Score based on query structure patterns.
|
|
271
|
+
* @param {string} lower — lowercased message
|
|
272
|
+
* @returns {number}
|
|
273
|
+
*/
|
|
274
|
+
export function scoreStructure(lower) {
|
|
275
|
+
let total = 0;
|
|
276
|
+
for (const [pattern, points] of STRUCTURE_PATTERNS) {
|
|
277
|
+
if (pattern.test(lower)) {
|
|
278
|
+
total += points;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Count "and" conjunctions
|
|
283
|
+
const andMatches = lower.match(/\band\b/g);
|
|
284
|
+
const andCount = andMatches ? andMatches.length : 0;
|
|
285
|
+
if (andCount > AND_CONJUNCTION_THRESHOLD) {
|
|
286
|
+
total += AND_CONJUNCTION_POINTS;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return total;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Bonus points when multiple frameworks are referenced.
|
|
294
|
+
* @param {string} lower — lowercased message
|
|
295
|
+
* @returns {number}
|
|
296
|
+
*/
|
|
297
|
+
export function scoreFrameworkDensity(lower) {
|
|
298
|
+
let matches = 0;
|
|
299
|
+
let totalPoints = 0;
|
|
300
|
+
for (const [keyword, points] of Object.entries(FRAMEWORK_KEYWORDS)) {
|
|
301
|
+
if (lower.includes(keyword)) {
|
|
302
|
+
matches += 1;
|
|
303
|
+
totalPoints += points;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Bonus for cross-framework queries (referencing 3+ frameworks)
|
|
308
|
+
if (matches >= 3) {
|
|
309
|
+
totalPoints += 3;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return totalPoints;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Public API
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Classify a query's complexity based on multiple heuristic signals.
|
|
321
|
+
* @param {string} message — The user's query string.
|
|
322
|
+
* @returns {number} Complexity level (1=SIMPLE, 2=MODERATE, 3=COMPLEX, 4=EXPERT).
|
|
323
|
+
*/
|
|
324
|
+
export function classify(message) {
|
|
325
|
+
const lower = message.toLowerCase();
|
|
326
|
+
|
|
327
|
+
let score = 0;
|
|
328
|
+
score += scoreLength(message);
|
|
329
|
+
score += scoreKeywords(lower, SIMPLE_KEYWORDS);
|
|
330
|
+
score += scoreKeywords(lower, COMPLEX_KEYWORDS);
|
|
331
|
+
score += scoreStructure(lower);
|
|
332
|
+
score += scoreFrameworkDensity(lower);
|
|
333
|
+
|
|
334
|
+
// Floor at 0 — negative scores are still SIMPLE
|
|
335
|
+
score = Math.max(score, 0);
|
|
336
|
+
|
|
337
|
+
if (score >= THRESHOLDS.expert) {
|
|
338
|
+
return COMPLEXITY.EXPERT;
|
|
339
|
+
}
|
|
340
|
+
if (score >= THRESHOLDS.complex) {
|
|
341
|
+
return COMPLEXITY.COMPLEX;
|
|
342
|
+
}
|
|
343
|
+
if (score >= THRESHOLDS.moderate) {
|
|
344
|
+
return COMPLEXITY.MODERATE;
|
|
345
|
+
}
|
|
346
|
+
return COMPLEXITY.SIMPLE;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Map a complexity level to a backend name.
|
|
351
|
+
*
|
|
352
|
+
* COMPLEX and EXPERT → "claude", SIMPLE and MODERATE → "ollama".
|
|
353
|
+
* The "litellm" backend is user-configured, not auto-routed.
|
|
354
|
+
*
|
|
355
|
+
* @param {number} level — The classified complexity level.
|
|
356
|
+
* @returns {string} Backend string: "ollama" or "claude".
|
|
357
|
+
*/
|
|
358
|
+
export function routeBackend(level) {
|
|
359
|
+
if (level >= COMPLEXITY.COMPLEX) {
|
|
360
|
+
return 'claude';
|
|
361
|
+
}
|
|
362
|
+
return 'ollama';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Classify a message and return both the complexity level and backend.
|
|
367
|
+
*
|
|
368
|
+
* Convenience function combining classify() and routeBackend().
|
|
369
|
+
*
|
|
370
|
+
* @param {string} message — The user's query string.
|
|
371
|
+
* @returns {[number, string]} Tuple of [complexity level, backend name].
|
|
372
|
+
*/
|
|
373
|
+
export function classifyAndRoute(message) {
|
|
374
|
+
const level = classify(message);
|
|
375
|
+
const backend = routeBackend(level);
|
|
376
|
+
return [level, backend];
|
|
377
|
+
}
|