Merge pull request 'addOSandCategoryLabels' (#4) from addOSandCategoryLabels into main

Reviewed-on: https://codeberg.org/charlottecroce/fylgja/pulls/4
This commit is contained in:
charlottecroce 2025-04-19 18:19:22 +00:00
commit 27d8ca32c0
8 changed files with 144 additions and 30 deletions

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ fylgja.yml
slack.yml slack.yml
sigma.db sigma.db
sigma-repo/ sigma-repo/
.VSCodeCounter

View file

@ -40,6 +40,13 @@ function getConversionResultBlocks(conversionResult) {
format: 'siem_rule_ndjson' format: 'siem_rule_ndjson'
}; };
// Extract logsource information or use defaults
const logsource = rule.logsource || {};
const product = logsource.product || 'N/A';
const category = logsource.category || 'N/A';
logger.debug(`${FILE_NAME}: Logsource info - Product: ${product}, Category: ${category}`);
// Truncate output if it's too long for Slack // Truncate output if it's too long for Slack
let output = conversionResult.output || ''; let output = conversionResult.output || '';
const maxOutputLength = 2900; // Slack has a limit of ~3000 chars in a code block const maxOutputLength = 2900; // Slack has a limit of ~3000 chars in a code block
@ -66,6 +73,19 @@ function getConversionResultBlocks(conversionResult) {
text: `*Rule ID:* ${rule.id}\n*Description:* ${rule.description}` text: `*Rule ID:* ${rule.id}\n*Description:* ${rule.description}`
} }
}, },
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*OS/Product:* ${product}`
},
{
type: 'mrkdwn',
text: `*Category:* ${category}`
}
]
},
{ {
type: 'section', type: 'section',
text: { text: {

View file

@ -112,7 +112,7 @@ function getSigmaRuleDetailsBlocks(details) {
'discovery': 'TA0007', 'discovery': 'TA0007',
'lateralmovement': 'TA0008', 'lateralmovement': 'TA0008',
'collection': 'TA0009', 'collection': 'TA0009',
'command-and-control': 'TA0011', 'commandandcontrol': 'TA0011',
'exfiltration': 'TA0010', 'exfiltration': 'TA0010',
'impact': 'TA0040' 'impact': 'TA0040'
}; };
@ -193,6 +193,28 @@ function getSigmaRuleDetailsBlocks(details) {
} }
]; ];
// Get logsource information from the details
const logsource = details.logsource || {};
const product = logsource.product || 'N/A';
const category = logsource.category || 'N/A';
logger.debug(`${FILE_NAME}: Logsource info - Product: ${product}, Category: ${category}`);
// Add logsource information section after severity/author
blocks.push({
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*OS/Product:* ${product}`
},
{
type: 'mrkdwn',
text: `*Category:* ${category}`
}
]
});
// Add divider for visual separation // Add divider for visual separation
blocks.push({ type: 'divider' }); blocks.push({ type: 'divider' });
@ -295,4 +317,4 @@ function getSigmaRuleDetailsBlocks(details) {
module.exports = { module.exports = {
getSigmaRuleDetailsBlocks getSigmaRuleDetailsBlocks
}; };

View file

@ -8,6 +8,7 @@
const logger = require('../../utils/logger'); const logger = require('../../utils/logger');
const { getFileName } = require('../../utils/file_utils'); const { getFileName } = require('../../utils/file_utils');
const { getProductEmoji } = require('../../utils/os_emojis');
const FILE_NAME = getFileName(__filename); const FILE_NAME = getFileName(__filename);
/** /**
@ -78,12 +79,15 @@ const getSearchResultBlocks = (keyword, results, pagination = {}) => {
const ruleId = safeRule.id || 'unknown'; const ruleId = safeRule.id || 'unknown';
logger.debug(`${FILE_NAME}: Adding result #${index + 1}: ${ruleId} - ${safeRule.title || 'Untitled'}`); logger.debug(`${FILE_NAME}: Adding result #${index + 1}: ${ruleId} - ${safeRule.title || 'Untitled'}`);
// Combine rule information and action button into a single line // Get product emoji
const osEmoji = getProductEmoji(safeRule.logsource && safeRule.logsource.product);
// Rule information and action button - with OS emoji before title and no ID field
blocks.push({ blocks.push({
"type": "section", "type": "section",
"text": { "text": {
"type": "mrkdwn", "type": "mrkdwn",
"text": `*${safeRule.title || 'Untitled Rule'}*\nID: \`${ruleId}\`` "text": `*${osEmoji}${safeRule.title || 'Untitled Rule'}*`
}, },
"accessory": { "accessory": {
"type": "button", "type": "button",

View file

@ -6,7 +6,7 @@
const logger = require('../../../utils/logger'); const logger = require('../../../utils/logger');
const { handleError } = require('../../../utils/error_handler'); const { handleError } = require('../../../utils/error_handler');
const { getSigmaRuleYaml } = require('../../../services/sigma/sigma_details_service'); const { getSigmaRuleYaml } = require('../../../services/sigma/sigma_details_service');
const { searchSigmaRules } = require('../../../services/sigma/sigma_search_service'); const { searchSigmaRules, searchAndConvertRules } = require('../../../services/sigma/sigma_search_service');
const { getYamlViewBlocks } = require('../../../blocks/sigma/sigma_view_yaml_block'); const { getYamlViewBlocks } = require('../../../blocks/sigma/sigma_view_yaml_block');
const { getSearchResultBlocks } = require('../../../blocks/sigma/sigma_search_results_block'); const { getSearchResultBlocks } = require('../../../blocks/sigma/sigma_search_results_block');
const { processRuleDetails } = require('./sigma_action_core'); const { processRuleDetails } = require('./sigma_action_core');
@ -62,7 +62,7 @@ const handlePaginationAction = async (body, ack, respond) => {
logger.info(`${FILE_NAME}: Processing pagination request for "${keyword}" (page ${page}, size ${pageSize})`); logger.info(`${FILE_NAME}: Processing pagination request for "${keyword}" (page ${page}, size ${pageSize})`);
// Perform the search with the new pagination parameters // Perform the search with the new pagination parameters
const searchResult = await searchSigmaRules(keyword, page, pageSize); const searchResult = await searchAndConvertRules(keyword, page, pageSize);
if (!searchResult.success) { if (!searchResult.success) {
logger.error(`${FILE_NAME}: Search failed during pagination: ${searchResult.message}`); logger.error(`${FILE_NAME}: Search failed during pagination: ${searchResult.message}`);

View file

@ -4,7 +4,7 @@
* Handles Sigma rule search requests from Slack commands * Handles Sigma rule search requests from Slack commands
*/ */
const { searchSigmaRules, searchSigmaRulesComplex } = require('../../services/sigma/sigma_search_service'); const { searchSigmaRules, searchSigmaRulesComplex, searchAndConvertRules } = require('../../services/sigma/sigma_search_service');
const logger = require('../../utils/logger'); const logger = require('../../utils/logger');
const { handleError } = require('../../utils/error_handler'); const { handleError } = require('../../utils/error_handler');
const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block'); const { getSearchResultBlocks } = require('../../blocks/sigma/sigma_search_results_block');
@ -69,7 +69,7 @@ const handleCommand = async (command, respond) => {
}); });
// Search for rules using the service function with pagination // Search for rules using the service function with pagination
const searchResult = await searchSigmaRules(keyword, page, pageSize); const searchResult = await searchAndConvertRules(keyword, page, pageSize);
logger.debug(`${FILE_NAME}: Search result status: ${searchResult.success}`); logger.debug(`${FILE_NAME}: Search result status: ${searchResult.success}`);
logger.debug(`${FILE_NAME}: Found ${searchResult.results?.length || 0} results out of ${searchResult.pagination?.totalResults || 0} total matches`); logger.debug(`${FILE_NAME}: Found ${searchResult.results?.length || 0} results out of ${searchResult.pagination?.totalResults || 0} total matches`);
logger.debug(`${FILE_NAME}: About to generate blocks for search results`); logger.debug(`${FILE_NAME}: About to generate blocks for search results`);
@ -240,6 +240,7 @@ const handleComplexSearch = async (command, respond) => {
); );
// Replace the header to indicate it's a complex search // Replace the header to indicate it's a complex search
// TODO: should be moved to dedicated block file
if (blocks && blocks.length > 0) { if (blocks && blocks.length > 0) {
blocks[0] = { blocks[0] = {
type: "header", type: "header",

View file

@ -25,15 +25,15 @@ async function getSigmaRuleDetails(ruleId) {
message: 'Missing rule ID' message: 'Missing rule ID'
}; };
} }
logger.info(`${FILE_NAME}: Running diagnostics for rule: ${ruleId}`); logger.info(`${FILE_NAME}: Running diagnostics for rule: ${ruleId}`);
logger.info(`${FILE_NAME}: Explaining rule ${ruleId}`); logger.info(`${FILE_NAME}: Explaining rule ${ruleId}`);
try { try {
// Run diagnostics on the rule content first // Run diagnostics on the rule content first
const diagnosticResult = await debugRuleContent(ruleId); const diagnosticResult = await debugRuleContent(ruleId);
logger.debug(`${FILE_NAME}: Diagnostic result: ${JSON.stringify(diagnosticResult || {})}`); logger.debug(`${FILE_NAME}: Diagnostic result: ${JSON.stringify(diagnosticResult || {})}`);
// Convert the rule ID to a structured object // Convert the rule ID to a structured object
const conversionResult = await convertSigmaRule(ruleId); const conversionResult = await convertSigmaRule(ruleId);
if (!conversionResult.success) { if (!conversionResult.success) {
@ -43,9 +43,9 @@ async function getSigmaRuleDetails(ruleId) {
message: conversionResult.message || `Failed to parse rule with ID ${ruleId}` message: conversionResult.message || `Failed to parse rule with ID ${ruleId}`
}; };
} }
const rule = conversionResult.rule; const rule = conversionResult.rule;
// Extra safety check // Extra safety check
if (!rule) { if (!rule) {
logger.error(`${FILE_NAME}: Converted rule is null for ID ${ruleId}`); logger.error(`${FILE_NAME}: Converted rule is null for ID ${ruleId}`);
@ -54,7 +54,7 @@ async function getSigmaRuleDetails(ruleId) {
message: `Failed to process rule with ID ${ruleId}` message: `Failed to process rule with ID ${ruleId}`
}; };
} }
// Create a simplified explanation with safe access to properties // Create a simplified explanation with safe access to properties
const explanation = { const explanation = {
id: rule.id || ruleId, id: rule.id || ruleId,
@ -62,27 +62,28 @@ async function getSigmaRuleDetails(ruleId) {
description: rule.description || 'No description provided', description: rule.description || 'No description provided',
author: rule.author || 'Unknown author', author: rule.author || 'Unknown author',
severity: rule.level || 'Unknown', severity: rule.level || 'Unknown',
logsource: rule.logsource || {}, // Add this line to include logsource info
detectionExplanation: extractDetectionCondition(rule), detectionExplanation: extractDetectionCondition(rule),
falsePositives: Array.isArray(rule.falsepositives) ? rule.falsepositives : falsePositives: Array.isArray(rule.falsepositives) ? rule.falsepositives :
typeof rule.falsepositives === 'string' ? [rule.falsepositives] : typeof rule.falsepositives === 'string' ? [rule.falsepositives] :
['None specified'], ['None specified'],
tags: Array.isArray(rule.tags) ? rule.tags : [], tags: Array.isArray(rule.tags) ? rule.tags : [],
references: Array.isArray(rule.references) ? rule.references : [] references: Array.isArray(rule.references) ? rule.references : []
}; };
logger.info(`${FILE_NAME}: Successfully explained rule ${ruleId}`); logger.info(`${FILE_NAME}: Successfully explained rule ${ruleId}`);
logger.debug(`${FILE_NAME}: Explanation properties: ${Object.keys(explanation).join(', ')}`); logger.debug(`${FILE_NAME}: Explanation properties: ${Object.keys(explanation).join(', ')}`);
return { return {
success: true, success: true,
explanation explanation
}; };
} catch (error) { } catch (error) {
logger.error(`${FILE_NAME}: Error explaining rule: ${error.message}`); logger.error(`${FILE_NAME}: Error explaining rule: ${error.message}`);
logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`);
return { return {
success: false, success: false,
message: `Error explaining rule: ${error.message}` message: `Error explaining rule: ${error.message}`
}; };
} }
} }
@ -102,13 +103,13 @@ async function getSigmaRuleYaml(ruleId) {
message: 'Missing rule ID' message: 'Missing rule ID'
}; };
} }
logger.info(`${FILE_NAME}: Getting YAML content for rule: ${ruleId}`); logger.info(`${FILE_NAME}: Getting YAML content for rule: ${ruleId}`);
try { try {
// Get YAML content from database // Get YAML content from database
const yamlResult = await getRuleYamlContent(ruleId); const yamlResult = await getRuleYamlContent(ruleId);
if (!yamlResult.success) { if (!yamlResult.success) {
logger.warn(`${FILE_NAME}: Failed to retrieve YAML for rule ${ruleId}: ${yamlResult.message}`); logger.warn(`${FILE_NAME}: Failed to retrieve YAML for rule ${ruleId}: ${yamlResult.message}`);
return { return {
@ -116,7 +117,7 @@ async function getSigmaRuleYaml(ruleId) {
message: yamlResult.message || `Failed to retrieve YAML for rule with ID ${ruleId}` message: yamlResult.message || `Failed to retrieve YAML for rule with ID ${ruleId}`
}; };
} }
// Add extra safety check for content // Add extra safety check for content
if (!yamlResult.content) { if (!yamlResult.content) {
logger.warn(`${FILE_NAME}: YAML content is empty for rule ${ruleId}`); logger.warn(`${FILE_NAME}: YAML content is empty for rule ${ruleId}`);
@ -126,9 +127,9 @@ async function getSigmaRuleYaml(ruleId) {
warning: 'YAML content is empty for this rule' warning: 'YAML content is empty for this rule'
}; };
} }
logger.debug(`${FILE_NAME}: Successfully retrieved YAML content with length: ${yamlResult.content.length}`); logger.debug(`${FILE_NAME}: Successfully retrieved YAML content with length: ${yamlResult.content.length}`);
// Return the YAML content // Return the YAML content
return { return {
success: true, success: true,

65
src/utils/os_emojis.js Normal file
View file

@ -0,0 +1,65 @@
/**
* os_emojis.js
*
* Provides emoji mappings for different products/platforms in Sigma rules
*/
/**
* Get the appropriate emoji for a product
* @param {string} product - The product/platform name
* @returns {string} - The corresponding emoji string
*/
const getProductEmoji = (product) => {
if (!product) return '';
const productLower = product.toLowerCase();
// Mapping of products to their respective emojis
const emojiMap = {
'aws': ':cloud:',
'azure': ':cloud:',
'bitbucket': ':bucket:',
'cisco': ':satellite_antenna:',
'django': ':snake:',
'dns': ':globe_with_meridians:',
'fortios': ':shield:',
'gcp': ':cloud:',
'github': ':octocat:',
'huawei': ':satellite_antenna:',
'juniper': ':satellite_antenna:',
'jvm': ':coffee:',
'kubernetes': ':wheel_of_dharma:',
'linux': ':penguin:',
'm365': ':envelope:',
'macos': ':apple:',
'modsecurity': ':shield:',
'nodejs': ':green_heart:',
'okta': ':key:',
'onelogin': ':key:',
'opencanary': ':bird:',
'paloalto': ':shield:',
'python': ':snake:',
'qualys': ':mag:',
'rpc_firewall': ':fire_extinguisher:',
'ruby_on_rails': ':gem:',
'spring': ':leaves:',
'sql': ':floppy_disk:',
'velocity': ':zap:',
'windows': ':window:',
'zeek': ':eyes:'
};
// Check if the product is directly in our map
for (const [key, emoji] of Object.entries(emojiMap)) {
if (productLower.includes(key)) {
return emoji + ' ';
}
}
// Default emoji for unknown products
return ':computer: ';
};
module.exports = {
getProductEmoji
};