@@ -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
140139cp custom-email custom-email.py /var/ossec/integrations/
@@ -152,8 +151,8 @@ chown root:wazuh /var/ossec/integrations/custom-email*
152151#### Script Configuration
153152
154153Edit 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
318295Write a sample alert and invoke the script directly:
319296
@@ -343,7 +320,7 @@ Expected:
3433202026-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
349326sed ' 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
357334sqlite3 /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:
367344001|openssl|Active|2|0
368345```
369346
370- #### Test 4 : Burst behaviour and concurrency
347+ #### Test 3 : Burst behaviour and concurrency
371348
372349Generate a burst of alerts for a handful of packages and confirm that no update
373350is 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
389408sudo -u wazuh /var/ossec/integrations/custom-email prune
390409tail -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.
0 commit comments