security-detections-mcp 3.1.0 → 3.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/README.md CHANGED
@@ -3,11 +3,135 @@
3
3
  An MCP (Model Context Protocol) server that lets LLMs query a unified database of **Sigma**, **Splunk ESCU**, **Elastic**, **KQL**, **Sublime**, and **CrowdStrike CQL** security detection rules.
4
4
 
5
5
  > **New here? Start with the [Setup Guide](./SETUP.md)** -- covers macOS, Windows (WSL & native), and Linux step by step.
6
+ >
7
+ > **Want it hosted? Skip the install entirely: [Hosted MCP Setup Guide](./docs/HOSTED_MCP.md)**
6
8
 
7
- ## What's New in 3.1 - New Detection Sources
9
+ ## Two Ways to Run It
10
+
11
+ **Local (full power)** — the npm package you're looking at. Runs on your machine, indexes your own detection repos, exposes all 81 tools. You need Node.js and ~10 minutes.
12
+
13
+ **Hosted (zero setup)** — a Streamable HTTP server at [`detect.michaelhaag.org/api/mcp/mcp`](https://detect.michaelhaag.org/mcp). Sign up, generate a token, paste one URL into your MCP client. ~25 read-only tools, always in sync with the latest content, 200 calls/day free. Read on for quick-install buttons.
14
+
15
+ ### Install — Local (Cursor)
16
+
17
+ [![Install Local MCP in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=security-detections&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInNlY3VyaXR5LWRldGVjdGlvbnMtbWNwIl0sImVudiI6eyJTSUdNQV9QQVRIUyI6Ii9wYXRoL3RvL3NpZ21hL3J1bGVzLC9wYXRoL3RvL3NpZ21hL3J1bGVzLXRocmVhdC1odW50aW5nIiwiU1BMVU5LX1BBVEhTIjoiL3BhdGgvdG8vc2VjdXJpdHlfY29udGVudC9kZXRlY3Rpb25zIiwiU1RPUllfUEFUSFMiOiIvcGF0aC90by9zZWN1cml0eV9jb250ZW50L3N0b3JpZXMiLCJFTEFTVElDX1BBVEhTIjoiL3BhdGgvdG8vZGV0ZWN0aW9uLXJ1bGVzL3J1bGVzIiwiS1FMX1BBVEhTIjoiL3BhdGgvdG8va3FsLXJ1bGVzIiwiU1VCTElNRV9QQVRIUyI6Ii9wYXRoL3RvL3N1YmxpbWUtcnVsZXMvZGV0ZWN0aW9uLXJ1bGVzIiwiQ1FMX0hVQl9QQVRIUyI6Ii9wYXRoL3RvL2NxbC1odWIvcXVlcmllcyJ9fQ==)
18
+
19
+ ### Install — Hosted (no setup, token required)
20
+
21
+ 1. **Create a token** at [detect.michaelhaag.org/account/tokens](https://detect.michaelhaag.org/account/tokens). Free tier: 200 calls/day, all read-only tools.
22
+ 2. **Click the button for your client** — replace `sdmcp_YOUR_TOKEN_HERE` in the resulting config with the token you just generated.
23
+
24
+ [![Install Hosted MCP in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=security-detections-hosted&config=eyJ1cmwiOiJodHRwczovL2RldGVjdC5taWNoYWVsaGFhZy5vcmcvYXBpL21jcC9tY3AiLCJoZWFkZXJzIjp7IkF1dGhvcml6YXRpb24iOiJCZWFyZXIgc2RtY3BfWU9VUl9UT0tFTl9IRVJFIn19)
25
+ [![Install Hosted MCP in VS Code](https://img.shields.io/badge/VS_Code-Install_Hosted_MCP-0078d4?style=for-the-badge&logo=visualstudiocode&logoColor=white)](vscode:mcp/install?%7B%22name%22%3A%22security-detections%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A//detect.michaelhaag.org/api/mcp/mcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20sdmcp_YOUR_TOKEN_HERE%22%7D%7D)
26
+ [![Install Hosted MCP in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_Hosted_MCP-24bfa5?style=for-the-badge&logo=visualstudiocode&logoColor=white)](vscode-insiders:mcp/install?%7B%22name%22%3A%22security-detections%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A//detect.michaelhaag.org/api/mcp/mcp%22%2C%22headers%22%3A%7B%22Authorization%22%3A%22Bearer%20sdmcp_YOUR_TOKEN_HERE%22%7D%7D)
27
+
28
+ **Claude Code** (CLI one-liner):
29
+
30
+ ```bash
31
+ claude mcp add --transport http security-detections \
32
+ https://detect.michaelhaag.org/api/mcp/mcp \
33
+ --header "Authorization: Bearer sdmcp_YOUR_TOKEN_HERE"
34
+ ```
35
+
36
+ **Claude Desktop** (via [`mcp-remote`](https://github.com/geelen/mcp-remote) — Desktop doesn't speak remote HTTP natively yet):
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "security-detections": {
42
+ "command": "npx",
43
+ "args": [
44
+ "-y",
45
+ "mcp-remote",
46
+ "https://detect.michaelhaag.org/api/mcp/mcp",
47
+ "--header",
48
+ "Authorization: Bearer sdmcp_YOUR_TOKEN_HERE"
49
+ ]
50
+ }
51
+ }
52
+ }
53
+ ```
54
+
55
+ **OpenAI Codex** (CLI):
56
+
57
+ ```bash
58
+ codex mcp add security-detections \
59
+ --transport http https://detect.michaelhaag.org/api/mcp/mcp \
60
+ --header "Authorization: Bearer sdmcp_YOUR_TOKEN_HERE"
61
+ ```
62
+
63
+ See the [Hosted MCP Setup Guide](./docs/HOSTED_MCP.md) for the full table of clients, the complete tool inventory, and troubleshooting tips.
64
+
65
+ ## What's New in 3.2
66
+
67
+ ### MITRE ATT&CK STIX Integration & Threat Actor Coverage
68
+
69
+ Ingest the official MITRE ATT&CK STIX bundle to auto-populate **172 threat actors**, **784 software/malware**, and **4,362 actor-technique relationships**. Answer "What's our coverage against APT29?" with a single tool call.
70
+
71
+ - **`analyze_actor_coverage`** - Coverage analysis against a specific threat actor: which of their known techniques have detections, coverage %, and prioritized gaps
72
+ - **`list_actors`** - Browse all 172 MITRE ATT&CK threat actors with technique counts
73
+ - **`compare_actor_coverage`** - Compare coverage across multiple actors, find shared gaps
74
+ - **`get_actor_profile`** - Full actor dossier: description, aliases, techniques, software, coverage status
75
+
76
+ ```
77
+ "What's our coverage against APT29?"
78
+
79
+ APT29 (Cozy Bear, Midnight Blizzard, NOBELIUM)
80
+ Techniques: 66 | Covered: 48 | Gaps: 18 | Coverage: 73%
81
+
82
+ By Tactic:
83
+ credential-access: 7/8 (88%) persistence: 12/15 (80%)
84
+ defense-evasion: 10/15 (67%) initial-access: 5/9 (56%)
85
+
86
+ Priority Gaps:
87
+ T1021.007 Cloud Services (lateral-movement)
88
+ T1556.006 Multi-Factor Authentication (credential-access, defense-evasion)
89
+ T1550.004 Web Session Cookie (defense-evasion, lateral-movement)
90
+ ```
91
+
92
+ **Setup**: Download `enterprise-attack.json` and set `ATTACK_STIX_PATH`:
93
+ ```bash
94
+ git clone https://github.com/mitre-attack/attack-stix-data.git
95
+ # Set ATTACK_STIX_PATH=/path/to/attack-stix-data/enterprise-attack/enterprise-attack.json
96
+ ```
97
+
98
+ ### Materialized Relationship Graph
99
+
100
+ Junction tables (`detection_techniques`, `technique_tactics`) replace JSON LIKE scans with indexed JOINs. Queries like `list_by_mitre` are now backed by proper SQL joins instead of `WHERE mitre_ids LIKE '%T1059%'` patterns. Dynamic tactic totals from STIX replace hardcoded values in coverage analysis.
101
+
102
+ ### Procedure-Level Coverage Analysis
103
+
104
+ Go beyond "we cover T1059.001" to answer **which specific behaviors** your detections actually catch. Auto-extracted from 8,200+ detection rules at index time.
105
+
106
+ - **`analyze_procedure_coverage`** - Break down a technique into behavioral procedures (encoded commands, download cradles, AMSI bypass, etc.) and show which ones your detections cover vs. miss
107
+ - **`compare_procedure_coverage`** - Cross-source matrix showing which source catches which procedures — reveals single-source gaps and redundancy
108
+ - **`generate_navigator_layer`** - Export ATT&CK Navigator JSON layers, filterable by source/tactic/severity/actor, ready to import at [mitre-attack.github.io/attack-navigator](https://mitre-attack.github.io/attack-navigator/)
109
+
110
+ ```
111
+ "Are we actually covered for credential dumping?"
112
+
113
+ T1003.001 — LSASS Memory
114
+ Detections: 109 | Depth: moderate | Procedures: 45/59 (76%)
115
+
116
+ ✓ LSASS Memory Access 40 detections [sigma, splunk_escu, elastic]
117
+ ✓ Mimikatz Usage 10 detections [sigma, elastic]
118
+ ✓ Comsvcs.dll MiniDump 6 detections [sigma, elastic, splunk_escu]
119
+ ✓ ProcDump LSASS Dump 8 detections [sigma, splunk_escu, elastic]
120
+ ✗ NanoDump / Custom Dumpers — single-source gap (sigma only)
121
+ ```
122
+
123
+ Procedures are **auto-extracted at index time** — when new detection sources are added, procedure coverage regenerates automatically. 16 high-priority techniques have hand-curated procedure data; the rest are auto-clustered from detection queries, descriptions, and process names.
124
+
125
+ ### New Detection Sources
8
126
 
9
127
  - **CrowdStrike CQL Hub** - Query and search CrowdStrike Query Language (CQL) detections from the CQL Hub community repository
10
- - **Sublime Rules** - Query and search Sublime Security detection rules for email-based threats
128
+ - **Sublime Rules** - Query and search Sublime Security detection rules for email-based threats (now mapped to MITRE ATT&CK techniques)
129
+
130
+ ## What's New in 3.1 - CQL Hub & Sublime Sources
131
+
132
+ - Added CrowdStrike CQL Hub and Sublime Security as detection sources
133
+ - Sublime rules now mapped to MITRE ATT&CK technique IDs via `attack_types` and `tactics_and_techniques`
134
+ - Updated ATT&CK Navigator layer generation to v18.1 / Navigator v5.3.1
11
135
 
12
136
  ## What's New in 3.0 - Autonomous Detection Platform
13
137
 
@@ -96,10 +220,6 @@ The autonomous pipeline integrates with existing MCPs:
96
220
 
97
221
  See the [Autonomous Platform Documentation](./docs/AUTONOMOUS.md) for full details, and the [E2E Testing Guide](./docs/E2E-TESTING-GUIDE.md) for per-SIEM setup (Splunk, Sentinel, Elastic, Sigma).
98
222
 
99
- [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=security-detections&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInNlY3VyaXR5LWRldGVjdGlvbnMtbWNwIl0sImVudiI6eyJTSUdNQV9QQVRIUyI6Ii9wYXRoL3RvL3NpZ21hL3J1bGVzLC9wYXRoL3RvL3NpZ21hL3J1bGVzLXRocmVhdC1odW50aW5nIiwiU1BMVU5LX1BBVEhTIjoiL3BhdGgvdG8vc2VjdXJpdHlfY29udGVudC9kZXRlY3Rpb25zIiwiU1RPUllfUEFUSFMiOiIvcGF0aC90by9zZWN1cml0eV9jb250ZW50L3N0b3JpZXMiLCJFTEFTVElDX1BBVEhTIjoiL3BhdGgvdG8vZGV0ZWN0aW9uLXJ1bGVzL3J1bGVzIiwiS1FMX1BBVEhTIjoiL3BhdGgvdG8va3FsLXJ1bGVzIn19)
100
-
101
- > **Detailed setup**: See the **[Setup Guide](./SETUP.md)** for step-by-step install on macOS, Windows (WSL & native), and Linux with troubleshooting for common issues.
102
-
103
223
  ## 🐛 Version 2.1.1 (Bug Fix)
104
224
 
105
225
  - **Fixed Windows EBUSY crash** - SQLite database recreation now handles Windows file locking with retry logic. Previously, Windows users would get `EBUSY: resource busy or locked` on startup.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * ATT&CK Data Access Layer
3
+ *
4
+ * Query functions for MITRE ATT&CK STIX-sourced data:
5
+ * threat actors, software, technique coverage, and actor-based gap analysis.
6
+ */
7
+ export interface AttackActor {
8
+ actor_id: string;
9
+ name: string;
10
+ aliases: string[];
11
+ description: string | null;
12
+ external_references: unknown[];
13
+ created: string | null;
14
+ modified: string | null;
15
+ }
16
+ export interface AttackTechnique {
17
+ technique_id: string;
18
+ name: string;
19
+ description: string | null;
20
+ platforms: string[];
21
+ data_sources: string[];
22
+ is_subtechnique: boolean;
23
+ parent_technique_id: string | null;
24
+ url: string | null;
25
+ }
26
+ export interface AttackSoftware {
27
+ software_id: string;
28
+ name: string;
29
+ software_type: string;
30
+ description: string | null;
31
+ platforms: string[];
32
+ aliases: string[];
33
+ }
34
+ export interface ActorTechnique {
35
+ technique_id: string;
36
+ technique_name: string;
37
+ description: string | null;
38
+ detection_count: number;
39
+ tactics: string[];
40
+ }
41
+ export interface ActorCoverageResult {
42
+ actor: AttackActor;
43
+ total_techniques: number;
44
+ covered_count: number;
45
+ gap_count: number;
46
+ coverage_percentage: number;
47
+ covered_techniques: ActorTechnique[];
48
+ gap_techniques: ActorTechnique[];
49
+ by_tactic: Record<string, {
50
+ total: number;
51
+ covered: number;
52
+ percentage: number;
53
+ }>;
54
+ }
55
+ export interface ActorListItem {
56
+ actor_id: string;
57
+ name: string;
58
+ aliases: string[];
59
+ technique_count: number;
60
+ }
61
+ /**
62
+ * Check if STIX data has been loaded into the database.
63
+ */
64
+ export declare function isStixLoaded(): boolean;
65
+ /**
66
+ * Find a threat actor by name or alias (case-insensitive).
67
+ */
68
+ export declare function getActorByName(name: string): AttackActor | null;
69
+ /**
70
+ * List all threat actors, optionally filtered by search term.
71
+ */
72
+ export declare function listActors(search?: string, limit?: number): ActorListItem[];
73
+ /**
74
+ * Get all techniques used by a specific actor, with detection coverage info.
75
+ */
76
+ export declare function getActorTechniques(actorId: string): ActorTechnique[];
77
+ /**
78
+ * Get full detection coverage analysis for a threat actor.
79
+ */
80
+ export declare function getActorCoverage(actorId: string, sourceType?: string): ActorCoverageResult;
81
+ /**
82
+ * Get all software used by a specific actor.
83
+ */
84
+ export declare function getSoftwareForActor(actorId: string): AttackSoftware[];
85
+ /**
86
+ * Get all threat actors that use a specific technique.
87
+ */
88
+ export declare function getTechniqueActors(techniqueId: string): AttackActor[];
89
+ /**
90
+ * Get a technique from the ATT&CK catalog.
91
+ */
92
+ export declare function getAttackTechnique(techniqueId: string): AttackTechnique | null;
93
+ /**
94
+ * Get total counts for ATT&CK data.
95
+ */
96
+ export declare function getAttackStats(): {
97
+ techniques: number;
98
+ actors: number;
99
+ software: number;
100
+ actor_technique_links: number;
101
+ software_technique_links: number;
102
+ };
@@ -0,0 +1,311 @@
1
+ /**
2
+ * ATT&CK Data Access Layer
3
+ *
4
+ * Query functions for MITRE ATT&CK STIX-sourced data:
5
+ * threat actors, software, technique coverage, and actor-based gap analysis.
6
+ */
7
+ import { getDb } from './connection.js';
8
+ import { safeJsonParse } from '../utils/helpers.js';
9
+ // =============================================================================
10
+ // HELPERS
11
+ // =============================================================================
12
+ function rowToActor(row) {
13
+ return {
14
+ actor_id: row.actor_id,
15
+ name: row.name,
16
+ aliases: safeJsonParse(row.aliases, []),
17
+ description: row.description || null,
18
+ external_references: safeJsonParse(row.external_references, []),
19
+ created: row.created || null,
20
+ modified: row.modified || null,
21
+ };
22
+ }
23
+ function rowToTechnique(row) {
24
+ return {
25
+ technique_id: row.technique_id,
26
+ name: row.name,
27
+ description: row.description || null,
28
+ platforms: safeJsonParse(row.platforms, []),
29
+ data_sources: safeJsonParse(row.data_sources, []),
30
+ is_subtechnique: row.is_subtechnique === 1,
31
+ parent_technique_id: row.parent_technique_id || null,
32
+ url: row.url || null,
33
+ };
34
+ }
35
+ function rowToSoftware(row) {
36
+ return {
37
+ software_id: row.software_id,
38
+ name: row.name,
39
+ software_type: row.software_type || 'unknown',
40
+ description: row.description || null,
41
+ platforms: safeJsonParse(row.platforms, []),
42
+ aliases: safeJsonParse(row.aliases, []),
43
+ };
44
+ }
45
+ // =============================================================================
46
+ // QUERY FUNCTIONS
47
+ // =============================================================================
48
+ /**
49
+ * Check if STIX data has been loaded into the database.
50
+ */
51
+ export function isStixLoaded() {
52
+ try {
53
+ const row = getDb().prepare('SELECT COUNT(*) as count FROM attack_actors').get();
54
+ return row.count > 0;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ /**
61
+ * Find a threat actor by name or alias (case-insensitive).
62
+ */
63
+ export function getActorByName(name) {
64
+ const database = getDb();
65
+ // Try exact name match first
66
+ let row = database.prepare('SELECT * FROM attack_actors WHERE name = ? COLLATE NOCASE').get(name);
67
+ if (row)
68
+ return rowToActor(row);
69
+ // Try alias match
70
+ row = database.prepare("SELECT * FROM attack_actors WHERE aliases LIKE ? COLLATE NOCASE").get(`%"${name}"%`);
71
+ if (row)
72
+ return rowToActor(row);
73
+ // Try partial name match
74
+ row = database.prepare('SELECT * FROM attack_actors WHERE name LIKE ? COLLATE NOCASE').get(`%${name}%`);
75
+ return row ? rowToActor(row) : null;
76
+ }
77
+ /**
78
+ * List all threat actors, optionally filtered by search term.
79
+ */
80
+ export function listActors(search, limit = 100) {
81
+ const database = getDb();
82
+ let query;
83
+ let params;
84
+ if (search) {
85
+ query = `
86
+ SELECT a.*, COUNT(at.technique_id) as technique_count
87
+ FROM attack_actors a
88
+ LEFT JOIN actor_techniques at ON a.actor_id = at.actor_id
89
+ WHERE a.name LIKE ? COLLATE NOCASE OR a.aliases LIKE ? COLLATE NOCASE
90
+ GROUP BY a.actor_id
91
+ ORDER BY technique_count DESC
92
+ LIMIT ?
93
+ `;
94
+ params = [`%${search}%`, `%${search}%`, limit];
95
+ }
96
+ else {
97
+ query = `
98
+ SELECT a.*, COUNT(at.technique_id) as technique_count
99
+ FROM attack_actors a
100
+ LEFT JOIN actor_techniques at ON a.actor_id = at.actor_id
101
+ GROUP BY a.actor_id
102
+ ORDER BY technique_count DESC
103
+ LIMIT ?
104
+ `;
105
+ params = [limit];
106
+ }
107
+ const rows = database.prepare(query).all(...params);
108
+ return rows.map(row => ({
109
+ actor_id: row.actor_id,
110
+ name: row.name,
111
+ aliases: safeJsonParse(row.aliases, []),
112
+ technique_count: row.technique_count || 0,
113
+ }));
114
+ }
115
+ /**
116
+ * Get all techniques used by a specific actor, with detection coverage info.
117
+ */
118
+ export function getActorTechniques(actorId) {
119
+ const database = getDb();
120
+ const rows = database.prepare(`
121
+ SELECT
122
+ at.technique_id,
123
+ COALESCE(atk.name, at.technique_id) as technique_name,
124
+ at.description,
125
+ COUNT(DISTINCT dt.detection_id) as detection_count
126
+ FROM actor_techniques at
127
+ LEFT JOIN attack_techniques atk ON at.technique_id = atk.technique_id
128
+ LEFT JOIN detection_techniques dt ON at.technique_id = dt.technique_id
129
+ WHERE at.actor_id = ?
130
+ GROUP BY at.technique_id
131
+ ORDER BY detection_count DESC
132
+ `).all(actorId);
133
+ // Get tactics for each technique
134
+ return rows.map(row => {
135
+ const techId = row.technique_id;
136
+ const tacticRows = database.prepare('SELECT tactic_name FROM technique_tactics WHERE technique_id = ?').all(techId);
137
+ return {
138
+ technique_id: techId,
139
+ technique_name: row.technique_name,
140
+ description: row.description || null,
141
+ detection_count: row.detection_count || 0,
142
+ tactics: tacticRows.map(r => r.tactic_name),
143
+ };
144
+ });
145
+ }
146
+ /**
147
+ * Get full detection coverage analysis for a threat actor.
148
+ */
149
+ export function getActorCoverage(actorId, sourceType) {
150
+ const database = getDb();
151
+ // Get the actor
152
+ const actorRow = database.prepare('SELECT * FROM attack_actors WHERE actor_id = ?').get(actorId);
153
+ if (!actorRow) {
154
+ throw new Error(`Actor not found: ${actorId}`);
155
+ }
156
+ const actor = rowToActor(actorRow);
157
+ // Get all techniques for this actor with detection counts
158
+ let detectionCountQuery;
159
+ let queryParams;
160
+ if (sourceType) {
161
+ detectionCountQuery = `
162
+ SELECT
163
+ at.technique_id,
164
+ COALESCE(atk.name, at.technique_id) as technique_name,
165
+ at.description,
166
+ COUNT(DISTINCT dt.detection_id) as detection_count
167
+ FROM actor_techniques at
168
+ LEFT JOIN attack_techniques atk ON at.technique_id = atk.technique_id
169
+ LEFT JOIN detection_techniques dt ON at.technique_id = dt.technique_id
170
+ LEFT JOIN detections d ON dt.detection_id = d.id AND d.source_type = ?
171
+ WHERE at.actor_id = ?
172
+ GROUP BY at.technique_id
173
+ ORDER BY detection_count DESC
174
+ `;
175
+ queryParams = [sourceType, actorId];
176
+ }
177
+ else {
178
+ detectionCountQuery = `
179
+ SELECT
180
+ at.technique_id,
181
+ COALESCE(atk.name, at.technique_id) as technique_name,
182
+ at.description,
183
+ COUNT(DISTINCT dt.detection_id) as detection_count
184
+ FROM actor_techniques at
185
+ LEFT JOIN attack_techniques atk ON at.technique_id = atk.technique_id
186
+ LEFT JOIN detection_techniques dt ON at.technique_id = dt.technique_id
187
+ WHERE at.actor_id = ?
188
+ GROUP BY at.technique_id
189
+ ORDER BY detection_count DESC
190
+ `;
191
+ queryParams = [actorId];
192
+ }
193
+ const rows = database.prepare(detectionCountQuery).all(...queryParams);
194
+ const covered = [];
195
+ const gaps = [];
196
+ const byTactic = {};
197
+ for (const row of rows) {
198
+ const techId = row.technique_id;
199
+ const detectionCount = row.detection_count || 0;
200
+ // Get tactics for this technique
201
+ const tacticRows = database.prepare('SELECT tactic_name FROM technique_tactics WHERE technique_id = ?').all(techId);
202
+ const tactics = tacticRows.map(r => r.tactic_name);
203
+ const entry = {
204
+ technique_id: techId,
205
+ technique_name: row.technique_name,
206
+ description: row.description || null,
207
+ detection_count: detectionCount,
208
+ tactics,
209
+ };
210
+ if (detectionCount > 0) {
211
+ covered.push(entry);
212
+ }
213
+ else {
214
+ gaps.push(entry);
215
+ }
216
+ // Aggregate by tactic
217
+ for (const tactic of tactics) {
218
+ if (!byTactic[tactic]) {
219
+ byTactic[tactic] = { total: 0, covered: 0, percentage: 0 };
220
+ }
221
+ byTactic[tactic].total++;
222
+ if (detectionCount > 0) {
223
+ byTactic[tactic].covered++;
224
+ }
225
+ }
226
+ }
227
+ // Calculate percentages
228
+ for (const tactic of Object.keys(byTactic)) {
229
+ byTactic[tactic].percentage = byTactic[tactic].total > 0
230
+ ? Math.round((byTactic[tactic].covered / byTactic[tactic].total) * 100)
231
+ : 0;
232
+ }
233
+ const totalTechniques = rows.length;
234
+ const coveragePercentage = totalTechniques > 0
235
+ ? Math.round((covered.length / totalTechniques) * 100)
236
+ : 0;
237
+ return {
238
+ actor,
239
+ total_techniques: totalTechniques,
240
+ covered_count: covered.length,
241
+ gap_count: gaps.length,
242
+ coverage_percentage: coveragePercentage,
243
+ covered_techniques: covered,
244
+ gap_techniques: gaps,
245
+ by_tactic: byTactic,
246
+ };
247
+ }
248
+ /**
249
+ * Get all software used by a specific actor.
250
+ */
251
+ export function getSoftwareForActor(actorId) {
252
+ const database = getDb();
253
+ // STIX models actor→software as "uses" relationships too,
254
+ // but we only stored actor→technique and software→technique.
255
+ // So we find software that shares techniques with this actor.
256
+ const rows = database.prepare(`
257
+ SELECT DISTINCT s.*
258
+ FROM attack_software s
259
+ JOIN software_techniques st ON s.software_id = st.software_id
260
+ WHERE st.technique_id IN (
261
+ SELECT technique_id FROM actor_techniques WHERE actor_id = ?
262
+ )
263
+ ORDER BY s.name
264
+ `).all(actorId);
265
+ return rows.map(rowToSoftware);
266
+ }
267
+ /**
268
+ * Get all threat actors that use a specific technique.
269
+ */
270
+ export function getTechniqueActors(techniqueId) {
271
+ const database = getDb();
272
+ const rows = database.prepare(`
273
+ SELECT a.*
274
+ FROM attack_actors a
275
+ JOIN actor_techniques at ON a.actor_id = at.actor_id
276
+ WHERE at.technique_id = ?
277
+ ORDER BY a.name
278
+ `).all(techniqueId);
279
+ return rows.map(rowToActor);
280
+ }
281
+ /**
282
+ * Get a technique from the ATT&CK catalog.
283
+ */
284
+ export function getAttackTechnique(techniqueId) {
285
+ const database = getDb();
286
+ const row = database.prepare('SELECT * FROM attack_techniques WHERE technique_id = ?').get(techniqueId);
287
+ return row ? rowToTechnique(row) : null;
288
+ }
289
+ /**
290
+ * Get total counts for ATT&CK data.
291
+ */
292
+ export function getAttackStats() {
293
+ const database = getDb();
294
+ try {
295
+ const techniques = database.prepare('SELECT COUNT(*) as c FROM attack_techniques').get().c;
296
+ const actors = database.prepare('SELECT COUNT(*) as c FROM attack_actors').get().c;
297
+ const software = database.prepare('SELECT COUNT(*) as c FROM attack_software').get().c;
298
+ const actorLinks = database.prepare('SELECT COUNT(*) as c FROM actor_techniques').get().c;
299
+ const swLinks = database.prepare('SELECT COUNT(*) as c FROM software_techniques').get().c;
300
+ return {
301
+ techniques,
302
+ actors,
303
+ software,
304
+ actor_technique_links: actorLinks,
305
+ software_technique_links: swLinks,
306
+ };
307
+ }
308
+ catch {
309
+ return { techniques: 0, actors: 0, software: 0, actor_technique_links: 0, software_technique_links: 0 };
310
+ }
311
+ }
@@ -62,6 +62,7 @@ export interface NavigatorLayerOptions {
62
62
  source_type?: 'sigma' | 'splunk_escu' | 'elastic' | 'kql' | 'sublime' | 'crowdstrike_cql';
63
63
  tactic?: string;
64
64
  severity?: string;
65
+ actor_name?: string;
65
66
  }
66
67
  export interface DetectionListItem {
67
68
  name: string;
@@ -196,6 +197,34 @@ export declare function suggestDetections(techniqueId: string, sourceType?: 'sig
196
197
  * Generate an ATT&CK Navigator layer from detection coverage.
197
198
  */
198
199
  export declare function generateNavigatorLayer(options: NavigatorLayerOptions): object;
200
+ /**
201
+ * Auto-extract procedure reference data from detections for a technique.
202
+ * Clusters detections by behavioral category and generates procedure entries.
203
+ */
204
+ export declare function autoExtractProcedures(techniqueId: string): {
205
+ technique_id: string;
206
+ procedures_generated: number;
207
+ detection_count: number;
208
+ };
209
+ /**
210
+ * Extract procedures for ALL techniques in the database.
211
+ * Loads hand-curated procedures first, then auto-extracts for the rest.
212
+ * Called after indexing completes.
213
+ */
214
+ export declare function extractAllProcedures(): {
215
+ techniques_processed: number;
216
+ procedures_generated: number;
217
+ hand_curated_loaded: number;
218
+ };
219
+ /**
220
+ * Populate detection_techniques and technique_tactics junction tables
221
+ * from existing detection data. Runs as a post-indexing bulk operation
222
+ * in a single transaction for performance.
223
+ */
224
+ export declare function populateJunctionTables(): {
225
+ detection_techniques: number;
226
+ technique_tactics: number;
227
+ };
199
228
  /**
200
229
  * Search detections returning only name, ID, and basic info.
201
230
  */