Merge pull request '[session 1] Release 2026-07: aria — ingest noise filter + tomcat CVE fix (develop → main)' (#15) from develop into main
CD - Production / build-and-deploy (push) Has started running

This commit was merged in pull request #15.
This commit is contained in:
2026-07-22 11:53:43 -04:00
4 changed files with 116 additions and 1 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
<parent>
<groupId>com.hiveops</groupId>
<artifactId>hiveops-bom</artifactId>
<version>1.0.8</version>
<version>1.0.9</version>
<relativePath/>
</parent>
@@ -8,11 +8,13 @@ import com.hiveops.aria.kafka.AriaThreatProducer;
import com.hiveops.aria.repository.ThreatEventRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
@Service
@RequiredArgsConstructor
@@ -24,6 +26,12 @@ public class ThreatEventIngestionService {
private final AriaThreatProducer producer;
private final SequenceDetectionService sequenceDetectionService;
/** Drop events that classify as INFO with no IOC and no attack phase before persisting (aria#12). */
@Value("${aria.ingest.drop-unclassified-info:true}")
private boolean dropUnclassifiedInfo;
private final AtomicLong droppedNoiseCount = new AtomicLong();
@Transactional
public void ingest(ThreatEventIngestionRequest request) {
for (ThreatEventItem item : request.getEvents()) {
@@ -39,6 +47,15 @@ public class ThreatEventIngestionService {
private void processEvent(ThreatEventIngestionRequest request, ThreatEventItem item) {
ThreatEventClassifier.ClassificationResult result = classifier.classify(item);
if (dropUnclassifiedInfo && isUnclassifiedNoise(result)) {
long dropped = droppedNoiseCount.incrementAndGet();
if (dropped == 1 || dropped % 100_000 == 0) {
log.info("ARIA ingest filter: dropped {} unclassified INFO events (no IOC, no attack phase) since startup — latest signal={} device={} (aria#12)",
dropped, item.getSignalKey(), request.getDeviceAgentId());
}
return;
}
ThreatEvent.EventType eventType;
try {
eventType = ThreatEvent.EventType.valueOf(item.getEventType());
@@ -76,6 +93,20 @@ public class ThreatEventIngestionService {
}
}
/**
* An event that classifies as INFO with no matched IOC and no attack phase carries no
* detection value: it is never published to Kafka (only HIGH/CRITICAL are) and cannot
* contribute to a precursor sequence (SequenceDetectionService ignores null-phase events
* and requires a PHYSICAL_ACCESS phase). Persisting it only bloats threat_event — this is
* the Windows 4663 object-access flood (aria#12): agents send bare "4663", which misses the
* classifier's "EVENT_4663" case and lands in the INFO default branch. Drop it before save().
*/
private boolean isUnclassifiedNoise(ThreatEventClassifier.ClassificationResult result) {
return result.severity() == ThreatEvent.ThreatSeverity.INFO
&& result.matchedIoc() == null
&& result.attackPhase() == null;
}
private boolean shouldPublish(ThreatEvent.ThreatSeverity severity) {
return severity == ThreatEvent.ThreatSeverity.CRITICAL
|| severity == ThreatEvent.ThreatSeverity.HIGH;
@@ -58,6 +58,11 @@ aria.internal.service-secret=${ARIA_SERVICE_SECRET:}
# Sequence detection window (minutes)
aria.sequence.window.minutes=${ARIA_SEQUENCE_WINDOW_MINUTES:15}
# Ingest noise filter — drop events that classify as INFO with no IOC match and no attack
# phase before they hit threat_event (they never publish to Kafka and can't form a sequence).
# Kills the Windows 4663 object-access flood (aria#12). Set false to persist everything again.
aria.ingest.drop-unclassified-info=${ARIA_DROP_UNCLASSIFIED_INFO:true}
# IOC Feed Refresh
aria.feed.enabled=${ARIA_FEED_ENABLED:true}
aria.feed.refresh.cron=${ARIA_FEED_REFRESH_CRON:0 0 3 * * *}
@@ -0,0 +1,79 @@
package com.hiveops.aria.service;
import com.hiveops.aria.dto.ThreatEventItem;
import com.hiveops.aria.entity.ThreatEvent.AttackPhase;
import com.hiveops.aria.entity.ThreatEvent.ThreatSeverity;
import com.hiveops.aria.repository.ThreatIocRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Pins the classifier behaviour the ingest noise filter relies on (aria#12).
*
* <p>An event is "unclassified noise" — and therefore dropped before persistence — exactly when it
* classifies as INFO with no matched IOC and no attack phase. These tests lock in which signals fall
* into that bucket and which do not.
*/
@ExtendWith(MockitoExtension.class)
class ThreatEventClassifierTest {
@Mock
private ThreatIocRepository iocRepository;
private ThreatEventClassifier classifier() {
return new ThreatEventClassifier(iocRepository);
}
private ThreatEventItem item(String signalKey, String eventType) {
ThreatEventItem item = new ThreatEventItem();
item.setSignalKey(signalKey);
item.setEventType(eventType);
return item;
}
private boolean isUnclassifiedNoise(ThreatEventClassifier.ClassificationResult r) {
return r.severity() == ThreatSeverity.INFO && r.matchedIoc() == null && r.attackPhase() == null;
}
@Test
void bareWindowsEventId4663_isUnclassifiedNoise_andGetsDropped() {
// Agents send the bare number "4663", which misses the classifier's "EVENT_4663" case and
// lands in the INFO default branch — this is the 68M-row flood in aria#12.
var result = classifier().classify(item("4663", "OS_EVENT"));
assertThat(result.severity()).isEqualTo(ThreatSeverity.INFO);
assertThat(result.attackPhase()).isNull();
assertThat(result.matchedIoc()).isNull();
assertThat(isUnclassifiedNoise(result)).isTrue();
}
@Test
void unknownSignal_isUnclassifiedNoise() {
var result = classifier().classify(item("SOMETHING_UNSEEN", "OS_EVENT"));
assertThat(isUnclassifiedNoise(result)).isTrue();
}
@Test
void properEvent4663_isClassified_andKept() {
// The correctly-formatted signal is HIGH / MALWARE_STAGING and must never be dropped.
var result = classifier().classify(item("EVENT_4663", "OS_EVENT"));
assertThat(result.severity()).isEqualTo(ThreatSeverity.HIGH);
assertThat(result.attackPhase()).isEqualTo(AttackPhase.MALWARE_STAGING);
assertThat(isUnclassifiedNoise(result)).isFalse();
}
@Test
void physicalSignal_isClassified_andKept() {
var result = classifier().classify(item("DOOR_OPEN", "PHYSICAL"));
assertThat(result.severity()).isEqualTo(ThreatSeverity.MEDIUM);
assertThat(result.attackPhase()).isEqualTo(AttackPhase.PHYSICAL_ACCESS);
assertThat(isUnclassifiedNoise(result)).isFalse();
}
}