@daemonitor/plugins 1.1.40 → 1.1.42
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.
|
@@ -45,12 +45,6 @@ const BATCH_PATH = /^\/wp-json\/batch\/v1|rest_route=\/?batch\/v1/i;
|
|
|
45
45
|
// wp-admin paths that answer 2xx to anonymous callers, so a 2xx on them proves
|
|
46
46
|
// nothing about being logged in. Everything else under /wp-admin/ redirects a
|
|
47
47
|
// logged-out visitor to the login form.
|
|
48
|
-
// The omnisend-integration mu-plugin's wp_send_json_error "Security check
|
|
49
|
-
// failed" gzips to exactly 79 bytes; success gzips to 77. Matching on size is
|
|
50
|
-
// the only signal the access log carries, so a change to that message or to
|
|
51
|
-
// nginx's gzip settings silences this rule.
|
|
52
|
-
const SUBSCRIBE_FAIL_BYTES = 79;
|
|
53
|
-
const SUBSCRIBE_REFERER = /\/subscribe\/?(?:[?#]|$)/;
|
|
54
48
|
const ANON_ADMIN = /^\/wp-admin\/(?:admin-ajax|admin-post|load-styles|load-scripts|install|setup-config)\.php$|^\/wp-admin\/(?:css|js|images|fonts)\//i;
|
|
55
49
|
// Read new bytes since last offset, advancing state. Handles rotation (file
|
|
56
50
|
// shrank → restart at 0) and partial trailing lines (buffered in remainder).
|
|
@@ -92,7 +86,7 @@ async function readNew(source, st) {
|
|
|
92
86
|
st.remainder = lines.pop() ?? ""; // last element is a partial line (no newline)
|
|
93
87
|
return lines;
|
|
94
88
|
}
|
|
95
|
-
export function analyze(lines, cfg, windowSec, st) {
|
|
89
|
+
export function analyze(lines, cfg, windowSec, st, rules = []) {
|
|
96
90
|
const burstThreshold = cfg.xmlrpcBurstThreshold ?? 20;
|
|
97
91
|
const batchIps = new Map(); // ip → count of batch/v1 → 207
|
|
98
92
|
// key (ip, or path for webshells) → the vhosts it was seen against. Only
|
|
@@ -122,7 +116,7 @@ export function analyze(lines, cfg, windowSec, st) {
|
|
|
122
116
|
const adminOkIps = new Set(); // ip that then loaded an authed wp-admin page (2xx)
|
|
123
117
|
const xmlrpcByIp = new Map(); // ip → POST xmlrpc count
|
|
124
118
|
const webshell = new Map(); // pathNoQuery → sample
|
|
125
|
-
const
|
|
119
|
+
const compiled = rules.map((rule) => ({ rule, referer: rule.referer ? new RegExp(rule.referer) : null, hits: new Map() }));
|
|
126
120
|
let total = 0;
|
|
127
121
|
for (const line of lines) {
|
|
128
122
|
const e = parseLine(line);
|
|
@@ -173,8 +167,18 @@ export function analyze(lines, cfg, windowSec, st) {
|
|
|
173
167
|
// every WP site is probed constantly for my.php / academy.php / etc., and
|
|
174
168
|
// those hits 404 (not there) or 403 (WAF-blocked). Alerting on the probes
|
|
175
169
|
// is pure noise; alert only when the shell actually RAN.
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
for (const { rule, referer, hits } of compiled) {
|
|
171
|
+
if (rule.method && rule.method !== e.method)
|
|
172
|
+
continue;
|
|
173
|
+
if (rule.path && rule.path !== pathNoQuery)
|
|
174
|
+
continue;
|
|
175
|
+
if (rule.status && !rule.status.includes(e.status))
|
|
176
|
+
continue;
|
|
177
|
+
if (rule.bytes && !rule.bytes.includes(e.bytes))
|
|
178
|
+
continue;
|
|
179
|
+
if (referer && !referer.test(e.referer))
|
|
180
|
+
continue;
|
|
181
|
+
hits.set(e.ip, (hits.get(e.ip) || 0) + 1);
|
|
178
182
|
noteHost(e.ip, e.host);
|
|
179
183
|
}
|
|
180
184
|
const executed = e.status >= 200 && e.status < 300;
|
|
@@ -229,13 +233,16 @@ export function analyze(lines, cfg, windowSec, st) {
|
|
|
229
233
|
});
|
|
230
234
|
}
|
|
231
235
|
}
|
|
232
|
-
for (const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
236
|
+
for (const { rule, hits } of compiled) {
|
|
237
|
+
for (const [ip, count] of hits) {
|
|
238
|
+
const title = rule.title || rule.name;
|
|
239
|
+
findings.push({
|
|
240
|
+
rule: rule.name, key: `rule:${rule.name}:${ip}`, severity: rule.severity || "warning", ip,
|
|
241
|
+
path: rule.path,
|
|
242
|
+
title,
|
|
243
|
+
detail: `${ip}: ${title} (${count}×)${onSites(ip)}`,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
239
246
|
}
|
|
240
247
|
const counts = {
|
|
241
248
|
total,
|
|
@@ -244,7 +251,7 @@ export function analyze(lines, cfg, windowSec, st) {
|
|
|
244
251
|
batchBlocked,
|
|
245
252
|
xmlrpcPosts: [...xmlrpcByIp.values()].reduce((a, b) => a + b, 0),
|
|
246
253
|
webshellHits: [...webshell.values()].reduce((a, b) => a + b.count, 0),
|
|
247
|
-
|
|
254
|
+
ruleHits: compiled.reduce((n, { hits }) => n + [...hits.values()].reduce((a, b) => a + b, 0), 0),
|
|
248
255
|
loginSuccess: st?.hideLogin ? 0 : [...loginOkIps].filter((ip) => adminOkIps.has(ip)).length,
|
|
249
256
|
loginRedirectsAlways: !!st?.hideLogin,
|
|
250
257
|
};
|
|
@@ -261,7 +268,7 @@ export function createLogWatchPlugin() {
|
|
|
261
268
|
if (Array.isArray(cfg.sources) && cfg.sources.length)
|
|
262
269
|
return cfg.sources;
|
|
263
270
|
if (cfg.path)
|
|
264
|
-
return [{ name: cfg.name, path: cfg.path, fromStart: cfg.fromStart }];
|
|
271
|
+
return [{ name: cfg.name, path: cfg.path, fromStart: cfg.fromStart, rules: cfg.rules }];
|
|
265
272
|
return [];
|
|
266
273
|
};
|
|
267
274
|
const refreshFn = async (plugin) => {
|
|
@@ -279,7 +286,7 @@ export function createLogWatchPlugin() {
|
|
|
279
286
|
}
|
|
280
287
|
try {
|
|
281
288
|
const lines = await readNew(source, st);
|
|
282
|
-
const { findings, counts } = analyze(lines, cfg, windowSec, st);
|
|
289
|
+
const { findings, counts } = analyze(lines, cfg, windowSec, st, source.rules);
|
|
283
290
|
await plugin.send({ name: label, source: label, path: source.path, window: windowSec, counts, findings }, uid);
|
|
284
291
|
}
|
|
285
292
|
catch (e) {
|
|
@@ -297,6 +304,6 @@ export function createLogWatchPlugin() {
|
|
|
297
304
|
refreshTimer = null;
|
|
298
305
|
states.clear();
|
|
299
306
|
};
|
|
300
|
-
return createMonitoringPlugin("logwatch", "logwatch", "Access-log security watcher (WP2Shell / webshell / xmlrpc /
|
|
307
|
+
return createMonitoringPlugin("logwatch", "logwatch", "Access-log security watcher (WP2Shell / webshell / xmlrpc / custom rules)", async () => { }, monitorFn, refreshFn, teardownFn);
|
|
301
308
|
}
|
|
302
309
|
export default createLogWatchPlugin;
|