fylgja/src/blocks/sigma/sigma_details_block.js
2025-04-16 18:01:35 -04:00

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
};