diff --git a/src/main/java/com/hiveops/aria/controller/IocFeedController.java b/src/main/java/com/hiveops/aria/controller/IocFeedController.java index 77db5a4..98e84a7 100644 --- a/src/main/java/com/hiveops/aria/controller/IocFeedController.java +++ b/src/main/java/com/hiveops/aria/controller/IocFeedController.java @@ -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> 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)); } } diff --git a/src/main/java/com/hiveops/aria/feed/MalwareBazaarFeedClient.java b/src/main/java/com/hiveops/aria/feed/MalwareBazaarFeedClient.java index 9493f79..9864bac 100644 --- a/src/main/java/com/hiveops/aria/feed/MalwareBazaarFeedClient.java +++ b/src/main/java/com/hiveops/aria/feed/MalwareBazaarFeedClient.java @@ -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 fetch() { + if (!isConfigured()) { + log.info("MalwareBazaar: no API key configured, skipping"); + return List.of(); + } List 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 body = new LinkedMultiValueMap<>(); body.add("query", "get_taginfo"); diff --git a/src/main/java/com/hiveops/aria/feed/ThreatFoxFeedClient.java b/src/main/java/com/hiveops/aria/feed/ThreatFoxFeedClient.java index a3b3766..8448ca1 100644 --- a/src/main/java/com/hiveops/aria/feed/ThreatFoxFeedClient.java +++ b/src/main/java/com/hiveops/aria/feed/ThreatFoxFeedClient.java @@ -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 fetch() { + if (!isConfigured()) { + log.info("ThreatFox: no API key configured, skipping"); + return List.of(); + } List 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", diff --git a/src/main/java/com/hiveops/aria/repository/IocFeedRunRepository.java b/src/main/java/com/hiveops/aria/repository/IocFeedRunRepository.java index 7683f4e..ca6fd43 100644 --- a/src/main/java/com/hiveops/aria/repository/IocFeedRunRepository.java +++ b/src/main/java/com/hiveops/aria/repository/IocFeedRunRepository.java @@ -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 { Optional 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); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ec5701e..a7ed365 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -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}