UNPKG

voyage-and-consumption-mcp-server

Version:

Voyage and consumption management server handling vessel voyages, fuel consumption, performance monitoring, and operational data with ERP access for data extraction

309 lines 13.2 kB
import { isValidImoForCompany, shouldBypassImoFiltering, getCompanyImoNumbers } from './imoUtils.js'; import { config } from './config.js'; import { logger } from './logger.js'; /** * Find IMO field in an object with various possible field names * @param obj - Object to search for IMO field * @returns Object with field name and value, or null if not found */ function findImoField(obj) { const imoFieldNames = ['imo', 'vesselImo', 'imoNumber', 'vessel_imo', 'IMO']; for (const fieldName of imoFieldNames) { if (obj && typeof obj === 'object' && obj.hasOwnProperty(fieldName)) { const value = obj[fieldName]; if (value !== null && value !== undefined && value !== '') { return { fieldName, value }; } } } return null; } /** * Recursively check for unauthorized IMO numbers in nested objects and arrays * @param obj - Object to check * @param path - Current path for debugging (optional) * @returns Object indicating if unauthorized IMO was found and its location */ function checkForUnauthorizedImo(obj, path = '') { if (!obj || typeof obj !== 'object') { return { found: false, location: '' }; } // Check direct IMO fields in current object const imoField = findImoField(obj); if (imoField && !isValidImoForCompany(imoField.value)) { const location = path ? `${path}.${imoField.fieldName}` : imoField.fieldName; return { found: true, location: `${location}: ${imoField.value}` }; } // Recursively check nested objects and arrays for (const key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key]; const currentPath = path ? `${path}.${key}` : key; if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { const result = checkForUnauthorizedImo(value[i], `${currentPath}[${i}]`); if (result.found) { return result; } } } else if (typeof value === 'object' && value !== null) { const result = checkForUnauthorizedImo(value, currentPath); if (result.found) { return result; } } } } return { found: false, location: '' }; } /** * Filter array items based on IMO authorization * @param array - Array to filter * @returns Object with filtered array and statistics */ function filterArrayByImo(array) { const stats = { itemsFiltered: 0, unauthorizedImos: [], processingTimeMs: 0 }; const startTime = Date.now(); const filtered = array.filter(item => { const unauthorizedCheck = checkForUnauthorizedImo(item); if (unauthorizedCheck.found) { stats.itemsFiltered++; stats.unauthorizedImos.push(unauthorizedCheck.location); logger.debug(`Filtered item with unauthorized IMO: ${unauthorizedCheck.location}`); return false; } return true; }); stats.processingTimeMs = Date.now() - startTime; return { filtered, stats }; } /** * Recursively filter response content for unauthorized IMO numbers * @param content - Content to filter * @returns Object with filtered content and statistics */ function filterResponseContent(content) { const stats = { itemsFiltered: 0, unauthorizedImos: [], processingTimeMs: 0 }; const startTime = Date.now(); try { // Handle arrays by filtering items with unauthorized IMOs if (Array.isArray(content)) { const result = filterArrayByImo(content); stats.itemsFiltered = result.stats.itemsFiltered; stats.unauthorizedImos = result.stats.unauthorizedImos; stats.processingTimeMs = Date.now() - startTime; return { filtered: result.filtered, stats }; } // Handle objects by recursively filtering properties if (typeof content === 'object' && content !== null) { const filtered = { ...content }; for (const key in content) { if (content.hasOwnProperty(key)) { const value = content[key]; if (Array.isArray(value)) { const result = filterArrayByImo(value); filtered[key] = result.filtered; stats.itemsFiltered += result.stats.itemsFiltered; stats.unauthorizedImos.push(...result.stats.unauthorizedImos); } else if (typeof value === 'object' && value !== null) { const result = filterResponseContent(value); filtered[key] = result.filtered; stats.itemsFiltered += result.stats.itemsFiltered; stats.unauthorizedImos.push(...result.stats.unauthorizedImos); } else { filtered[key] = value; } } } stats.processingTimeMs = Date.now() - startTime; return { filtered, stats }; } // Return primitive values unchanged stats.processingTimeMs = Date.now() - startTime; return { filtered: content, stats }; } catch (error) { logger.error(`Error during response filtering: ${error.message}`); stats.processingTimeMs = Date.now() - startTime; return { filtered: content, stats }; } } /** * Main function to filter tool responses by company IMO authorization * @param response - Tool response to filter * @returns Filtered tool response */ export async function filterResponseByCompanyImos(response) { const { companyName } = config; const startTime = Date.now(); // Skip filtering for admin companies if (!companyName || shouldBypassImoFiltering(companyName)) { logger.debug('Skipping response filtering for admin company or missing company name'); return response; } // Skip filtering if no company IMOs are loaded const companyImos = getCompanyImoNumbers(); if (companyImos.length === 0) { logger.debug('Skipping response filtering - no company IMOs loaded'); return response; } const filteredResponse = []; let totalItemsFiltered = 0; let totalUnauthorizedImos = []; try { for (const item of response) { if (item.type === 'text') { try { // Try to parse as JSON const parsedContent = JSON.parse(item.text); const result = filterResponseContent(parsedContent); totalItemsFiltered += result.stats.itemsFiltered; totalUnauthorizedImos.push(...result.stats.unauthorizedImos); // Check if all hits were filtered out and provide informative message if (parsedContent.hits && Array.isArray(parsedContent.hits)) { const originalHitsCount = parsedContent.hits.length; const filteredHitsCount = result.filtered.hits ? result.filtered.hits.length : 0; if (originalHitsCount > 0 && filteredHitsCount === 0 && result.stats.itemsFiltered > 0) { // All hits were filtered out - return informative message const unauthorizedImos = [...new Set(result.stats.unauthorizedImos.map(imo => imo.split(': ')[1]))]; const errorMessage = `The queried vessel${unauthorizedImos.length > 1 ? 's are' : ' is'} not under ${companyName}. ` + `IMO number${unauthorizedImos.length > 1 ? 's' : ''} ${unauthorizedImos.join(', ')} ${unauthorizedImos.length > 1 ? 'are' : 'is'} not associated with this company.`; filteredResponse.push({ ...item, text: errorMessage }); continue; } } // Update response with filtered content filteredResponse.push({ ...item, text: JSON.stringify(result.filtered, null, 2) }); } catch (parseError) { // Not JSON or parsing failed - pass through unchanged filteredResponse.push(item); } } else { // Non-text items pass through unchanged filteredResponse.push(item); } } const processingTime = Date.now() - startTime; // Log filtering results if (totalItemsFiltered > 0) { logger.warn(`Filtered ${totalItemsFiltered} unauthorized items from response`, { companyName, itemsRemoved: totalItemsFiltered, unauthorizedImos: totalUnauthorizedImos.slice(0, 10), // Limit log size processingTimeMs: processingTime }); } else { logger.debug('No items filtered from response', { companyName, processingTimeMs: processingTime }); } return filteredResponse; } catch (error) { logger.error(`Error during response filtering: ${error.message}`); // Return original response on error to avoid breaking functionality return response; } } /** * Filter search results before artifact creation to prevent unauthorized data leakage * This is critical for tools like universal_voyage_search that create persistent snapshots * @param searchResults - Array of search result objects * @returns Object with filtered results and statistics */ export function filterSearchResultsByCompanyImo(searchResults) { const { companyName } = config; const startTime = Date.now(); const stats = { itemsFiltered: 0, unauthorizedImos: [], processingTimeMs: 0 }; // Skip filtering for admin companies if (!companyName || shouldBypassImoFiltering(companyName)) { logger.debug('Skipping search result filtering for admin company or missing company name'); stats.processingTimeMs = Date.now() - startTime; return { filtered: searchResults, stats }; } // Skip filtering if no company IMOs are loaded const companyImos = getCompanyImoNumbers(); if (companyImos.length === 0) { logger.debug('Skipping search result filtering - no company IMOs loaded'); stats.processingTimeMs = Date.now() - startTime; return { filtered: searchResults, stats }; } try { const filtered = searchResults.filter(result => { // Check for unauthorized IMO in the result document const unauthorizedCheck = checkForUnauthorizedImo(result); if (unauthorizedCheck.found) { stats.itemsFiltered++; stats.unauthorizedImos.push(unauthorizedCheck.location); logger.debug(`Filtered search result with unauthorized IMO: ${unauthorizedCheck.location}`); return false; } return true; }); stats.processingTimeMs = Date.now() - startTime; // Log filtering results for security monitoring if (stats.itemsFiltered > 0) { logger.warn(`Filtered ${stats.itemsFiltered} unauthorized search results before artifact creation`, { companyName, itemsRemoved: stats.itemsFiltered, unauthorizedImos: stats.unauthorizedImos.slice(0, 10), // Limit log size processingTimeMs: stats.processingTimeMs, securityEvent: 'search_result_filtering' }); } else { logger.debug('No unauthorized search results filtered', { companyName, totalResults: searchResults.length, processingTimeMs: stats.processingTimeMs }); } return { filtered, stats }; } catch (error) { logger.error(`Error during search result filtering: ${error.message}`); stats.processingTimeMs = Date.now() - startTime; // Return original results on error to avoid breaking functionality return { filtered: searchResults, stats }; } } /** * Get filtering statistics for monitoring * @returns Object with current filtering configuration */ export function getFilteringStats() { const { companyName } = config; const companyImos = getCompanyImoNumbers(); return { companyName: companyName || 'Not set', isFilteringEnabled: !!(companyName && !shouldBypassImoFiltering(companyName) && companyImos.length > 0), companyImoCount: companyImos.length, isAdminCompany: companyName ? shouldBypassImoFiltering(companyName) : false }; } //# sourceMappingURL=responseFilter.js.map