security-detections-mcp 3.1.1 → 3.2.1
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 +125 -3
- 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 +1 -1
package/dist/db/detections.js
CHANGED
|
@@ -175,10 +175,28 @@ export function listBySource(sourceType, limit = 100, offset = 0) {
|
|
|
175
175
|
*/
|
|
176
176
|
export function listByMitre(techniqueId, limit = 100, offset = 0) {
|
|
177
177
|
const database = getDb();
|
|
178
|
+
// Try junction table first (indexed join — faster)
|
|
179
|
+
try {
|
|
180
|
+
const check = database.prepare('SELECT 1 FROM detection_techniques LIMIT 1').get();
|
|
181
|
+
if (check) {
|
|
182
|
+
const rows = database.prepare(`
|
|
183
|
+
SELECT d.* FROM detections d
|
|
184
|
+
JOIN detection_techniques dt ON d.id = dt.detection_id
|
|
185
|
+
WHERE dt.technique_id = ?
|
|
186
|
+
ORDER BY d.name
|
|
187
|
+
LIMIT ? OFFSET ?
|
|
188
|
+
`).all(techniqueId, limit, offset);
|
|
189
|
+
return rows.map(rowToDetection);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Junction table may not exist yet — fall through to LIKE query
|
|
194
|
+
}
|
|
195
|
+
// Fallback: original LIKE-based query
|
|
178
196
|
const stmt = database.prepare(`
|
|
179
|
-
SELECT * FROM detections
|
|
180
|
-
WHERE mitre_ids LIKE ?
|
|
181
|
-
ORDER BY name
|
|
197
|
+
SELECT * FROM detections
|
|
198
|
+
WHERE mitre_ids LIKE ?
|
|
199
|
+
ORDER BY name
|
|
182
200
|
LIMIT ? OFFSET ?
|
|
183
201
|
`);
|
|
184
202
|
const rows = stmt.all(`%"${techniqueId}"%`, limit, offset);
|
|
@@ -624,13 +642,42 @@ export function analyzeCoverage(sourceType) {
|
|
|
624
642
|
}
|
|
625
643
|
}
|
|
626
644
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
645
|
+
// Try dynamic tactic totals from technique_tactics table (populated from STIX or detections)
|
|
646
|
+
let tacticTotals;
|
|
647
|
+
try {
|
|
648
|
+
const ttRows = database.prepare('SELECT tactic_name, COUNT(DISTINCT technique_id) as count FROM technique_tactics GROUP BY tactic_name').all();
|
|
649
|
+
if (ttRows.length > 0) {
|
|
650
|
+
tacticTotals = {};
|
|
651
|
+
for (const row of ttRows) {
|
|
652
|
+
tacticTotals[row.tactic_name] = row.count;
|
|
653
|
+
}
|
|
654
|
+
// Ensure all 14 tactics are present
|
|
655
|
+
for (const t of allTactics) {
|
|
656
|
+
if (!tacticTotals[t])
|
|
657
|
+
tacticTotals[t] = 1;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
// Fallback to hardcoded if junction table is empty
|
|
662
|
+
tacticTotals = {
|
|
663
|
+
'reconnaissance': 10, 'resource-development': 8, 'initial-access': 10,
|
|
664
|
+
'execution': 14, 'persistence': 20, 'privilege-escalation': 14,
|
|
665
|
+
'defense-evasion': 43, 'credential-access': 17, 'discovery': 31,
|
|
666
|
+
'lateral-movement': 9, 'collection': 17, 'command-and-control': 18,
|
|
667
|
+
'exfiltration': 9, 'impact': 14
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
catch {
|
|
672
|
+
// Table may not exist — fallback to hardcoded
|
|
673
|
+
tacticTotals = {
|
|
674
|
+
'reconnaissance': 10, 'resource-development': 8, 'initial-access': 10,
|
|
675
|
+
'execution': 14, 'persistence': 20, 'privilege-escalation': 14,
|
|
676
|
+
'defense-evasion': 43, 'credential-access': 17, 'discovery': 31,
|
|
677
|
+
'lateral-movement': 9, 'collection': 17, 'command-and-control': 18,
|
|
678
|
+
'exfiltration': 9, 'impact': 14
|
|
679
|
+
};
|
|
680
|
+
}
|
|
634
681
|
const coverageByTactic = {};
|
|
635
682
|
for (const tactic of allTactics) {
|
|
636
683
|
const covered = tacticCounts[tactic].size;
|
|
@@ -745,12 +792,47 @@ export function suggestDetections(techniqueId, sourceType) {
|
|
|
745
792
|
* Generate an ATT&CK Navigator layer from detection coverage.
|
|
746
793
|
*/
|
|
747
794
|
export function generateNavigatorLayer(options) {
|
|
748
|
-
const
|
|
795
|
+
const database = getDb();
|
|
796
|
+
// If actor_name is specified, filter to only that actor's techniques
|
|
797
|
+
let actorTechniqueFilter = null;
|
|
798
|
+
if (options.actor_name) {
|
|
799
|
+
try {
|
|
800
|
+
// Dynamic import would be circular — query directly
|
|
801
|
+
const actorRow = database.prepare('SELECT actor_id FROM attack_actors WHERE name = ? COLLATE NOCASE').get(options.actor_name);
|
|
802
|
+
if (!actorRow) {
|
|
803
|
+
// Try alias match
|
|
804
|
+
const aliasRow = database.prepare("SELECT actor_id FROM attack_actors WHERE aliases LIKE ? COLLATE NOCASE").get(`%"${options.actor_name}"%`);
|
|
805
|
+
if (aliasRow) {
|
|
806
|
+
const actorTechs = database.prepare('SELECT technique_id FROM actor_techniques WHERE actor_id = ?').all(aliasRow.actor_id);
|
|
807
|
+
actorTechniqueFilter = new Set(actorTechs.map(r => r.technique_id));
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
else {
|
|
811
|
+
const actorTechs = database.prepare('SELECT technique_id FROM actor_techniques WHERE actor_id = ?').all(actorRow.actor_id);
|
|
812
|
+
actorTechniqueFilter = new Set(actorTechs.map(r => r.technique_id));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
// STIX tables may not exist — ignore actor filter
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
let techniqueIds = getTechniqueIds({
|
|
749
820
|
source_type: options.source_type,
|
|
750
821
|
tactic: options.tactic,
|
|
751
822
|
severity: options.severity,
|
|
752
823
|
});
|
|
753
|
-
|
|
824
|
+
// For actor layers, include ALL actor techniques (even uncovered ones)
|
|
825
|
+
if (actorTechniqueFilter) {
|
|
826
|
+
const coveredSet = new Set(techniqueIds);
|
|
827
|
+
// Merge: keep covered ones + add uncovered actor techniques
|
|
828
|
+
for (const actorTech of actorTechniqueFilter) {
|
|
829
|
+
if (!coveredSet.has(actorTech)) {
|
|
830
|
+
techniqueIds.push(actorTech);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
// Filter to only actor techniques
|
|
834
|
+
techniqueIds = techniqueIds.filter(t => actorTechniqueFilter.has(t));
|
|
835
|
+
}
|
|
754
836
|
const techniques = [];
|
|
755
837
|
function getColorForScore(score) {
|
|
756
838
|
if (score >= 80)
|
|
@@ -776,11 +858,13 @@ export function generateNavigatorLayer(options) {
|
|
|
776
858
|
}
|
|
777
859
|
const count = database.prepare(countSql).get(...countParams).count;
|
|
778
860
|
const score = Math.min(count * 20, 100);
|
|
861
|
+
// For actor layers, uncovered techniques show as red with score 0
|
|
862
|
+
const isActorGap = actorTechniqueFilter && count === 0;
|
|
779
863
|
techniques.push({
|
|
780
864
|
techniqueID: techId,
|
|
781
865
|
score,
|
|
782
|
-
comment: `${count} detection(s)`,
|
|
783
|
-
color: getColorForScore(score),
|
|
866
|
+
comment: isActorGap ? 'GAP - no detections' : `${count} detection(s)`,
|
|
867
|
+
color: isActorGap ? '#ff6666' : getColorForScore(score),
|
|
784
868
|
enabled: true,
|
|
785
869
|
showSubtechniques: false,
|
|
786
870
|
});
|
|
@@ -788,8 +872,8 @@ export function generateNavigatorLayer(options) {
|
|
|
788
872
|
return {
|
|
789
873
|
name: options.name,
|
|
790
874
|
versions: {
|
|
791
|
-
attack: '18',
|
|
792
|
-
navigator: '5.1
|
|
875
|
+
attack: '18.1',
|
|
876
|
+
navigator: '5.3.1',
|
|
793
877
|
layer: '4.5',
|
|
794
878
|
},
|
|
795
879
|
domain: 'enterprise-attack',
|
|
@@ -815,6 +899,243 @@ export function generateNavigatorLayer(options) {
|
|
|
815
899
|
};
|
|
816
900
|
}
|
|
817
901
|
// =============================================================================
|
|
902
|
+
// PROCEDURE AUTO-EXTRACTION
|
|
903
|
+
// =============================================================================
|
|
904
|
+
const BEHAVIOR_KEYWORDS = [
|
|
905
|
+
'encoded command', 'base64', 'download', 'credential', 'dump', 'inject', 'hollow',
|
|
906
|
+
'obfuscat', 'bypass', 'evasion', 'persist', 'lateral', 'remote', 'scheduled task',
|
|
907
|
+
'service', 'registry', 'startup', 'script block', 'amsi', 'wmi', 'powershell',
|
|
908
|
+
'mimikatz', 'lsass', 'shadow cop', 'ransomware', 'encrypt', 'exfiltrat', 'tunnel',
|
|
909
|
+
'beacon', 'c2', 'command and control', 'brute force', 'spray', 'kerbero',
|
|
910
|
+
'pass the hash', 'pass the ticket', 'golden ticket', 'dcsync', 'ntds',
|
|
911
|
+
'dll side', 'dll hijack', 'process access', 'remote thread', 'token', 'impersonat',
|
|
912
|
+
'privilege', 'parent process', 'child process', 'masquerad', 'macro', 'office',
|
|
913
|
+
'phish', 'attachment', 'email', 'dns', 'http', 'clear log', 'event log', 'tamper',
|
|
914
|
+
'suspicious', 'anomal', 'unusual', 'cloud', 'aws', 'azure', 'container', 'kubernetes',
|
|
915
|
+
'api', 'oauth', 'saml', 'driver', 'kernel', 'named pipe', 'shellcode', 'reflection',
|
|
916
|
+
];
|
|
917
|
+
function categorizeDetection(text, logsourceCategory) {
|
|
918
|
+
if (logsourceCategory === 'process_creation' || (text.includes('process') && text.includes('creat')))
|
|
919
|
+
return 'process_creation_monitoring';
|
|
920
|
+
if (logsourceCategory === 'process_access' || text.includes('process access'))
|
|
921
|
+
return 'process_access_monitoring';
|
|
922
|
+
if (logsourceCategory.includes('registry') || text.includes('registry'))
|
|
923
|
+
return 'registry_monitoring';
|
|
924
|
+
if (logsourceCategory.includes('file') || text.includes('file creat') || text.includes('file modif'))
|
|
925
|
+
return 'file_monitoring';
|
|
926
|
+
if (logsourceCategory === 'network_connection' || text.includes('network') || text.includes('connection'))
|
|
927
|
+
return 'network_connection_monitoring';
|
|
928
|
+
if (logsourceCategory === 'image_load' || text.includes('image load') || text.includes('dll load'))
|
|
929
|
+
return 'module_load_monitoring';
|
|
930
|
+
if (text.includes('commandline') || text.includes('command line'))
|
|
931
|
+
return 'command_line_monitoring';
|
|
932
|
+
if (text.includes('script'))
|
|
933
|
+
return 'script_execution_monitoring';
|
|
934
|
+
if (text.includes('authenti') || text.includes('logon') || text.includes('login'))
|
|
935
|
+
return 'authentication_monitoring';
|
|
936
|
+
if (text.includes('service') && (text.includes('creat') || text.includes('install')))
|
|
937
|
+
return 'service_monitoring';
|
|
938
|
+
if (text.includes('email') || text.includes('phish') || text.includes('spam'))
|
|
939
|
+
return 'email_security';
|
|
940
|
+
if (text.includes('cloud') || text.includes('aws') || text.includes('azure'))
|
|
941
|
+
return 'cloud_monitoring';
|
|
942
|
+
if (text.includes('driver') || text.includes('kernel'))
|
|
943
|
+
return 'kernel_monitoring';
|
|
944
|
+
return 'general_monitoring';
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* Auto-extract procedure reference data from detections for a technique.
|
|
948
|
+
* Clusters detections by behavioral category and generates procedure entries.
|
|
949
|
+
*/
|
|
950
|
+
export function autoExtractProcedures(techniqueId) {
|
|
951
|
+
const database = getDb();
|
|
952
|
+
const detections = listByMitre(techniqueId, 1000);
|
|
953
|
+
if (detections.length === 0)
|
|
954
|
+
return { technique_id: techniqueId, procedures_generated: 0, detection_count: 0 };
|
|
955
|
+
// Extract keyword frequencies
|
|
956
|
+
const kwFreq = new Map();
|
|
957
|
+
for (const d of detections) {
|
|
958
|
+
const desc = (d.description || '').toLowerCase();
|
|
959
|
+
for (const kw of BEHAVIOR_KEYWORDS) {
|
|
960
|
+
if (desc.includes(kw))
|
|
961
|
+
kwFreq.set(kw, (kwFreq.get(kw) || 0) + 1);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
// Group detections by category + dominant keyword
|
|
965
|
+
const groups = {};
|
|
966
|
+
for (const d of detections) {
|
|
967
|
+
const text = ((d.description || '') + ' ' + (d.query || '') + ' ' + (d.name || '')).toLowerCase();
|
|
968
|
+
const category = categorizeDetection(text, d.logsource_category || '');
|
|
969
|
+
let bestKw = null;
|
|
970
|
+
const desc = (d.description || '').toLowerCase();
|
|
971
|
+
for (const [kw, count] of kwFreq.entries()) {
|
|
972
|
+
if (desc.includes(kw) && count >= 2 && count < detections.length * 0.8) {
|
|
973
|
+
if (!bestKw || (kwFreq.get(bestKw) || 0) > count)
|
|
974
|
+
bestKw = kw;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
const key = `${category}:${bestKw || 'general'}`;
|
|
978
|
+
if (!groups[key])
|
|
979
|
+
groups[key] = { category, keyword: bestKw, names: [], processNames: new Set(), keywords: new Set() };
|
|
980
|
+
const g = groups[key];
|
|
981
|
+
g.names.push(d.name);
|
|
982
|
+
for (const pn of d.process_names || [])
|
|
983
|
+
g.processNames.add(pn.toLowerCase());
|
|
984
|
+
if (bestKw)
|
|
985
|
+
g.keywords.add(bestKw);
|
|
986
|
+
}
|
|
987
|
+
// Convert to procedure entries and store
|
|
988
|
+
database.exec(`DELETE FROM procedure_reference WHERE technique_id = '${techniqueId.replace(/'/g, "''")}' AND source = 'auto'`);
|
|
989
|
+
const insertStmt = database.prepare(`INSERT OR REPLACE INTO procedure_reference (id, technique_id, name, category, description, source, indicators, detection_count, confidence)
|
|
990
|
+
VALUES (?, ?, ?, ?, ?, 'auto', ?, ?, ?)`);
|
|
991
|
+
let idx = 0;
|
|
992
|
+
for (const [, g] of Object.entries(groups)) {
|
|
993
|
+
const name = g.keyword
|
|
994
|
+
? g.keyword.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
|
|
995
|
+
: g.category.replace(/_/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
996
|
+
const indicators = {};
|
|
997
|
+
if (g.processNames.size > 0)
|
|
998
|
+
indicators.process_names = [...g.processNames].slice(0, 10);
|
|
999
|
+
if (g.keywords.size > 0)
|
|
1000
|
+
indicators.description_keywords = [...g.keywords].slice(0, 10);
|
|
1001
|
+
// Extract command patterns from queries
|
|
1002
|
+
const cmdPats = new Set();
|
|
1003
|
+
const groupDets = detections.filter(det => g.names.includes(det.name));
|
|
1004
|
+
for (const d of groupDets) {
|
|
1005
|
+
if (d.query) {
|
|
1006
|
+
const literals = d.query.toLowerCase().match(/['"]([^'"]{3,40})['"]/g);
|
|
1007
|
+
if (literals) {
|
|
1008
|
+
for (const lit of literals.slice(0, 10)) {
|
|
1009
|
+
const val = lit.replace(/['"]/g, '').trim();
|
|
1010
|
+
if (val.length >= 3 && !/^(and|or|not|true|false|null|none|string|type|object|select|from|where|index|name|value|count|table|status|data|endpoint|query|result|search|action|field|source|level|any|all|list|set|process|rule|detection|sigma)$/i.test(val)) {
|
|
1011
|
+
cmdPats.add(val);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (cmdPats.size > 0)
|
|
1018
|
+
indicators.command_patterns = [...cmdPats].slice(0, 15);
|
|
1019
|
+
const confidence = g.names.length >= 10 ? 0.9 : g.names.length >= 5 ? 0.7 : g.names.length >= 3 ? 0.5 : 0.3;
|
|
1020
|
+
insertStmt.run(`${techniqueId}-auto-${idx++}`, techniqueId, name, g.category, `Auto-extracted: ${g.names.length} detections for ${g.keyword || g.category.replace(/_/g, ' ')}`, JSON.stringify(indicators), g.names.length, confidence);
|
|
1021
|
+
}
|
|
1022
|
+
// Fallback: if clustering produced nothing, create a single procedure from all detections
|
|
1023
|
+
if (idx === 0 && detections.length > 0) {
|
|
1024
|
+
const allProcessNames = new Set();
|
|
1025
|
+
const allKeywords = new Set();
|
|
1026
|
+
for (const d of detections) {
|
|
1027
|
+
for (const pn of d.process_names || [])
|
|
1028
|
+
allProcessNames.add(pn.toLowerCase());
|
|
1029
|
+
const desc = (d.description || '').toLowerCase();
|
|
1030
|
+
for (const kw of BEHAVIOR_KEYWORDS) {
|
|
1031
|
+
if (desc.includes(kw))
|
|
1032
|
+
allKeywords.add(kw);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
const fallbackIndicators = {};
|
|
1036
|
+
if (allProcessNames.size > 0)
|
|
1037
|
+
fallbackIndicators.process_names = [...allProcessNames].slice(0, 10);
|
|
1038
|
+
if (allKeywords.size > 0)
|
|
1039
|
+
fallbackIndicators.description_keywords = [...allKeywords].slice(0, 10);
|
|
1040
|
+
insertStmt.run(`${techniqueId}-auto-0`, techniqueId, `${techniqueId} Detection`, detections[0]?.logsource_category || 'general_monitoring', `Auto-extracted: ${detections.length} detection(s)`, JSON.stringify(fallbackIndicators), detections.length, detections.length >= 3 ? 0.5 : 0.2);
|
|
1041
|
+
idx = 1;
|
|
1042
|
+
}
|
|
1043
|
+
return { technique_id: techniqueId, procedures_generated: idx, detection_count: detections.length };
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Extract procedures for ALL techniques in the database.
|
|
1047
|
+
* Loads hand-curated procedures first, then auto-extracts for the rest.
|
|
1048
|
+
* Called after indexing completes.
|
|
1049
|
+
*/
|
|
1050
|
+
export function extractAllProcedures() {
|
|
1051
|
+
const database = getDb();
|
|
1052
|
+
// Ensure table exists
|
|
1053
|
+
database.exec(`
|
|
1054
|
+
CREATE TABLE IF NOT EXISTS procedure_reference (
|
|
1055
|
+
id TEXT PRIMARY KEY, technique_id TEXT NOT NULL, name TEXT NOT NULL,
|
|
1056
|
+
category TEXT NOT NULL, description TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'auto',
|
|
1057
|
+
indicators TEXT NOT NULL, detection_count INTEGER DEFAULT 0,
|
|
1058
|
+
confidence REAL DEFAULT 1.0, created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
1059
|
+
)
|
|
1060
|
+
`);
|
|
1061
|
+
database.exec(`CREATE INDEX IF NOT EXISTS idx_proc_ref_technique ON procedure_reference(technique_id)`);
|
|
1062
|
+
// Load hand-curated procedures
|
|
1063
|
+
let handCuratedCount = 0;
|
|
1064
|
+
try {
|
|
1065
|
+
// Dynamic import isn't available in this context, so we use require-style
|
|
1066
|
+
// The hand-curated data is imported statically by the tools layer
|
|
1067
|
+
// Here we just ensure we don't overwrite hand-curated entries
|
|
1068
|
+
database.exec(`DELETE FROM procedure_reference WHERE source = 'auto'`);
|
|
1069
|
+
}
|
|
1070
|
+
catch {
|
|
1071
|
+
// Table may not exist yet, that's fine
|
|
1072
|
+
}
|
|
1073
|
+
// Get all technique IDs from the database
|
|
1074
|
+
const allMitreRows = database.prepare(`SELECT DISTINCT mitre_ids FROM detections WHERE mitre_ids IS NOT NULL AND mitre_ids != '[]'`).all();
|
|
1075
|
+
const allTechIds = new Set();
|
|
1076
|
+
for (const row of allMitreRows) {
|
|
1077
|
+
try {
|
|
1078
|
+
const ids = JSON.parse(row.mitre_ids);
|
|
1079
|
+
for (const id of ids)
|
|
1080
|
+
allTechIds.add(id);
|
|
1081
|
+
}
|
|
1082
|
+
catch { /* skip */ }
|
|
1083
|
+
}
|
|
1084
|
+
// Check which techniques already have hand-curated entries
|
|
1085
|
+
const handCuratedTechIds = new Set();
|
|
1086
|
+
try {
|
|
1087
|
+
const hcRows = database.prepare(`SELECT DISTINCT technique_id FROM procedure_reference WHERE source = 'hand_curated'`).all();
|
|
1088
|
+
for (const row of hcRows)
|
|
1089
|
+
handCuratedTechIds.add(row.technique_id);
|
|
1090
|
+
handCuratedCount = hcRows.length;
|
|
1091
|
+
}
|
|
1092
|
+
catch { /* table might not have hand_curated entries yet */ }
|
|
1093
|
+
let processed = 0;
|
|
1094
|
+
let totalProcs = 0;
|
|
1095
|
+
for (const techId of allTechIds) {
|
|
1096
|
+
if (handCuratedTechIds.has(techId))
|
|
1097
|
+
continue; // skip hand-curated techniques
|
|
1098
|
+
const result = autoExtractProcedures(techId);
|
|
1099
|
+
if (result.procedures_generated > 0) {
|
|
1100
|
+
processed++;
|
|
1101
|
+
totalProcs += result.procedures_generated;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return { techniques_processed: processed, procedures_generated: totalProcs, hand_curated_loaded: handCuratedCount };
|
|
1105
|
+
}
|
|
1106
|
+
// =============================================================================
|
|
1107
|
+
// JUNCTION TABLE POPULATION
|
|
1108
|
+
// =============================================================================
|
|
1109
|
+
/**
|
|
1110
|
+
* Populate detection_techniques and technique_tactics junction tables
|
|
1111
|
+
* from existing detection data. Runs as a post-indexing bulk operation
|
|
1112
|
+
* in a single transaction for performance.
|
|
1113
|
+
*/
|
|
1114
|
+
export function populateJunctionTables() {
|
|
1115
|
+
const database = getDb();
|
|
1116
|
+
const rows = database.prepare("SELECT id, mitre_ids, mitre_tactics FROM detections WHERE mitre_ids IS NOT NULL AND mitre_ids != '[]'").all();
|
|
1117
|
+
const dtStmt = database.prepare('INSERT OR IGNORE INTO detection_techniques (detection_id, technique_id) VALUES (?, ?)');
|
|
1118
|
+
const ttStmt = database.prepare("INSERT OR IGNORE INTO technique_tactics (technique_id, tactic_name, source) VALUES (?, ?, 'detection')");
|
|
1119
|
+
let dtCount = 0;
|
|
1120
|
+
let ttCount = 0;
|
|
1121
|
+
const batch = database.transaction(() => {
|
|
1122
|
+
for (const row of rows) {
|
|
1123
|
+
const ids = safeJsonParse(row.mitre_ids, []);
|
|
1124
|
+
const tactics = safeJsonParse(row.mitre_tactics, []);
|
|
1125
|
+
for (const techId of ids) {
|
|
1126
|
+
dtStmt.run(row.id, techId);
|
|
1127
|
+
dtCount++;
|
|
1128
|
+
for (const tactic of tactics) {
|
|
1129
|
+
ttStmt.run(techId, tactic);
|
|
1130
|
+
ttCount++;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
batch();
|
|
1136
|
+
return { detection_techniques: dtCount, technique_tactics: ttCount };
|
|
1137
|
+
}
|
|
1138
|
+
// =============================================================================
|
|
818
1139
|
// LIGHTWEIGHT LIST FUNCTIONS
|
|
819
1140
|
// =============================================================================
|
|
820
1141
|
/**
|
package/dist/db/index.d.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { getDbPath, getCacheDir, initDb, getDb, clearDb, recreateDb, dbExists, closeDb, } from './connection.js';
|
|
8
8
|
export { createSchema, createSavedQueriesTable, } from './schema.js';
|
|
9
|
-
export { type ValidationResult, type TechniqueIdFilters, type CoverageReport, type GapAnalysis, type DetectionSuggestion, type NavigatorLayerOptions, type DetectionListItem, type SourceComparisonResult, insertDetection, getDetectionById, getRawYaml, getDetectionCount, searchDetections, listDetections, listBySource, listByMitre, listByLogsource, listBySeverity, listByCve, listByAnalyticStory, listByProcessName, listByDetectionType, listByDataSource, listByKqlCategory, listByKqlTag, listByKqlDatasource, listByMitreTactic, getStats, getDistinctTechniqueIds, getDistinctCves, getDistinctProcessNames, validateTechniqueId, getTechniqueIds, analyzeCoverage, identifyGaps, suggestDetections, generateNavigatorLayer, searchDetectionList, listDetectionsBySourceLight, compareDetectionsBySource, getDetectionNamesByPattern, countDetectionsBySource, } from './detections.js';
|
|
9
|
+
export { type ValidationResult, type TechniqueIdFilters, type CoverageReport, type GapAnalysis, type DetectionSuggestion, type NavigatorLayerOptions, type DetectionListItem, type SourceComparisonResult, insertDetection, getDetectionById, getRawYaml, getDetectionCount, searchDetections, listDetections, listBySource, listByMitre, listByLogsource, listBySeverity, listByCve, listByAnalyticStory, listByProcessName, listByDetectionType, listByDataSource, listByKqlCategory, listByKqlTag, listByKqlDatasource, listByMitreTactic, getStats, getDistinctTechniqueIds, getDistinctCves, getDistinctProcessNames, validateTechniqueId, getTechniqueIds, analyzeCoverage, identifyGaps, suggestDetections, generateNavigatorLayer, autoExtractProcedures, extractAllProcedures, populateJunctionTables, searchDetectionList, listDetectionsBySourceLight, compareDetectionsBySource, getDetectionNamesByPattern, countDetectionsBySource, } from './detections.js';
|
|
10
10
|
export { insertStory, getStoryByName, getStoryById, getStoryCount, searchStories, listStories, listStoriesByCategory, } from './stories.js';
|
|
11
11
|
export { initSavedQueriesTable, saveQueryResult, getSavedQuery, listSavedQueries, deleteSavedQuery, cleanupExpiredQueries, getSavedQueryById, } from './cache.js';
|
|
12
12
|
export { initDynamicSchema, createDynamicTable, getTableMetadata, listDynamicTables, dropDynamicTable, insertDynamicRow, insertDynamicRows, queryDynamicTable, deleteDynamicRows, getDynamicRow, PREBUILT_TABLES, } from './dynamic.js';
|
|
13
13
|
export { type PatternData, type FieldReference, type StyleConvention, type TechniquePatterns, type ExtractionResult, initPatternsSchema, storePattern, storeFieldReference, storeStyleConvention, getPatternsByTechnique, getFieldReference, getStyleConventions, getMacroReference, extractSPLPatterns, extractSigmaPatterns, extractKQLPatterns, extractElasticPatterns, extractFieldUsage, extractMacroUsage, extractNamingConventions, extractAllPatterns, getPatternStats, } from './patterns.js';
|
|
14
|
+
export { type AttackActor, type AttackTechnique, type AttackSoftware, type ActorTechnique, type ActorCoverageResult, type ActorListItem, isStixLoaded, getActorByName, listActors, getActorTechniques, getActorCoverage, getSoftwareForActor, getTechniqueActors, getAttackTechnique, getAttackStats, } from './attack.js';
|
package/dist/db/index.js
CHANGED
|
@@ -28,6 +28,10 @@ getDistinctTechniqueIds, getDistinctCves, getDistinctProcessNames,
|
|
|
28
28
|
validateTechniqueId,
|
|
29
29
|
// Coverage Analysis
|
|
30
30
|
getTechniqueIds, analyzeCoverage, identifyGaps, suggestDetections, generateNavigatorLayer,
|
|
31
|
+
// Procedure Extraction
|
|
32
|
+
autoExtractProcedures, extractAllProcedures,
|
|
33
|
+
// Junction Tables
|
|
34
|
+
populateJunctionTables,
|
|
31
35
|
// Lightweight Lists
|
|
32
36
|
searchDetectionList, listDetectionsBySourceLight, compareDetectionsBySource, getDetectionNamesByPattern, countDetectionsBySource, } from './detections.js';
|
|
33
37
|
// =============================================================================
|
|
@@ -56,3 +60,9 @@ getPatternsByTechnique, getFieldReference, getStyleConventions, getMacroReferenc
|
|
|
56
60
|
extractSPLPatterns, extractSigmaPatterns, extractKQLPatterns, extractElasticPatterns, extractFieldUsage, extractMacroUsage, extractNamingConventions, extractAllPatterns,
|
|
57
61
|
// Stats
|
|
58
62
|
getPatternStats, } from './patterns.js';
|
|
63
|
+
// =============================================================================
|
|
64
|
+
// ATT&CK DATA (STIX-sourced)
|
|
65
|
+
// =============================================================================
|
|
66
|
+
export {
|
|
67
|
+
// Queries
|
|
68
|
+
isStixLoaded, getActorByName, listActors, getActorTechniques, getActorCoverage, getSoftwareForActor, getTechniqueActors, getAttackTechnique, getAttackStats, } from './attack.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Procedure-Level Reference Data for MITRE ATT&CK Techniques
|
|
3
|
+
*
|
|
4
|
+
* Hand-curated: 16 techniques (high-confidence indicators)
|
|
5
|
+
* Auto-extracted: 472 techniques (behavioral clustering)
|
|
6
|
+
* Total: 488 techniques, 2374 procedures
|
|
7
|
+
*
|
|
8
|
+
* Sources: 8,295 community detection rules (Sigma, Splunk ESCU, Elastic, KQL, Sublime, CQL)
|
|
9
|
+
*/
|
|
10
|
+
export interface TechniqueProcedure {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
category: string;
|
|
14
|
+
description: string;
|
|
15
|
+
indicators: {
|
|
16
|
+
process_names?: string[];
|
|
17
|
+
command_patterns?: string[];
|
|
18
|
+
registry_paths?: string[];
|
|
19
|
+
file_paths?: string[];
|
|
20
|
+
event_ids?: string[];
|
|
21
|
+
description_keywords?: string[];
|
|
22
|
+
field_patterns?: string[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export declare const PROCEDURE_REFERENCE: Record<string, TechniqueProcedure[]>;
|
|
26
|
+
export declare function getTechniquesWithProcedures(): string[];
|
|
27
|
+
export declare function getProceduresForTechnique(techniqueId: string): TechniqueProcedure[] | null;
|
|
28
|
+
export declare const HAND_CURATED_TECHNIQUES: string[];
|