Skip to content

Commit 3b4cf19

Browse files
authored
Feature log details (#57) (#58)
* feat(logs): Add ModSecurity specific fields to log types feat(api): Enhance log parsing for ModSecurity events feat(ui): Add ScrollArea component feat(logs): Implement Log Details Dialog feat(logs): Enhance log table with ModSecurity details and interactive rows
1 parent 30aebac commit 3b4cf19

File tree

7 files changed

+463
-26
lines changed

7 files changed

+463
-26
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ yarn-error.log*
77
pnpm-debug.log*
88
lerna-debug.log*
99

10+
# But allow component directories named 'logs'
11+
!apps/web/src/components/logs/
12+
!apps/api/src/domains/logs/
13+
1014
# Dependencies
1115
node_modules
1216

apps/api/src/domains/logs/logs.types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,16 @@ export interface ParsedLogEntry {
1515
path?: string;
1616
statusCode?: number;
1717
responseTime?: number;
18+
// ModSecurity specific fields
19+
ruleId?: string;
20+
severity?: string;
21+
tags?: string[];
22+
uri?: string;
23+
uniqueId?: string;
24+
file?: string;
25+
line?: string;
26+
data?: string;
27+
fullMessage?: string; // Store complete log message without truncation
1828
}
1929

2030
export interface LogFilterOptions {

apps/api/src/domains/logs/services/log-parser.service.ts

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export function parseErrorLogLine(line: string, index: number): ParsedLogEntry |
7070

7171
if (!match) return null;
7272

73-
const [, timeStr, levelStr, message] = match;
73+
const [, timeStr, levelStr, fullMessageText] = match;
7474

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

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

95+
// Check if this is a ModSecurity error log entry
96+
if (fullMessageText.includes('ModSecurity:')) {
97+
// Use ModSecurity parser for better extraction
98+
return parseModSecLogLine(line, index);
99+
}
100+
95101
return {
96102
id: `error_${Date.now()}_${index}`,
97103
timestamp,
98104
level,
99105
type: 'error',
100106
source: 'nginx',
101-
message: message.substring(0, 200), // Truncate long messages
107+
message: fullMessageText.substring(0, 500), // Show more context but still truncate for display
108+
fullMessage: fullMessageText, // Store complete message
102109
ip
103110
};
104111
} catch (error) {
@@ -109,7 +116,7 @@ export function parseErrorLogLine(line: string, index: number): ParsedLogEntry |
109116

110117
/**
111118
* Parse ModSecurity audit log line
112-
* Format varies, look for key patterns
119+
* Format varies, look for key patterns and extract all relevant fields
113120
*/
114121
export function parseModSecLogLine(line: string, index: number): ParsedLogEntry | null {
115122
try {
@@ -134,36 +141,91 @@ export function parseModSecLogLine(line: string, index: number): ParsedLogEntry
134141
}
135142
}
136143

137-
// Extract message
144+
// Extract Rule ID - [id "942100"]
145+
const ruleIdMatch = line.match(/\[id "([^"]+)"\]/);
146+
const ruleId = ruleIdMatch ? ruleIdMatch[1] : undefined;
147+
148+
// Extract message (msg) - [msg "SQL Injection Attack Detected via libinjection"]
138149
const msgMatch = line.match(/\[msg "([^"]+)"\]/);
139-
const message = msgMatch ? msgMatch[1] : line.substring(0, 200);
150+
const message = msgMatch ? msgMatch[1] : 'ModSecurity Alert';
140151

141-
// Extract IP
142-
const ipMatch = line.match(/\[client ([\d.]+)\]/) || line.match(/\[hostname "([\d.]+)"\]/);
143-
const ip = ipMatch ? ipMatch[1] : undefined;
152+
// Extract severity - [severity "2"]
153+
const severityMatch = line.match(/\[severity "([^"]+)"\]/);
154+
const severity = severityMatch ? severityMatch[1] : undefined;
155+
156+
// Extract all tags - [tag "application-multi"] [tag "language-multi"] ...
157+
const tagMatches = line.matchAll(/\[tag "([^"]+)"\]/g);
158+
const tags: string[] = [];
159+
for (const match of tagMatches) {
160+
tags.push(match[1]);
161+
}
162+
163+
// Extract IP - from [client 52.186.182.85] or [hostname "10.0.0.203"]
164+
const clientIpMatch = line.match(/\[client ([\d.]+)\]/);
165+
const hostnameMatch = line.match(/\[hostname "([^"]+)"\]/);
166+
const ip = clientIpMatch ? clientIpMatch[1] : (hostnameMatch ? hostnameMatch[1] : undefined);
144167

145-
// Extract request info
168+
// Extract URI - [uri "/device.rsp"]
169+
const uriMatch = line.match(/\[uri "([^"]+)"\]/);
170+
const uri = uriMatch ? uriMatch[1] : undefined;
171+
172+
// Extract unique ID - [unique_id "176094161071.529267"]
173+
const uniqueIdMatch = line.match(/\[unique_id "([^"]+)"\]/);
174+
const uniqueId = uniqueIdMatch ? uniqueIdMatch[1] : undefined;
175+
176+
// Extract file - [file "/etc/nginx/modsec/coreruleset/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf"]
177+
const fileMatch = line.match(/\[file "([^"]+)"\]/);
178+
const file = fileMatch ? fileMatch[1] : undefined;
179+
180+
// Extract line number - [line "46"]
181+
const lineMatch = line.match(/\[line "([^"]+)"\]/);
182+
const lineNumber = lineMatch ? lineMatch[1] : undefined;
183+
184+
// Extract data field if present - [data "..."]
185+
const dataMatch = line.match(/\[data "([^"]+)"\]/);
186+
const data = dataMatch ? dataMatch[1] : undefined;
187+
188+
// Extract request info from log line
146189
const methodMatch = line.match(/"(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS) ([^"]+)"/);
147190
const method = methodMatch ? methodMatch[1] : undefined;
148-
const path = methodMatch ? methodMatch[2] : undefined;
191+
const path = methodMatch ? methodMatch[2] : (uri || undefined);
149192

150-
// Determine level
193+
// Determine level based on content
151194
let level: 'info' | 'warning' | 'error' = 'warning';
152195
if (line.includes('Access denied') || line.includes('blocked')) {
153196
level = 'error';
197+
} else if (line.includes('Warning')) {
198+
level = 'warning';
154199
}
155200

201+
// Extract status code
202+
const statusMatch = line.match(/with code (\d+)/);
203+
const statusCode = statusMatch ? parseInt(statusMatch[1]) : undefined;
204+
205+
// Store full message without truncation
206+
const fullMessage = line;
207+
156208
return {
157209
id: `modsec_${Date.now()}_${index}`,
158210
timestamp,
159211
level,
160212
type: 'error',
161213
source: 'modsecurity',
162214
message: `ModSecurity: ${message}`,
215+
fullMessage, // Complete log without truncation
163216
ip,
164217
method,
165218
path,
166-
statusCode: line.includes('403') ? 403 : undefined
219+
statusCode,
220+
// ModSecurity specific fields
221+
ruleId,
222+
severity,
223+
tags,
224+
uri,
225+
uniqueId,
226+
file,
227+
line: lineNumber,
228+
data
167229
};
168230
} catch (error) {
169231
logger.warn(`Failed to parse ModSecurity log line: ${line}`);

0 commit comments

Comments
 (0)