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
CD - Production / build-and-deploy (push) Has started running
This commit was merged in pull request #15.
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user