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 +126 -6
- package/dist/db/attack.d.ts +102 -0
- package/dist/db/attack.js +311 -0
- package/dist/db/detections.d.ts +29 -0
- package/dist/db/detections.js +337 -16
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +10 -0
- package/dist/db/procedure-reference.d.ts +28 -0
- package/dist/db/procedure-reference.js +51772 -0
- package/dist/db/schema.js +114 -0
- package/dist/db.d.ts +1 -0
- package/dist/db.js +2 -2
- package/dist/index.js +30 -1
- package/dist/parsers/kql.js +2 -2
- package/dist/parsers/stix.d.ts +28 -0
- package/dist/parsers/stix.js +207 -0
- package/dist/parsers/sublime.js +41 -1
- package/dist/resources/index.js +79 -6
- package/dist/tools/detections/actor-analysis.d.ts +7 -0
- package/dist/tools/detections/actor-analysis.js +251 -0
- package/dist/tools/detections/analysis.js +460 -1
- package/dist/tools/detections/index.d.ts +2 -0
- package/dist/tools/detections/index.js +5 -1
- package/package.json +4 -1
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
|
-
##
|
|
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
|
+
[](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
|
+
[](https://cursor.com/en/install-mcp?name=security-detections-hosted&config=eyJ1cmwiOiJodHRwczovL2RldGVjdC5taWNoYWVsaGFhZy5vcmcvYXBpL21jcC9tY3AiLCJoZWFkZXJzIjp7IkF1dGhvcml6YXRpb24iOiJCZWFyZXIgc2RtY3BfWU9VUl9UT0tFTl9IRVJFIn19)
|
|
25
|
+
[](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
|
+
[](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
|
-
[](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
|
+
}
|
package/dist/db/detections.d.ts
CHANGED
|
@@ -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
|
*/
|