Skip to content

Commit

Permalink
Merge pull request #5 from elastic/add_hunt_support
Browse files Browse the repository at this point in the history
[FR] Add Support for Threat Hunting Queries and Language Filtering
  • Loading branch information
eric-forte-elastic authored Jan 15, 2025
2 parents 951485d + b4676ad commit fcf2960
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 8 deletions.
142 changes: 134 additions & 8 deletions parseRuleData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,148 @@ async function getPrebuiltDetectionRules(
tagSummaries: Map<string, TagSummary>
) {
let count = 0;
type Technique = {
id: string;
name: string;
reference: string;
subtechnique?: { id: string; reference: string }[];
};

type Tactic = {
id: string;
name: string;
reference: string;
};

type Threat = {
framework: string;
technique?: Technique[];
tactic?: Tactic;
};

const convertHuntMitre = function (mitreData: string[]): Threat[] {
const threat: Threat[] = [];

mitreData.forEach((item) => {
if (item.startsWith('TA')) {
threat.push({
framework: "MITRE ATT&CK",
tactic: {
id: item,
name: "",
reference: `https://attack.mitre.org/tactics/${item}/`,
},
technique: [], // Ensure technique is an empty array if not present
});
} else if (item.startsWith('T')) {
const parts = item.split('.');
const techniqueId = parts[0];
const subtechniqueId = parts[1];

const technique: Technique = {
id: techniqueId,
name: "",
reference: `https://attack.mitre.org/techniques/${techniqueId}/`,
};

if (subtechniqueId) {
technique.subtechnique = [
{
id: `${techniqueId}.${subtechniqueId}`,
reference: `https://attack.mitre.org/techniques/${techniqueId}/${subtechniqueId}/`,
},
];
}

// Find the last added threat with a tactic to add the technique to it
const lastThreat = threat[threat.length - 1];
if (lastThreat && lastThreat.tactic && lastThreat.technique) {
lastThreat.technique.push(technique);
} else {
threat.push({
framework: "MITRE ATT&CK",
tactic: {
id: "",
name: "",
reference: "",
},
technique: [technique],
});
}
}
});

return threat;
};

const addRule = function (buffer) {
const ruleContent = toml.parse(buffer);

// Check if ruleContent.rule and ruleContent.hunt exist
const ruleId = ruleContent.rule?.rule_id || ruleContent.hunt?.uuid;
if (!ruleId) {
throw new Error('Neither rule_id nor hunt.uuid is available');
}

// Initialize ruleContent.rule and ruleContent.metadata if they are undefined
ruleContent.rule = ruleContent.rule || {};
ruleContent.metadata = ruleContent.metadata || {};

// Helper function to set default values if they do not exist
const setDefault = (obj, key, defaultValue) => {
if (!obj[key]) {
obj[key] = defaultValue;
}
};

// Use default tags if ruleContent.rule.tags does not exist
const tags = ruleContent.rule.tags || ["Hunt Type: Hunt"];
setDefault(ruleContent.rule, 'tags', ["Hunt Type: Hunt"]);

// Add a tag based on the language
const language = ruleContent.rule?.language;
if (language) {
tags.push(`Language: ${language}`);
}

// Add creation_date and updated_date if they do not exist
const defaultDate = new Date(0).toISOString();
setDefault(ruleContent.metadata, 'creation_date', defaultDate);
setDefault(ruleContent.metadata, 'updated_date', defaultDate);

// Use current date as default updated_date if it does not exist
const updatedDate = new Date(ruleContent.metadata.updated_date.replace(/\//g, '-'));

// Use hunt.name if rule.name does not exist
const ruleName = ruleContent.rule.name || ruleContent.hunt.name || 'Unknown Rule';

// Set other default values if they do not exist
setDefault(ruleContent.metadata, 'integration', ruleContent.hunt?.integration);
setDefault(ruleContent.rule, 'query', ruleContent.hunt?.query);
setDefault(ruleContent.rule, 'license', "Elastic License v2");
setDefault(ruleContent.rule, 'description', ruleContent.hunt?.description);

// Convert hunt.mitre to rule.threat if hunt.mitre exists
if (ruleContent.hunt?.mitre) {
ruleContent.rule.threat = convertHuntMitre(ruleContent.hunt.mitre);
}

ruleSummaries.push({
id: ruleContent.rule.rule_id,
name: ruleContent.rule.name,
tags: ruleContent.rule.tags,
updated_date: new Date(
ruleContent.metadata.updated_date.replace(/\//g, '-')
),
id: ruleId,
name: ruleName,
tags: tags,
updated_date: updatedDate,
});
for (const t of ruleContent.rule.tags) {

for (const t of tags) {
addTagSummary(t, tagSummaries);
}

fs.writeFileSync(
`${RULES_OUTPUT_PATH}${ruleContent.rule.rule_id}.json`,
`${RULES_OUTPUT_PATH}${ruleId}.json`,
JSON.stringify(ruleContent)
);

count++;
};

Expand All @@ -70,6 +195,7 @@ async function getPrebuiltDetectionRules(
parser.on('entry', entry => {
if (
(entry.path.match(/^elastic-detection-rules-.*\/rules\/.*\.toml$/) ||
entry.path.match(/^elastic-detection-rules-.*\/hunting\/.*\.toml$/) ||
entry.path.match(
/^elastic-detection-rules-.*\/rules_building_block\/.*\.toml$/
)) &&
Expand Down
15 changes: 15 additions & 0 deletions src/components/home/home_hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,21 @@ const HomeHero: FunctionComponent<RuleFilterProps> = ({
tagFilter={tagFilter}
onTagChange={onTagChange}
/>

<RuleFilter
displayName="Threat Hunt Queries"
icon="eye"
tagList={tagSummaries.filter(x => x.tag_type == 'Hunt Type')}
tagFilter={tagFilter}
onTagChange={onTagChange}
/>
<RuleFilter
displayName="Rule Languages"
icon="menu"
tagList={tagSummaries.filter(x => x.tag_type == 'Language')}
tagFilter={tagFilter}
onTagChange={onTagChange}
/>
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down
8 changes: 8 additions & 0 deletions src/lib/ruledata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export const ruleFilterTypeMap = {
color: 'default',
icon: 'database',
},
'Hunt Type': {
color: 'default',
icon: 'eye',
},
OS: {
color: 'success',
icon: 'compute',
Expand All @@ -23,4 +27,8 @@ export const ruleFilterTypeMap = {
color: 'hollow',
icon: 'layers',
},
Language: {
color: 'default',
icon: 'menu',
},
};

0 comments on commit fcf2960

Please sign in to comment.