parseRuleData.ts (268 lines of code) (raw):
import * as tar from 'tar';
import { PassThrough } from 'stream';
import * as toml from 'toml';
import * as fs from 'fs';
import axios from 'axios';
interface RuleSummary {
id: string;
name: string;
tags: Array<string>;
updated_date: Date;
}
interface TagSummary {
tag_type: string;
tag_name: string;
tag_full: string;
count: number;
}
function addTagSummary(t: string, tagSummaries: Map<string, TagSummary>) {
const parts = t.split(': ');
let s = tagSummaries.get(t);
if (s == undefined) {
s = {
tag_type: parts[0],
tag_name: parts[1],
tag_full: t,
count: 0,
};
}
s.count++;
tagSummaries.set(t, s);
}
const RULES_OUTPUT_PATH = './src/data/rules/';
async function getPrebuiltDetectionRules(
ruleSummaries: RuleSummary[],
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: ruleId,
name: ruleName,
tags: tags,
updated_date: updatedDate,
});
for (const t of tags) {
addTagSummary(t, tagSummaries);
}
fs.writeFileSync(
`${RULES_OUTPUT_PATH}${ruleId}.json`,
JSON.stringify(ruleContent)
);
count++;
};
const githubRulesTarballUrl =
'https://api.github.com/repos/elastic/detection-rules/tarball';
const res = await axios.get(githubRulesTarballUrl, {
responseType: 'stream',
});
const parser = res.data.pipe(new tar.Parse());
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$/
)) &&
!entry.path.match(/\/_deprecated\//)
) {
const contentStream = new PassThrough();
entry.pipe(contentStream);
let buf = Buffer.alloc(0);
contentStream.on('data', function (d) {
buf = Buffer.concat([buf, d]);
});
contentStream.on('end', () => {
addRule(buf);
});
} else {
entry.resume();
}
});
await new Promise(resolve => parser.on('finish', resolve));
console.log(`loaded ${count} rules from prebuilt repository`);
}
const integrationsTagMap = new Map<string, string>([
['Living off the Land', 'Tactic: Defensive Evasion'],
['DGA', 'Tactic: Command and Control'],
['Lateral Movement Detection', 'Tactic: Lateral Movement'],
['Data Exfiltration', 'Tactic: Exfiltration'],
['Host', 'Domain: Endpoint'],
['User', 'Domain: User'],
['ML', 'Rule Type: Machine Learning'],
]);
async function getPackageRules(
name: string,
displayName: string,
ruleSummaries: RuleSummary[],
tagSummaries: Map<string, TagSummary>
) {
const githubRulesListUrl = `https://api.github.com/repos/elastic/integrations/contents/packages/${name}/kibana/security_rule`;
const githubRulesCommitsUrl = `https://api.github.com/repos/elastic/integrations/commits?path=packages%2F${name}%2Fkibana%2Fsecurity_rule&page=1&per_page=1`;
const rulesCommitsResponse = await axios.get(githubRulesCommitsUrl);
const updatedDate = new Date(
rulesCommitsResponse.data[0].commit.committer.date
);
const ruleListResponse = await axios.get(githubRulesListUrl);
for (const r of ruleListResponse.data) {
const ruleContent = await axios.get(r.download_url);
const tags = ruleContent.data.attributes.tags
.filter(x => x != 'Elastic')
.map(x => {
if (integrationsTagMap.has(x)) {
return integrationsTagMap.get(x);
} else {
return x;
}
});
tags.push('Use Case: Threat Detection');
// for now, map the tags to look more like the prebuild rules package
ruleSummaries.push({
id: ruleContent.data.id,
name: ruleContent.data.attributes.name,
tags: tags,
updated_date: updatedDate,
});
for (const t of tags) {
addTagSummary(t, tagSummaries);
}
const mappedRuleContent = {
metadata: {
updated_date: updatedDate,
source_integration: name,
source_integration_name: displayName,
},
rule: ruleContent.data.attributes,
};
mappedRuleContent.rule.tags = tags;
fs.writeFileSync(
`${RULES_OUTPUT_PATH}${ruleContent.data.id}.json`,
JSON.stringify(mappedRuleContent)
);
}
console.log(
`loaded ${ruleListResponse.data.length} rules from integration package '${name}'`
);
}
async function precomputeRuleSummaries() {
const ruleSummaries: RuleSummary[] = [];
const tagSummaries = new Map<string, TagSummary>();
fs.mkdirSync(RULES_OUTPUT_PATH, { recursive: true });
await getPrebuiltDetectionRules(ruleSummaries, tagSummaries);
console.log(`loaded ${ruleSummaries.length} rules`);
console.log(`example rule:`);
console.log(ruleSummaries[0]);
console.log(`found ${tagSummaries.size} tags`);
console.log(`example tag:`);
console.log(tagSummaries.get('Data Source: APM'));
const newestRules = ruleSummaries.sort(
(a, b) => b.updated_date.getTime() - a.updated_date.getTime()
);
console.log(
`Parsed ${newestRules.length} rules. Newest rule is '${newestRules[0].name}', updated '${newestRules[0].updated_date}'.`
);
fs.writeFileSync('./src/data/newestRules.json', JSON.stringify(newestRules));
const popularTags = Array.from(tagSummaries.values()).sort(
(a, b) => b.count - a.count
);
console.log(
`Parsed ${popularTags.length} tags. Most popular tag is '${popularTags[0].tag_full}' with '${popularTags[0].count}' rules.`
);
fs.writeFileSync('./src/data/tagSummaries.json', JSON.stringify(popularTags));
}
(async () => {
await precomputeRuleSummaries();
})();