refactor search handler and service into multiple files

This commit is contained in:
Charlotte Croce 2025-04-20 22:02:02 -04:00
parent b98502284a
commit 1b8ba03c8b
11 changed files with 840 additions and 309 deletions

View file

@ -0,0 +1,150 @@
/**
* sigma_basic_search_service.js
*
* Service for basic keyword searches of Sigma rules
*/
const { searchRules } = require('../../sigma_db/queries');
const { convertSigmaRule } = require('./sigma_converter_service');
const logger = require('../../utils/logger');
const { getFileName } = require('../../utils/file_utils');
const { validateKeyword, validatePagination } = require('./utils/search_validation_utils');
const { createPaginationInfo, createEmptyResponse } = require('./utils/search_pagination_utils');
const FILE_NAME = getFileName(__filename);
/**
* Searches for Sigma rules by keyword
* @param {string} keyword - Keyword to search for
* @param {number} page - Page number (1-based)
* @param {number} pageSize - Results per page
* @returns {Promise<Object>} Search results with pagination
*/
async function searchSigmaRules(keyword, page = 1, pageSize = 10) {
// Validate keyword
const keywordValidation = validateKeyword(keyword);
if (!keywordValidation.isValid) {
return {
success: false,
message: keywordValidation.message
};
}
// Validate pagination
const { page: validatedPage, pageSize: validatedPageSize, offset } =
validatePagination(page, pageSize);
logger.info(`${FILE_NAME}: Searching for Sigma rules with keyword: "${keywordValidation.trimmedKeyword}" (page ${validatedPage}, size ${validatedPageSize})`);
try {
// Perform the database search
const searchResult = await searchRules(keywordValidation.trimmedKeyword, validatedPageSize, offset);
// Extract results and total count
let allResults = [];
let totalCount = 0;
if (searchResult) {
if (Array.isArray(searchResult)) {
allResults = searchResult;
totalCount = searchResult.length;
} else if (typeof searchResult === 'object') {
if (Array.isArray(searchResult.results)) {
allResults = searchResult.results;
totalCount = searchResult.totalCount || 0;
} else if (searchResult.totalCount !== undefined) {
totalCount = searchResult.totalCount;
}
}
}
logger.debug(`${FILE_NAME}: Found ${allResults.length} results of ${totalCount} total`);
// Handle no results case
if (allResults.length === 0 && totalCount === 0) {
return createEmptyResponse(
`No rules found matching "${keywordValidation.trimmedKeyword}"`,
createPaginationInfo(validatedPage, validatedPageSize, 0)
);
}
// Create pagination info
const pagination = createPaginationInfo(validatedPage, validatedPageSize, totalCount);
// Check for valid page number
if (offset >= totalCount && totalCount > 0) {
return createEmptyResponse(
`No results on page ${validatedPage}. Try a previous page.`,
pagination
);
}
// Return successful results
return {
success: true,
results: allResults,
count: allResults.length,
pagination
};
} catch (error) {
logger.error(`${FILE_NAME}: Error searching for rules: ${error.message}`);
return {
success: false,
message: `Error searching for rules: ${error.message}`
};
}
}
/**
* Searches and converts Sigma rules (with full rule details)
* @param {string} keyword - Keyword to search for
* @param {number} page - Page number (1-based)
* @param {number} pageSize - Results per page
* @returns {Promise<Object>} Search results with converted rules
*/
async function searchAndConvertRules(keyword, page = 1, pageSize = 10) {
try {
// Perform the basic search
const searchResult = await searchSigmaRules(keyword, page, pageSize);
if (!searchResult.success || !searchResult.results || searchResult.results.length === 0) {
return searchResult;
}
logger.debug(`${FILE_NAME}: Converting ${searchResult.results.length} search results to full rule objects`);
// Convert each rule to full rule object
const convertedResults = [];
for (const result of searchResult.results) {
try {
const conversionResult = await convertSigmaRule(result.id);
if (conversionResult.success && conversionResult.rule) {
convertedResults.push(conversionResult.rule);
} else {
logger.warn(`${FILE_NAME}: Failed to convert rule ${result.id}`);
}
} catch (error) {
logger.error(`${FILE_NAME}: Error converting rule ${result.id}: ${error.message}`);
}
}
// Return results with original pagination info
return {
success: true,
results: convertedResults,
count: convertedResults.length,
originalCount: searchResult.results.length,
pagination: searchResult.pagination
};
} catch (error) {
logger.error(`${FILE_NAME}: Error in searchAndConvertRules: ${error.message}`);
return {
success: false,
message: `Error searching and converting rules: ${error.message}`
};
}
}
module.exports = {
searchSigmaRules,
searchAndConvertRules
};

View file

