fix(aria): feed clients — @Param binding, auth keys, thread error logging
CD - Develop / build-and-deploy (push) Failing after 12m28s

- IocFeedRunRepository: add @Param("feedName") to countRunning JPQL query
  (missing binding silently crashed background threads)
- IocFeedController: uncaught exception handler on feed threads so failures
  appear in logs instead of disappearing silently
- MalwareBazaarFeedClient/ThreatFoxFeedClient: abuse.ch now requires API
  key (auth.abuse.ch); add Auth-Key header + isConfigured() skip guard
- application.properties: add aria.feed.malware-bazaar.api-key and
  aria.feed.threat-fox.api-key properties

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 21:59:50 -04:00
parent bdc2504635
commit 583aff9c2d
5 changed files with 46 additions and 5 deletions
@@ -4,6 +4,7 @@ import com.hiveops.aria.entity.IocFeedRun;
import com.hiveops.aria.repository.IocFeedRunRepository;
import com.hiveops.aria.service.IocFeedService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
@@ -16,8 +17,16 @@ import java.util.Optional;
@RestController
@RequestMapping("/api/aria/feeds")
@RequiredArgsConstructor
@Slf4j
public class IocFeedController {
private Thread feedThread(String name, Runnable task) {
Thread t = new Thread(task, name);
t.setUncaughtExceptionHandler((thread, ex) ->
log.error("IOC feed thread {} failed: {}", thread.getName(), ex.getMessage(), ex));
return t;
}
private final IocFeedService feedService;
private final IocFeedRunRepository feedRunRepository;
@@ -49,7 +58,7 @@ public class IocFeedController {
@PostMapping("/refresh")
@PreAuthorize("hasRole('BCOS_ADMIN')")
public ResponseEntity<Map<String, String>> refreshAll() {
new Thread(() -> feedService.refreshAll(), "ioc-feed-manual-all").start();
feedThread("ioc-feed-manual-all", feedService::refreshAll).start();
return ResponseEntity.accepted().body(Map.of("status", "refresh_started"));
}
@@ -61,7 +70,7 @@ public class IocFeedController {
if (!feedService.feedNames().contains(upper)) {
return ResponseEntity.badRequest().body(Map.of("error", "Unknown feed: " + feedName));
}
new Thread(() -> feedService.refreshFeed(upper), "ioc-feed-manual-" + upper).start();
feedThread("ioc-feed-manual-" + upper, () -> feedService.refreshFeed(upper)).start();
return ResponseEntity.accepted().body(Map.of("status", "refresh_started", "feed", upper));
}
}
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hiveops.aria.entity.ThreatIoc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -18,7 +19,8 @@ import java.util.ArrayList;
import java.util.List;
/**
* Fetches ATM-related malware samples from MalwareBazaar (abuse.ch) — no API key required.
* Fetches ATM-related malware samples from MalwareBazaar (abuse.ch).
* Requires an API key from https://auth.abuse.ch/
*/
@Component
@Slf4j
@@ -27,9 +29,20 @@ public class MalwareBazaarFeedClient {
private static final String API_URL = "https://mb-api.abuse.ch/api/v1/";
private static final ObjectMapper MAPPER = new ObjectMapper();
@Value("${aria.feed.malware-bazaar.api-key:}")
private String apiKey;
private final RestTemplate restTemplate = new RestTemplate();
public boolean isConfigured() {
return apiKey != null && !apiKey.isBlank();
}
public List<ThreatIoc> fetch() {
if (!isConfigured()) {
log.info("MalwareBazaar: no API key configured, skipping");
return List.of();
}
List<ThreatIoc> result = new ArrayList<>();
// Query recent samples tagged with ATM-related tags
@@ -46,6 +59,7 @@ public class MalwareBazaarFeedClient {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.set("Auth-Key", apiKey);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("query", "get_taginfo");
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hiveops.aria.entity.ThreatIoc;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@@ -17,7 +18,8 @@ import java.util.List;
import java.util.Map;
/**
* Fetches ATM-related IOCs from ThreatFox (abuse.ch) — no API key required.
* Fetches ATM-related IOCs from ThreatFox (abuse.ch).
* Requires an API key from https://auth.abuse.ch/
*/
@Component
@Slf4j
@@ -26,9 +28,20 @@ public class ThreatFoxFeedClient {
private static final String API_URL = "https://threatfox-api.abuse.ch/api/v1/";
private static final ObjectMapper MAPPER = new ObjectMapper();
@Value("${aria.feed.threat-fox.api-key:}")
private String apiKey;
private final RestTemplate restTemplate = new RestTemplate();
public boolean isConfigured() {
return apiKey != null && !apiKey.isBlank();
}
public List<ThreatIoc> fetch() {
if (!isConfigured()) {
log.info("ThreatFox: no API key configured, skipping");
return List.of();
}
List<ThreatIoc> result = new ArrayList<>();
// Search by ATM-specific malware families
@@ -48,6 +61,7 @@ public class ThreatFoxFeedClient {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Auth-Key", apiKey);
String body = MAPPER.writeValueAsString(Map.of(
"query", "iocs_by_malware_family",
@@ -70,6 +84,7 @@ public class ThreatFoxFeedClient {
try {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Auth-Key", apiKey);
String body = MAPPER.writeValueAsString(Map.of(
"query", "get_iocs",
@@ -5,6 +5,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
@@ -15,5 +16,5 @@ public interface IocFeedRunRepository extends JpaRepository<IocFeedRun, Long> {
Optional<IocFeedRun> findTopByFeedNameOrderByStartedAtDesc(String feedName);
@Query("SELECT COUNT(r) FROM IocFeedRun r WHERE r.feedName = :feedName AND r.status = 'RUNNING'")
long countRunning(String feedName);
long countRunning(@Param("feedName") String feedName);
}
@@ -62,6 +62,8 @@ aria.sequence.window.minutes=${ARIA_SEQUENCE_WINDOW_MINUTES:15}
aria.feed.enabled=${ARIA_FEED_ENABLED:true}
aria.feed.refresh.cron=${ARIA_FEED_REFRESH_CRON:0 0 3 * * *}
aria.feed.otx.api-key=${ARIA_FEED_OTX_API_KEY:}
aria.feed.malware-bazaar.api-key=${ARIA_FEED_MALWARE_BAZAAR_API_KEY:}
aria.feed.threat-fox.api-key=${ARIA_FEED_THREAT_FOX_API_KEY:}
# Auto-response — disabled by default for safe initial deployment
aria.autoresponse.shutdown.enabled=${ARIA_AUTORESPONSE_SHUTDOWN_ENABLED:false}