From 832a7749389b104bbb2ef2281d2424be19667710 Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Mon, 26 Oct 2026 14:49:43 -0400 Subject: [PATCH 1/2] Fix separate Alarm Manager occurrences for traditional Syslog matches --- includes/alarmmanager.php | 9 +++++++++ tests/regression/alarmmanager_forward_test.php | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/includes/alarmmanager.php b/includes/alarmmanager.php index de067123..55bd3546 100644 --- a/includes/alarmmanager.php +++ b/includes/alarmmanager.php @@ -34,6 +34,15 @@ function syslog_alarmmanager_forward(array $alert, array $matches, int $count, b throw new RuntimeException('source_identity_missing'); } } + if ((int) $alert['method'] === 0 && !$recovered && !$state && count($matches) > 1) { + $accepted = true; + foreach ($matches as $match) { + if (!syslog_alarmmanager_forward($alert, [$match], 1)) { + $accepted = false; + } + } + return $accepted; + } usort($matches, static fn($a, $b) => (int) $a['seq'] <=> (int) $b['seq']); $first = $matches[0]; $identity = hash('sha256', json_encode([$alert['id'], $collector, array_map(static fn($m) => [$m['seq'], $m['logtime']], $matches)], JSON_THROW_ON_ERROR)); diff --git a/tests/regression/alarmmanager_forward_test.php b/tests/regression/alarmmanager_forward_test.php index bb922ed6..fc88f7f8 100644 --- a/tests/regression/alarmmanager_forward_test.php +++ b/tests/regression/alarmmanager_forward_test.php @@ -59,7 +59,20 @@ function check($condition, $message) { $GLOBALS['checks']++; if (!$condition) { check($second['event_id'] !== $first['event_id'] && $second['alarm_key'] !== $first['alarm_key'], 'Independent occurrence has distinct event and stream'); $empty_state = ['last_sequence' => 0, 'episode_id' => '', 'active' => 0, 'acknowledged' => 0]; check(alarmmanager_transition($empty_state, $first)[1] === 'occurrence' && alarmmanager_transition($empty_state, $second)[1] === 'occurrence', 'Real Alarm Manager lifecycle accepts each independent stream'); +$batch = [$matches[0], array_replace($matches[0], ['seq' => 102, 'host' => 'other.example', 'message' => 'second message'])]; +$before = count($submitted); +check(syslog_alarmmanager_forward($rule, $batch, 2), 'Traditional batch accepted'); +$events = array_column(array_slice($submitted, $before), 'event'); +check(count($events) === 2 && $events[0] === $second, 'Each match is a separate occurrence with stable identity across batches'); +check($events[1]['device']['hostname'] === 'other.example' && !str_contains($events[1]['details'], 'sensitive test message') && $events[1]['context']['metric_value'] === '1', 'Occurrence contains only its own host and message'); +check($events[0]['alarm_key'] !== $events[1]['alarm_key'], 'Batch occurrences use independent streams'); +$response = 'failed'; +$before = count($submitted); +check(!syslog_alarmmanager_forward($rule, $batch, 2) && count($submitted) === $before + 2, 'Failure retains fallback and still attempts every match'); +$response = 'accepted'; $rule['method'] = 1; +$before = count($submitted); +check(syslog_alarmmanager_forward($rule, $batch, 5) && count($submitted) === $before + 1, 'Threshold batch remains one event'); $rule['_threshold_state'] = ['source_poller_id' => 2, 'breach_key' => str_repeat('a', 40)]; check(syslog_alarmmanager_forward($rule, $matches, 5), 'Threshold raise accepted'); $raised = end($submitted)['event']; From c80561c3d5dacdfded25714a5ebde8cd554180b5 Mon Sep 17 00:00:00 2001 From: Sean Mancini Date: Mon, 26 Oct 2026 14:55:51 -0400 Subject: [PATCH 2/2] Test per-message Alarm Manager forwarding in Pest --- tests/Unit/AlarmManagerForwardingTest.php | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/Unit/AlarmManagerForwardingTest.php b/tests/Unit/AlarmManagerForwardingTest.php index 5763d7f1..25a17c0a 100644 --- a/tests/Unit/AlarmManagerForwardingTest.php +++ b/tests/Unit/AlarmManagerForwardingTest.php @@ -91,6 +91,43 @@ function alarmmanager_test_matches(): array { expect($first['alarm_key'])->not->toBe($next['alarm_key']); }); +it('submits traditional matches separately with stable identities across batches', function () { + $rule = array_replace(alarmmanager_test_rule(), ['method' => 0]); + $matches = alarmmanager_test_matches(); + $matches[] = array_replace($matches[0], ['seq' => 10, 'host' => 'other-router', 'message' => 'second message']); + expect(syslog_alarmmanager_forward($rule, $matches, 2))->toBeTrue(); + [$first, $second] = $GLOBALS['alarmmanager_test_events']; + expect($GLOBALS['alarmmanager_test_events'])->toHaveCount(2); + expect($first['transition'])->toBe('occurrence'); + expect($first['details'])->not->toContain('second message'); + expect($second['device']['hostname'])->toBe('other-router'); + expect($second['details'])->not->toContain('fixture'); + expect($second['context']['metric_value'])->toBe('1'); + expect($first['alarm_key'])->not->toBe($second['alarm_key']); + expect(syslog_alarmmanager_forward($rule, [$matches[0]], 1))->toBeTrue(); + expect($GLOBALS['alarmmanager_test_events'][2])->toBe($first); +}); + +it('attempts every traditional match and retains fallback after partial acceptance', function () { + $rule = array_replace(alarmmanager_test_rule(), ['method' => 0]); + $matches = alarmmanager_test_matches(); + $matches[] = array_replace($matches[0], ['seq' => 10]); + test_override('api_plugin_hook_function', static function ($hook, $envelope) { + $GLOBALS['alarmmanager_test_events'][] = $envelope['event']; + return $envelope + ['status' => $envelope['event']['sequence'] === 9 ? 'failed' : 'accepted', 'event_row_id' => 42]; + }); + expect(syslog_alarmmanager_forward($rule, $matches, 2))->toBeFalse(); + expect($GLOBALS['alarmmanager_test_events'])->toHaveCount(2); +}); + +it('keeps threshold matches grouped', function () { + $matches = alarmmanager_test_matches(); + $matches[] = array_replace($matches[0], ['seq' => 10, 'message' => 'second message']); + expect(syslog_alarmmanager_forward(alarmmanager_test_rule(), $matches, 5))->toBeTrue(); + expect($GLOBALS['alarmmanager_test_events'])->toHaveCount(1); + expect($GLOBALS['alarmmanager_test_events'][0]['details'])->toContain('fixture', 'second message'); +}); + it('correlates threshold recovery and records acceptance without resending on local failure', function ($write) { $rule = alarmmanager_test_rule(); $rule['_threshold_state'] = ['source_poller_id' => 2, 'breach_key' => str_repeat('a', 40)];