honeyweb-core 2.0.2 → 2.0.4
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/ai/gemini.js +54 -29
- package/ai/index.js +4 -27
- package/ai/prompt-templates.js +22 -42
- package/config/defaults.js +14 -37
- package/config/index.js +7 -35
- package/config/schema.js +1 -17
- package/detection/behavioral.js +26 -118
- package/detection/bot-detector.js +11 -62
- package/detection/index.js +19 -35
- package/detection/patterns.js +14 -51
- package/detection/rate-limiter.js +15 -86
- package/detection/traversal.js +42 -0
- package/index.js +1 -13
- package/middleware/blocklist-checker.js +1 -15
- package/middleware/dashboard.js +218 -214
- package/middleware/index.js +17 -39
- package/middleware/request-analyzer.js +29 -70
- package/middleware/trap-injector.js +3 -28
- package/package.json +1 -1
- package/storage/bot-tracker.js +9 -56
- package/storage/index.js +3 -12
- package/storage/json-store.js +15 -89
- package/storage/memory-store.js +9 -61
- package/utils/cache.js +5 -48
- package/utils/dns-verify.js +8 -45
- package/utils/event-emitter.js +7 -39
- package/utils/logger.js +9 -35
package/storage/memory-store.js
CHANGED
|
@@ -1,45 +1,28 @@
|
|
|
1
1
|
// honeyweb-core/storage/memory-store.js
|
|
2
|
-
// In-memory storage for testing
|
|
3
2
|
|
|
4
3
|
class MemoryStore {
|
|
5
4
|
constructor(config) {
|
|
6
|
-
this.bannedIPs = new Map();
|
|
7
|
-
this.banDuration = config.banDuration || 86400000;
|
|
5
|
+
this.bannedIPs = new Map();
|
|
6
|
+
this.banDuration = config.banDuration || 86400000;
|
|
8
7
|
this.autoCleanup = config.autoCleanup !== false;
|
|
9
8
|
|
|
10
|
-
// Start auto-cleanup if enabled
|
|
11
9
|
if (this.autoCleanup) {
|
|
12
|
-
this.cleanupInterval = setInterval(() =>
|
|
13
|
-
this._cleanupExpired();
|
|
14
|
-
}, 60000); // Clean up every minute
|
|
10
|
+
this.cleanupInterval = setInterval(() => this._cleanupExpired(), 60000);
|
|
15
11
|
}
|
|
16
12
|
}
|
|
17
13
|
|
|
18
|
-
/**
|
|
19
|
-
* Get all banned IPs
|
|
20
|
-
* @returns {Promise<Array>}
|
|
21
|
-
*/
|
|
22
14
|
async getAll() {
|
|
23
15
|
const result = [];
|
|
24
16
|
for (const [ip, data] of this.bannedIPs.entries()) {
|
|
25
|
-
result.push({
|
|
26
|
-
ip,
|
|
27
|
-
...data
|
|
28
|
-
});
|
|
17
|
+
result.push({ ip, ...data });
|
|
29
18
|
}
|
|
30
19
|
return result;
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
/**
|
|
34
|
-
* Check if IP is banned
|
|
35
|
-
* @param {string} ip
|
|
36
|
-
* @returns {Promise<boolean>}
|
|
37
|
-
*/
|
|
38
22
|
async isBanned(ip) {
|
|
39
23
|
const entry = this.bannedIPs.get(ip);
|
|
40
24
|
if (!entry) return false;
|
|
41
25
|
|
|
42
|
-
// Check if expired
|
|
43
26
|
if (entry.expiry && Date.now() > entry.expiry) {
|
|
44
27
|
this.bannedIPs.delete(ip);
|
|
45
28
|
return false;
|
|
@@ -48,12 +31,6 @@ class MemoryStore {
|
|
|
48
31
|
return true;
|
|
49
32
|
}
|
|
50
33
|
|
|
51
|
-
/**
|
|
52
|
-
* Ban an IP
|
|
53
|
-
* @param {string} ip
|
|
54
|
-
* @param {string} reason
|
|
55
|
-
* @returns {Promise<void>}
|
|
56
|
-
*/
|
|
57
34
|
async ban(ip, reason = 'Suspicious activity') {
|
|
58
35
|
this.bannedIPs.set(ip, {
|
|
59
36
|
reason,
|
|
@@ -62,19 +39,10 @@ class MemoryStore {
|
|
|
62
39
|
});
|
|
63
40
|
}
|
|
64
41
|
|
|
65
|
-
/**
|
|
66
|
-
* Unban an IP
|
|
67
|
-
* @param {string} ip
|
|
68
|
-
* @returns {Promise<void>}
|
|
69
|
-
*/
|
|
70
42
|
async unban(ip) {
|
|
71
43
|
this.bannedIPs.delete(ip);
|
|
72
44
|
}
|
|
73
45
|
|
|
74
|
-
/**
|
|
75
|
-
* Clean up expired bans
|
|
76
|
-
* @private
|
|
77
|
-
*/
|
|
78
46
|
_cleanupExpired() {
|
|
79
47
|
const now = Date.now();
|
|
80
48
|
for (const [ip, entry] of this.bannedIPs.entries()) {
|
|
@@ -84,44 +52,24 @@ class MemoryStore {
|
|
|
84
52
|
}
|
|
85
53
|
}
|
|
86
54
|
|
|
87
|
-
/**
|
|
88
|
-
* Get statistics
|
|
89
|
-
* @returns {Promise<Object>}
|
|
90
|
-
*/
|
|
91
55
|
async getStats() {
|
|
92
56
|
const now = Date.now();
|
|
93
|
-
let active = 0;
|
|
94
|
-
let expired = 0;
|
|
57
|
+
let active = 0, expired = 0;
|
|
95
58
|
|
|
96
59
|
for (const entry of this.bannedIPs.values()) {
|
|
97
|
-
if (!entry.expiry || now <= entry.expiry)
|
|
98
|
-
|
|
99
|
-
} else {
|
|
100
|
-
expired++;
|
|
101
|
-
}
|
|
60
|
+
if (!entry.expiry || now <= entry.expiry) active++;
|
|
61
|
+
else expired++;
|
|
102
62
|
}
|
|
103
63
|
|
|
104
|
-
return {
|
|
105
|
-
total: this.bannedIPs.size,
|
|
106
|
-
active,
|
|
107
|
-
expired
|
|
108
|
-
};
|
|
64
|
+
return { total: this.bannedIPs.size, active, expired };
|
|
109
65
|
}
|
|
110
66
|
|
|
111
|
-
/**
|
|
112
|
-
* Clear all bans
|
|
113
|
-
*/
|
|
114
67
|
clear() {
|
|
115
68
|
this.bannedIPs.clear();
|
|
116
69
|
}
|
|
117
70
|
|
|
118
|
-
/**
|
|
119
|
-
* Cleanup and stop intervals
|
|
120
|
-
*/
|
|
121
71
|
destroy() {
|
|
122
|
-
if (this.cleanupInterval)
|
|
123
|
-
clearInterval(this.cleanupInterval);
|
|
124
|
-
}
|
|
72
|
+
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
125
73
|
}
|
|
126
74
|
}
|
|
127
75
|
|
package/utils/cache.js
CHANGED
|
@@ -1,26 +1,16 @@
|
|
|
1
1
|
// honeyweb-core/utils/cache.js
|
|
2
|
-
// LRU cache implementation for DNS verification and blocklist
|
|
3
2
|
|
|
4
3
|
class LRUCache {
|
|
5
4
|
constructor(maxSize = 1000, ttl = 3600000) {
|
|
6
5
|
this.maxSize = maxSize;
|
|
7
|
-
this.ttl = ttl;
|
|
6
|
+
this.ttl = ttl;
|
|
8
7
|
this.cache = new Map();
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
/**
|
|
12
|
-
* Get value from cache
|
|
13
|
-
* @param {string} key
|
|
14
|
-
* @returns {*} - Value or undefined if not found/expired
|
|
15
|
-
*/
|
|
16
10
|
get(key) {
|
|
17
11
|
const item = this.cache.get(key);
|
|
12
|
+
if (!item) return undefined;
|
|
18
13
|
|
|
19
|
-
if (!item) {
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Check if expired
|
|
24
14
|
if (Date.now() > item.expiry) {
|
|
25
15
|
this.cache.delete(key);
|
|
26
16
|
return undefined;
|
|
@@ -29,76 +19,43 @@ class LRUCache {
|
|
|
29
19
|
// Move to end (most recently used)
|
|
30
20
|
this.cache.delete(key);
|
|
31
21
|
this.cache.set(key, item);
|
|
32
|
-
|
|
33
22
|
return item.value;
|
|
34
23
|
}
|
|
35
24
|
|
|
36
|
-
/**
|
|
37
|
-
* Set value in cache
|
|
38
|
-
* @param {string} key
|
|
39
|
-
* @param {*} value
|
|
40
|
-
* @param {number} customTTL - Optional custom TTL for this entry
|
|
41
|
-
*/
|
|
42
25
|
set(key, value, customTTL) {
|
|
43
|
-
|
|
44
|
-
if (this.cache.has(key)) {
|
|
45
|
-
this.cache.delete(key);
|
|
46
|
-
}
|
|
26
|
+
if (this.cache.has(key)) this.cache.delete(key);
|
|
47
27
|
|
|
48
|
-
// Evict oldest if at capacity
|
|
49
28
|
if (this.cache.size >= this.maxSize) {
|
|
50
29
|
const firstKey = this.cache.keys().next().value;
|
|
51
30
|
this.cache.delete(firstKey);
|
|
52
31
|
}
|
|
53
32
|
|
|
54
|
-
const ttl = customTTL || this.ttl;
|
|
55
33
|
this.cache.set(key, {
|
|
56
34
|
value,
|
|
57
|
-
expiry: Date.now() + ttl
|
|
35
|
+
expiry: Date.now() + (customTTL || this.ttl)
|
|
58
36
|
});
|
|
59
37
|
}
|
|
60
38
|
|
|
61
|
-
/**
|
|
62
|
-
* Check if key exists and is not expired
|
|
63
|
-
* @param {string} key
|
|
64
|
-
* @returns {boolean}
|
|
65
|
-
*/
|
|
66
39
|
has(key) {
|
|
67
40
|
return this.get(key) !== undefined;
|
|
68
41
|
}
|
|
69
42
|
|
|
70
|
-
/**
|
|
71
|
-
* Delete key from cache
|
|
72
|
-
* @param {string} key
|
|
73
|
-
*/
|
|
74
43
|
delete(key) {
|
|
75
44
|
this.cache.delete(key);
|
|
76
45
|
}
|
|
77
46
|
|
|
78
|
-
/**
|
|
79
|
-
* Clear all entries
|
|
80
|
-
*/
|
|
81
47
|
clear() {
|
|
82
48
|
this.cache.clear();
|
|
83
49
|
}
|
|
84
50
|
|
|
85
|
-
/**
|
|
86
|
-
* Get cache size
|
|
87
|
-
* @returns {number}
|
|
88
|
-
*/
|
|
89
51
|
size() {
|
|
90
52
|
return this.cache.size;
|
|
91
53
|
}
|
|
92
54
|
|
|
93
|
-
/**
|
|
94
|
-
* Clean up expired entries
|
|
95
|
-
*/
|
|
96
55
|
cleanup() {
|
|
97
56
|
const now = Date.now();
|
|
98
57
|
for (const [key, item] of this.cache.entries()) {
|
|
99
|
-
if (now > item.expiry)
|
|
100
|
-
this.cache.delete(key);
|
|
101
|
-
}
|
|
58
|
+
if (now > item.expiry) this.cache.delete(key);
|
|
102
59
|
}
|
|
103
60
|
}
|
|
104
61
|
}
|
package/utils/dns-verify.js
CHANGED
|
@@ -1,92 +1,55 @@
|
|
|
1
1
|
// honeyweb-core/utils/dns-verify.js
|
|
2
|
-
// DNS verification utilities for bot whitelist
|
|
3
2
|
|
|
4
3
|
const dns = require('dns').promises;
|
|
5
4
|
const LRUCache = require('./cache');
|
|
6
5
|
|
|
7
6
|
class DNSVerifier {
|
|
8
7
|
constructor(cacheTTL = 86400000) {
|
|
9
|
-
// Cache DNS results for 24 hours by default
|
|
10
8
|
this.cache = new LRUCache(500, cacheTTL);
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
/**
|
|
14
|
-
* Verify if IP belongs to claimed bot domain
|
|
15
|
-
* @param {string} ip - IP address to verify
|
|
16
|
-
* @param {string} expectedDomain - Expected domain pattern (e.g., 'googlebot.com')
|
|
17
|
-
* @returns {Promise<Object>} - { verified: boolean, hostname: string, error: string }
|
|
18
|
-
*/
|
|
19
11
|
async verify(ip, expectedDomain) {
|
|
20
|
-
// Check cache first
|
|
21
12
|
const cacheKey = `${ip}:${expectedDomain}`;
|
|
22
|
-
if (this.cache.has(cacheKey))
|
|
23
|
-
return this.cache.get(cacheKey);
|
|
24
|
-
}
|
|
13
|
+
if (this.cache.has(cacheKey)) return this.cache.get(cacheKey);
|
|
25
14
|
|
|
26
15
|
try {
|
|
27
|
-
//
|
|
16
|
+
// Reverse DNS: IP -> hostname
|
|
28
17
|
const hostnames = await dns.reverse(ip);
|
|
29
18
|
|
|
30
19
|
if (!hostnames || hostnames.length === 0) {
|
|
31
|
-
const result = {
|
|
32
|
-
verified: false,
|
|
33
|
-
hostname: null,
|
|
34
|
-
error: 'No reverse DNS record found'
|
|
35
|
-
};
|
|
20
|
+
const result = { verified: false, hostname: null, error: 'No reverse DNS record found' };
|
|
36
21
|
this.cache.set(cacheKey, result);
|
|
37
22
|
return result;
|
|
38
23
|
}
|
|
39
24
|
|
|
40
25
|
const hostname = hostnames[0];
|
|
41
26
|
|
|
42
|
-
// Step 2: Check if hostname matches expected domain
|
|
43
27
|
if (!hostname.endsWith(expectedDomain)) {
|
|
44
|
-
const result = {
|
|
45
|
-
verified: false,
|
|
46
|
-
hostname,
|
|
47
|
-
error: `Hostname ${hostname} does not match expected domain ${expectedDomain}`
|
|
48
|
-
};
|
|
28
|
+
const result = { verified: false, hostname, error: `Hostname ${hostname} does not match ${expectedDomain}` };
|
|
49
29
|
this.cache.set(cacheKey, result);
|
|
50
30
|
return result;
|
|
51
31
|
}
|
|
52
32
|
|
|
53
|
-
//
|
|
33
|
+
// Forward DNS: hostname -> IP (verification)
|
|
54
34
|
const addresses = await dns.resolve4(hostname);
|
|
55
35
|
|
|
56
36
|
if (!addresses.includes(ip)) {
|
|
57
|
-
const result = {
|
|
58
|
-
verified: false,
|
|
59
|
-
hostname,
|
|
60
|
-
error: 'Forward DNS lookup does not match original IP'
|
|
61
|
-
};
|
|
37
|
+
const result = { verified: false, hostname, error: 'Forward DNS does not match original IP' };
|
|
62
38
|
this.cache.set(cacheKey, result);
|
|
63
39
|
return result;
|
|
64
40
|
}
|
|
65
41
|
|
|
66
|
-
|
|
67
|
-
const result = {
|
|
68
|
-
verified: true,
|
|
69
|
-
hostname,
|
|
70
|
-
error: null
|
|
71
|
-
};
|
|
42
|
+
const result = { verified: true, hostname, error: null };
|
|
72
43
|
this.cache.set(cacheKey, result);
|
|
73
44
|
return result;
|
|
74
45
|
|
|
75
46
|
} catch (err) {
|
|
76
|
-
const result = {
|
|
77
|
-
verified: false,
|
|
78
|
-
hostname: null,
|
|
79
|
-
error: err.message
|
|
80
|
-
};
|
|
81
|
-
// Cache failures for shorter time (5 minutes)
|
|
47
|
+
const result = { verified: false, hostname: null, error: err.message };
|
|
82
48
|
this.cache.set(cacheKey, result, 300000);
|
|
83
49
|
return result;
|
|
84
50
|
}
|
|
85
51
|
}
|
|
86
52
|
|
|
87
|
-
/**
|
|
88
|
-
* Clear DNS cache
|
|
89
|
-
*/
|
|
90
53
|
clearCache() {
|
|
91
54
|
this.cache.clear();
|
|
92
55
|
}
|
package/utils/event-emitter.js
CHANGED
|
@@ -1,64 +1,32 @@
|
|
|
1
1
|
// honeyweb-core/utils/event-emitter.js
|
|
2
|
-
//
|
|
2
|
+
// Events: trap:triggered, threat:detected, ip:banned, request:blocked, ai:analysis
|
|
3
3
|
|
|
4
4
|
const { EventEmitter } = require('events');
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* HoneyWeb Event Emitter
|
|
8
|
-
* Provides a centralized event system for middleware extensibility
|
|
9
|
-
*
|
|
10
|
-
* Events:
|
|
11
|
-
* - 'trap:triggered' - { ip, path, timestamp }
|
|
12
|
-
* - 'threat:detected' - { ip, threats, threatLevel, timestamp }
|
|
13
|
-
* - 'ip:banned' - { ip, reason, timestamp }
|
|
14
|
-
* - 'request:blocked' - { ip, reason, timestamp }
|
|
15
|
-
* - 'ai:analysis' - { ip, report, timestamp }
|
|
16
|
-
*/
|
|
17
6
|
class HoneyWebEvents extends EventEmitter {
|
|
18
7
|
constructor() {
|
|
19
8
|
super();
|
|
20
|
-
this.setMaxListeners(20);
|
|
9
|
+
this.setMaxListeners(20);
|
|
21
10
|
}
|
|
22
11
|
|
|
23
12
|
emitTrapTriggered(ip, path) {
|
|
24
|
-
this.emit('trap:triggered', {
|
|
25
|
-
ip,
|
|
26
|
-
path,
|
|
27
|
-
timestamp: Date.now()
|
|
28
|
-
});
|
|
13
|
+
this.emit('trap:triggered', { ip, path, timestamp: Date.now() });
|
|
29
14
|
}
|
|
30
15
|
|
|
31
16
|
emitThreatDetected(ip, threats, threatLevel) {
|
|
32
|
-
this.emit('threat:detected', {
|
|
33
|
-
ip,
|
|
34
|
-
threats,
|
|
35
|
-
threatLevel,
|
|
36
|
-
timestamp: Date.now()
|
|
37
|
-
});
|
|
17
|
+
this.emit('threat:detected', { ip, threats, threatLevel, timestamp: Date.now() });
|
|
38
18
|
}
|
|
39
19
|
|
|
40
20
|
emitIpBanned(ip, reason) {
|
|
41
|
-
this.emit('ip:banned', {
|
|
42
|
-
ip,
|
|
43
|
-
reason,
|
|
44
|
-
timestamp: Date.now()
|
|
45
|
-
});
|
|
21
|
+
this.emit('ip:banned', { ip, reason, timestamp: Date.now() });
|
|
46
22
|
}
|
|
47
23
|
|
|
48
24
|
emitRequestBlocked(ip, reason) {
|
|
49
|
-
this.emit('request:blocked', {
|
|
50
|
-
ip,
|
|
51
|
-
reason,
|
|
52
|
-
timestamp: Date.now()
|
|
53
|
-
});
|
|
25
|
+
this.emit('request:blocked', { ip, reason, timestamp: Date.now() });
|
|
54
26
|
}
|
|
55
27
|
|
|
56
28
|
emitAiAnalysis(ip, report) {
|
|
57
|
-
this.emit('ai:analysis', {
|
|
58
|
-
ip,
|
|
59
|
-
report,
|
|
60
|
-
timestamp: Date.now()
|
|
61
|
-
});
|
|
29
|
+
this.emit('ai:analysis', { ip, report, timestamp: Date.now() });
|
|
62
30
|
}
|
|
63
31
|
}
|
|
64
32
|
|
package/utils/logger.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
// honeyweb-core/utils/logger.js
|
|
2
|
-
// Structured logging utility
|
|
3
2
|
|
|
4
|
-
const LOG_LEVELS = {
|
|
5
|
-
debug: 0,
|
|
6
|
-
info: 1,
|
|
7
|
-
warn: 2,
|
|
8
|
-
error: 3
|
|
9
|
-
};
|
|
3
|
+
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
10
4
|
|
|
11
5
|
class Logger {
|
|
12
6
|
constructor(config = {}) {
|
|
@@ -23,48 +17,31 @@ class Logger {
|
|
|
23
17
|
const timestamp = new Date().toISOString();
|
|
24
18
|
|
|
25
19
|
if (this.format === 'json') {
|
|
26
|
-
return JSON.stringify({
|
|
27
|
-
timestamp,
|
|
28
|
-
level: level.toUpperCase(),
|
|
29
|
-
message,
|
|
30
|
-
...meta
|
|
31
|
-
});
|
|
20
|
+
return JSON.stringify({ timestamp, level: level.toUpperCase(), message, ...meta });
|
|
32
21
|
}
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
const metaStr = Object.keys(meta).length > 0
|
|
36
|
-
? ' ' + JSON.stringify(meta)
|
|
37
|
-
: '';
|
|
23
|
+
const metaStr = Object.keys(meta).length > 0 ? ' ' + JSON.stringify(meta) : '';
|
|
38
24
|
return `${this.prefix} [${level.toUpperCase()}] ${message}${metaStr}`;
|
|
39
25
|
}
|
|
40
26
|
|
|
41
27
|
debug(message, meta) {
|
|
42
|
-
if (this._shouldLog('debug'))
|
|
43
|
-
console.log(this._formatMessage('debug', message, meta));
|
|
44
|
-
}
|
|
28
|
+
if (this._shouldLog('debug')) console.log(this._formatMessage('debug', message, meta));
|
|
45
29
|
}
|
|
46
30
|
|
|
47
31
|
info(message, meta) {
|
|
48
|
-
if (this._shouldLog('info'))
|
|
49
|
-
console.log(this._formatMessage('info', message, meta));
|
|
50
|
-
}
|
|
32
|
+
if (this._shouldLog('info')) console.log(this._formatMessage('info', message, meta));
|
|
51
33
|
}
|
|
52
34
|
|
|
53
35
|
warn(message, meta) {
|
|
54
|
-
if (this._shouldLog('warn'))
|
|
55
|
-
console.warn(this._formatMessage('warn', message, meta));
|
|
56
|
-
}
|
|
36
|
+
if (this._shouldLog('warn')) console.warn(this._formatMessage('warn', message, meta));
|
|
57
37
|
}
|
|
58
38
|
|
|
59
39
|
error(message, meta) {
|
|
60
|
-
if (this._shouldLog('error'))
|
|
61
|
-
console.error(this._formatMessage('error', message, meta));
|
|
62
|
-
}
|
|
40
|
+
if (this._shouldLog('error')) console.error(this._formatMessage('error', message, meta));
|
|
63
41
|
}
|
|
64
42
|
|
|
65
|
-
// Convenience methods for common HoneyWeb events
|
|
66
43
|
trapTriggered(ip, path) {
|
|
67
|
-
this.warn(
|
|
44
|
+
this.warn(`TRAP TRIGGERED by ${ip} on ${path}`, { ip, path, event: 'trap_triggered' });
|
|
68
45
|
}
|
|
69
46
|
|
|
70
47
|
ipBanned(ip, reason) {
|
|
@@ -73,10 +50,7 @@ class Logger {
|
|
|
73
50
|
|
|
74
51
|
threatDetected(ip, threats, level) {
|
|
75
52
|
this.warn(`THREAT DETECTED from ${ip}: ${threats.join(', ')} (level: ${level})`, {
|
|
76
|
-
ip,
|
|
77
|
-
threats,
|
|
78
|
-
threatLevel: level,
|
|
79
|
-
event: 'threat_detected'
|
|
53
|
+
ip, threats, threatLevel: level, event: 'threat_detected'
|
|
80
54
|
});
|
|
81
55
|
}
|
|
82
56
|
|