Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

# But allow component directories named 'logs'
!apps/web/src/components/logs/
!apps/api/src/domains/logs/

# Dependencies
node_modules

Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/domains/logs/logs.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ export interface ParsedLogEntry {
path?: string;
statusCode?: number;
responseTime?: number;
// ModSecurity specific fields
ruleId?: string;
severity?: string;
tags?: string[];
uri?: string;
uniqueId?: string;
file?: string;
line?: string;
data?: string;
fullMessage?: string; // Store complete log message without truncation
}

export interface LogFilterOptions {
Expand Down
88 changes: 75 additions & 13 deletions apps/api/src/domains/logs/services/log-parser.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function parseErrorLogLine(line: string, index: number): ParsedLogEntry |

if (!match) return null;

const [, timeStr, levelStr, message] = match;
const [, timeStr, levelStr, fullMessageText] = match;

// Parse time: 2025/03/29 14:35:18
const timestamp = timeStr.replace(/\//g, '-').replace(' ', 'T') + 'Z';
Expand All @@ -89,16 +89,23 @@ export function parseErrorLogLine(line: string, index: number): ParsedLogEntry |
const level = levelMap[levelStr] || 'error';

// Extract IP if present
const ipMatch = message.match(/client: ([\d.]+)/);
const ipMatch = fullMessageText.match(/client: ([\d.]+)/);
const ip = ipMatch ? ipMatch[1] : undefined;

// Check if this is a ModSecurity error log entry
if (fullMessageText.includes('ModSecurity:')) {
// Use ModSecurity parser for better extraction
return parseModSecLogLine(line, index);
}

return {
id: `error_${Date.now()}_${index}`,
timestamp,
level,
type: 'error',
source: 'nginx',
message: message.substring(0, 200), // Truncate long messages
message: fullMessageText.substring(0, 500), // Show more context but still truncate for display
fullMessage: fullMessageText, // Store complete message
ip
};
} catch (error) {
Expand All @@ -109,7 +116,7 @@ export function parseErrorLogLine(line: string, index: number): ParsedLogEntry |

/**
* Parse ModSecurity audit log line
* Format varies, look for key patterns
* Format varies, look for key patterns and extract all relevant fields
*/
export function parseModSecLogLine(line: string, index: number): ParsedLogEntry | null {
try {
Expand All @@ -134,36 +141,91 @@ export function parseModSecLogLine(line: string, index: number): ParsedLogEntry
}
}

// Extract message
// Extract Rule ID - [id "942100"]
const ruleIdMatch = line.match(/\[id "([^"]+)"\]/);
const ruleId = ruleIdMatch ? ruleIdMatch[1] : undefined;

// Extract message (msg) - [msg "SQL Injection Attack Detected via libinjection"]
const msgMatch = line.match(/\[msg "([^"]+)"\]/);
const message = msgMatch ? msgMatch[1] : line.substring(0, 200);
const message = msgMatch ? msgMatch[1] : 'ModSecurity Alert';

// Extract IP
const ipMatch = line.match(/\[client ([\d.]+)\]/) || line.match(/\[hostname "([\d.]+)"\]/);
const ip = ipMatch ? ipMatch[1] : undefined;
// Extract severity - [severity "2"]
const severityMatch = line.match(/\[severity "([^"]+)"\]/);
const severity = severityMatch ? severityMatch[1] : undefined;

// Extract all tags - [tag "application-multi"] [tag "language-multi"] ...
const tagMatches = line.matchAll(/\[tag "([^"]+)"\]/g);
const tags: string[] = [];
for (const match of tagMatches) {
tags.push(match[1]);
}

// Extract IP - from [client 52.186.182.85] or [hostname "10.0.0.203"]
const clientIpMatch = line.match(/\[client ([\d.]+)\]/);
const hostnameMatch = line.match(/\[hostname "([^"]+)"\]/);
const ip = clientIpMatch ? clientIpMatch[1] : (hostnameMatch ? hostnameMatch[1] : undefined);

// Extract request info
// Extract URI - [uri "/device.rsp"]
const uriMatch = line.match(/\[uri "([^"]+)"\]/);
const uri = uriMatch ? uriMatch[1] : undefined;

// Extract unique ID - [unique_id "176094161071.529267"]
const uniqueIdMatch = line.match(/\[unique_id "([^"]+)"\]/);
const uniqueId = uniqueIdMatch ? uniqueIdMatch[1] : undefined;

// Extract file - [file "/etc/nginx/modsec/coreruleset/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"]
const fileMatch = line.match(/\[file "([^"]+)"\]/);
const file = fileMatch ? fileMatch[1] : undefined;

// Extract line number - [line "46"]
const lineMatch = line.match(/\[line "([^"]+)"\]/);
const lineNumber = lineMatch ? lineMatch[1] : undefined;

// Extract data field if present - [data "..."]
const dataMatch = line.match(/\[data "([^"]+)"\]/);
const data = dataMatch ? dataMatch[1] : undefined;

// Extract request info from log line
const methodMatch = line.match(/"(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) ([^"]+)"/);
const method = methodMatch ? methodMatch[1] : undefined;
const path = methodMatch ? methodMatch[2] : undefined;
const path = methodMatch ? methodMatch[2] : (uri || undefined);

// Determine level
// Determine level based on content
let level: 'info' | 'warning' | 'error' = 'warning';
if (line.includes('Access denied') || line.includes('blocked')) {
level = 'error';
} else if (line.includes('Warning')) {
level = 'warning';
}

// Extract status code
const statusMatch = line.match(/with code (\d+)/);
const statusCode = statusMatch ? parseInt(statusMatch[1]) : undefined;

// Store full message without truncation
const fullMessage = line;

return {
id: `modsec_${Date.now()}_${index}`,
timestamp,
level,
type: 'error',
source: 'modsecurity',
message: `ModSecurity: ${message}`,
fullMessage, // Complete log without truncation
ip,
method,
path,
statusCode: line.includes('403') ? 403 : undefined
statusCode,
// ModSecurity specific fields
ruleId,
severity,
tags,
uri,
uniqueId,
file,
line: lineNumber,
data
};
} catch (error) {
logger.warn(`Failed to parse ModSecurity log line: ${line}`);
Expand Down
Loading