@ -0,0 +1,149 @@
/**
* sigma_complex_search_service.js
*
* Service for complex query searches of Sigma rules
*/
const { searchRulesComplex } = require('../../sigma_db/queries');
const { parseComplexQuery } = require('../../lang/query_parser');
const { convertSigmaRule } = require('./sigma_converter_service');
const logger = require('../../utils/logger');
const { getFileName } = require('../../utils/file_utils');
const { validateQueryString, validatePagination } = require('./utils/search_validation_utils');
const { createPaginationInfo, createEmptyResponse } = require('./utils/search_pagination_utils');
const FILE_NAME = getFileName(__filename);
/**
* Performs a complex search for Sigma rules
* @param {string} queryString - Complex query string
* @param {number} page - Page number (1-based)
* @param {number} pageSize - Results per page
* @returns {Promise<Object>} Search results with pagination
*/
async function searchSigmaRulesComplex(queryString, page = 1, pageSize = 10) {
// Validate query string
const queryValidation = validateQueryString(queryString);
if (!queryValidation.isValid) {
return {
success: false,
message: queryValidation.message
};
}
// Validate pagination
const { page: validatedPage, pageSize: validatedPageSize, offset } =
validatePagination(page, pageSize);
logger.info(`${FILE_NAME}: Performing complex search with query: "${queryValidation.trimmedQuery}" (page ${validatedPage}, size ${validatedPageSize})`);
try {
// Parse the complex query
const parsedQuery = parseComplexQuery(queryValidation.trimmedQuery);
if (!parsedQuery.valid) {
logger.warn(`${FILE_NAME}: Invalid complex query: ${parsedQuery.error}`);
return {
success: false,
message: `Invalid query: ${parsedQuery.error}`
};
}
// Execute complex search
const searchResult = await searchRulesComplex(parsedQuery, validatedPageSize, offset);
// Extract results and total count
let allResults = [];
let totalCount = 0;
if (searchResult) {
if (Array.isArray(searchResult.results)) {
allResults = searchResult.results;
totalCount = searchResult.totalCount || 0;
}
}
logger.debug(`${FILE_NAME}: Found ${allResults.length} results of ${totalCount} total`);
// Handle no results case
if (allResults.length === 0) {
return createEmptyResponse(
'No rules found matching the complex query criteria',
createPaginationInfo(validatedPage, validatedPageSize, totalCount)
);
}
// Create pagination info
const pagination = createPaginationInfo(validatedPage, validatedPageSize, totalCount);
// Return successful results
return {
success: true,
results: allResults,
count: allResults.length,
query: parsedQuery,
pagination
};
} catch (error) {
logger.error(`${FILE_NAME}: Error in complex search: ${error.message}`);
return {
success: false,
message: `Error performing complex search: ${error.message}`
};
}
}
/**
* Performs complex search and converts results to full rule objects
* @param {string} queryString - Complex query string
* @param {number} page - Page number (1-based)
* @param {number} pageSize - Results per page
* @returns {Promise<Object>} Search results with converted rules
*/
async function searchAndConvertRulesComplex(queryString, page = 1, pageSize = 10) {
try {
// Perform complex search
const searchResult = await searchSigmaRulesComplex(queryString, page, pageSize);
if (!searchResult.success || !searchResult.results || searchResult.results.length === 0) {
return searchResult;
}
logger.debug(`${FILE_NAME}: Converting ${searchResult.results.length} complex search results to full rule objects`);
// Convert each result to full rule object
const convertedResults = [];
for (const result of searchResult.results) {
try {
const conversionResult = await convertSigmaRule(result.id);
if (conversionResult.success && conversionResult.rule) {
convertedResults.push(conversionResult.rule);
} else {
logger.warn(`${FILE_NAME}: Failed to convert rule ${result.id}`);
}
} catch (error) {
logger.error(`${FILE_NAME}: Error converting rule ${result.id}: ${error.message}`);
}
}
// Return results with original pagination info
return {
success: true,
results: convertedResults,
count: convertedResults.length,
originalCount: searchResult.results.length,
query: searchResult.query,
pagination: searchResult.pagination
};
} catch (error) {
logger.error(`${FILE_NAME}: Error in searchAndConvertRulesComplex: ${error.message}`);
return {
success: false,
message: `Error searching and converting rules: ${error.message}`
};
}
}
module.exports = {
searchSigmaRulesComplex,
searchAndConvertRulesComplex
};

View file

