Skip to content

Commit d654e9e

Browse files
committed
Ship only the integration and its README
Drop test_dedup.py from the integration folder: the deliverable is the script and its documentation. The window rules it asserted are now a documented manual procedure instead, ageing first_seen with sqlite3 so both the no-slide and the rollover behaviour can be confirmed in seconds rather than over a day. Also round the window_age_h helper query, which truncated a 23 hour old anchor to 22.
1 parent 1cb29ea commit d654e9e

2 files changed

Lines changed: 59 additions & 114 deletions

File tree

integrations/vulnerability_email_dedup/README.md

Lines changed: 59 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,14 @@ graph TD
126126

127127
#### Using the Integration Files
128128

129-
This integration ships three files:
129+
This integration ships two files:
130130

131131
| File | Purpose |
132132
| --- | --- |
133133
| `custom-email.py` | The integration logic. |
134134
| `custom-email` | Standard Wazuh shell wrapper that invokes the script with the embedded Python interpreter. |
135-
| `test_dedup.py` | Self-check for the window logic. Not deployed to the manager. |
136135

137-
Copy the two runtime files to the manager:
136+
Copy both to the manager:
138137

139138
```bash
140139
cp custom-email custom-email.py /var/ossec/integrations/
@@ -152,8 +151,8 @@ chown root:wazuh /var/ossec/integrations/custom-email*
152151
#### Script Configuration
153152

154153
Edit the configuration block at the top of `custom-email.py`. Every value also
155-
accepts an environment variable override, which is how the test harness
156-
redirects mail and storage into a sandbox without editing the file.
154+
accepts an environment variable override, so mail and storage can be redirected
155+
into a sandbox for testing without editing the deployed file.
157156

158157
| Setting | Environment variable | Default | Description |
159158
| --- | --- | --- | --- |
@@ -291,29 +290,7 @@ seen package or records the CVE and exits silently.
291290

292291
### Integration Testing
293292

294-
#### Test 1: Window logic self-check
295-
296-
Run the bundled self-check from the integration directory. It exercises the
297-
window rules against a temporary database, sends no mail, and needs nothing
298-
outside the standard library:
299-
300-
```bash
301-
python3 test_dedup.py
302-
```
303-
304-
Expected:
305-
306-
```
307-
OK: all de-duplication window checks passed
308-
```
309-
310-
It asserts the behaviours that would otherwise take a day of wall-clock time to
311-
observe: that a burst of 1,000 alerts for one package produces exactly one
312-
email with no lost counts, that a flood arriving inside the window neither
313-
notifies nor moves the anchor, that the next alert after the window elapses
314-
notifies and re-anchors, and that prune removes only fully elapsed rows.
315-
316-
#### Test 2: First alert for a package sends an email
293+
#### Test 1: First alert for a package sends an email
317294

318295
Write a sample alert and invoke the script directly:
319296

@@ -343,7 +320,7 @@ Expected:
343320
2026-07-18T10:00:01 INFO Sent vuln alert package=openssl agent=001 cve=CVE-2026-0001
344321
```
345322

346-
#### Test 3: A second CVE for the same package is suppressed
323+
#### Test 2: A second CVE for the same package is suppressed
347324

348325
```bash
349326
sed 's/CVE-2026-0001/CVE-2026-0002/g' /tmp/alert1.json > /tmp/alert2.json
@@ -356,7 +333,7 @@ default `INFO` level. The row instead shows the incremented count:
356333
```bash
357334
sqlite3 /var/ossec/logs/vuln_dedup.db \
358335
"SELECT agent_id, package, status, cve_count,
359-
CAST((strftime('%s','now') - first_seen) / 3600 AS INT) AS window_age_h
336+
CAST(ROUND((strftime('%s','now') - first_seen) / 3600.0) AS INT) AS window_age_h
360337
FROM dedup;"
361338
```
362339

@@ -367,7 +344,7 @@ alert did not move it:
367344
001|openssl|Active|2|0
368345
```
369346

370-
#### Test 4: Burst behaviour and concurrency
347+
#### Test 3: Burst behaviour and concurrency
371348

