From 181eade8c43f10c351b23499d2c1d513e44f04f3 Mon Sep 17 00:00:00 2001 From: Charlotte Croce Date: Fri, 18 Apr 2025 13:49:10 -0400 Subject: [PATCH] create NLP command for details --- src/app.js | 29 ++-- src/handlers/fylgja_command_handler.js | 142 ++++++++++++++++++++ src/handlers/sigma/sigma_stats_handler.js | 2 +- src/lang/command_parser.js | 105 +++++++++++++++ src/lang/command_patterns.js | 78 +++++++++++ src/services/sigma/sigma_details_service.js | 2 +- 6 files changed, 348 insertions(+), 10 deletions(-) create mode 100644 src/handlers/fylgja_command_handler.js create mode 100644 src/lang/command_parser.js create mode 100644 src/lang/command_patterns.js diff --git a/src/app.js b/src/app.js index f2cc711..1a17bb3 100644 --- a/src/app.js +++ b/src/app.js @@ -3,7 +3,7 @@ * * Main application file for Fylgja Slack bot * Initializes the Slack Bolt app with custom ExpressReceiver Registers command handlers - * + * Now supports the universal /fylgja command */ const { App, ExpressReceiver } = require('@slack/bolt'); const fs = require('fs'); @@ -13,16 +13,14 @@ const { SIGMA_CLI_PATH, SIGMA_CLI_CONFIG, SLACK_CONFIG } = require('./config/app const { getFileName } = require('./utils/file_utils'); const FILE_NAME = getFileName(__filename); -// Import individual command handlers +// Import the unified fylgja command handler +const fylgjaCommandHandler = require('./handlers/fylgja_command_handler'); + const sigmaDetailsHandler = require('./handlers/sigma/sigma_details_handler'); const sigmaSearchHandler = require('./handlers/sigma/sigma_search_handler'); const sigmaCreateHandler = require('./handlers/sigma/sigma_create_handler'); // Import the action registry const sigmaActionRegistry = require('./handlers/sigma/actions/sigma_action_registry'); -//const configCommand = require('./commands/config/index.js'); -//const alertsCommand = require('./commands/alerts/index.js'); -//const caseCommand = require('./commands/case/index.js'); -//const statsCommand = require('./commands/stats/index.js'); // Verify sigma-cli is installed if (!fs.existsSync(SIGMA_CLI_PATH)) { @@ -49,8 +47,23 @@ const app = new App({ receiver: expressReceiver }); -// Register individual command handlers for all sigma commands -logger.info('Registering command handlers'); +// Register the unified fylgja command handler +logger.info('Registering unified fylgja command handler'); +app.command('/fylgja', async ({ command, ack, respond }) => { + try { + await ack(); + logger.info(`Received fylgja command: ${command.text}`); + await fylgjaCommandHandler.handleCommand(command, respond); + } catch (error) { + logger.error(`Error handling fylgja command: ${error.message}`); + logger.debug(`Error stack: ${error.stack}`); + await respond({ + text: `An error occurred: ${error.message}`, + response_type: 'ephemeral' + }); + } +}); + // Register sigma command handlers directly app.command('/sigma-create', async ({ command, ack, respond }) => { diff --git a/src/handlers/fylgja_command_handler.js b/src/handlers/fylgja_command_handler.js new file mode 100644 index 0000000..191e0b2 --- /dev/null +++ b/src/handlers/fylgja_command_handler.js @@ -0,0 +1,142 @@ +/** + * fylgja_command_handler.js + * + * Unified command handler for the Fylgja Slack bot. + * Processes natural language commands and routes to appropriate handlers. + */ +const logger = require('../utils/logger'); +const { handleError } = require('../utils/error_handler'); +const FILE_NAME = 'fylgja_command_handler.js'; + +// Import command handlers +const sigmaDetailsHandler = require('./sigma/sigma_details_handler'); +const sigmaSearchHandler = require('./sigma/sigma_search_handler'); +const sigmaCreateHandler = require('./sigma/sigma_create_handler'); +const sigmaStatsHandler = require('./sigma/sigma_stats_handler'); + +// Import language processing utilities +const commandParser = require('../lang/command_parser'); + +/** + * Handle the universal fylgja command + * + * @param {Object} command - The Slack command object + * @param {Function} respond - Function to send response back to Slack + */ +const handleCommand = async (command, respond) => { + try { + if (!command || !command.text) { + logger.warn(`${FILE_NAME}: Empty command received for fylgja`); + await respond({ + text: 'Please provide a command. Try `/fylgja help` for available commands.', + response_type: 'ephemeral' + }); + return; + } + + logger.info(`${FILE_NAME}: Processing fylgja command: ${command.text}`); + + // Parse the natural language command + const parsedCommand = await commandParser.parseCommand(command.text); + + if (!parsedCommand.success) { + logger.warn(`${FILE_NAME}: Failed to parse command: ${command.text}`); + await respond({ + text: `I couldn't understand that command. ${parsedCommand.message || ''}`, + response_type: 'ephemeral' + }); + return; + } + + // Route to the appropriate handler based on the parsed command + await routeCommand(parsedCommand.command, command, respond); + + } catch (error) { + await handleError(error, `${FILE_NAME}: Command handler`, respond, { + responseType: 'ephemeral' + }); + } +}; + +/** + * Route the command to the appropriate handler + * + * @param {Object} parsedCommand - The parsed command object + * @param {Object} originalCommand - The original Slack command object + * @param {Function} respond - Function to send response back to Slack + */ +const routeCommand = async (parsedCommand, originalCommand, respond) => { + const { action, module, params } = parsedCommand; + + // Create a modified command object with the extracted parameters + const modifiedCommand = { + ...originalCommand, + text: params.join(' ') + }; + + // Log the routing decision + logger.debug(`${FILE_NAME}: Routing command - Action: ${action}, Module: ${module}, Params: ${JSON.stringify(params)}`); + + // Route to the appropriate handler + switch (`${module}:${action}`) { + case 'sigma:details': + case 'sigma:explain': + await sigmaDetailsHandler.handleCommand(modifiedCommand, respond); + break; + + case 'sigma:search': + await sigmaSearchHandler.handleCommand(modifiedCommand, respond); + break; + + case 'sigma:create': + await sigmaCreateHandler.handleCommand(modifiedCommand, respond); + break; + + case 'sigma:stats': + await sigmaStatsHandler.handleCommand(modifiedCommand, respond); + break; + + case 'help:general': + await showHelp(respond); + break; + + default: + logger.warn(`${FILE_NAME}: Unknown command combination: ${module}:${action}`); + await respond({ + text: `I don't know how to ${action} in ${module}. Try \`/fylgja help\` for available commands.`, + response_type: 'ephemeral' + }); + } +}; + +/** + * Show help information + * + * @param {Function} respond - Function to send response back to Slack + */ +const showHelp = async (respond) => { + await respond({ + text: "Here are some example commands you can use with Fylgja:", + blocks: [ + { + type: "section", + text: { + type: "mrkdwn", + text: "*Fylgja Commands*\nHere are some example commands you can use:" + } + }, + { + type: "section", + text: { + type: "mrkdwn", + text: "• `/fylgja explain rule from sigma where id=`\n• `/fylgja search sigma for `\n• `/fylgja create rule in sigma with `\n• `/fylgja show stats for sigma`" + } + } + ], + response_type: 'ephemeral' + }); +}; + +module.exports = { + handleCommand +}; \ No newline at end of file diff --git a/src/handlers/sigma/sigma_stats_handler.js b/src/handlers/sigma/sigma_stats_handler.js index a9c571c..5b49a63 100644 --- a/src/handlers/sigma/sigma_stats_handler.js +++ b/src/handlers/sigma/sigma_stats_handler.js @@ -7,7 +7,7 @@ const logger = require('../../utils/logger'); const { handleError } = require('../../utils/error_handler'); const { getSigmaStats } = require('../../services/sigma/sigma_stats_service'); -const { getStatsBlocks } = require('../../blocks/sigma_stats_block'); +const { getStatsBlocks } = require('../../blocks/sigma/sigma_stats_block'); const { getFileName } = require('../../utils/file_utils'); const FILE_NAME = getFileName(__filename); diff --git a/src/lang/command_parser.js b/src/lang/command_parser.js new file mode 100644 index 0000000..c24c6a6 --- /dev/null +++ b/src/lang/command_parser.js @@ -0,0 +1,105 @@ +/** + * command_parser.js + * + * Provides functionality for parsing commands for the Fylgja bot + */ +const logger = require('../utils/logger'); +const FILE_NAME = 'command_parser.js'; + +// Import language patterns and synonyms +const commandPatterns = require('./command_patterns'); + +/** + * Parse a natural language command into a structured command object + * + * @param {string} commandText - The natural language command text + * @returns {Promise} Result object with success flag and parsed command or error message + */ +const parseCommand = async (commandText) => { + try { + logger.debug(`${FILE_NAME}: Parsing command: ${commandText}`); + + if (!commandText || typeof commandText !== 'string') { + return { + success: false, + message: 'Empty or invalid command.' + }; + } + + // Convert to lowercase for case-insensitive matching + const normalizedCommand = commandText.toLowerCase().trim(); + + // TODO + // Handle help command separately + if (normalizedCommand === 'help') { + return { + success: true, + command: { + action: 'general', + module: 'help', + params: [] + } + }; + } + + // Try to match command against known patterns + for (const pattern of commandPatterns) { + const match = matchPattern(normalizedCommand, pattern); + if (match) { + logger.debug(`${FILE_NAME}: Command matched pattern: ${pattern.name}`); + return { + success: true, + command: match + }; + } + } + + // If we reach here, no pattern matched + logger.warn(`${FILE_NAME}: No pattern matched for command: ${commandText}`); + return { + success: false, + message: "I couldn't understand that command. Try `/fylgja help` for examples." + }; + } catch (error) { + logger.error(`${FILE_NAME}: Error parsing command: ${error.message}`); + logger.debug(`${FILE_NAME}: Error stack: ${error.stack}`); + return { + success: false, + message: `Error parsing command: ${error.message}` + }; + } +}; + +/** + * Match a command against a pattern + * + * @param {string} command - The normalized command text + * @param {Object} pattern - The pattern object to match against + * @returns {Object|null} Parsed command object or null if no match + */ +const matchPattern = (command, pattern) => { + // Check if the command matches the regex pattern + const match = pattern.regex.exec(command); + if (!match) { + return null; + } + + // Extract parameters based on the pattern's parameter mapping + const params = []; + for (const paramIndex of pattern.params) { + if (match[paramIndex]) { + params.push(match[paramIndex].trim()); + } + } + + // Return the structured command + return { + action: pattern.action, + module: pattern.module, + params + }; +}; + +module.exports = { + parseCommand +}; \ No newline at end of file diff --git a/src/lang/command_patterns.js b/src/lang/command_patterns.js new file mode 100644 index 0000000..599f272 --- /dev/null +++ b/src/lang/command_patterns.js @@ -0,0 +1,78 @@ +/** + * command_patterns.js + * + * Defines pattern matching rules for natural language commands + * Each pattern includes a regex and mapping for parameter extraction + */ + +/** + * Command patterns array + * Each pattern object contains: + * - name: A descriptive name for the pattern + * - regex: A regular expression to match the command + * - action: The action to perform (e.g., details, search) + * - module: The module to use (e.g., sigma, alerts) + * - params: Array of capturing group indices to extract parameters + */ +const commandPatterns = [ + // Sigma details patterns + { + name: 'sigma-details-direct', + regex: /^(explain|get|show|display|details|info|about)\s+(rule|detection)\s+(from\s+)?sigma\s+(where\s+)?(id=|id\s+is\s+|with\s+id\s+)(.+)$/i, + action: 'details', + module: 'sigma', + params: [6] // rule ID is in capturing group 6 + }, + { + name: 'sigma-details-simple', + regex: /^(details|explain)\s+(.+)$/i, + action: 'details', + module: 'sigma', + params: [2] // rule ID is in capturing group 2 + }, + + // Sigma search patterns + { + name: 'sigma-search', + regex: /^(search|find|look\s+for)\s+(rules|detections)?\s*(in|from)?\s*sigma\s+(for|where|with)?\s+(.+)$/i, + action: 'search', + module: 'sigma', + params: [5] // search query is in capturing group 5 + }, + { + name: 'sigma-search-simple', + regex: /^(search|find)\s+(.+)$/i, + action: 'search', + module: 'sigma', + params: [2] // search query is in capturing group 2 + }, + + // Sigma create patterns + { + name: 'sigma-create', + regex: /^(create|new|add)\s+(rule|detection)\s+(in|to|for)?\s*sigma\s+(with|using)?\s+(.+)$/i, + action: 'create', + module: 'sigma', + params: [5] // creation parameters in capturing group 5 + }, + + // Sigma stats patterns + { + name: 'sigma-stats', + regex: /^(stats|statistics|metrics|counts)\s+(for|about|on|of)?\s*sigma$/i, + action: 'stats', + module: 'sigma', + params: [] + }, + { + name: 'sigma-stats-show', + regex: /^(show|get|display)\s+(stats|statistics|metrics|counts)\s+(for|about|on|of)?\s*sigma$/i, + action: 'stats', + module: 'sigma', + params: [] + } + + // Additional command patterns for other modules can be added here + ]; + + module.exports = commandPatterns; \ No newline at end of file diff --git a/src/services/sigma/sigma_details_service.js b/src/services/sigma/sigma_details_service.js index dad7db1..2f43019 100644 --- a/src/services/sigma/sigma_details_service.js +++ b/src/services/sigma/sigma_details_service.js @@ -147,4 +147,4 @@ async function getSigmaRuleYaml(ruleId) { module.exports = { explainSigmaRule, getSigmaRuleYaml -}; \ No newline at end of file +};