// validators.js - Input Validation and Sanitization
const { z } = require('zod');
const os = require('os');
const path = require('path');
const config = require('./config');
/**
* Get valid network interfaces from the system
*/
function getValidInterfaces() {
const interfaces = os.networkInterfaces();
const validInterfaces = Object.keys(interfaces);
// Filter by allowed interfaces if configured
if (config.network.allowedInterfaces.length > 0) {
return validInterfaces.filter(iface =>
config.network.allowedInterfaces.includes(iface)
);
}
return validInterfaces;
}
/**
* Validate IPv4 address with proper range checking
*/
function isValidIPv4(ip) {
const parts = ip.split('.');
if (parts.length !== 4) return false;
return parts.every(part => {
const num = parseInt(part, 10);
return num >= 0 && num <= 255 && part === num.toString();
});
}
/**
* Validate network interface name
*/
const interfaceSchema = z.string()
.min(1, 'Interface name cannot be empty')
.max(15, 'Interface name too long')
.regex(/^[a-zA-Z0-9_-]+$/, 'Interface name contains invalid characters')
.refine(
(iface) => getValidInterfaces().includes(iface),
(iface) => ({
message: `Invalid interface. Available: ${getValidInterfaces().join(', ')}`
})
);
/**
* Validate capture duration
*/
const durationSchema = z.number()
.int('Duration must be an integer')
.min(config.security.minCaptureDuration, `Duration must be at least ${config.security.minCaptureDuration} second(s)`)
.max(config.security.maxCaptureDuration, `Duration cannot exceed ${config.security.maxCaptureDuration} seconds`)
.finite('Duration must be finite');
/**
* Validate IPv4 address
*/
const ipv4Schema = z.string()
.min(7, 'IP address too short')
.max(15, 'IP address too long')
.regex(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, 'Invalid IP address format')
.refine(isValidIPv4, 'Invalid IP address (octets must be 0-255)');
/**
* Validate PCAP file path
*/
function validatePcapPath(pcapPath) {
// Basic validation
if (!pcapPath || typeof pcapPath !== 'string') {
throw new Error('PCAP path must be a non-empty string');
}
if (pcapPath.length > 4096) {
throw new Error('PCAP path too long');
}
// Check for path traversal attempts
if (pcapPath.includes('..')) {
throw new Error('Path traversal detected');
}
// Validate characters (allow alphanumeric, /, _, -, .)
if (!/^[a-zA-Z0-9/_.-]+$/.test(pcapPath)) {
throw new Error('PCAP path contains invalid characters');
}
// Check file extension
const ext = path.extname(pcapPath).toLowerCase();
if (ext !== '.pcap' && ext !== '.pcapng') {
throw new Error('Invalid file extension (must be .pcap or .pcapng)');
}
// Resolve to absolute path
const resolvedPath = path.resolve(pcapPath);
// Check if within allowed directories
const isAllowed = config.security.allowedPcapDirs.some(allowedDir =>
resolvedPath.startsWith(allowedDir)
);
if (!isAllowed) {
throw new Error(
`Access denied: File must be in one of these directories: ${config.security.allowedPcapDirs.join(', ')}`
);
}
return resolvedPath;
}
/**
* Sanitize packet data to remove sensitive information
*/
function sanitizePacketData(packets) {
if (!config.security.sanitizeSensitiveData) {
return packets;
}
const sensitiveFields = [
'http.authorization',
'http.cookie',
'http.set_cookie',
'http.proxy_authorization',
'ftp.request.arg',
'telnet.data',
'smtp.auth.password',
'imap.auth.password',
'pop.auth.password'
];
return packets.map(packet => {
if (!packet._source?.layers) return packet;
const sanitized = JSON.parse(JSON.stringify(packet));
sensitiveFields.forEach(field => {
if (sanitized._source.layers[field]) {
sanitized._source.layers[field] = ['[REDACTED]'];
}
});
return sanitized;
});
}
/**
* Sanitize error messages for user display
*/
function sanitizeError(error, includeDetails = false) {
// In development, show full errors
if (config.server.environment === 'development' && includeDetails) {
return error.message;
}
// In production, use generic messages for security
const errorMap = {
'EACCES': 'Permission denied',
'ENOENT': 'File or resource not found',
'EPERM': 'Operation not permitted',
'ETIMEDOUT': 'Operation timed out',
'ECONNREFUSED': 'Connection refused'
};
// Check for known error codes
if (error.code && errorMap[error.code]) {
return errorMap[error.code];
}
// Check for validation errors (safe to show)
if (error.name === 'ZodError') {
return `Validation error: ${error.errors[0]?.message || 'Invalid input'}`;
}
// Generic message for unknown errors
return 'An error occurred during the operation';
}
/**
* Validate and sanitize all tool arguments
*/
const validators = {
capturePackets: z.object({
interface: interfaceSchema.optional().default(config.network.defaultInterface),
duration: durationSchema.optional().default(5)
}),
getSummaryStats: z.object({
interface: interfaceSchema.optional().default(config.network.defaultInterface),
duration: durationSchema.optional().default(5)
}),
getConversations: z.object({
interface: interfaceSchema.optional().default(config.network.defaultInterface),
duration: durationSchema.optional().default(5)
}),
checkThreats: z.object({
interface: interfaceSchema.optional().default(config.network.defaultInterface),
duration: durationSchema.optional().default(5)
}),
checkIpThreats: z.object({
ip: ipv4Schema
}),
analyzePcap: z.object({
pcapPath: z.string().min(1, 'Path cannot be empty'),
includeUrls: z.boolean().optional().default(true),
includeProtocols: z.boolean().optional().default(true)
}),
extractCredentials: z.object({
pcapPath: z.string().min(1, 'Path cannot be empty'),
includeHashes: z.boolean().optional().default(false)
})
};
module.exports = {
validators,
getValidInterfaces,
validatePcapPath,
sanitizePacketData,
sanitizeError,
isValidIPv4
};