Skip to content

Commit fcf2960

Browse files
Merge pull request #5 from elastic/add_hunt_support
[FR] Add Support for Threat Hunting Queries and Language Filtering
2 parents 951485d + b4676ad commit fcf2960

File tree

3 files changed

+157
-8
lines changed

3 files changed

+157
-8
lines changed

parseRuleData.ts

Lines changed: 134 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,148 @@ async function getPrebuiltDetectionRules(
4141
tagSummaries: Map<string, TagSummary>
4242
) {
4343
let count = 0;
44+
type Technique = {
45+
id: string;
46+
name: string;
47+
reference: string;
48+
subtechnique?: { id: string; reference: string }[];
49+
};
50+
51+
type Tactic = {
52+
id: string;
53+
name: string;
54+
reference: string;
55+
};
56+
57+
type Threat = {
58+
framework: string;
59+
technique?: Technique[];
60+
tactic?: Tactic;
61+
};
62+
63+
const convertHuntMitre = function (mitreData: string[]): Threat[] {
64+
const threat: Threat[] = [];
65+
66+
mitreData.forEach((item) => {
67+
if (item.startsWith('TA')) {
68+
threat.push({
69+
framework: "MITRE ATT&CK",
70+
tactic: {
71+
id: item,
72+
name: "",
73+
reference: `https://attack.mitre.org/tactics/${item}/`,
74+
},
75+
technique: [], // Ensure technique is an empty array if not present
76+
});
77+
} else if (item.startsWith('T')) {
78+
const parts = item.split('.');
79+
const techniqueId = parts[0];
80+
const subtechniqueId = parts[1];
81+
82+
const technique: Technique = {
83+
id: techniqueId,
84+
name: "",
85+
reference: `https://attack.mitre.org/techniques/${techniqueId}/`,
86+
};
87+
88+
if (subtechniqueId) {
89+
technique.subtechnique = [
90+
{
91+
id: `${techniqueId}.${subtechniqueId}`,
92+
reference: `https://attack.mitre.org/techniques/${techniqueId}/${subtechniqueId}/`,
93+
},
94+
];
95+
}
96+
97+
// Find the last added threat with a tactic to add the technique to it
98+
const lastThreat = threat[threat.length - 1];
99+
if (lastThreat && lastThreat.tactic && lastThreat.technique) {
100+
lastThreat.technique.push(technique);
101+
} else {
102+
threat.push({
103+
framework: "MITRE ATT&CK",
104+
tactic: {
105+
id: "",
106+
name: "",
107+
reference: "",
108+
},
109+
technique: [technique],
110+
});
111+
}
112+
}
113+
});
114+
115+
return threat;
116+
};
117+
44118
const addRule = function (buffer) {
45119
const ruleContent = toml.parse(buffer);
120+
121+
// Check if ruleContent.rule and ruleContent.hunt exist
122+
const ruleId = ruleContent.rule?.rule_id || ruleContent.hunt?.uuid;
123+
if (!ruleId) {
124+
throw new Error('Neither rule_id nor hunt.uuid is available');
125+
}
126+
127+
// Initialize ruleContent.rule and ruleContent.metadata if they are undefined
128+
ruleContent.rule = ruleContent.rule || {};
129+
ruleContent.metadata = ruleContent.metadata || {};
130+
131+
// Helper function to set default values if they do not exist
132+
const setDefault = (obj, key, defaultValue) => {
133+
if (!obj[key]) {
134+
obj[key] = defaultValue;
135+
}
136+
};
137+
138+
// Use default tags if ruleContent.rule.tags does not exist
139+
const tags = ruleContent.rule.tags || ["Hunt Type: Hunt"];
140+
setDefault(ruleContent.rule, 'tags', ["Hunt Type: Hunt"]);
141+
142+
// Add a tag based on the language
143+
const language = ruleContent.rule?.language;
144+
if (language) {
145+
tags.push(`Language: ${language}`);
146+
}
147+
148+
// Add creation_date and updated_date if they do not exist
149+
const defaultDate = new Date(0).toISOString();
150+
setDefault(ruleContent.metadata, 'creation_date', defaultDate);
151+
setDefault(ruleContent.metadata, 'updated_date', defaultDate);
152+
153+
// Use current date as default updated_date if it does not exist
154+
const updatedDate = new Date(ruleContent.metadata.updated_date.replace(/\//g, '-'));
155+
156+
// Use hunt.name if rule.name does not exist
157+
const ruleName = ruleContent.rule.name || ruleContent.hunt.name || 'Unknown Rule';
158+
159+
// Set other default values if they do not exist
160+
setDefault(ruleContent.metadata, 'integration', ruleContent.hunt?.integration);
161+
setDefault(ruleContent.rule, 'query', ruleContent.hunt?.query);
162+
setDefault(ruleContent.rule, 'license', "Elastic License v2");
163+
setDefault(ruleContent.rule, 'description', ruleContent.hunt?.description);
164+
165+
// Convert hunt.mitre to rule.threat if hunt.mitre exists
166+
if (ruleContent.hunt?.mitre) {
167+
ruleContent.rule.threat = convertHuntMitre(ruleContent.hunt.mitre);
168+
}
169+
46170
ruleSummaries.push({
47-
id: ruleContent.rule.rule_id,
48-
name: ruleContent.rule.name,
49-
tags: ruleContent.rule.tags,
50-
updated_date: new Date(
51-
ruleContent.metadata.updated_date.replace(/\//g, '-')
52-
),
171+
id: ruleId,
172+
name: ruleName,
173+
tags: tags,
174+
updated_date: updatedDate,
53175
});
54-
for (const t of ruleContent.rule.tags) {
176+
177+
for (const t of tags) {
55178
addTagSummary(t, tagSummaries);
56179
}
180+
57181
fs.writeFileSync(
58-
`${RULES_OUTPUT_PATH}${ruleContent.rule.rule_id}.json`,
182+
`${RULES_OUTPUT_PATH}${ruleId}.json`,
59183
JSON.stringify(ruleContent)
60184
);
185+
61186
count++;
62187
};
63188

@@ -70,6 +195,7 @@ async function getPrebuiltDetectionRules(
70195
parser.on('entry', entry => {
71196
if (
72197
(entry.path.match(/^elastic-detection-rules-.*\/rules\/.*\.toml$/) ||
198+
entry.path.match(/^elastic-detection-rules-.*\/hunting\/.*\.toml$/) ||
73199
entry.path.match(
74200
/^elastic-detection-rules-.*\/rules_building_block\/.*\.toml$/
75201
)) &&

src/components/home/home_hero.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,21 @@ const HomeHero: FunctionComponent<RuleFilterProps> = ({
151151
tagFilter={tagFilter}
152152
onTagChange={onTagChange}
153153
/>
154+
155+
<RuleFilter
156+
displayName="Threat Hunt Queries"
157+
icon="eye"
158+
tagList={tagSummaries.filter(x => x.tag_type == 'Hunt Type')}
159+
tagFilter={tagFilter}
160+
onTagChange={onTagChange}
161+
/>
162+
<RuleFilter
163+
displayName="Rule Languages"
164+
icon="menu"
165+
tagList={tagSummaries.filter(x => x.tag_type == 'Language')}
166+
tagFilter={tagFilter}
167+
onTagChange={onTagChange}
168+
/>
154169
</EuiFlexGrid>
155170
</EuiFlexItem>
156171
</EuiFlexGroup>

src/lib/ruledata.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export const ruleFilterTypeMap = {
1111
color: 'default',
1212
icon: 'database',
1313
},
14+
'Hunt Type': {
15+
color: 'default',
16+
icon: 'eye',
17+
},
1418
OS: {
1519
color: 'success',
1620
icon: 'compute',
@@ -23,4 +27,8 @@ export const ruleFilterTypeMap = {
2327
color: 'hollow',
2428
icon: 'layers',
2529
},
30+
Language: {
31+
color: 'default',
32+
icon: 'menu',
33+
},
2634
};

0 commit comments

Comments
 (0)