@ -0,0 +1,85 @@
/**
* search_pagination_utils.js
*
* Utility functions for handling search pagination
*/
const logger = require('../../../utils/logger');
const { getFileName } = require('../../../utils/file_utils');
const FILE_NAME = getFileName(__filename);
/**
* Creates a standard pagination info object
* @param {number} page - Current page number (1-based)
* @param {number} pageSize - Size of each page
* @param {number} totalCount - Total number of results
* @returns {Object} Standardized pagination object
*/
function createPaginationInfo(page, pageSize, totalCount) {
const totalPages = Math.ceil(totalCount / pageSize);
const offset = (page - 1) * pageSize;
const hasMore = (offset + pageSize) < totalCount;
return {
currentPage: page,
pageSize: pageSize,
totalPages: totalPages,
totalResults: totalCount,
hasMore: hasMore
};
}
/**
* Extracts pagination parameters from command text
* @param {string} text - The command text to parse
* @returns {Object} Extracted parameters and cleaned text
*/
function extractPaginationParams(text) {
if (!text) return { text: '', page: 1, pageSize: 10 };
let cleanedText = text.trim();
let page = 1;
let pageSize = 10;
// Extract page parameter
const pagingMatch = cleanedText.match(/(.+)\s+page=(\d+)$/i);
if (pagingMatch) {
cleanedText = pagingMatch[1].trim();
page = parseInt(pagingMatch[2], 10) || 1;
logger.debug(`${FILE_NAME}: Extracted page ${page} from command text`);
}
// Extract limit parameter
const limitMatch = cleanedText.match(/(.+)\s+limit=(\d+)$/i);
if (limitMatch) {
cleanedText = limitMatch[1].trim();
pageSize = parseInt(limitMatch[2], 10) || 10;
logger.debug(`${FILE_NAME}: Extracted limit ${pageSize} from command text`);
}
return {
text: cleanedText,
page,
pageSize
};
}
/**
* Creates a standard response object for empty result sets
* @param {string} message - Message to include in the response
* @param {Object} pagination - Pagination info
* @returns {Object} Standardized empty response
*/
function createEmptyResponse(message, pagination) {
return {
success: true,
results: [],
message,
pagination
};
}
module.exports = {
createPaginationInfo,
extractPaginationParams,
createEmptyResponse
};

View file

@ -0,0 +1,105 @@
/**
* search_validation_utils.js
*
* Utility functions for validating search parameters
*/
const logger = require('../../../utils/logger');
const { getFileName } = require('../../../utils/file_utils');
const FILE_NAME = getFileName(__filename);
/**
* Validates a search keyword
* @param {string} keyword - The keyword to validate
* @returns {Object} Validation result object
*/
function validateKeyword(keyword) {
if (!keyword || typeof keyword !== 'string') {
logger.warn(`${FILE_NAME}: Cannot search rules: Missing or invalid keyword`);
return {
isValid: false,
message: 'Missing or invalid search keyword'
};
}
const trimmedKeyword = keyword.trim();
if (trimmedKeyword.length === 0) {
logger.warn(`${FILE_NAME}: Cannot search rules: Empty keyword after trimming`);
return {
isValid: false,
message: 'Search keyword cannot be empty'
};
}
return {
isValid: true,
trimmedKeyword
};
}
/**
* Validates query string for complex search
* @param {string} queryString - The query string to validate
* @returns {Object} Validation result object
*/
function validateQueryString(queryString) {
if (!queryString || typeof queryString !== 'string') {
logger.warn(`${FILE_NAME}: Cannot perform complex search: Missing or invalid query string`);
return {
isValid: false,
message: 'Missing or invalid complex query'
};
}
const trimmedQuery = queryString.trim();
if (trimmedQuery.length === 0) {
logger.warn(`${FILE_NAME}: Cannot perform complex search: Empty query after trimming`);
return {
isValid: false,
message: 'Complex query cannot be empty'
};
}
return {
isValid: true,
trimmedQuery
};
}
/**
* Validates pagination parameters
* @param {number} page - Page number (1-based)
* @param {number} pageSize - Number of results per page
* @param {number} maxPageSize - Maximum allowed page size
* @returns {Object} Validated pagination parameters
*/
function validatePagination(page = 1, pageSize = 10, maxPageSize = 100) {
let validatedPage = page;
let validatedPageSize = pageSize;
if (typeof validatedPage !== 'number' || validatedPage < 1) {
logger.warn(`${FILE_NAME}: Invalid page number: ${page}, defaulting to 1`);
validatedPage = 1;
}
if (typeof validatedPageSize !== 'number' || validatedPageSize < 1) {
logger.warn(`${FILE_NAME}: Invalid page size: ${pageSize}, defaulting to 10`);
validatedPageSize = 10;
} else if (validatedPageSize > maxPageSize) {
logger.warn(`${FILE_NAME}: Page size ${pageSize} exceeds max ${maxPageSize}, limiting to ${maxPageSize}`);
validatedPageSize = maxPageSize;
}
const offset = (validatedPage - 1) * validatedPageSize;
return {
page: validatedPage,
pageSize: validatedPageSize,
offset
};
}
module.exports = {
validateKeyword,
validateQueryString,
validatePagination
};