@panguard-ai/threat-cloud 0.1.0
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/dist/audit-logger.d.ts +46 -0
- package/dist/audit-logger.d.ts.map +1 -0
- package/dist/audit-logger.js +105 -0
- package/dist/audit-logger.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +115 -0
- package/dist/cli.js.map +1 -0
- package/dist/correlation-engine.d.ts +41 -0
- package/dist/correlation-engine.d.ts.map +1 -0
- package/dist/correlation-engine.js +313 -0
- package/dist/correlation-engine.js.map +1 -0
- package/dist/database.d.ts +63 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +444 -0
- package/dist/database.js.map +1 -0
- package/dist/feed-distributor.d.ts +36 -0
- package/dist/feed-distributor.d.ts.map +1 -0
- package/dist/feed-distributor.js +125 -0
- package/dist/feed-distributor.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/ioc-store.d.ts +83 -0
- package/dist/ioc-store.d.ts.map +1 -0
- package/dist/ioc-store.js +278 -0
- package/dist/ioc-store.js.map +1 -0
- package/dist/query-handlers.d.ts +40 -0
- package/dist/query-handlers.d.ts.map +1 -0
- package/dist/query-handlers.js +211 -0
- package/dist/query-handlers.js.map +1 -0
- package/dist/reputation-engine.d.ts +44 -0
- package/dist/reputation-engine.d.ts.map +1 -0
- package/dist/reputation-engine.js +169 -0
- package/dist/reputation-engine.js.map +1 -0
- package/dist/rule-generator.d.ts +47 -0
- package/dist/rule-generator.d.ts.map +1 -0
- package/dist/rule-generator.js +238 -0
- package/dist/rule-generator.js.map +1 -0
- package/dist/scheduler.d.ts +52 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +143 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/server.d.ts +99 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +809 -0
- package/dist/server.js.map +1 -0
- package/dist/sighting-store.d.ts +61 -0
- package/dist/sighting-store.d.ts.map +1 -0
- package/dist/sighting-store.js +191 -0
- package/dist/sighting-store.js.map +1 -0
- package/dist/types.d.ts +352 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Logger — provenance tracking for all write operations
|
|
3
|
+
* 稽核日誌 — 所有寫入操作的溯源追蹤
|
|
4
|
+
*
|
|
5
|
+
* Every mutation (IoC create/update, sighting, threat upload, rule publish)
|
|
6
|
+
* is logged with actor, IP, timestamp, and action details.
|
|
7
|
+
*
|
|
8
|
+
* @module @panguard-ai/threat-cloud/audit-logger
|
|
9
|
+
*/
|
|
10
|
+
import type Database from 'better-sqlite3';
|
|
11
|
+
import type { AuditAction, AuditLogEntry, AuditLogQuery, PaginatedResponse } from './types.js';
|
|
12
|
+
export declare class AuditLogger {
|
|
13
|
+
private readonly db;
|
|
14
|
+
private readonly insertStmt;
|
|
15
|
+
constructor(db: Database.Database);
|
|
16
|
+
/**
|
|
17
|
+
* Log an auditable action.
|
|
18
|
+
* 記錄可稽核的操作
|
|
19
|
+
*/
|
|
20
|
+
log(action: AuditAction, entityType: string, entityId: string, context?: {
|
|
21
|
+
actorHash?: string;
|
|
22
|
+
ipAddress?: string;
|
|
23
|
+
details?: Record<string, unknown>;
|
|
24
|
+
}): void;
|
|
25
|
+
/**
|
|
26
|
+
* Hash an API key for audit logging (never log raw keys).
|
|
27
|
+
* 將 API key 雜湊化用於稽核日誌(永遠不記錄原始 key)
|
|
28
|
+
*/
|
|
29
|
+
static hashApiKey(apiKey: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Query audit log with filters.
|
|
32
|
+
* 查詢稽核日誌
|
|
33
|
+
*/
|
|
34
|
+
query(params: AuditLogQuery): PaginatedResponse<AuditLogEntry>;
|
|
35
|
+
/**
|
|
36
|
+
* Get audit trail for a specific entity.
|
|
37
|
+
* 取得特定實體的稽核記錄
|
|
38
|
+
*/
|
|
39
|
+
getEntityTrail(entityType: string, entityId: string): AuditLogEntry[];
|
|
40
|
+
/**
|
|
41
|
+
* Purge old audit entries beyond retention.
|
|
42
|
+
* 清除超齡的稽核記錄
|
|
43
|
+
*/
|
|
44
|
+
purgeOldEntries(olderThan: string): number;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=audit-logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-logger.d.ts","sourceRoot":"","sources":["../src/audit-logger.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA2B/F,qBAAa,WAAW;IAGV,OAAO,CAAC,QAAQ,CAAC,EAAE;IAF/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;gBAEnB,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAOlD;;;OAGG;IACH,GAAG,CACD,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QACP,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KAC9B,GACL,IAAI;IAWP;;;OAGG;IACH,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAKzC;;;OAGG;IACH,KAAK,CAAC,MAAM,EAAE,aAAa,GAAG,iBAAiB,CAAC,aAAa,CAAC;IA2C9D;;;OAGG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,aAAa,EAAE;IAWrE;;;OAGG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;CAI3C"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Logger — provenance tracking for all write operations
|
|
3
|
+
* 稽核日誌 — 所有寫入操作的溯源追蹤
|
|
4
|
+
*
|
|
5
|
+
* Every mutation (IoC create/update, sighting, threat upload, rule publish)
|
|
6
|
+
* is logged with actor, IP, timestamp, and action details.
|
|
7
|
+
*
|
|
8
|
+
* @module @panguard-ai/threat-cloud/audit-logger
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
function rowToEntry(row) {
|
|
12
|
+
return {
|
|
13
|
+
id: row.id,
|
|
14
|
+
action: row.action,
|
|
15
|
+
entityType: row.entity_type,
|
|
16
|
+
entityId: row.entity_id,
|
|
17
|
+
actorHash: row.actor_hash,
|
|
18
|
+
ipAddress: row.ip_address,
|
|
19
|
+
details: row.details,
|
|
20
|
+
createdAt: row.created_at,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export class AuditLogger {
|
|
24
|
+
db;
|
|
25
|
+
insertStmt;
|
|
26
|
+
constructor(db) {
|
|
27
|
+
this.db = db;
|
|
28
|
+
this.insertStmt = this.db.prepare(`INSERT INTO audit_log (action, entity_type, entity_id, actor_hash, ip_address, details)
|
|
29
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Log an auditable action.
|
|
33
|
+
* 記錄可稽核的操作
|
|
34
|
+
*/
|
|
35
|
+
log(action, entityType, entityId, context = {}) {
|
|
36
|
+
this.insertStmt.run(action, entityType, entityId, context.actorHash ?? '', context.ipAddress ?? '', JSON.stringify(context.details ?? {}));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Hash an API key for audit logging (never log raw keys).
|
|
40
|
+
* 將 API key 雜湊化用於稽核日誌(永遠不記錄原始 key)
|
|
41
|
+
*/
|
|
42
|
+
static hashApiKey(apiKey) {
|
|
43
|
+
if (!apiKey)
|
|
44
|
+
return '';
|
|
45
|
+
return createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Query audit log with filters.
|
|
49
|
+
* 查詢稽核日誌
|
|
50
|
+
*/
|
|
51
|
+
query(params) {
|
|
52
|
+
const conditions = [];
|
|
53
|
+
const values = [];
|
|
54
|
+
if (params.action) {
|
|
55
|
+
conditions.push('action = ?');
|
|
56
|
+
values.push(params.action);
|
|
57
|
+
}
|
|
58
|
+
if (params.entityType) {
|
|
59
|
+
conditions.push('entity_type = ?');
|
|
60
|
+
values.push(params.entityType);
|
|
61
|
+
}
|
|
62
|
+
if (params.entityId) {
|
|
63
|
+
conditions.push('entity_id = ?');
|
|
64
|
+
values.push(params.entityId);
|
|
65
|
+
}
|
|
66
|
+
if (params.since) {
|
|
67
|
+
conditions.push('created_at > ?');
|
|
68
|
+
values.push(params.since);
|
|
69
|
+
}
|
|
70
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
71
|
+
const safeLimit = Math.min(Math.max(1, params.limit ?? 50), 500);
|
|
72
|
+
const total = this.db.prepare(`SELECT COUNT(*) as count FROM audit_log ${where}`).get(...values).count;
|
|
73
|
+
const rows = this.db
|
|
74
|
+
.prepare(`SELECT * FROM audit_log ${where} ORDER BY created_at DESC LIMIT ?`)
|
|
75
|
+
.all(...values, safeLimit);
|
|
76
|
+
return {
|
|
77
|
+
items: rows.map(rowToEntry),
|
|
78
|
+
total,
|
|
79
|
+
page: 1,
|
|
80
|
+
limit: safeLimit,
|
|
81
|
+
hasMore: safeLimit < total,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get audit trail for a specific entity.
|
|
86
|
+
* 取得特定實體的稽核記錄
|
|
87
|
+
*/
|
|
88
|
+
getEntityTrail(entityType, entityId) {
|
|
89
|
+
const rows = this.db
|
|
90
|
+
.prepare(`SELECT * FROM audit_log
|
|
91
|
+
WHERE entity_type = ? AND entity_id = ?
|
|
92
|
+
ORDER BY created_at ASC`)
|
|
93
|
+
.all(entityType, entityId);
|
|
94
|
+
return rows.map(rowToEntry);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Purge old audit entries beyond retention.
|
|
98
|
+
* 清除超齡的稽核記錄
|
|
99
|
+
*/
|
|
100
|
+
purgeOldEntries(olderThan) {
|
|
101
|
+
const result = this.db.prepare('DELETE FROM audit_log WHERE created_at < ?').run(olderThan);
|
|
102
|
+
return result.changes;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=audit-logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit-logger.js","sourceRoot":"","sources":["../src/audit-logger.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;GAKG"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Threat Cloud CLI entry point
|
|
4
|
+
* 威脅雲 CLI 入口點
|
|
5
|
+
*
|
|
6
|
+
* Usage: threat-cloud [--port 8080] [--host 0.0.0.0] [--db ./data/threats.db]
|
|
7
|
+
*/
|
|
8
|
+
import { ThreatCloudServer } from './server.js';
|
|
9
|
+
const MIN_API_KEY_LENGTH = 32;
|
|
10
|
+
function parseArgs(args) {
|
|
11
|
+
const config = {};
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
switch (args[i]) {
|
|
14
|
+
case '--port': {
|
|
15
|
+
const port = Number(args[++i]);
|
|
16
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
17
|
+
console.error(`Error: Invalid port number. Must be 1-65535.`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
config.port = port;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
case '--host':
|
|
24
|
+
config.host = args[++i];
|
|
25
|
+
break;
|
|
26
|
+
case '--db':
|
|
27
|
+
config.dbPath = args[++i];
|
|
28
|
+
break;
|
|
29
|
+
case '--backup-dir':
|
|
30
|
+
config.backupDir = args[++i];
|
|
31
|
+
break;
|
|
32
|
+
case '--api-key': {
|
|
33
|
+
const keys = (args[++i] ?? '').split(',').filter(Boolean);
|
|
34
|
+
const weak = keys.filter((k) => k.length < MIN_API_KEY_LENGTH);
|
|
35
|
+
if (weak.length > 0) {
|
|
36
|
+
console.error(`Error: API keys must be at least ${MIN_API_KEY_LENGTH} characters. ` +
|
|
37
|
+
`Generate with: openssl rand -hex 32`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
config.apiKeyRequired = true;
|
|
41
|
+
config.apiKeys = keys;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
case '--help':
|
|
45
|
+
console.log(`
|
|
46
|
+
Threat Cloud Server - Collective Threat Intelligence Backend
|
|
47
|
+
|
|
48
|
+
Usage: threat-cloud [options]
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--port <number> Listen port (default: 8080, range: 1-65535)
|
|
52
|
+
--host <string> Listen host (default: 127.0.0.1)
|
|
53
|
+
--db <path> SQLite database path (default: ./threat-cloud.db)
|
|
54
|
+
--api-key <keys> Comma-separated API keys (min ${MIN_API_KEY_LENGTH} chars each)
|
|
55
|
+
--backup-dir <path> Backup directory (default: ./backups)
|
|
56
|
+
--help Show this help
|
|
57
|
+
|
|
58
|
+
Generate API key:
|
|
59
|
+
openssl rand -hex 32
|
|
60
|
+
`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return config;
|
|
65
|
+
}
|
|
66
|
+
async function main() {
|
|
67
|
+
const args = parseArgs(process.argv.slice(2));
|
|
68
|
+
// Environment variables override defaults; CLI args override env
|
|
69
|
+
const envApiKeys = process.env['TC_API_KEYS']?.split(',').filter(Boolean) ?? [];
|
|
70
|
+
// Validate env API keys length in production
|
|
71
|
+
if (process.env['NODE_ENV'] === 'production' && envApiKeys.length === 0) {
|
|
72
|
+
console.error('Error: TC_API_KEYS is required in production mode.');
|
|
73
|
+
console.error('Generate with: openssl rand -hex 32');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const weakEnvKeys = envApiKeys.filter((k) => k.length < MIN_API_KEY_LENGTH);
|
|
77
|
+
if (weakEnvKeys.length > 0) {
|
|
78
|
+
console.warn(`WARNING: ${weakEnvKeys.length} API key(s) shorter than ${MIN_API_KEY_LENGTH} chars. ` +
|
|
79
|
+
`Generate stronger keys with: openssl rand -hex 32`);
|
|
80
|
+
}
|
|
81
|
+
const config = {
|
|
82
|
+
port: args.port ?? Number(process.env['TC_PORT'] ?? '8080'),
|
|
83
|
+
host: args.host ?? process.env['TC_HOST'] ?? '127.0.0.1',
|
|
84
|
+
dbPath: args.dbPath ?? process.env['TC_DB_PATH'] ?? './threat-cloud.db',
|
|
85
|
+
apiKeyRequired: args.apiKeyRequired ?? envApiKeys.length > 0,
|
|
86
|
+
apiKeys: args.apiKeys ?? envApiKeys,
|
|
87
|
+
rateLimitPerMinute: 120,
|
|
88
|
+
};
|
|
89
|
+
const backupDir = args.backupDir ?? process.env['TC_BACKUP_DIR'] ?? './backups';
|
|
90
|
+
const server = new ThreatCloudServer(config);
|
|
91
|
+
const shutdown = async () => {
|
|
92
|
+
console.log('\nShutting down Threat Cloud server...');
|
|
93
|
+
await server.stop();
|
|
94
|
+
process.exit(0);
|
|
95
|
+
};
|
|
96
|
+
process.on('SIGINT', () => void shutdown());
|
|
97
|
+
process.on('SIGTERM', () => void shutdown());
|
|
98
|
+
await server.start();
|
|
99
|
+
// Schedule daily backup (3am UTC by default)
|
|
100
|
+
const runBackup = () => {
|
|
101
|
+
const dest = server.getScheduler().runBackup(backupDir);
|
|
102
|
+
if (dest) {
|
|
103
|
+
console.log(`[Backup] Database backed up to ${dest}`);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.error('[Backup] Database backup failed');
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
// Initial backup on startup
|
|
110
|
+
runBackup();
|
|
111
|
+
// Daily backup interval
|
|
112
|
+
setInterval(runBackup, 24 * 60 * 60 * 1000);
|
|
113
|
+
}
|
|
114
|
+
void main();
|
|
115
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;GAKG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGhD,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAE9B,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,MAAM,GAAmD,EAAE,CAAC;IAClE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,QAAQ,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;YAChB,KAAK,QAAQ,CAAC,CAAC,CAAC;gBACd,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC/B,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;oBAC5C,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;oBAC9D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClB,CAAC;gBACD,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;gBACnB,MAAM;YACR,CAAC;YACD,KAAK,QAAQ;gBACX,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBACxB,MAAM;YACR,KAAK,MAAM;gBACT,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC1B,MAAM;YACR,KAAK,cAAc;gBACjB,MAAM,CAAC,SAAS,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC7B,MAAM;YACR,KAAK,WAAW,CAAC,CAAC,CAAC;gBACjB,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,kBAAkB,CAAC,CAAC;gBAC/D,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACpB,OAAO,CAAC,KAAK,CACX,oCAAoC,kBAAkB,eAAe;wBACnE,qCAAqC,CACxC,CAAC;oBACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAClB,CAAC;gBACD,MAAM,CAAC,cAAc,GAAG,IAAI,CAAC;gBAC7B,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;gBACtB,MAAM;YACR,CAAC;YACD,KAAK,QAAQ;gBACX,OAAO,CAAC,GAAG,CAAC;;;;;;;;;uDASmC,kBAAkB;;;;;;CAMxE,CAAC,CAAC;gBACK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAE9C,iEAAiE;IACjE,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;IAEhF,6CAA6C;IAC7C,IAAI,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,YAAY,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxE,OAAO,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;QACpE,OAAO,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,kBAAkB,CAAC,CAAC;IAC5E,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC3B,OAAO,CAAC,IAAI,CACV,YAAY,WAAW,CAAC,MAAM,4BAA4B,kBAAkB,UAAU;YACpF,mDAAmD,CACtD,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAiB;QAC3B,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,MAAM,CAAC;QAC3D,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,WAAW;QACxD,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,mBAAmB;QACvE,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC;QAC5D,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,UAAU;QACnC,kBAAkB,EAAE,GAAG;KACxB,CAAC;IAEF,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,WAAW,CAAC;IAEhF,MAAM,MAAM,GAAG,IAAI,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAE7C,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,OAAO,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;QACtD,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,EAAE,CAAC,CAAC;IAE7C,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IAErB,6CAA6C;IAC7C,MAAM,SAAS,GAAG,GAAG,EAAE;QACrB,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QACxD,IAAI,IAAI,EAAE,CAAC;YACT,OAAO,CAAC,GAAG,CAAC,kCAAkC,IAAI,EAAE,CAAC,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC,CAAC;IAEF,4BAA4B;IAC5B,SAAS,EAAE,CAAC;IAEZ,wBAAwB;IACxB,WAAW,CAAC,SAAS,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AAC9C,CAAC;AAED,KAAK,IAAI,EAAE,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threat Correlation Engine
|
|
3
|
+
* 威脅關聯引擎
|
|
4
|
+
*
|
|
5
|
+
* Groups related threat events into campaigns based on:
|
|
6
|
+
* 1. Same source IP within a time window (IP cluster)
|
|
7
|
+
* 2. Same attack pattern across multiple IPs (pattern cluster)
|
|
8
|
+
*
|
|
9
|
+
* @module @panguard-ai/threat-cloud/correlation-engine
|
|
10
|
+
*/
|
|
11
|
+
import type Database from 'better-sqlite3';
|
|
12
|
+
import type { CorrelationConfig, Campaign, CampaignScanResult, CampaignStats, PaginationParams, PaginatedResponse, EnrichedThreatEvent } from './types.js';
|
|
13
|
+
export declare class CorrelationEngine {
|
|
14
|
+
private readonly db;
|
|
15
|
+
private readonly config;
|
|
16
|
+
constructor(db: Database.Database, config?: Partial<CorrelationConfig>);
|
|
17
|
+
/** Create campaigns table if not exists / 建立 campaigns 表 */
|
|
18
|
+
private ensureTable;
|
|
19
|
+
/**
|
|
20
|
+
* Scan for new campaigns in uncorrelated events.
|
|
21
|
+
* 掃描未關聯的事件,建立新的攻擊活動
|
|
22
|
+
*/
|
|
23
|
+
scanForCampaigns(): CampaignScanResult;
|
|
24
|
+
/** Get campaign by ID / 取得 campaign */
|
|
25
|
+
getCampaign(campaignId: string): Campaign | null;
|
|
26
|
+
/** List campaigns / 列表 campaigns */
|
|
27
|
+
listCampaigns(pagination: PaginationParams, status?: string): PaginatedResponse<Campaign>;
|
|
28
|
+
/** Get campaign events / 取得 campaign 事件 */
|
|
29
|
+
getCampaignEvents(campaignId: string): EnrichedThreatEvent[];
|
|
30
|
+
/** Get campaign statistics / 取得攻擊活動統計 */
|
|
31
|
+
getCampaignStats(): CampaignStats;
|
|
32
|
+
/** Cluster events by time window / 依時間窗口聚類事件 */
|
|
33
|
+
private clusterByTimeWindow;
|
|
34
|
+
/** Generate deterministic campaign ID / 產生確定性 campaign ID */
|
|
35
|
+
private generateCampaignId;
|
|
36
|
+
/** Pick the maximum severity / 選擇最高嚴重度 */
|
|
37
|
+
private pickMaxSeverity;
|
|
38
|
+
/** Upsert campaign / 新增或更新攻擊活動 */
|
|
39
|
+
private upsertCampaign;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=correlation-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation-engine.d.ts","sourceRoot":"","sources":["../src/correlation-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EACV,iBAAiB,EACjB,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAgDpB,qBAAa,iBAAiB;IAI1B,OAAO,CAAC,QAAQ,CAAC,EAAE;IAHrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;gBAGxB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACtC,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC;IAMrC,4DAA4D;IAC5D,OAAO,CAAC,WAAW;IAuBnB;;;OAGG;IACH,gBAAgB,IAAI,kBAAkB;IAiJtC,uCAAuC;IACvC,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAOhD,oCAAoC;IACpC,aAAa,CAAC,UAAU,EAAE,gBAAgB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAAC,QAAQ,CAAC;IAyBzF,2CAA2C;IAC3C,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,mBAAmB,EAAE;IA2B5D,yCAAyC;IACzC,gBAAgB,IAAI,aAAa;IAmCjC,gDAAgD;IAChD,OAAO,CAAC,mBAAmB;IAoB3B,6DAA6D;IAC7D,OAAO,CAAC,kBAAkB;IAO1B,0CAA0C;IAC1C,OAAO,CAAC,eAAe;IAQvB,kCAAkC;IAClC,OAAO,CAAC,cAAc;CA+BvB"}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threat Correlation Engine
|
|
3
|
+
* 威脅關聯引擎
|
|
4
|
+
*
|
|
5
|
+
* Groups related threat events into campaigns based on:
|
|
6
|
+
* 1. Same source IP within a time window (IP cluster)
|
|
7
|
+
* 2. Same attack pattern across multiple IPs (pattern cluster)
|
|
8
|
+
*
|
|
9
|
+
* @module @panguard-ai/threat-cloud/correlation-engine
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
/** Default config / 預設配置 */
|
|
13
|
+
const DEFAULT_CONFIG = {
|
|
14
|
+
timeWindowMinutes: 60,
|
|
15
|
+
minEventsForCampaign: 3,
|
|
16
|
+
minIPsForPatternCampaign: 5,
|
|
17
|
+
scanWindowHours: 24,
|
|
18
|
+
};
|
|
19
|
+
/** Convert DB row to Campaign / 轉換 DB 列 */
|
|
20
|
+
function rowToCampaign(row) {
|
|
21
|
+
return {
|
|
22
|
+
campaignId: row.campaign_id,
|
|
23
|
+
name: row.name,
|
|
24
|
+
campaignType: row.campaign_type,
|
|
25
|
+
firstSeen: row.first_seen,
|
|
26
|
+
lastSeen: row.last_seen,
|
|
27
|
+
eventCount: row.event_count,
|
|
28
|
+
uniqueIPs: row.unique_ips,
|
|
29
|
+
attackTypes: JSON.parse(row.attack_types),
|
|
30
|
+
mitreTechniques: JSON.parse(row.mitre_techniques),
|
|
31
|
+
regions: JSON.parse(row.regions),
|
|
32
|
+
severity: row.severity,
|
|
33
|
+
status: row.status,
|
|
34
|
+
createdAt: row.created_at,
|
|
35
|
+
updatedAt: row.updated_at,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export class CorrelationEngine {
|
|
39
|
+
db;
|
|
40
|
+
config;
|
|
41
|
+
constructor(db, config) {
|
|
42
|
+
this.db = db;
|
|
43
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
44
|
+
this.ensureTable();
|
|
45
|
+
}
|
|
46
|
+
/** Create campaigns table if not exists / 建立 campaigns 表 */
|
|
47
|
+
ensureTable() {
|
|
48
|
+
this.db.exec(`
|
|
49
|
+
CREATE TABLE IF NOT EXISTS campaigns (
|
|
50
|
+
campaign_id TEXT PRIMARY KEY,
|
|
51
|
+
name TEXT NOT NULL,
|
|
52
|
+
campaign_type TEXT NOT NULL CHECK(campaign_type IN ('ip_cluster','pattern_cluster','manual')),
|
|
53
|
+
first_seen TEXT NOT NULL,
|
|
54
|
+
last_seen TEXT NOT NULL,
|
|
55
|
+
event_count INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
unique_ips INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
attack_types TEXT NOT NULL DEFAULT '[]',
|
|
58
|
+
mitre_techniques TEXT NOT NULL DEFAULT '[]',
|
|
59
|
+
regions TEXT NOT NULL DEFAULT '[]',
|
|
60
|
+
severity TEXT NOT NULL DEFAULT 'medium',
|
|
61
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active','resolved','false_positive')),
|
|
62
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
63
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
64
|
+
);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_campaigns_status ON campaigns(status);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_campaigns_last_seen ON campaigns(last_seen);
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Scan for new campaigns in uncorrelated events.
|
|
71
|
+
* 掃描未關聯的事件,建立新的攻擊活動
|
|
72
|
+
*/
|
|
73
|
+
scanForCampaigns() {
|
|
74
|
+
const startTime = Date.now();
|
|
75
|
+
let newCampaigns = 0;
|
|
76
|
+
const updatedCampaigns = 0;
|
|
77
|
+
let eventsCorrelated = 0;
|
|
78
|
+
const sinceDate = new Date(Date.now() - this.config.scanWindowHours * 60 * 60 * 1000).toISOString();
|
|
79
|
+
// Fetch uncorrelated events within scan window
|
|
80
|
+
const events = this.db
|
|
81
|
+
.prepare(`SELECT id, attack_source_ip, attack_type, mitre_techniques, timestamp, region, severity
|
|
82
|
+
FROM enriched_threats
|
|
83
|
+
WHERE campaign_id IS NULL AND received_at > ?
|
|
84
|
+
ORDER BY timestamp ASC`)
|
|
85
|
+
.all(sinceDate);
|
|
86
|
+
if (events.length === 0) {
|
|
87
|
+
return {
|
|
88
|
+
newCampaigns: 0,
|
|
89
|
+
updatedCampaigns: 0,
|
|
90
|
+
eventsCorrelated: 0,
|
|
91
|
+
duration: Date.now() - startTime,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// --- IP Cluster detection ---
|
|
95
|
+
const byIP = new Map();
|
|
96
|
+
for (const e of events) {
|
|
97
|
+
const list = byIP.get(e.attack_source_ip) ?? [];
|
|
98
|
+
list.push(e);
|
|
99
|
+
byIP.set(e.attack_source_ip, list);
|
|
100
|
+
}
|
|
101
|
+
const assignedIds = new Set();
|
|
102
|
+
const updateCampaignId = this.db.prepare('UPDATE enriched_threats SET campaign_id = ? WHERE id = ?');
|
|
103
|
+
this.db.transaction(() => {
|
|
104
|
+
for (const [ip, ipEvents] of byIP) {
|
|
105
|
+
if (ipEvents.length < this.config.minEventsForCampaign)
|
|
106
|
+
continue;
|
|
107
|
+
// Check time window clustering
|
|
108
|
+
const clustered = this.clusterByTimeWindow(ipEvents);
|
|
109
|
+
for (const cluster of clustered) {
|
|
110
|
+
if (cluster.length < this.config.minEventsForCampaign)
|
|
111
|
+
continue;
|
|
112
|
+
const campaignId = this.generateCampaignId(cluster.map((e) => e.id));
|
|
113
|
+
const attackTypes = [...new Set(cluster.map((e) => e.attack_type))];
|
|
114
|
+
const allTechniques = cluster.flatMap((e) => JSON.parse(e.mitre_techniques));
|
|
115
|
+
const techniques = [...new Set(allTechniques)];
|
|
116
|
+
const regions = [...new Set(cluster.map((e) => e.region))];
|
|
117
|
+
const timestamps = cluster.map((e) => e.timestamp).sort();
|
|
118
|
+
const maxSeverity = this.pickMaxSeverity(cluster.map((e) => e.severity));
|
|
119
|
+
this.upsertCampaign({
|
|
120
|
+
campaignId,
|
|
121
|
+
name: `IP ${ip}: ${attackTypes.join(', ')}`,
|
|
122
|
+
campaignType: 'ip_cluster',
|
|
123
|
+
firstSeen: timestamps[0],
|
|
124
|
+
lastSeen: timestamps[timestamps.length - 1],
|
|
125
|
+
eventCount: cluster.length,
|
|
126
|
+
uniqueIPs: 1,
|
|
127
|
+
attackTypes,
|
|
128
|
+
mitreTechniques: techniques,
|
|
129
|
+
regions,
|
|
130
|
+
severity: maxSeverity,
|
|
131
|
+
});
|
|
132
|
+
for (const e of cluster) {
|
|
133
|
+
updateCampaignId.run(campaignId, e.id);
|
|
134
|
+
assignedIds.add(e.id);
|
|
135
|
+
}
|
|
136
|
+
newCampaigns++;
|
|
137
|
+
eventsCorrelated += cluster.length;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// --- Pattern Cluster detection ---
|
|
141
|
+
const unassigned = events.filter((e) => !assignedIds.has(e.id));
|
|
142
|
+
const byPattern = new Map();
|
|
143
|
+
for (const e of unassigned) {
|
|
144
|
+
const techniques = JSON.parse(e.mitre_techniques).sort().join(',');
|
|
145
|
+
const key = `${e.attack_type}|${techniques}`;
|
|
146
|
+
const list = byPattern.get(key) ?? [];
|
|
147
|
+
list.push(e);
|
|
148
|
+
byPattern.set(key, list);
|
|
149
|
+
}
|
|
150
|
+
for (const [pattern, patternEvents] of byPattern) {
|
|
151
|
+
const distinctIPs = new Set(patternEvents.map((e) => e.attack_source_ip));
|
|
152
|
+
if (distinctIPs.size < this.config.minIPsForPatternCampaign)
|
|
153
|
+
continue;
|
|
154
|
+
const campaignId = this.generateCampaignId(patternEvents.map((e) => e.id));
|
|
155
|
+
const [attackType] = pattern.split('|');
|
|
156
|
+
const allTechniques = patternEvents.flatMap((e) => JSON.parse(e.mitre_techniques));
|
|
157
|
+
const techniques = [...new Set(allTechniques)];
|
|
158
|
+
const regions = [...new Set(patternEvents.map((e) => e.region))];
|
|
159
|
+
const timestamps = patternEvents.map((e) => e.timestamp).sort();
|
|
160
|
+
const maxSeverity = this.pickMaxSeverity(patternEvents.map((e) => e.severity));
|
|
161
|
+
this.upsertCampaign({
|
|
162
|
+
campaignId,
|
|
163
|
+
name: `Pattern: ${attackType} from ${distinctIPs.size} IPs`,
|
|
164
|
+
campaignType: 'pattern_cluster',
|
|
165
|
+
firstSeen: timestamps[0],
|
|
166
|
+
lastSeen: timestamps[timestamps.length - 1],
|
|
167
|
+
eventCount: patternEvents.length,
|
|
168
|
+
uniqueIPs: distinctIPs.size,
|
|
169
|
+
attackTypes: [attackType],
|
|
170
|
+
mitreTechniques: techniques,
|
|
171
|
+
regions,
|
|
172
|
+
severity: maxSeverity,
|
|
173
|
+
});
|
|
174
|
+
for (const e of patternEvents) {
|
|
175
|
+
updateCampaignId.run(campaignId, e.id);
|
|
176
|
+
}
|
|
177
|
+
newCampaigns++;
|
|
178
|
+
eventsCorrelated += patternEvents.length;
|
|
179
|
+
}
|
|
180
|
+
})();
|
|
181
|
+
return {
|
|
182
|
+
newCampaigns,
|
|
183
|
+
updatedCampaigns,
|
|
184
|
+
eventsCorrelated,
|
|
185
|
+
duration: Date.now() - startTime,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/** Get campaign by ID / 取得 campaign */
|
|
189
|
+
getCampaign(campaignId) {
|
|
190
|
+
const row = this.db.prepare('SELECT * FROM campaigns WHERE campaign_id = ?').get(campaignId);
|
|
191
|
+
return row ? rowToCampaign(row) : null;
|
|
192
|
+
}
|
|
193
|
+
/** List campaigns / 列表 campaigns */
|
|
194
|
+
listCampaigns(pagination, status) {
|
|
195
|
+
const where = status ? 'WHERE status = ?' : '';
|
|
196
|
+
const params = status ? [status] : [];
|
|
197
|
+
const safeLimit = Math.min(Math.max(1, pagination.limit), 1000);
|
|
198
|
+
const offset = (Math.max(1, pagination.page) - 1) * safeLimit;
|
|
199
|
+
const total = this.db.prepare(`SELECT COUNT(*) as count FROM campaigns ${where}`).get(...params).count;
|
|
200
|
+
const rows = this.db
|
|
201
|
+
.prepare(`SELECT * FROM campaigns ${where} ORDER BY last_seen DESC LIMIT ? OFFSET ?`)
|
|
202
|
+
.all(...params, safeLimit, offset);
|
|
203
|
+
return {
|
|
204
|
+
items: rows.map(rowToCampaign),
|
|
205
|
+
total,
|
|
206
|
+
page: pagination.page,
|
|
207
|
+
limit: safeLimit,
|
|
208
|
+
hasMore: offset + safeLimit < total,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/** Get campaign events / 取得 campaign 事件 */
|
|
212
|
+
getCampaignEvents(campaignId) {
|
|
213
|
+
const rows = this.db
|
|
214
|
+
.prepare('SELECT * FROM enriched_threats WHERE campaign_id = ? ORDER BY timestamp ASC')
|
|
215
|
+
.all(campaignId);
|
|
216
|
+
return rows.map((r) => ({
|
|
217
|
+
id: r['id'],
|
|
218
|
+
sourceType: r['source_type'],
|
|
219
|
+
attackSourceIP: r['attack_source_ip'],
|
|
220
|
+
attackType: r['attack_type'],
|
|
221
|
+
mitreTechniques: JSON.parse(r['mitre_techniques']),
|
|
222
|
+
sigmaRuleMatched: r['sigma_rule_matched'],
|
|
223
|
+
timestamp: r['timestamp'],
|
|
224
|
+
industry: r['industry'],
|
|
225
|
+
region: r['region'],
|
|
226
|
+
confidence: r['confidence'],
|
|
227
|
+
severity: r['severity'],
|
|
228
|
+
serviceType: r['service_type'],
|
|
229
|
+
skillLevel: r['skill_level'],
|
|
230
|
+
intent: r['intent'],
|
|
231
|
+
tools: r['tools'] ? JSON.parse(r['tools']) : undefined,
|
|
232
|
+
eventHash: r['event_hash'],
|
|
233
|
+
receivedAt: r['received_at'],
|
|
234
|
+
campaignId: r['campaign_id'],
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
/** Get campaign statistics / 取得攻擊活動統計 */
|
|
238
|
+
getCampaignStats() {
|
|
239
|
+
const total = this.db.prepare('SELECT COUNT(*) as count FROM campaigns').get().count;
|
|
240
|
+
const active = this.db.prepare("SELECT COUNT(*) as count FROM campaigns WHERE status = 'active'").get().count;
|
|
241
|
+
const correlated = this.db
|
|
242
|
+
.prepare('SELECT COUNT(*) as count FROM enriched_threats WHERE campaign_id IS NOT NULL')
|
|
243
|
+
.get().count;
|
|
244
|
+
const topTypes = this.db
|
|
245
|
+
.prepare(`SELECT attack_type as type, COUNT(*) as count
|
|
246
|
+
FROM enriched_threats WHERE campaign_id IS NOT NULL
|
|
247
|
+
GROUP BY attack_type ORDER BY count DESC LIMIT 10`)
|
|
248
|
+
.all();
|
|
249
|
+
return {
|
|
250
|
+
totalCampaigns: total,
|
|
251
|
+
activeCampaigns: active,
|
|
252
|
+
totalCorrelatedEvents: correlated,
|
|
253
|
+
topAttackTypes: topTypes,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// -------------------------------------------------------------------------
|
|
257
|
+
// Private helpers / 私有輔助方法
|
|
258
|
+
// -------------------------------------------------------------------------
|
|
259
|
+
/** Cluster events by time window / 依時間窗口聚類事件 */
|
|
260
|
+
clusterByTimeWindow(events) {
|
|
261
|
+
if (events.length === 0)
|
|
262
|
+
return [];
|
|
263
|
+
const sorted = [...events].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
264
|
+
const windowMs = this.config.timeWindowMinutes * 60 * 1000;
|
|
265
|
+
const clusters = [[sorted[0]]];
|
|
266
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
267
|
+
const current = new Date(sorted[i].timestamp).getTime();
|
|
268
|
+
const clusterStart = new Date(clusters[clusters.length - 1][0].timestamp).getTime();
|
|
269
|
+
if (current - clusterStart <= windowMs) {
|
|
270
|
+
clusters[clusters.length - 1].push(sorted[i]);
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
clusters.push([sorted[i]]);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return clusters;
|
|
277
|
+
}
|
|
278
|
+
/** Generate deterministic campaign ID / 產生確定性 campaign ID */
|
|
279
|
+
generateCampaignId(eventIds) {
|
|
280
|
+
const sorted = [...eventIds].sort((a, b) => a - b);
|
|
281
|
+
const hash = createHash('sha256').update(sorted.join(',')).digest('hex').slice(0, 8);
|
|
282
|
+
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
283
|
+
return `C-${date}-${hash}`;
|
|
284
|
+
}
|
|
285
|
+
/** Pick the maximum severity / 選擇最高嚴重度 */
|
|
286
|
+
pickMaxSeverity(severities) {
|
|
287
|
+
const order = ['critical', 'high', 'medium', 'low'];
|
|
288
|
+
for (const s of order) {
|
|
289
|
+
if (severities.includes(s))
|
|
290
|
+
return s;
|
|
291
|
+
}
|
|
292
|
+
return 'medium';
|
|
293
|
+
}
|
|
294
|
+
/** Upsert campaign / 新增或更新攻擊活動 */
|
|
295
|
+
upsertCampaign(c) {
|
|
296
|
+
this.db
|
|
297
|
+
.prepare(`INSERT INTO campaigns
|
|
298
|
+
(campaign_id, name, campaign_type, first_seen, last_seen, event_count, unique_ips,
|
|
299
|
+
attack_types, mitre_techniques, regions, severity)
|
|
300
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
301
|
+
ON CONFLICT(campaign_id) DO UPDATE SET
|
|
302
|
+
last_seen = excluded.last_seen,
|
|
303
|
+
event_count = excluded.event_count,
|
|
304
|
+
unique_ips = excluded.unique_ips,
|
|
305
|
+
attack_types = excluded.attack_types,
|
|
306
|
+
mitre_techniques = excluded.mitre_techniques,
|
|
307
|
+
regions = excluded.regions,
|
|
308
|
+
severity = excluded.severity,
|
|
309
|
+
updated_at = datetime('now')`)
|
|
310
|
+
.run(c.campaignId, c.name, c.campaignType, c.firstSeen, c.lastSeen, c.eventCount, c.uniqueIPs, JSON.stringify(c.attackTypes), JSON.stringify(c.mitreTechniques), JSON.stringify(c.regions), c.severity);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
//# sourceMappingURL=correlation-engine.js.map
|