298 lines
9.2 KiB
JavaScript
298 lines
9.2 KiB
JavaScript
/**
|
|
* sigma_details_block.js
|
|
*
|
|
* Creates Slack Block Kit blocks for displaying Sigma rule explanations
|
|
*
|
|
*/
|
|
const logger = require('../../utils/logger');
|
|
|
|
const { getFileName } = require('../../utils/file_utils');
|
|
const FILE_NAME = getFileName(__filename);
|
|
|
|
/**
|
|
* Create Slack block kit blocks for rule explanation
|
|
*
|
|
* @param {Object} details - The rule details object containing all rule metadata
|
|
* @returns {Array} Formatted Slack blocks ready for display
|
|
*/
|
|
function getRuleExplanationBlocks(details) {
|
|
logger.debug(`${FILE_NAME}: Creating rule explanation blocks for rule: ${details?.id || 'unknown'}`);
|
|
|
|
if (!details) {
|
|
logger.error('Failed to create explanation blocks: No details object provided');
|
|
return [
|
|
{
|
|
type: 'section',
|
|
text: {
|
|
type: 'mrkdwn',
|
|
text: 'Error: No explanation data provided'
|
|
}
|
|
}
|
|
];
|
|
}
|
|
|
|
// Map severity levels to emojis for visual representation
|
|
const severityConfig = {
|
|
'critical': { emoji: '🔴', text: 'Critical' },
|
|
'high': { emoji: '🟠', text: 'High' },
|
|
'medium': { emoji: '🟡', text: 'Medium' },
|
|
'low': { emoji: '🟢', text: 'Low' },
|
|
'informational': { emoji: '🔵', text: 'Info' }
|
|
};
|
|
|
|
// Normalize severity to lowercase for matching
|
|
const normalizedSeverity = (details.severity || '').toLowerCase();
|
|
const severityInfo = severityConfig[normalizedSeverity] || { emoji: '⚪', text: details.severity || 'Unknown' };
|
|
|
|
// Create a formatted severity indicator
|
|
const severityDisplay = `${severityInfo.emoji} *${severityInfo.text}*`;
|
|
|
|
logger.debug(`Rule severity: ${normalizedSeverity} (${severityDisplay})`);
|
|
|
|
/**
|
|
* Format tags with MITRE links where applicable
|
|
*
|
|
* @param {Array} tags - Array of tag strings to format
|
|
* @returns {Array} Array of formatted tags with links where appropriate
|
|
*/
|
|
const formatTags = (tags = []) => {
|
|
if (!tags || tags.length === 0 || (tags.length === 2 && tags.includes('error'))) {
|
|
logger.debug('No valid tags to format');
|
|
return [];
|
|
}
|
|
|
|
logger.debug(`Formatting ${tags.length} tags`);
|
|
|
|
return tags.map(tag => {
|
|
let formattedTag = tag.trim();
|
|
let url = '';
|
|
let displayText = formattedTag;
|
|
|
|
// Handle MITRE ATT&CK Technique IDs
|
|
if (/^T\d{4}(\.\d{3})?$/.test(formattedTag)) {
|
|
// Technique ID (e.g., T1234 or T1234.001)
|
|
url = `https://attack.mitre.org/techniques/${formattedTag}/`;
|
|
logger.debug(`Formatted MITRE technique: ${formattedTag}`);
|
|
}
|
|
// Handle MITRE ATT&CK Tactic IDs
|
|
else if (/^TA\d{4}$/.test(formattedTag)) {
|
|
// Tactic ID (e.g., TA0001)
|
|
url = `https://attack.mitre.org/tactics/${formattedTag}/`;
|
|
logger.debug(`Formatted MITRE tactic: ${formattedTag}`);
|
|
}
|
|
// Handle CWE IDs
|
|
else if (/^S\d{4}$/.test(formattedTag)) {
|
|
// CWE ID
|
|
url = `https://cwe.mitre.org/data/definitions/${formattedTag.substring(1)}.html`;
|
|
logger.debug(`Formatted CWE: ${formattedTag}`);
|
|
}
|
|
// Handle attack.* tactics
|
|
else if (formattedTag.startsWith('attack.')) {
|
|
const tacticName = formattedTag.substring(7); // Remove 'attack.' prefix
|
|
|
|
// Handle specific techniques with T#### format
|
|
if (/^t\d{4}(\.\d{3})?$/.test(tacticName)) {
|
|
const techniqueId = tacticName.toUpperCase();
|
|
url = `https://attack.mitre.org/techniques/${techniqueId}/`;
|
|
displayText = techniqueId;
|
|
logger.debug(`Formatted MITRE technique from attack. format: ${techniqueId}`);
|
|
}
|
|
// Handle regular tactics
|
|
else {
|
|
// Map common tactics to their MITRE ATT&CK IDs
|
|
const tacticMappings = {
|
|
'reconnaissance': 'TA0043',
|
|
'resourcedevelopment': 'TA0042',
|
|
'initialaccess': 'TA0001',
|
|
'execution': 'TA0002',
|
|
'persistence': 'TA0003',
|
|
'privilegeescalation': 'TA0004',
|
|
'defenseevasion': 'TA0005',
|
|
'credentialaccess': 'TA0006',
|
|
'discovery': 'TA0007',
|
|
'lateralmovement': 'TA0008',
|
|
'collection': 'TA0009',
|
|
'command-and-control': 'TA0011',
|
|
'exfiltration': 'TA0010',
|
|
'impact': 'TA0040'
|
|
};
|
|
|
|
// Remove hyphens and convert to lowercase for matching
|
|
const normalizedTactic = tacticName.toLowerCase().replace(/-/g, '');
|
|
|
|
if (tacticMappings[normalizedTactic]) {
|
|
url = `https://attack.mitre.org/tactics/${tacticMappings[normalizedTactic]}/`;
|
|
logger.debug(`Mapped tactic ${tacticName} to ${tacticMappings[normalizedTactic]}`);
|
|
} else {
|
|
// If we don't have a specific mapping, try a search
|
|
url = `https://attack.mitre.org/search/?q=${encodeURIComponent(tacticName)}`;
|
|
logger.debug(`Created search URL for unmapped tactic: ${tacticName}`);
|
|
}
|
|
|
|
// Format the display text with proper capitalization
|
|
displayText = tacticName.replace(/-/g, ' ')
|
|
.split(' ')
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
.join(' ');
|
|
}
|
|
}
|
|
// Handle CVE IDs
|
|
else if (/^CVE-\d{4}-\d{4,}$/i.test(formattedTag)) {
|
|
url = `https://nvd.nist.gov/vuln/detail/${formattedTag.toUpperCase()}`;
|
|
displayText = formattedTag.toUpperCase();
|
|
logger.debug(`Formatted CVE: ${displayText}`);
|
|
}
|
|
|
|
if (url) {
|
|
return `<${url}|${displayText}>`;
|
|
} else {
|
|
return displayText;
|
|
}
|
|
});
|
|
};
|
|
|
|
// Define header based on title - check if it contains error messages
|
|
const isErrorMessage = details.title.toLowerCase().includes('error') ||
|
|
details.title.toLowerCase().includes('missing');
|
|
|
|
if (isErrorMessage) {
|
|
logger.warn(`Rule appears to have errors: ${details.title}`);
|
|
}
|
|
|
|
// Start with header block
|
|
const blocks = [
|
|
{
|
|
type: 'header',
|
|
text: {
|
|
type: 'plain_text',
|
|
text: details.title || 'Rule Explanation',
|
|
emoji: true
|
|
}
|
|
},
|
|
{
|
|
type: 'context',
|
|
elements: [
|
|
{
|
|
type: 'mrkdwn',
|
|
text: `ID: ${details.id || 'Unknown'}`
|
|
}
|
|
]
|
|
},
|
|
{
|
|
type: 'section',
|
|
fields: [
|
|
{
|
|
type: 'mrkdwn',
|
|
text: `*Severity:* ${severityDisplay}`
|
|
},
|
|
{
|
|
type: 'mrkdwn',
|
|
text: `*Author:* ${details.author || 'Unknown'}`
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
// Add divider for visual separation
|
|
blocks.push({ type: 'divider' });
|
|
|
|
// Add description section
|
|
blocks.push({
|
|
type: 'section',
|
|
text: {
|
|
type: 'mrkdwn',
|
|
text: `*Description:*\n${details.description || 'No description available'}`
|
|
}
|
|
});
|
|
|
|
// Detection explanation section - only add if not an error case or has useful detection info
|
|
if (!isErrorMessage || details.detectionExplanation !== 'Content missing') {
|
|
blocks.push({
|
|
type: 'section',
|
|
text: {
|
|
type: 'mrkdwn',
|
|
text: `*What This Rule Detects:*\n${details.detectionExplanation || 'No detection information available'}`
|
|
}
|
|
});
|
|
}
|
|
|
|
// False positives section - only add if not an error case with N/A values
|
|
if (details.falsePositives && !details.falsePositives.includes('N/A - Content missing')) {
|
|
const fpItems = Array.isArray(details.falsePositives)
|
|
? details.falsePositives.map(item => `• ${item}`).join('\n')
|
|
: `• ${details.falsePositives}`;
|
|
|
|
blocks.push({
|
|
type: 'section',
|
|
text: {
|
|
type: 'mrkdwn',
|
|
text: `*Possible False Positives:*\n${fpItems || 'None specified'}`
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add tags if they exist and are formatted
|
|
const formattedTags = formatTags(details.tags);
|
|
if (formattedTags.length > 0) {
|
|
logger.debug(`Added ${formattedTags.length} formatted tags to the block`);
|
|
blocks.push({
|
|
type: 'context',
|
|
elements: [
|
|
{
|
|
type: 'mrkdwn',
|
|
text: `*Tags:* ${formattedTags.join(' | ')}`
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
// If this is an error message, add a troubleshooting section
|
|
if (isErrorMessage) {
|
|
blocks.push({
|
|
type: 'section',
|
|
text: {
|
|
type: 'mrkdwn',
|
|
text: ':warning: *Troubleshooting:*\nThis rule appears to have issues in the database. You may want to check the rule import process or run a database maintenance task to fix this issue.'
|
|
}
|
|
});
|
|
}
|
|
|
|
// Add action buttons for better interactivity
|
|
blocks.push({
|
|
type: 'actions',
|
|
elements: [
|
|
{
|
|
type: 'button',
|
|
text: {
|
|
type: 'plain_text',
|
|
text: 'View YAML',
|
|
emoji: true
|
|
},
|
|
action_id: 'view_yaml',
|
|
value: `view_yaml_${details.id}`
|
|
},
|
|
{
|
|
type: 'button',
|
|
text: {
|
|
type: 'plain_text',
|
|
text: 'Convert to SIEM Rule',
|
|
emoji: true
|
|
},
|
|
action_id: 'convert_rule_to_siem',
|
|
value: `convert_rule_to_siem_${details.id}`
|
|
},
|
|
]
|
|
});
|
|
|
|
// Add a divider at the end
|
|
blocks.push({
|
|
type: 'divider'
|
|
});
|
|
|
|
logger.debug(`Created ${blocks.length} blocks for rule explanation`);
|
|
return blocks;
|
|
}
|
|
|
|
module.exports = {
|
|
getRuleExplanationBlocks
|
|
};
|