create NLP command for details

This commit is contained in:
Charlotte Croce 2025-04-18 13:49:10 -04:00
parent 31d6296c6e
commit 181eade8c4
6 changed files with 348 additions and 10 deletions

View file

@ -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 }) => {

View file

@ -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=<rule_id>`\n• `/fylgja search sigma for <query>`\n• `/fylgja create rule in sigma with <parameters>`\n• `/fylgja show stats for sigma`"
}
}
],
response_type: 'ephemeral'
});
};
module.exports = {
handleCommand
};

View file

@ -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);

105
src/lang/command_parser.js Normal file
View file

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

View file

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