372349
Generate a burst of alerts for a handful of packages and confirm that no update
373350
is lost. The invariant to check is that the sum of `cve_count` across all rows
@@ -383,19 +360,66 @@ sqlite3 /var/ossec/logs/vuln_dedup.db \
383360
"SELECT agent_id, package, status, cve_count FROM dedup ORDER BY cve_count DESC LIMIT 10;"
384361
```
385362

363+
#### Test 4: Window rollover, without waiting 24 hours
364+
365+
The two rules worth confirming are that the window does not slide, and that it
366+
reopens 24 hours after the **first** alert. Both can be checked immediately by
367+
ageing the row's anchor by hand instead of waiting.
368+
369+
First, prove the window does not slide. Age the anchor to 23 hours old, replay
370+
an alert, and re-read the anchor:
371+
372+
```bash
373+
sqlite3 /var/ossec/logs/vuln_dedup.db \
374+
"UPDATE dedup SET first_seen = first_seen - 23*3600 WHERE package='openssl';"
375+
376+
sudo -u wazuh /var/ossec/integrations/custom-email /tmp/alert2.json
377+
378+
sqlite3 /var/ossec/logs/vuln_dedup.db \
379+
"SELECT cve_count,
380+
CAST(ROUND((strftime('%s','now') - first_seen) / 3600.0) AS INT) AS window_age_h
381+
FROM dedup WHERE package='openssl';"
382+
```
383+
384+
No email is sent, and `window_age_h` is still `23`. The count rose but the
385+
anchor did not move, so replaying more alerts can never push the window
386+
forward.
387+
388+
Now age it past the window and replay again:
389+
390+
```bash
391+
sqlite3 /var/ossec/logs/vuln_dedup.db \
392+
"UPDATE dedup SET first_seen = first_seen - 2*3600 WHERE package='openssl';"
393+
394+
sudo -u wazuh /var/ossec/integrations/custom-email /tmp/alert2.json
395+
tail -n 1 /var/ossec/logs/custom-email_integration.log
396+
```
397+
398+
A fresh email is sent and the row re-anchors, so `cve_count` returns to `1` and
399+
`window_age_h` to `0`:
400+
401+
```
402+
2026-07-18T10:10:00 INFO Sent vuln alert package=openssl agent=001 cve=CVE-2026-0002
403+
```
404+
386405
#### Test 5: Maintenance mode
387406

388407
```bash
389408
sudo -u wazuh /var/ossec/integrations/custom-email prune
390409
tail -n 1 /var/ossec/logs/custom-email_integration.log
391410
```
392411

393-
Expected, since the rows just created are still inside their window:
412+
Expected, since the row was just re-anchored and is inside its window:
394413

395414
```
396-
2026-07-18T10:05:00 INFO Prune complete: removed 0 elapsed row(s).
415+
2026-07-18T10:15:00 INFO Prune complete: removed 0 elapsed row(s).
397416
```
398417

418+
Age it past the window again and prune removes it, which is the housekeeping
419+
path. Removing an elapsed row changes no behaviour: the next alert for that
420+
package simply creates a new row and sends one email, exactly as an elapsed row
421+
would have.
422+
399423
---
400424

401425
### Troubleshooting
@@ -420,8 +444,8 @@ Expected, since the rows just created are still inside their window:
420444
- **Adapted by**: Leon Fuller.
421445
- **Tested versions**: Wazuh manager 4.x with Vulnerability Detection enabled
422446
and the 4.8+ vulnerability alert schema. Python 3 standard library only, no
423-
third-party dependencies. Logic validated on Python 3.11 by the bundled
424-
`test_dedup.py`, plus a synthetic 2,000-alert burst at 64-way concurrency: 300
447+
third-party dependencies. Logic validated on Python 3.11: the window rules
448+
above, plus a synthetic 2,000-alert burst at 64-way concurrency, where 300
425449
distinct keys produced 300 emails, `SUM(cve_count)` matched the 2,000 alerts
426450
fed in, and no lock errors occurred.
427451
- **Maintainer**: Leon Fuller.

integrations/vulnerability_email_dedup/test_dedup.py

Lines changed: 0 additions & 79 deletions
This file was deleted.

0 commit comments

Comments
 (0)