mm_os 3.2.9 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -1
- package/core/base/mqtt/index.js +1109 -1106
- package/core/base/web/index.js +245 -156
- package/core/com/event/README.md +4 -4
- package/core/com/event/com.json +3 -3
- package/core/com/event/config.tpl.json +18 -18
- package/core/com/event/drive.js +132 -132
- package/core/com/event/index.js +344 -344
- package/core/com/event/script.js +25 -25
- package/core/com/middleware/com.js +152 -151
- package/core/com/socket/config.tpl.json +2 -2
- package/core/com/socket/drive.js +2 -2
- package/core/com/socket/index.js +1 -1
- package/core/com/sql/drive.js +7 -7
- package/core/com/static/index.js +1 -1
- package/index.js +34 -5
- package/middleware/cors/index.js +112 -96
- package/middleware/cors/middleware.json +18 -7
- package/middleware/csrf/index.js +202 -0
- package/middleware/csrf/middleware.json +24 -0
- package/middleware/ip_firewall/index.js +476 -0
- package/middleware/ip_firewall/middleware.json +109 -0
- package/middleware/mqtt_base/middleware.json +2 -1
- package/middleware/security_audit/index.js +543 -0
- package/middleware/security_audit/middleware.json +48 -0
- package/middleware/waf/index.js +273 -7
- package/middleware/waf/middleware.json +2 -1
- package/middleware/waf_ddos/index.js +520 -0
- package/middleware/waf_ddos/middleware.json +38 -0
- package/middleware/waf_xss/index.js +269 -0
- package/middleware/waf_xss/middleware.json +18 -0
- package/middleware/web_after/middleware.json +2 -1
- package/middleware/web_base/middleware.json +2 -1
- package/middleware/web_before/middleware.json +3 -2
- package/middleware/web_check/middleware.json +2 -1
- package/middleware/web_main/middleware.json +2 -1
- package/middleware/web_proxy/middleware.json +2 -1
- package/middleware/web_render/middleware.json +2 -1
- package/middleware/web_socket/middleware.json +4 -3
- package/middleware/web_static/middleware.json +2 -1
- package/package.json +28 -15
- package/middleware/log/index.js +0 -32
- package/middleware/log/middleware.json +0 -9
- package/middleware/performance/index.js +0 -143
- package/middleware/performance/middleware.json +0 -16
- package/middleware/rate_limit/index.js +0 -112
- package/middleware/rate_limit/middleware.json +0 -10
- package/middleware/waf_ip/index.js +0 -168
- package/middleware/waf_ip/middleware.json +0 -10
- package/nodemon.json +0 -31
- package/package.txt +0 -1
- package/rps.bat +0 -3
- package/test.js +0 -10
- package/tps.bat +0 -3
- package/update.bat +0 -1
- package//347/263/273/347/273/237/346/236/266/346/236/204/350/257/204/344/274/260/344/270/216/344/274/230/345/214/226/345/273/272/350/256/256.md +0 -599
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 综合日志审计系统中间件
|
|
6
|
+
* 合并了原log中间件的轻量级HTTP日志和security_audit的安全审计功能
|
|
7
|
+
*/
|
|
8
|
+
class Middleware {
|
|
9
|
+
/**
|
|
10
|
+
* 构造函数
|
|
11
|
+
*/
|
|
12
|
+
constructor() {
|
|
13
|
+
this.logFiles = new Map();
|
|
14
|
+
this.default = {
|
|
15
|
+
// 启用日志审计
|
|
16
|
+
enable: true,
|
|
17
|
+
// 日志模式: 'light'(轻量模式,仅记录基本HTTP信息), 'full'(完整审计模式)
|
|
18
|
+
log_mode: 'light',
|
|
19
|
+
// 日志级别: debug, info, warn, error, critical
|
|
20
|
+
log_level: 'info',
|
|
21
|
+
// 日志输出方式: file, database, both
|
|
22
|
+
output: 'file',
|
|
23
|
+
// 文件日志配置
|
|
24
|
+
file: {
|
|
25
|
+
// 日志目录
|
|
26
|
+
dir: './log/security',
|
|
27
|
+
// 是否按日期分割日志
|
|
28
|
+
daily_rotate: true,
|
|
29
|
+
// 日志保留天数
|
|
30
|
+
max_days: 30,
|
|
31
|
+
// 是否压缩旧日志
|
|
32
|
+
compress: true
|
|
33
|
+
},
|
|
34
|
+
// 数据库日志配置
|
|
35
|
+
database: {
|
|
36
|
+
// 数据库类型: mongodb, mysql
|
|
37
|
+
type: 'mysql',
|
|
38
|
+
// 表名或集合名
|
|
39
|
+
collection: 'sys_audit_log'
|
|
40
|
+
},
|
|
41
|
+
// 需要审计的事件类型(仅在full模式下有效)
|
|
42
|
+
events: {
|
|
43
|
+
// 认证事件
|
|
44
|
+
authentication: true,
|
|
45
|
+
// 授权事件
|
|
46
|
+
authorization: true,
|
|
47
|
+
// 数据访问事件
|
|
48
|
+
data_access: true,
|
|
49
|
+
// 数据修改事件
|
|
50
|
+
data_modification: true,
|
|
51
|
+
// 系统事件
|
|
52
|
+
system: true,
|
|
53
|
+
// 安全事件
|
|
54
|
+
security: true,
|
|
55
|
+
// API访问事件
|
|
56
|
+
api_access: true
|
|
57
|
+
},
|
|
58
|
+
// 忽略的路径
|
|
59
|
+
ignore_paths: ['/health', '/favicon.ico'],
|
|
60
|
+
// 敏感数据字段列表,记录时会被脱敏
|
|
61
|
+
sensitive_fields: [
|
|
62
|
+
'password', 'passwd', 'pwd',
|
|
63
|
+
'token', 'key', 'secret',
|
|
64
|
+
'credit_card', 'cc', 'cvv',
|
|
65
|
+
'phone', 'mobile',
|
|
66
|
+
'id_card', 'ssn', 'social'
|
|
67
|
+
],
|
|
68
|
+
// 最大日志大小(字节)
|
|
69
|
+
max_log_size: 10485760, // 10MB
|
|
70
|
+
// 是否记录请求体
|
|
71
|
+
log_request_body: true,
|
|
72
|
+
// 是否记录响应体
|
|
73
|
+
log_response_body: false,
|
|
74
|
+
// 是否记录详细的用户信息
|
|
75
|
+
log_user_details: true,
|
|
76
|
+
// 是否记录IP地址
|
|
77
|
+
log_ip_address: true,
|
|
78
|
+
// 是否记录User-Agent
|
|
79
|
+
log_user_agent: true,
|
|
80
|
+
// 轻量模式下请求体最大长度
|
|
81
|
+
light_log_max_body_length: 1024
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 初始化中间件
|
|
87
|
+
* @param {Object} config 配置项
|
|
88
|
+
* @param {Object} next 下一个中间件
|
|
89
|
+
*/
|
|
90
|
+
init(config, next) {
|
|
91
|
+
config = Object.assign({}, this.default, config);
|
|
92
|
+
this.config = config;
|
|
93
|
+
|
|
94
|
+
// 只有在非轻量模式或输出到文件时才创建日志目录和启动轮转检查
|
|
95
|
+
if (config.enable && config.log_mode !== 'light' &&
|
|
96
|
+
(config.output === 'file' || config.output === 'both')) {
|
|
97
|
+
this.ensureLogDirExists();
|
|
98
|
+
this.startLogRotationCheck();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return next(config);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 执行中间件
|
|
106
|
+
* @param {Object} ctx Koa上下文
|
|
107
|
+
* @param {Function} next 下一个中间件
|
|
108
|
+
*/
|
|
109
|
+
async run(ctx, next) {
|
|
110
|
+
const config = this.config;
|
|
111
|
+
|
|
112
|
+
if (!config.enable) {
|
|
113
|
+
return await next();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const path = ctx.path;
|
|
117
|
+
|
|
118
|
+
// 检查是否应该忽略该路径
|
|
119
|
+
if (config.ignore_paths.some(p => path.startsWith(p))) {
|
|
120
|
+
return await next();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 根据日志模式执行不同的处理逻辑
|
|
124
|
+
if (config.log_mode === 'light') {
|
|
125
|
+
// 轻量模式:仅记录基本HTTP信息(合并了原log中间件的功能)
|
|
126
|
+
await this.handleLightLog(ctx, next);
|
|
127
|
+
} else {
|
|
128
|
+
// 完整审计模式:记录详细的安全审计日志
|
|
129
|
+
await this.handleFullAudit(ctx, next);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 处理轻量级日志(原log中间件功能)
|
|
135
|
+
* @param {Object} ctx Koa上下文
|
|
136
|
+
* @param {Function} next 下一个中间件
|
|
137
|
+
*/
|
|
138
|
+
async handleLightLog(ctx, next) {
|
|
139
|
+
try {
|
|
140
|
+
const config = this.config;
|
|
141
|
+
const url = ctx.path + ctx.querystring;
|
|
142
|
+
let body = "";
|
|
143
|
+
|
|
144
|
+
if (config.log_request_body && ctx.request.body) {
|
|
145
|
+
try {
|
|
146
|
+
body = JSON.stringify(ctx.request.body);
|
|
147
|
+
if (body.length > config.light_log_max_body_length) {
|
|
148
|
+
body = body.substring(0, config.light_log_max_body_length) + "..."
|
|
149
|
+
}
|
|
150
|
+
} catch (jsonError) {
|
|
151
|
+
body = "[无法序列化请求体]";
|
|
152
|
+
if ($.log && $.log.error) {
|
|
153
|
+
$.log.error('日志中间件JSON序列化错误:', jsonError);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 使用系统日志记录HTTP请求
|
|
159
|
+
if ($.log && $.log.http) {
|
|
160
|
+
$.log.http(`${ctx.method}\t${url}\t${body}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await next();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
// 确保请求可以继续处理
|
|
166
|
+
if ($.log && $.log.error) {
|
|
167
|
+
$.log.error('log中间件错误:', error);
|
|
168
|
+
}
|
|
169
|
+
await next();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 处理完整审计日志(原security_audit功能)
|
|
175
|
+
* @param {Object} ctx Koa上下文
|
|
176
|
+
* @param {Function} next 下一个中间件
|
|
177
|
+
*/
|
|
178
|
+
async handleFullAudit(ctx, next) {
|
|
179
|
+
const config = this.config;
|
|
180
|
+
|
|
181
|
+
// 记录请求开始时间
|
|
182
|
+
const startTime = Date.now();
|
|
183
|
+
const requestBody = config.log_request_body ? this.sanitizeData(ctx.request.body) : null;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await next();
|
|
187
|
+
|
|
188
|
+
// 记录API访问事件
|
|
189
|
+
if (config.events.api_access) {
|
|
190
|
+
const logData = this.createApiAccessLog(ctx, startTime, requestBody);
|
|
191
|
+
this.logSecurityEvent('info', 'API_ACCESS', logData);
|
|
192
|
+
}
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// 记录错误事件
|
|
195
|
+
const errorLog = this.createErrorLog(ctx, error, startTime, requestBody);
|
|
196
|
+
this.logSecurityEvent('error', 'REQUEST_ERROR', errorLog);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 确保日志目录存在
|
|
203
|
+
*/
|
|
204
|
+
ensureLogDirExists() {
|
|
205
|
+
const logDir = this.getLogDir();
|
|
206
|
+
if (!fs.existsSync(logDir)) {
|
|
207
|
+
try {
|
|
208
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
209
|
+
} catch (error) {
|
|
210
|
+
// 创建日志目录失败已通过日志系统记录
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 其余方法保持不变
|
|
216
|
+
getLogDir() {
|
|
217
|
+
const baseDir = this.config.file.dir;
|
|
218
|
+
return baseDir.startsWith('/') ? baseDir : path.join($.runPath, baseDir);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getLogFileName() {
|
|
222
|
+
const config = this.config;
|
|
223
|
+
let fileName = 'security_audit';
|
|
224
|
+
|
|
225
|
+
if (config.file.daily_rotate) {
|
|
226
|
+
const date = new Date();
|
|
227
|
+
const dateStr = date.toISOString().split('T')[0];
|
|
228
|
+
fileName += `_${dateStr}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fileName += '.log';
|
|
232
|
+
return path.join(this.getLogDir(), fileName);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async logSecurityEvent(level, eventType, data) {
|
|
236
|
+
if (!this.shouldLogLevel(level)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const logEntry = {
|
|
241
|
+
timestamp: new Date().toISOString(),
|
|
242
|
+
level: level,
|
|
243
|
+
event_type: eventType,
|
|
244
|
+
...data
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
if (this.config.output === 'file' || this.config.output === 'both') {
|
|
248
|
+
this.writeToFile(logEntry);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.config.output === 'database' || this.config.output === 'both') {
|
|
252
|
+
await this.writeToDatabase(logEntry);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 安全审计日志已通过文件或数据库记录,控制台输出已移除
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
shouldLogLevel(level) {
|
|
259
|
+
const levels = ['debug', 'info', 'warn', 'error', 'critical'];
|
|
260
|
+
const configLevelIndex = levels.indexOf(this.config.log_level);
|
|
261
|
+
const logLevelIndex = levels.indexOf(level);
|
|
262
|
+
return logLevelIndex >= configLevelIndex;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
writeToFile(logEntry) {
|
|
266
|
+
try {
|
|
267
|
+
const logFile = this.getLogFileName();
|
|
268
|
+
const logLine = JSON.stringify(logEntry) + '\n';
|
|
269
|
+
|
|
270
|
+
fs.appendFileSync(logFile, logLine, 'utf8');
|
|
271
|
+
|
|
272
|
+
this.checkLogFileSize(logFile);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// 文件写入错误已通过日志系统记录
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async writeToDatabase(logEntry) {
|
|
279
|
+
try {
|
|
280
|
+
const config = this.config.database;
|
|
281
|
+
|
|
282
|
+
if (config.type === 'mongodb' && $.mongodb_admin) {
|
|
283
|
+
const db = $.mongodb_admin('sys');
|
|
284
|
+
await db.save(config.collection, logEntry);
|
|
285
|
+
} else if (config.type === 'mysql' && $.mysql_admin) {
|
|
286
|
+
const db = $.mysql_admin('sys');
|
|
287
|
+
db.table = config.collection;
|
|
288
|
+
await db.add(logEntry);
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
// 数据库写入错误已通过日志系统记录
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
getClientIp(ctx) {
|
|
296
|
+
const headers = ctx.headers;
|
|
297
|
+
const xForwardedFor = headers['x-forwarded-for'];
|
|
298
|
+
if (xForwardedFor) {
|
|
299
|
+
const ips = xForwardedFor.split(',').map(ip => ip.trim());
|
|
300
|
+
for (let i = 0; i < ips.length; i++) {
|
|
301
|
+
const ip = ips[i];
|
|
302
|
+
if (ip && ip !== 'unknown' && ip !== '127.0.0.1' && ip !== '::1') {
|
|
303
|
+
return ip;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return headers['x-real-ip'] ||
|
|
309
|
+
headers['x-client-ip'] ||
|
|
310
|
+
headers['cf-connecting-ip'] ||
|
|
311
|
+
headers['fastly-client-ip'] ||
|
|
312
|
+
headers['true-client-ip'] ||
|
|
313
|
+
ctx.ip;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
createApiAccessLog(ctx, startTime, requestBody) {
|
|
317
|
+
const config = this.config;
|
|
318
|
+
const endTime = Date.now();
|
|
319
|
+
const responseTime = endTime - startTime;
|
|
320
|
+
|
|
321
|
+
let logData = {
|
|
322
|
+
request: {
|
|
323
|
+
method: ctx.method,
|
|
324
|
+
path: ctx.path,
|
|
325
|
+
query: this.sanitizeData(ctx.query),
|
|
326
|
+
headers: this.sanitizeHeaders(ctx.headers),
|
|
327
|
+
response_time: responseTime
|
|
328
|
+
},
|
|
329
|
+
response: {
|
|
330
|
+
status: ctx.status
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
if (requestBody) {
|
|
335
|
+
logData.request.body = requestBody;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (config.log_response_body && ctx.body && typeof ctx.body === 'object') {
|
|
339
|
+
logData.response.body = this.sanitizeData(ctx.body);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (config.log_ip_address) {
|
|
343
|
+
logData.client = {
|
|
344
|
+
ip: this.getClientIp(ctx)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (config.log_user_agent && ctx.headers['user-agent']) {
|
|
349
|
+
if (!logData.client) logData.client = {};
|
|
350
|
+
logData.client.user_agent = ctx.headers['user-agent'];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (config.log_user_details && ctx.user) {
|
|
354
|
+
logData.user = this.sanitizeUserInfo(ctx.user);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return logData;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
createErrorLog(ctx, error, startTime, requestBody) {
|
|
361
|
+
const baseLog = this.createApiAccessLog(ctx, startTime, requestBody);
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
...baseLog,
|
|
365
|
+
error: {
|
|
366
|
+
name: error.name,
|
|
367
|
+
message: error.message,
|
|
368
|
+
stack: error.stack ? error.stack.substring(0, 1000) : null
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
sanitizeData(data) {
|
|
374
|
+
if (!data || typeof data !== 'object') {
|
|
375
|
+
return data;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const sensitiveFields = this.config.sensitive_fields;
|
|
379
|
+
const result = Array.isArray(data) ? [] : {};
|
|
380
|
+
|
|
381
|
+
for (const key in data) {
|
|
382
|
+
const value = data[key];
|
|
383
|
+
const lowerKey = key.toLowerCase();
|
|
384
|
+
|
|
385
|
+
const isSensitive = sensitiveFields.some(field => lowerKey.includes(field));
|
|
386
|
+
|
|
387
|
+
if (isSensitive) {
|
|
388
|
+
result[key] = this.maskSensitiveValue(value);
|
|
389
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
390
|
+
result[key] = this.sanitizeData(value);
|
|
391
|
+
} else {
|
|
392
|
+
result[key] = value;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
maskSensitiveValue(value) {
|
|
400
|
+
if (typeof value === 'string') {
|
|
401
|
+
if (value.length <= 4) {
|
|
402
|
+
return '****';
|
|
403
|
+
} else if (value.length <= 10) {
|
|
404
|
+
return value.charAt(0) + '*'.repeat(value.length - 2) + value.charAt(value.length - 1);
|
|
405
|
+
} else {
|
|
406
|
+
return value.substring(0, 3) + '*'.repeat(value.length - 6) + value.substring(value.length - 3);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return '******';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
sanitizeHeaders(headers) {
|
|
413
|
+
if (!headers) return {};
|
|
414
|
+
|
|
415
|
+
const result = {};
|
|
416
|
+
const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key'];
|
|
417
|
+
|
|
418
|
+
for (const key in headers) {
|
|
419
|
+
const lowerKey = key.toLowerCase();
|
|
420
|
+
if (sensitiveHeaders.some(h => lowerKey.includes(h))) {
|
|
421
|
+
result[key] = '****';
|
|
422
|
+
} else {
|
|
423
|
+
result[key] = headers[key];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
sanitizeUserInfo(userInfo) {
|
|
431
|
+
return this.sanitizeData(userInfo);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
checkLogFileSize(logFile) {
|
|
435
|
+
try {
|
|
436
|
+
const stats = fs.statSync(logFile);
|
|
437
|
+
if (stats.size > this.config.max_log_size) {
|
|
438
|
+
this.rotateLogFile(logFile);
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
// 日志文件大小检查错误已通过日志系统记录
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
rotateLogFile(logFile) {
|
|
446
|
+
try {
|
|
447
|
+
const timestamp = new Date().getTime();
|
|
448
|
+
const rotatedFile = `${logFile}.${timestamp}`;
|
|
449
|
+
|
|
450
|
+
fs.renameSync(logFile, rotatedFile);
|
|
451
|
+
|
|
452
|
+
// 日志文件轮转操作已完成
|
|
453
|
+
} catch (error) {
|
|
454
|
+
// 日志文件轮转错误已通过日志系统记录
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
startLogRotationCheck() {
|
|
459
|
+
setInterval(() => {
|
|
460
|
+
this.cleanupOldLogs();
|
|
461
|
+
}, 3600000);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
cleanupOldLogs() {
|
|
465
|
+
try {
|
|
466
|
+
const logDir = this.getLogDir();
|
|
467
|
+
const maxDays = this.config.file.max_days;
|
|
468
|
+
const now = Date.now();
|
|
469
|
+
const maxAge = maxDays * 24 * 60 * 60 * 1000;
|
|
470
|
+
|
|
471
|
+
const files = fs.readdirSync(logDir);
|
|
472
|
+
for (const file of files) {
|
|
473
|
+
if (file.startsWith('security_audit') && (file.endsWith('.log') || file.endsWith('.log.'))) {
|
|
474
|
+
const filePath = path.join(logDir, file);
|
|
475
|
+
const stats = fs.statSync(filePath);
|
|
476
|
+
|
|
477
|
+
if (now - stats.mtimeMs > maxAge) {
|
|
478
|
+
fs.unlinkSync(filePath);
|
|
479
|
+
// 旧日志文件已清理
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} catch (error) {
|
|
484
|
+
// 旧日志清理错误已通过日志系统记录
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
logAuthentication(action, username, success, additionalInfo = {}) {
|
|
489
|
+
if (this.config.events.authentication) {
|
|
490
|
+
this.logSecurityEvent(
|
|
491
|
+
success ? 'info' : 'warn',
|
|
492
|
+
'AUTHENTICATION',
|
|
493
|
+
{
|
|
494
|
+
action: action,
|
|
495
|
+
username: username,
|
|
496
|
+
success: success,
|
|
497
|
+
...additionalInfo
|
|
498
|
+
}
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
logAuthorization(username, resource, action, success, additionalInfo = {}) {
|
|
504
|
+
if (this.config.events.authorization) {
|
|
505
|
+
this.logSecurityEvent(
|
|
506
|
+
success ? 'info' : 'warn',
|
|
507
|
+
'AUTHORIZATION',
|
|
508
|
+
{
|
|
509
|
+
username: username,
|
|
510
|
+
resource: resource,
|
|
511
|
+
action: action,
|
|
512
|
+
success: success,
|
|
513
|
+
...additionalInfo
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 创建中间件实例
|
|
521
|
+
const middleware = new Middleware();
|
|
522
|
+
|
|
523
|
+
// 导出符合系统期望的函数
|
|
524
|
+
exports = module.exports = function(server, config) {
|
|
525
|
+
// 初始化中间件
|
|
526
|
+
const middlewareHandler = middleware.init(config, function(config) {
|
|
527
|
+
// 返回中间件的run方法作为实际的处理函数
|
|
528
|
+
return function(ctx, next) {
|
|
529
|
+
return middleware.run(ctx, next);
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// 直接使用server.use注册中间件
|
|
534
|
+
server.use(middlewareHandler);
|
|
535
|
+
|
|
536
|
+
return server;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// 保留原始审计方法,供其他模块调用
|
|
540
|
+
exports.logAuthentication = middleware.logAuthentication.bind(middleware);
|
|
541
|
+
exports.logAuthorization = middleware.logAuthorization.bind(middleware);
|
|
542
|
+
exports.logSecurityEvent = middleware.logSecurityEvent.bind(middleware);
|
|
543
|
+
exports.middleware = middleware;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "security_audit",
|
|
3
|
+
"title": "综合日志审计系统",
|
|
4
|
+
"type": "security",
|
|
5
|
+
"state": 1,
|
|
6
|
+
"sort": 20,
|
|
7
|
+
"description": "合并了轻量级HTTP请求日志和安全审计功能的综合日志审计系统",
|
|
8
|
+
"config": {
|
|
9
|
+
"enable": true,
|
|
10
|
+
"log_mode": "light",
|
|
11
|
+
"log_level": "info",
|
|
12
|
+
"output": "file",
|
|
13
|
+
"file": {
|
|
14
|
+
"dir": "./log/security",
|
|
15
|
+
"daily_rotate": true,
|
|
16
|
+
"max_days": 30,
|
|
17
|
+
"compress": true
|
|
18
|
+
},
|
|
19
|
+
"database": {
|
|
20
|
+
"type": "mysql",
|
|
21
|
+
"collection": "sys_audit_log"
|
|
22
|
+
},
|
|
23
|
+
"events": {
|
|
24
|
+
"authentication": true,
|
|
25
|
+
"authorization": true,
|
|
26
|
+
"data_access": true,
|
|
27
|
+
"data_modification": true,
|
|
28
|
+
"system": true,
|
|
29
|
+
"security": true,
|
|
30
|
+
"api_access": true
|
|
31
|
+
},
|
|
32
|
+
"ignore_paths": ["/health", "/favicon.ico", "/static/"],
|
|
33
|
+
"sensitive_fields": [
|
|
34
|
+
"password", "passwd", "pwd",
|
|
35
|
+
"token", "key", "secret",
|
|
36
|
+
"credit_card", "cc", "cvv",
|
|
37
|
+
"phone", "mobile",
|
|
38
|
+
"id_card", "ssn", "social"
|
|
39
|
+
],
|
|
40
|
+
"max_log_size": 10485760,
|
|
41
|
+
"log_request_body": true,
|
|
42
|
+
"log_response_body": false,
|
|
43
|
+
"log_user_details": true,
|
|
44
|
+
"log_ip_address": true,
|
|
45
|
+
"log_user_agent": true,
|
|
46
|
+
"light_log_max_body_length": 1024
|
|
47
|
+
}
|
|
48
|
+
}
|