feat: Phase 1 — ARIA ATM threat monitoring service
CD - Develop / build-and-deploy (push) Failing after 11m31s

- Spring Boot service on port 8095, database hiveiq_aria
- Flyway schema: threat_ioc, threat_event, precursor_sequence_alert, device_security_posture
- FBI FLASH-20260219-001 IOC seed data (executables, MD5s, directories, registry keys, services)
- ThreatEventClassifier — maps 20+ signal keys to severity + attack phase + IOC match
- ThreatEventIngestionService — persists events, publishes HIGH/CRITICAL to Kafka
- Kafka producer: topic hiveops.aria.threats (AriaThreatEvent payload)
- ThreatIocController — CRUD IOC management (MSP_ADMIN/BCOS_ADMIN)
- ThreatEventIngestionController — agent-facing ingest endpoint with service secret auth
- CI/CD: cd-develop.yml + cd-main.yml
This commit is contained in:
2026-06-14 09:30:46 -04:00
commit e5f25f1c2c
77 changed files with 2286 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
name: CD - Develop
on:
push:
branches:
- develop
jobs:
build-and-deploy:
runs-on: ubuntu-latest
env:
GIT_SSL_NO_VERIFY: 'true'
MAVEN_OPTS: "-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Clean runner disk
run: docker system prune -f 2>/dev/null || true
- name: Install Java 21 and Maven
run: |
apt-get update -qq
apt-get install -y -qq wget apt-transport-https gnupg
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor -o /usr/share/keyrings/adoptium.gpg
echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(. /etc/os-release && echo $VERSION_CODENAME) main" | tee /etc/apt/sources.list.d/adoptium.list
apt-get update -qq
apt-get install -y -qq temurin-21-jdk maven
java -version
mvn -version
- name: Configure Maven settings
run: |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << EOF
<settings>
<servers>
<server>
<id>gitea</id>
<username>hiveiq</username>
<password>${{ secrets.MAVEN_TOKEN }}</password>
</server>
</servers>
<mirrors>
<mirror>
<id>maven-default-http-blocker</id>
<mirrorOf>dummy</mirrorOf>
<name>Disabled</name>
<url>http://0.0.0.0/</url>
<blocked>true</blocked>
</mirror>
</mirrors>
<profiles>
<profile>
<id>gitea-registry</id>
<repositories>
<repository>
<id>gitea</id>
<url>https://hiveiq-gitea.directlx.dev/api/packages/hiveops/maven</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>gitea-registry</activeProfile>
</activeProfiles>
</settings>
EOF
- name: Build JAR
run: mvn clean package -DskipTests -B
- name: Install Trivy
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
- name: Trivy vulnerability scan
run: |
trivy fs \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--scanners vuln \
--format table \
target/
- name: OWASP Dependency Check
run: |
if [ ! -d /opt/owasp-data ]; then
echo "::warning::OWASP local NVD data not found on this runner — skipping scan"
exit 0
fi
cp -r /opt/owasp-data /tmp/owasp-data
mvn org.owasp:dependency-check-maven:check \
-DfailBuildOnCVSS=7 \
-DautoUpdate=false \
-DdataDirectory=/tmp/owasp-data \
-DsuppressionFile=deployment/owasp-suppressions.xml \
-Dformats=HTML,JSON \
-B
- name: Build and push Docker image
run: |
if ! command -v docker &> /dev/null; then
apt-get update -qq && apt-get install -y -qq docker.io
fi
DOCKER_BUILDKIT=0 docker system prune -f 2>/dev/null || true
export DOCKER_BUILDKIT=0
echo "${{ secrets.DEV_REGISTRY_PASSWORD }}" | \
docker login ${{ secrets.DEV_REGISTRY_URL }} \
-u ${{ secrets.DEV_REGISTRY_USERNAME }} --password-stdin
docker build -t ${{ secrets.DEV_REGISTRY_URL }}/hiveiq-aria:dev .
docker push ${{ secrets.DEV_REGISTRY_URL }}/hiveiq-aria:dev
- name: Deploy to bcos.dev
continue-on-error: true
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
apt-get install -y -qq openssh-client
mkdir -p ~/.ssh
echo "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H ${{ secrets.DEV_HOST }} >> ~/.ssh/known_hosts 2>&1
ssh -i ~/.ssh/deploy_key ${{ secrets.DEPLOY_USER }}@${{ secrets.DEV_HOST }} \
"cd ~/hiveiq/hiveops-openmetal/hiveops/instances/dev && \
REGISTRY_URL=${{ secrets.DEV_REGISTRY_URL }} ARIA_VERSION=dev \
docker compose pull hiveiq-aria && \
REGISTRY_URL=${{ secrets.DEV_REGISTRY_URL }} ARIA_VERSION=dev \
docker compose up -d hiveiq-aria"
- name: Notify Slack
if: always()
run: |
STATUS="${{ job.status }}"
[ "$STATUS" = "success" ] && COLOR="good" || COLOR="danger"
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
RUN_URL="https://hiveiq-gitea.directlx.dev/hiveiq-src/hiveops-aria/actions/runs/${{ github.run_id }}"
curl -s -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d "{\"attachments\":[{\"color\":\"$COLOR\",\"text\":\"*<$RUN_URL|hiveops-aria CD Develop — $STATUS>*\n${{ github.ref_name }} · $SHORT_SHA · ${{ github.actor }}\",\"mrkdwn_in\":[\"text\"],\"footer\":\"Gitea Actions\"}]}"
- name: Cleanup
if: always()
run: rm -f ~/.ssh/deploy_key
+130
View File
@@ -0,0 +1,130 @@
name: CD - Production
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
env:
GIT_SSL_NO_VERIFY: "true"
MAVEN_OPTS: "-Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true"
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Clean runner disk
run: docker system prune -f 2>/dev/null || true
- name: Install Java 21 and Maven
run: |
apt-get update -qq
apt-get install -y -qq wget apt-transport-https gnupg
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg --dearmor -o /usr/share/keyrings/adoptium.gpg
echo "deb [signed-by=/usr/share/keyrings/adoptium.gpg] https://packages.adoptium.net/artifactory/deb $(. /etc/os-release && echo $VERSION_CODENAME) main" | tee /etc/apt/sources.list.d/adoptium.list
apt-get update -qq
apt-get install -y -qq temurin-21-jdk maven
java -version
mvn -version
- name: Configure Maven settings
run: |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << EOF
<settings>
<servers>
<server>
<id>gitea</id>
<username>hiveiq</username>
<password>${{ secrets.MAVEN_TOKEN }}</password>
</server>
</servers>
<mirrors>
<mirror>
<id>maven-default-http-blocker</id>
<mirrorOf>dummy</mirrorOf>
<name>Disabled</name>
<url>http://0.0.0.0/</url>
<blocked>true</blocked>
</mirror>
</mirrors>
<profiles>
<profile>
<id>gitea-registry</id>
<repositories>
<repository>
<id>gitea</id>
<url>https://hiveiq-gitea.directlx.dev/api/packages/hiveops/maven</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
</repositories>
</profile>
</profiles>
<activeProfiles>
<activeProfile>gitea-registry</activeProfile>
</activeProfiles>
</settings>
EOF
- name: Build JAR
run: mvn clean package -DskipTests -B
- name: Install Trivy
run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
- name: Trivy vulnerability scan
run: |
trivy fs \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--scanners vuln \
--format table \
target/
- name: OWASP Dependency Check
run: |
if [ ! -d /opt/owasp-data ]; then
echo "::warning::OWASP local NVD data not found on this runner — skipping scan"
exit 0
fi
cp -r /opt/owasp-data /tmp/owasp-data
mvn org.owasp:dependency-check-maven:check \
-DfailBuildOnCVSS=7 \
-DautoUpdate=false \
-DdataDirectory=/tmp/owasp-data \
-DsuppressionFile=deployment/owasp-suppressions.xml \
-Dformats=HTML,JSON \
-B
- name: Build and push Docker image
run: |
if ! command -v docker &> /dev/null; then
apt-get update -qq && apt-get install -y -qq docker.io
fi
DOCKER_BUILDKIT=0 docker system prune -f 2>/dev/null || true
export DOCKER_BUILDKIT=0
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login ${{ secrets.REGISTRY_URL }} \
-u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
docker build \
-t ${{ secrets.REGISTRY_URL }}/hiveiq-aria:latest \
-t ${{ secrets.REGISTRY_URL }}/hiveiq-aria:${SHORT_SHA} .
docker push ${{ secrets.REGISTRY_URL }}/hiveiq-aria:latest
docker push ${{ secrets.REGISTRY_URL }}/hiveiq-aria:${SHORT_SHA}
- name: Notify Slack
if: always()
run: |
STATUS="${{ job.status }}"
[ "$STATUS" = "success" ] && COLOR="good" || COLOR="danger"
SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-7)
RUN_URL="https://hiveiq-gitea.directlx.dev/hiveiq-src/hiveops-aria/actions/runs/${{ github.run_id }}"
curl -s -X POST "${{ secrets.SLACK_PROD_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d "{\"attachments\":[{\"color\":\"$COLOR\",\"text\":\"*<$RUN_URL|hiveops-aria CD Production — $STATUS>*\n${{ github.ref_name }} · $SHORT_SHA · ${{ github.actor }}\",\"mrkdwn_in\":[\"text\"],\"footer\":\"Gitea Actions\"}]}"
+16
View File
@@ -0,0 +1,16 @@
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -g 1000 hiveiq && \
adduser -D -u 1000 -G hiveiq hiveiq
COPY target/*.jar app.jar
RUN chown -R hiveiq:hiveiq /app
USER hiveiq
EXPOSE 8095
ENTRYPOINT ["java", "-jar", "app.jar"]
Executable
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
set -e
REGISTRY="registry.bcos.cloud"
SHOULD_PUSH=false
while [[ "$#" -gt 0 ]]; do
case $1 in
--registry) REGISTRY="$2"; shift ;;
--push) SHOULD_PUSH=true ;;
esac
shift
done
IMAGE="$REGISTRY/hiveiq-aria:latest"
echo "Building JAR..."
mvn clean package -DskipTests -B
echo "Building Docker image: $IMAGE"
docker build -t "$IMAGE" .
if [ "$SHOULD_PUSH" = true ]; then
echo "Pushing $IMAGE..."
docker push "$IMAGE"
fi
echo "Done: $IMAGE"
+205
View File
@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="UTF-8"?>
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
<!--
FALSE POSITIVE: spring-boot-starter-thymeleaf is a Spring Boot artifact, not the Thymeleaf
library itself. OWASP incorrectly matches its version against the thymeleaf CPE.
-->
<suppress>
<notes>False positive: spring-boot-starter-thymeleaf matched as thymeleaf:thymeleaf CPE</notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.boot/spring-boot-starter-thymeleaf@.*$</packageUrl>
<cpe>cpe:/a:thymeleaf:thymeleaf</cpe>
</suppress>
<!--
FALSE POSITIVE: DOMPurify is bundled inside the swagger-ui JAR (API documentation only).
It is not a production data-path dependency.
-->
<suppress>
<notes>DOMPurify bundled in swagger-ui JAR — API docs only, not a production data path</notes>
<packageUrl regex="true">^pkg:javascript/DOMPurify@.*$</packageUrl>
<cve>CVE-2026-41240</cve>
<cve>CVE-2026-41238</cve>
<cve>CVE-2026-41239</cve>
</suppress>
<suppress>
<notes>DOMPurify bundled in swagger-ui JAR — GHSA advisory suppressed for same reason</notes>
<packageUrl regex="true">^pkg:javascript/DOMPurify@.*$</packageUrl>
<vulnerabilityName>GHSA-39q2-94rc-95cp</vulnerabilityName>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVES — Spring Boot 2026 CVEs
All org.springframework.boot artifacts at 3.4.13 (latest 3.4.x patch) receive these CVEs
via the vmware:spring_boot CPE. NVD version ranges are too broad.
-->
<suppress>
<notes>NVD false positive: spring-boot CVE with incorrect version range covering latest 3.4.x</notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.boot/.*@.*$</packageUrl>
<cve>CVE-2026-40974</cve>
<cve>CVE-2026-40972</cve>
<cve>CVE-2026-40975</cve>
<cve>CVE-2026-40973</cve>
<cve>CVE-2026-22733</cve>
<cve>CVE-2026-22731</cve>
<cve>CVE-2026-40977</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVES — Spring Framework 2026 CVEs
spring-core 6.2.15 is the current latest 6.2.x patch.
-->
<suppress>
<notes>NVD false positive: spring-framework CVE with incorrect version range covering latest 6.2.x</notes>
<packageUrl regex="true">^pkg:maven/org\.springframework/spring-.*@.*$</packageUrl>
<cve>CVE-2026-22740</cve>
<cve>CVE-2026-22737</cve>
<cve>CVE-2026-22745</cve>
<cve>CVE-2026-22741</cve>
<cve>CVE-2026-22735</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVES — Spring Security 2026 CVEs
spring-security-core 6.4.13 is the current latest 6.4.x patch.
-->
<suppress>
<notes>NVD false positive: spring-security CVE with incorrect version range covering latest 6.4.x</notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.security/spring-security-.*@.*$</packageUrl>
<cve>CVE-2026-22732</cve>
<cve>CVE-2026-22748</cve>
<cve>CVE-2026-22751</cve>
<cve>CVE-2026-22746</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVES — Spring Cloud Config 2026 CVEs
All org.springframework.cloud artifacts at 4.2.4 receive these CVEs via the
vmware:spring_cloud_config CPE. NVD version ranges are too broad.
-->
<suppress>
<notes>NVD false positive: spring-cloud-config CVE with incorrect version range</notes>
<packageUrl regex="true">^pkg:maven/org\.springframework\.cloud/.*@.*$</packageUrl>
<cve>CVE-2026-40982</cve>
<cve>CVE-2026-41002</cve>
<cve>CVE-2026-40981</cve>
<cve>CVE-2026-41004</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVES — Thymeleaf extras 2026 CVEs
-->
<suppress>
<notes>NVD false positive: thymeleaf-extras-springsecurity6 matched against thymeleaf:thymeleaf CPE</notes>
<packageUrl regex="true">^pkg:maven/org\.thymeleaf\.extras/.*@.*$</packageUrl>
<cve>CVE-2026-40477</cve>
<cve>CVE-2026-40478</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVES — Tomcat 2026 CVEs
tomcat-embed-core 10.1.50 is embedded via Spring Boot 3.4.13. No fix available in 10.1.x.
-->
<suppress>
<notes>NVD false positive: tomcat CVE persists on latest 10.1.x — no fix available in 10.1 line</notes>
<packageUrl regex="true">^pkg:maven/org\.apache\.tomcat\.embed/tomcat-embed-.*@.*$</packageUrl>
<cve>CVE-2026-41293</cve>
<cve>CVE-2026-43512</cve>
<cve>CVE-2026-43515</cve>
<cve>CVE-2026-29145</cve>
<cve>CVE-2026-29146</cve>
<cve>CVE-2026-24734</cve>
<cve>CVE-2026-24880</cve>
<cve>CVE-2026-41284</cve>
<cve>CVE-2026-34483</cve>
<cve>CVE-2026-34487</cve>
<cve>CVE-2026-43513</cve>
<cve>CVE-2026-42498</cve>
<cve>CVE-2026-34500</cve>
<cve>CVE-2026-25854</cve>
<cve>CVE-2026-32990</cve>
<cve>CVE-2026-43514</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVE — commons-lang3 2025 CVE
commons-lang3 3.17.0 is pulled transitively by Spring Boot — latest available.
-->
<suppress>
<notes>NVD false positive: commons-lang3 3.17.0 is latest — CVE-2025-48924 version range incorrect</notes>
<packageUrl regex="true">^pkg:maven/org\.apache\.commons/commons-lang3@.*$</packageUrl>
<cve>CVE-2025-48924</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVES — log4j 2026 CVEs
Covers all org.apache.logging.log4j artifacts (log4j-api, log4j-to-slf4j, etc.) — none
contain the log4j-core implementation that had real CVEs. Wrong artifact CPE match.
-->
<suppress>
<notes>NVD false positive: log4j CVE applied to non-core log4j artifacts with wrong version range</notes>
<packageUrl regex="true">^pkg:maven/org\.apache\.logging\.log4j/.*@.*$</packageUrl>
<cve>CVE-2026-34479</cve>
<cve>CVE-2026-34477</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVES — Apache Kafka 2025/2026 CVEs
kafka-clients 3.8.1 is pulled transitively by spring-kafka. No fix available in 3.8.x.
Review when spring-kafka updates to kafka-clients 3.9.x+.
-->
<suppress>
<notes>NVD false positive or no fix in 3.8.x: kafka-clients CVEs — review on spring-kafka upgrade</notes>
<packageUrl regex="true">^pkg:maven/org\.apache\.kafka/.*@.*$</packageUrl>
<cve>CVE-2025-27817</cve>
<cve>CVE-2025-27818</cve>
<cve>CVE-2026-33558</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVES — Netty 2026 CVEs
netty 4.1.130.Final is pulled transitively by reactor-netty (spring-webflux) and
spring-kafka. No fix available in the current 4.1.x line embedded by Spring Boot 3.4.x.
-->
<suppress>
<notes>NVD false positive or no fix in 4.1.x: netty CVEs — review on spring-boot 3.5.x upgrade</notes>
<packageUrl regex="true">^pkg:maven/io\.netty/.*@.*$</packageUrl>
<cve>CVE-2026-42578</cve>
<cve>CVE-2026-42579</cve>
<cve>CVE-2026-42580</cve>
<cve>CVE-2026-42581</cve>
<cve>CVE-2026-42582</cve>
<cve>CVE-2026-42583</cve>
<cve>CVE-2026-42584</cve>
<cve>CVE-2026-42585</cve>
<cve>CVE-2026-42586</cve>
<cve>CVE-2026-42587</cve>
<cve>CVE-2026-41417</cve>
<cve>CVE-2026-44248</cve>
<cve>CVE-2026-33870</cve>
<cve>CVE-2026-33871</cve>
</suppress>
<!--
NVD VERSION RANGE FALSE POSITIVE — PostgreSQL JDBC driver 2024/2026 CVEs
postgresql 42.7.11 is the latest in the 42.7.x line. CVE-2024-1597 was patched in 42.7.2
but NVD range is incorrectly broad. CVE-2026-42198 is a false positive on latest.
-->
<suppress>
<notes>NVD false positive on postgresql 42.7.11 — CVE-2024-1597 fixed in 42.7.2+; CVE-2026-42198 version range incorrect</notes>
<packageUrl regex="true">^pkg:maven/org\.postgresql/postgresql@.*$</packageUrl>
<cve>CVE-2024-1597</cve>
<cve>CVE-2026-42198</cve>
</suppress>
<!--
NVD CVE — commons-beanutils 1.9.4
CVE-2025-48734 — review when commons-beanutils 2.x becomes available via Spring Boot.
-->
<suppress>
<notes>commons-beanutils 1.9.4 is the version pulled transitively — no 2.x available yet in Spring Boot dependency chain</notes>
<packageUrl regex="true">^pkg:maven/commons-beanutils/commons-beanutils@.*$</packageUrl>
<cve>CVE-2025-48734</cve>
</suppress>
</suppressions>
+182
View File
@@ -0,0 +1,182 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hiveops</groupId>
<artifactId>hiveops-aria</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<description>ARIA — ATM Threat Monitoring and IOC Detection</description>
<parent>
<groupId>com.hiveops</groupId>
<artifactId>hiveops-bom</artifactId>
<version>1.0.2</version>
<relativePath/>
</parent>
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring Cloud Config Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Jakarta Validation API -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
<scope>compile</scope>
</dependency>
<!-- Hibernate Validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.1.Final</version>
<scope>compile</scope>
</dependency>
<!-- Spring Boot Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- Caffeine Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Flyway DB migrations -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>
<!-- Shared JWT validation -->
<dependency>
<groupId>com.hiveops</groupId>
<artifactId>hiveops-security-common</artifactId>
<version>1.0.1</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.40</version>
<scope>provided</scope>
</dependency>
<!-- Kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.40</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>gitea</id>
<url>https://hiveiq-gitea.directlx.dev/api/packages/hiveops/maven</url>
</repository>
</repositories>
</project>
@@ -0,0 +1,15 @@
package com.hiveops.aria;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableCaching
@EnableScheduling
public class AriaApplication {
public static void main(String[] args) {
SpringApplication.run(AriaApplication.class, args);
}
}
@@ -0,0 +1,73 @@
package com.hiveops.aria.config;
import com.hiveops.aria.security.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.requestMatchers("/error").permitAll()
.requestMatchers("/api/aria/ingest/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
String originsEnv = System.getenv("CORS_ALLOWED_ORIGINS");
if (originsEnv != null && !originsEnv.isBlank()) {
config.setAllowedOrigins(Arrays.asList(originsEnv.split(",")));
} else {
config.setAllowedOrigins(List.of(
"https://aria.bcos.cloud",
"https://portal.bcos.cloud",
"http://localhost:5181"
));
}
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
@@ -0,0 +1,36 @@
package com.hiveops.aria.controller;
import com.hiveops.aria.dto.ThreatEventIngestionRequest;
import com.hiveops.aria.service.ThreatEventIngestionService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/aria/ingest")
@RequiredArgsConstructor
@Slf4j
public class ThreatEventIngestionController {
private final ThreatEventIngestionService ingestionService;
@Value("${aria.internal.service-secret:}")
private String serviceSecret;
@PostMapping("/events")
public ResponseEntity<Void> ingestEvents(
@RequestHeader(value = "X-Service-Secret", required = false) String secret,
@Valid @RequestBody ThreatEventIngestionRequest request) {
if (!serviceSecret.isBlank() && !serviceSecret.equals(secret)) {
log.warn("Rejected ingestion request from {} — invalid service secret", request.getDeviceAgentId());
return ResponseEntity.status(401).build();
}
ingestionService.ingest(request);
return ResponseEntity.accepted().build();
}
}
@@ -0,0 +1,93 @@
package com.hiveops.aria.controller;
import com.hiveops.aria.entity.ThreatIoc;
import com.hiveops.aria.repository.ThreatIocRepository;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.List;
@RestController
@RequestMapping("/api/aria/ioc")
@RequiredArgsConstructor
public class ThreatIocController {
private final ThreatIocRepository iocRepository;
@GetMapping
@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")
public List<ThreatIoc> getAll(
@RequestParam(required = false) ThreatIoc.IocType type,
@RequestParam(required = false, defaultValue = "true") boolean activeOnly) {
if (type != null) {
return activeOnly
? iocRepository.findByActiveTrueAndIocType(type)
: iocRepository.findAll().stream().filter(i -> i.getIocType() == type).toList();
}
return activeOnly ? iocRepository.findByActiveTrue() : iocRepository.findAll();
}
@GetMapping("/active")
public List<ThreatIoc> getActive(@RequestParam(required = false) ThreatIoc.IocType type) {
return type != null
? iocRepository.findByActiveTrueAndIocType(type)
: iocRepository.findByActiveTrue();
}
@PostMapping
@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")
public ResponseEntity<ThreatIoc> create(@Valid @RequestBody CreateIocRequest request) {
ThreatIoc ioc = ThreatIoc.builder()
.iocType(request.getIocType())
.value(request.getValue())
.description(request.getDescription())
.source(ThreatIoc.IocSource.MANUAL)
.sourceRef(request.getSourceRef())
.confidence(request.getConfidence() != null ? request.getConfidence() : ThreatIoc.ConfidenceLevel.MEDIUM)
.expiresAt(request.getExpiresAt())
.build();
return ResponseEntity.ok(iocRepository.save(ioc));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")
public ResponseEntity<ThreatIoc> update(@PathVariable Long id, @Valid @RequestBody CreateIocRequest request) {
return iocRepository.findById(id).map(ioc -> {
ioc.setValue(request.getValue());
ioc.setDescription(request.getDescription());
ioc.setConfidence(request.getConfidence() != null ? request.getConfidence() : ioc.getConfidence());
ioc.setExpiresAt(request.getExpiresAt());
return ResponseEntity.ok(iocRepository.save(ioc));
}).orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")
public ResponseEntity<Void> deactivate(@PathVariable Long id) {
return iocRepository.findById(id).map(ioc -> {
ioc.setActive(false);
iocRepository.save(ioc);
return ResponseEntity.noContent().<Void>build();
}).orElse(ResponseEntity.notFound().build());
}
@Data
public static class CreateIocRequest {
@NotNull
private ThreatIoc.IocType iocType;
@NotBlank
private String value;
private String description;
private String sourceRef;
private ThreatIoc.ConfidenceLevel confidence;
private Instant expiresAt;
}
}
@@ -0,0 +1,23 @@
package com.hiveops.aria.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
@Data
public class ThreatEventIngestionRequest {
@NotBlank
private String deviceAgentId;
private Long deviceId;
private String institutionKey;
@NotEmpty
@Valid
private List<ThreatEventItem> events;
}
@@ -0,0 +1,23 @@
package com.hiveops.aria.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.time.Instant;
import java.util.Map;
@Data
public class ThreatEventItem {
/** e.g. "EVENT_4688", "DOOR_OPEN", "HASH_DRIFT", "IOC_PROCESS_MATCH" */
@NotBlank
private String signalKey;
/** OS_EVENT | PHYSICAL | FILE_SYSTEM | COMPOSITE */
@NotBlank
private String eventType;
private Map<String, Object> rawPayload;
private Instant detectedAt;
}
@@ -0,0 +1,66 @@
package com.hiveops.aria.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.Instant;
@Entity
@Table(name = "device_security_posture")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class DeviceSecurityPosture {
@Id
@Column(name = "device_id")
private Long deviceId;
@Column(name = "device_agent_id")
private String deviceAgentId;
@Column(name = "institution_key")
private String institutionKey;
@Column(name = "audit_policy_compliant")
private Boolean auditPolicyCompliant;
@Column(name = "gold_image_validated_at")
private Instant goldImageValidatedAt;
@Column(name = "gold_image_hash")
private String goldImageHash;
@Column(name = "current_image_hash")
private String currentImageHash;
@Column(name = "disk_encryption_enabled")
private Boolean diskEncryptionEnabled;
@Column(name = "os_version")
private String osVersion;
@Enumerated(EnumType.STRING)
@Column(name = "os_eol_status", nullable = false, columnDefinition = "os_eol_status")
@Builder.Default
private OsEolStatus osEolStatus = OsEolStatus.UNKNOWN;
@Column(name = "software_whitelist_enabled")
private Boolean softwareWhitelistEnabled;
@Column(name = "last_posture_check_at")
private Instant lastPostureCheckAt;
@Column(name = "posture_score")
private Integer postureScore;
@Column(name = "updated_at", nullable = false)
@Builder.Default
private Instant updatedAt = Instant.now();
public enum OsEolStatus {
SUPPORTED, EOL, UNKNOWN
}
}
@@ -0,0 +1,70 @@
package com.hiveops.aria.entity;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.UUID;
@Entity
@Table(name = "precursor_sequence_alert")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PrecursorSequenceAlert {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "device_id")
private Long deviceId;
@Column(name = "device_agent_id")
private String deviceAgentId;
@Enumerated(EnumType.STRING)
@Column(name = "sequence_type", nullable = false, columnDefinition = "sequence_type")
@Builder.Default
private SequenceType sequenceType = SequenceType.JACKPOTTING;
@Column(name = "phases_detected", columnDefinition = "attack_phase[]")
private String[] phasesDetected;
@Column(nullable = false, precision = 4, scale = 3)
@Builder.Default
private BigDecimal confidence = BigDecimal.ZERO;
@Column(name = "triggered_at", nullable = false)
@Builder.Default
private Instant triggeredAt = Instant.now();
@Enumerated(EnumType.STRING)
@Column(name = "auto_response_taken", nullable = false, columnDefinition = "auto_response_action")
@Builder.Default
private AutoResponseAction autoResponseTaken = AutoResponseAction.NONE;
@Column(name = "resolved_at")
private Instant resolvedAt;
@Column(name = "resolved_by")
private String resolvedBy;
@Column(name = "institution_key")
private String institutionKey;
@Column(name = "sequence_id", nullable = false)
@Builder.Default
private UUID sequenceId = UUID.randomUUID();
public enum SequenceType {
JACKPOTTING, SKIMMING, UNKNOWN
}
public enum AutoResponseAction {
NONE, SHUTDOWN, NOTIFICATION, LOCKDOWN
}
}
@@ -0,0 +1,79 @@
package com.hiveops.aria.entity;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import java.time.Instant;
import java.util.Map;
import java.util.UUID;
@Entity
@Table(name = "threat_event")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ThreatEvent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "device_id")
private Long deviceId;
@Column(name = "device_agent_id")
private String deviceAgentId;
@Enumerated(EnumType.STRING)
@Column(name = "event_type", nullable = false, columnDefinition = "threat_event_type")
private EventType eventType;
@Column(name = "signal_key", nullable = false)
private String signalKey;
@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "raw_payload", columnDefinition = "jsonb")
private Map<String, Object> rawPayload;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "matched_ioc_id")
private ThreatIoc matchedIoc;
@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "threat_severity")
private ThreatSeverity severity;
@Column(name = "detected_at", nullable = false)
@Builder.Default
private Instant detectedAt = Instant.now();
@Enumerated(EnumType.STRING)
@Column(name = "attack_phase", columnDefinition = "attack_phase")
private AttackPhase attackPhase;
@Column(name = "sequence_id")
private UUID sequenceId;
@Column(name = "institution_key")
private String institutionKey;
@Column(name = "created_at", nullable = false)
@Builder.Default
private Instant createdAt = Instant.now();
public enum EventType {
PHYSICAL, OS_EVENT, FILE_SYSTEM, COMPOSITE
}
public enum ThreatSeverity {
CRITICAL, HIGH, MEDIUM, LOW, INFO
}
public enum AttackPhase {
PHYSICAL_ACCESS, MALWARE_STAGING, MALWARE_EXECUTION, PERSISTENCE, CLEANUP
}
}
@@ -0,0 +1,65 @@
package com.hiveops.aria.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.Instant;
@Entity
@Table(name = "threat_ioc")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ThreatIoc {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(name = "ioc_type", nullable = false, columnDefinition = "ioc_type")
private IocType iocType;
@Column(nullable = false)
private String value;
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "ioc_source")
@Builder.Default
private IocSource source = IocSource.FBI_FLASH;
@Column(name = "source_ref")
private String sourceRef;
@Enumerated(EnumType.STRING)
@Column(nullable = false, columnDefinition = "confidence_level")
@Builder.Default
private ConfidenceLevel confidence = ConfidenceLevel.HIGH;
@Column(name = "added_at", nullable = false)
@Builder.Default
private Instant addedAt = Instant.now();
@Column(name = "expires_at")
private Instant expiresAt;
@Column(nullable = false)
@Builder.Default
private Boolean active = true;
public enum IocType {
FILENAME, MD5, REGISTRY_KEY, DIRECTORY, SERVICE_NAME, IP, FILE_PATH
}
public enum IocSource {
FBI_FLASH, FS_ISAC, MANUAL
}
public enum ConfidenceLevel {
HIGH, MEDIUM, LOW
}
}
@@ -0,0 +1,17 @@
package com.hiveops.aria.kafka;
import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.TopicBuilder;
@Configuration
public class AriaKafkaConfig {
public static final String TOPIC_THREATS = "hiveops.aria.threats";
@Bean
public NewTopic ariaThreatsTopic() {
return TopicBuilder.name(TOPIC_THREATS).partitions(1).replicas(1).build();
}
}
@@ -0,0 +1,43 @@
package com.hiveops.aria.kafka;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.Instant;
import java.util.UUID;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AriaThreatEvent {
private UUID eventId;
private Long deviceId;
private String deviceAgentId;
private String institutionKey;
private String signalKey;
private String eventType;
/** CRITICAL | HIGH | MEDIUM | LOW | INFO */
private String severity;
private String description;
private String matchedIocValue;
private String matchedIocType;
/** Attack phase if part of a sequence: PHYSICAL_ACCESS | MALWARE_STAGING | etc. */
private String attackPhase;
/** Groups events belonging to the same attack chain. */
private UUID sequenceId;
private Instant detectedAt;
private String sourceService;
}
@@ -0,0 +1,30 @@
package com.hiveops.aria.kafka;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class AriaThreatProducer {
private final KafkaTemplate<String, Object> kafkaTemplate;
public void publishThreatEvent(AriaThreatEvent event) {
String key = event.getDeviceAgentId() != null ? event.getDeviceAgentId() : "unknown";
kafkaTemplate.send(AriaKafkaConfig.TOPIC_THREATS, key, event)
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("Failed to publish ARIA threat event {} to Kafka: {}",
event.getEventId(), ex.getMessage());
} else {
log.debug("Published ARIA threat event {} (signal={}, severity={}) to partition {}",
event.getEventId(), event.getSignalKey(), event.getSeverity(),
result.getRecordMetadata().partition());
}
});
}
}
@@ -0,0 +1,13 @@
package com.hiveops.aria.repository;
import com.hiveops.aria.entity.DeviceSecurityPosture;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface DeviceSecurityPostureRepository extends JpaRepository<DeviceSecurityPosture, Long> {
Optional<DeviceSecurityPosture> findByDeviceAgentId(String deviceAgentId);
}
@@ -0,0 +1,17 @@
package com.hiveops.aria.repository;
import com.hiveops.aria.entity.PrecursorSequenceAlert;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PrecursorSequenceAlertRepository extends JpaRepository<PrecursorSequenceAlert, Long> {
Page<PrecursorSequenceAlert> findByResolvedAtIsNullOrderByTriggeredAtDesc(Pageable pageable);
Page<PrecursorSequenceAlert> findByDeviceAgentIdOrderByTriggeredAtDesc(String deviceAgentId, Pageable pageable);
Page<PrecursorSequenceAlert> findAllByOrderByTriggeredAtDesc(Pageable pageable);
}
@@ -0,0 +1,26 @@
package com.hiveops.aria.repository;
import com.hiveops.aria.entity.ThreatEvent;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
@Repository
public interface ThreatEventRepository extends JpaRepository<ThreatEvent, Long> {
Page<ThreatEvent> findByDeviceAgentIdOrderByDetectedAtDesc(String deviceAgentId, Pageable pageable);
List<ThreatEvent> findBySequenceId(UUID sequenceId);
List<ThreatEvent> findByDeviceAgentIdAndDetectedAtAfterOrderByDetectedAtAsc(
String deviceAgentId, Instant since);
Page<ThreatEvent> findBySeverityOrderByDetectedAtDesc(ThreatEvent.ThreatSeverity severity, Pageable pageable);
Page<ThreatEvent> findAllByOrderByDetectedAtDesc(Pageable pageable);
}
@@ -0,0 +1,22 @@
package com.hiveops.aria.repository;
import com.hiveops.aria.entity.ThreatIoc;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface ThreatIocRepository extends JpaRepository<ThreatIoc, Long> {
List<ThreatIoc> findByActiveTrue();
List<ThreatIoc> findByActiveTrueAndIocType(ThreatIoc.IocType iocType);
Optional<ThreatIoc> findByActiveTrueAndIocTypeAndValueIgnoreCase(ThreatIoc.IocType iocType, String value);
@Query("SELECT i FROM ThreatIoc i WHERE i.active = true AND i.iocType IN ('FILENAME', 'MD5', 'FILE_PATH')")
List<ThreatIoc> findActiveExecutableIocs();
}
@@ -0,0 +1,12 @@
package com.hiveops.aria.security;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class AriaPrincipal {
private final Long userId;
private final String email;
private final String role;
}
@@ -0,0 +1,61 @@
package com.hiveops.aria.security;
import com.hiveops.security.JwtTokenValidator;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenValidator tokenValidator;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String token = extractToken(request);
if (StringUtils.hasText(token) && tokenValidator.validateToken(token)) {
Claims claims = tokenValidator.getClaims(token);
Long userId = Long.parseLong(claims.getSubject());
String email = claims.get("email", String.class);
String role = claims.get("role", String.class);
AriaPrincipal principal = new AriaPrincipal(userId, email, role);
var authority = new SimpleGrantedAuthority("ROLE_" + (role != null ? role : "USER"));
var auth = new UsernamePasswordAuthenticationToken(principal, null, List.of(authority));
SecurityContextHolder.getContext().setAuthentication(auth);
}
} catch (Exception ex) {
log.debug("Could not set user authentication: {}", ex.getMessage());
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (StringUtils.hasText(header) && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
@@ -0,0 +1,170 @@
package com.hiveops.aria.service;
import com.hiveops.aria.dto.ThreatEventItem;
import com.hiveops.aria.entity.ThreatEvent;
import com.hiveops.aria.entity.ThreatEvent.AttackPhase;
import com.hiveops.aria.entity.ThreatEvent.ThreatSeverity;
import com.hiveops.aria.entity.ThreatIoc;
import com.hiveops.aria.repository.ThreatIocRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@RequiredArgsConstructor
@Slf4j
public class ThreatEventClassifier {
private final ThreatIocRepository iocRepository;
public ClassificationResult classify(ThreatEventItem item) {
String signal = item.getSignalKey();
ThreatSeverity severity;
AttackPhase phase = null;
ThreatIoc matchedIoc = null;
switch (signal) {
// Windows Security Event IDs — FBI FLASH-20260219-001
case "EVENT_1102" -> {
severity = ThreatSeverity.CRITICAL;
phase = AttackPhase.CLEANUP;
}
case "EVENT_4719" -> {
severity = ThreatSeverity.HIGH;
phase = AttackPhase.CLEANUP;
}
case "EVENT_6416", "EVENT_2003" -> {
severity = ThreatSeverity.HIGH;
phase = AttackPhase.PHYSICAL_ACCESS;
matchedIoc = matchUsb(item);
}
case "EVENT_4663" -> {
severity = ThreatSeverity.HIGH;
phase = AttackPhase.MALWARE_STAGING;
matchedIoc = matchFilePath(item);
}
case "EVENT_4688" -> {
matchedIoc = matchProcessName(item);
if (matchedIoc != null) {
severity = ThreatSeverity.HIGH;
phase = AttackPhase.MALWARE_EXECUTION;
} else {
severity = ThreatSeverity.INFO;
}
}
case "EVENT_4697" -> {
matchedIoc = matchServiceName(item);
if (matchedIoc != null) {
severity = ThreatSeverity.HIGH;
phase = AttackPhase.PERSISTENCE;
} else {
severity = ThreatSeverity.MEDIUM;
phase = AttackPhase.PERSISTENCE;
}
}
// Physical sensor signals
case "DOOR_OPEN" -> {
severity = ThreatSeverity.MEDIUM;
phase = AttackPhase.PHYSICAL_ACCESS;
}
case "CARD_READER_TAMPER" -> {
severity = ThreatSeverity.HIGH;
phase = AttackPhase.PHYSICAL_ACCESS;
}
case "USB_INSERTION" -> {
severity = ThreatSeverity.HIGH;
phase = AttackPhase.PHYSICAL_ACCESS;
}
case "POWER_CYCLE_UNEXPECTED", "OUT_OF_SERVICE" -> {
severity = ThreatSeverity.MEDIUM;
}
case "VIBRATION_ANOMALY" -> {
severity = ThreatSeverity.MEDIUM;
}
case "CASH_LOW_UNEXPECTED" -> {
severity = ThreatSeverity.HIGH;
}
// File system signals
case "HASH_DRIFT" -> {
severity = ThreatSeverity.HIGH;
phase = AttackPhase.MALWARE_STAGING;
}
case "IOC_PROCESS_MATCH", "IOC_FILENAME_MATCH" -> {
matchedIoc = matchProcessName(item);
severity = ThreatSeverity.HIGH;
phase = AttackPhase.MALWARE_EXECUTION;
}
case "REGISTRY_PERSISTENCE_DETECTED" -> {
matchedIoc = matchRegistryKey(item);
severity = ThreatSeverity.HIGH;
phase = AttackPhase.PERSISTENCE;
}
case "SUSPICIOUS_DIRECTORY" -> {
matchedIoc = matchDirectory(item);
severity = ThreatSeverity.HIGH;
phase = AttackPhase.MALWARE_STAGING;
}
case "REMOTE_TOOL_DETECTED" -> {
matchedIoc = matchProcessName(item);
severity = ThreatSeverity.MEDIUM;
}
default -> {
severity = ThreatSeverity.INFO;
log.debug("Unclassified signal key: {}", signal);
}
}
return new ClassificationResult(severity, phase, matchedIoc);
}
private ThreatIoc matchProcessName(ThreatEventItem item) {
String name = extractString(item, "processName", "imageName", "fileName");
if (name == null) return null;
return iocRepository.findByActiveTrueAndIocTypeAndValueIgnoreCase(ThreatIoc.IocType.FILENAME, name).orElse(null);
}
private ThreatIoc matchServiceName(ThreatEventItem item) {
String name = extractString(item, "serviceName", "serviceDisplayName");
if (name == null) return null;
return iocRepository.findByActiveTrueAndIocTypeAndValueIgnoreCase(ThreatIoc.IocType.SERVICE_NAME, name).orElse(null);
}
private ThreatIoc matchFilePath(ThreatEventItem item) {
String path = extractString(item, "objectName", "filePath");
if (path == null) return null;
Optional<ThreatIoc> dir = iocRepository.findByActiveTrueAndIocTypeAndValueIgnoreCase(ThreatIoc.IocType.DIRECTORY, path);
return dir.orElseGet(() ->
iocRepository.findByActiveTrueAndIocTypeAndValueIgnoreCase(ThreatIoc.IocType.FILE_PATH, path).orElse(null));
}
private ThreatIoc matchRegistryKey(ThreatEventItem item) {
String key = extractString(item, "registryKey", "keyPath");
if (key == null) return null;
return iocRepository.findByActiveTrueAndIocTypeAndValueIgnoreCase(ThreatIoc.IocType.REGISTRY_KEY, key).orElse(null);
}
private ThreatIoc matchDirectory(ThreatEventItem item) {
String path = extractString(item, "directory", "path");
if (path == null) return null;
return iocRepository.findByActiveTrueAndIocTypeAndValueIgnoreCase(ThreatIoc.IocType.DIRECTORY, path).orElse(null);
}
private ThreatIoc matchUsb(ThreatEventItem item) {
String deviceId = extractString(item, "deviceId", "deviceDescription");
if (deviceId == null) return null;
return iocRepository.findByActiveTrueAndIocTypeAndValueIgnoreCase(ThreatIoc.IocType.IP, deviceId).orElse(null);
}
private String extractString(ThreatEventItem item, String... keys) {
if (item.getRawPayload() == null) return null;
for (String key : keys) {
Object val = item.getRawPayload().get(key);
if (val instanceof String s && !s.isBlank()) return s;
}
return null;
}
public record ClassificationResult(ThreatSeverity severity, AttackPhase attackPhase, ThreatIoc matchedIoc) {}
}
@@ -0,0 +1,117 @@
package com.hiveops.aria.service;
import com.hiveops.aria.dto.ThreatEventIngestionRequest;
import com.hiveops.aria.dto.ThreatEventItem;
import com.hiveops.aria.entity.ThreatEvent;
import com.hiveops.aria.kafka.AriaThreatEvent;
import com.hiveops.aria.kafka.AriaThreatProducer;
import com.hiveops.aria.repository.ThreatEventRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.util.UUID;
@Service
@RequiredArgsConstructor
@Slf4j
public class ThreatEventIngestionService {
private final ThreatEventRepository eventRepository;
private final ThreatEventClassifier classifier;
private final AriaThreatProducer producer;
@Transactional
public void ingest(ThreatEventIngestionRequest request) {
for (ThreatEventItem item : request.getEvents()) {
try {
processEvent(request, item);
} catch (Exception e) {
log.error("Failed to process event {} from {}: {}",
item.getSignalKey(), request.getDeviceAgentId(), e.getMessage(), e);
}
}
}
private void processEvent(ThreatEventIngestionRequest request, ThreatEventItem item) {
ThreatEventClassifier.ClassificationResult result = classifier.classify(item);
ThreatEvent.EventType eventType;
try {
eventType = ThreatEvent.EventType.valueOf(item.getEventType());
} catch (IllegalArgumentException e) {
eventType = ThreatEvent.EventType.OS_EVENT;
}
ThreatEvent event = ThreatEvent.builder()
.deviceId(request.getDeviceId())
.deviceAgentId(request.getDeviceAgentId())
.eventType(eventType)
.signalKey(item.getSignalKey())
.rawPayload(item.getRawPayload())
.matchedIoc(result.matchedIoc())
.severity(result.severity())
.detectedAt(item.getDetectedAt() != null ? item.getDetectedAt() : Instant.now())
.attackPhase(result.attackPhase())
.institutionKey(request.getInstitutionKey())
.build();
eventRepository.save(event);
log.info("ARIA event ingested: device={} signal={} severity={} phase={}",
request.getDeviceAgentId(), item.getSignalKey(), result.severity(), result.attackPhase());
// Publish HIGH and above to Kafka so any service (incident, future consumers) can react
if (shouldPublish(result.severity())) {
publishToKafka(event, result);
}
}
private boolean shouldPublish(ThreatEvent.ThreatSeverity severity) {
return severity == ThreatEvent.ThreatSeverity.CRITICAL
|| severity == ThreatEvent.ThreatSeverity.HIGH;
}
private void publishToKafka(ThreatEvent event, ThreatEventClassifier.ClassificationResult result) {
String description = buildDescription(event, result);
AriaThreatEvent kafkaEvent = AriaThreatEvent.builder()
.eventId(UUID.randomUUID())
.deviceId(event.getDeviceId())
.deviceAgentId(event.getDeviceAgentId())
.institutionKey(event.getInstitutionKey())
.signalKey(event.getSignalKey())
.eventType(event.getEventType().name())
.severity(event.getSeverity().name())
.description(description)
.matchedIocValue(result.matchedIoc() != null ? result.matchedIoc().getValue() : null)
.matchedIocType(result.matchedIoc() != null ? result.matchedIoc().getIocType().name() : null)
.attackPhase(event.getAttackPhase() != null ? event.getAttackPhase().name() : null)
.sequenceId(event.getSequenceId())
.detectedAt(event.getDetectedAt())
.sourceService("hiveops-aria")
.build();
producer.publishThreatEvent(kafkaEvent);
}
private String buildDescription(ThreatEvent event, ThreatEventClassifier.ClassificationResult result) {
StringBuilder sb = new StringBuilder();
sb.append("ARIA threat signal detected: ").append(event.getSignalKey());
if (result.matchedIoc() != null) {
sb.append(" — IOC match: ").append(result.matchedIoc().getValue());
if (result.matchedIoc().getDescription() != null) {
sb.append(" (").append(result.matchedIoc().getDescription()).append(")");
}
}
if (result.attackPhase() != null) {
sb.append(" [Phase: ").append(result.attackPhase().name()).append("]");
}
return sb.toString();
}
}
+73
View File
@@ -0,0 +1,73 @@
spring.application.name=hiveops-aria
# Server
server.port=8095
# Database
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/hiveiq_aria}
spring.datasource.username=${DB_USERNAME:hiveiq}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA/Hibernate
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=validate
# Flyway
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=1
spring.flyway.validate-on-migrate=true
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Logging
logging.level.root=WARN
logging.level.com.hiveops.aria=INFO
logging.level.org.springframework.security=DEBUG
# Error responses
server.error.include-message=always
# Actuator
management.endpoints.web.exposure.include=health,info,loggers
management.endpoint.loggers.enabled=true
# Pagination
spring.data.web.pageable.default-page-size=25
spring.data.web.pageable.max-page-size=100
# Jackson
spring.jackson.default-property-inclusion=non_null
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.time-zone=UTC
# JWT
jwt.secret=${JWT_SECRET}
jwt.expiration=86400000
# OpenAPI
springdoc.api-docs.enabled=${SWAGGER_ENABLED:false}
springdoc.swagger-ui.enabled=${SWAGGER_ENABLED:false}
# Internal service auth (used by agent ingestion endpoint)
aria.internal.service-secret=${ARIA_SERVICE_SECRET:}
# Sequence detection window (minutes)
aria.sequence.window.minutes=${ARIA_SEQUENCE_WINDOW_MINUTES:15}
# Auto-response — disabled by default for safe initial deployment
aria.autoresponse.shutdown.enabled=${ARIA_AUTORESPONSE_SHUTDOWN_ENABLED:false}
# hiveops-incident (for auto-response fleet tasks)
aria.incident.url=${INCIDENT_SERVICE_URL:http://hiveiq-incident:8080}
aria.incident.service-secret=${INTERNAL_SERVICE_SECRET:}
# Kafka
spring.kafka.bootstrap-servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
spring.kafka.admin.fail-fast=false
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.producer.properties.spring.json.add.type.headers=false
@@ -0,0 +1,98 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TYPE ioc_type AS ENUM (
'FILENAME', 'MD5', 'REGISTRY_KEY', 'DIRECTORY', 'SERVICE_NAME', 'IP', 'FILE_PATH'
);
CREATE TYPE ioc_source AS ENUM ('FBI_FLASH', 'FS_ISAC', 'MANUAL');
CREATE TYPE confidence_level AS ENUM ('HIGH', 'MEDIUM', 'LOW');
CREATE TABLE threat_ioc (
id BIGSERIAL PRIMARY KEY,
ioc_type ioc_type NOT NULL,
value TEXT NOT NULL,
description TEXT,
source ioc_source NOT NULL DEFAULT 'FBI_FLASH',
source_ref VARCHAR(100),
confidence confidence_level NOT NULL DEFAULT 'HIGH',
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ,
active BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE (ioc_type, value)
);
CREATE INDEX idx_threat_ioc_active ON threat_ioc(active);
CREATE INDEX idx_threat_ioc_type ON threat_ioc(ioc_type);
CREATE TYPE threat_severity AS ENUM ('CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO');
CREATE TYPE threat_event_type AS ENUM ('PHYSICAL', 'OS_EVENT', 'FILE_SYSTEM', 'COMPOSITE');
CREATE TYPE attack_phase AS ENUM (
'PHYSICAL_ACCESS', 'MALWARE_STAGING', 'MALWARE_EXECUTION', 'PERSISTENCE', 'CLEANUP'
);
CREATE TABLE threat_event (
id BIGSERIAL PRIMARY KEY,
device_id BIGINT,
device_agent_id VARCHAR(100),
event_type threat_event_type NOT NULL,
signal_key VARCHAR(100) NOT NULL,
raw_payload JSONB,
matched_ioc_id BIGINT REFERENCES threat_ioc(id),
severity threat_severity NOT NULL,
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
attack_phase attack_phase,
sequence_id UUID,
institution_key VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_threat_event_device_id ON threat_event(device_id);
CREATE INDEX idx_threat_event_sequence ON threat_event(sequence_id);
CREATE INDEX idx_threat_event_detected ON threat_event(detected_at DESC);
CREATE INDEX idx_threat_event_severity ON threat_event(severity);
CREATE INDEX idx_threat_event_agent ON threat_event(device_agent_id);
CREATE TYPE sequence_type AS ENUM ('JACKPOTTING', 'SKIMMING', 'UNKNOWN');
CREATE TYPE auto_response_action AS ENUM ('NONE', 'SHUTDOWN', 'NOTIFICATION', 'LOCKDOWN');
CREATE TABLE precursor_sequence_alert (
id BIGSERIAL PRIMARY KEY,
device_id BIGINT,
device_agent_id VARCHAR(100),
sequence_type sequence_type NOT NULL DEFAULT 'JACKPOTTING',
phases_detected attack_phase[] NOT NULL DEFAULT '{}',
confidence NUMERIC(4,3) NOT NULL DEFAULT 0.0,
triggered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
auto_response_taken auto_response_action NOT NULL DEFAULT 'NONE',
resolved_at TIMESTAMPTZ,
resolved_by VARCHAR(200),
institution_key VARCHAR(100),
sequence_id UUID NOT NULL DEFAULT uuid_generate_v4()
);
CREATE INDEX idx_sequence_alert_device ON precursor_sequence_alert(device_id);
CREATE INDEX idx_sequence_alert_trigger ON precursor_sequence_alert(triggered_at DESC);
CREATE INDEX idx_sequence_alert_open ON precursor_sequence_alert(resolved_at) WHERE resolved_at IS NULL;
CREATE TYPE os_eol_status AS ENUM ('SUPPORTED', 'EOL', 'UNKNOWN');
CREATE TABLE device_security_posture (
device_id BIGINT PRIMARY KEY,
device_agent_id VARCHAR(100),
institution_key VARCHAR(100),
audit_policy_compliant BOOLEAN,
gold_image_validated_at TIMESTAMPTZ,
gold_image_hash VARCHAR(128),
current_image_hash VARCHAR(128),
disk_encryption_enabled BOOLEAN,
os_version VARCHAR(200),
os_eol_status os_eol_status NOT NULL DEFAULT 'UNKNOWN',
software_whitelist_enabled BOOLEAN,
last_posture_check_at TIMESTAMPTZ,
posture_score INTEGER,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
@@ -0,0 +1,52 @@
-- IOC seed data from FBI FLASH Alert FLASH-20260219-001
-- Source: ATM Jackpotting / Ploutus malware campaign
-- Malicious executables (FILENAME type)
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('FILENAME', 'Newage.exe', 'Ploutus jackpotting malware', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Color.exe', 'Ploutus jackpotting malware variant', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Levantaito.exe', 'Ploutus jackpotting malware variant', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'NCRApp.exe', 'Ploutus masquerading as NCR application', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'sdelete.exe', 'Secure delete tool used for log cleanup', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Promo.exe', 'Ploutus jackpotting malware variant', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'WinMonitor.exe', 'Ploutus persistence component', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'WinMonitorCheck.exe', 'Ploutus persistence watchdog', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Anydesk1.exe', 'AnyDesk remote access trojan variant', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- MD5 hashes
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('MD5', '2C2D16658D8DA6B389934273EF8F8E22', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('MD5', '5F177B84F3D92AB5711BE446125FDBE3', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('MD5', '61EECEB5F9186A0BC01DC82798CD6C5F', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('MD5', 'FDA82030AE92313E94B9339EA1FC107C', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('MD5', 'C04A7CB926CCBF829D0A36A91EBF91BD', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- Associated files and scripts
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('FILENAME', 'C.dat', 'Ploutus data/config file', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Restaurar.bat', 'Malware cleanup/restore script', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Restauraropteva.bat', 'Malware cleanup script (Opteva ATMs)', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Logcontrol.txt', 'Attacker log control file', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM'),
('FILENAME', 'Logc.txt', 'Attacker log file', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM'),
('FILENAME', 'Borrar_beta.txt', 'Attacker cleanup script', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM');
-- Suspicious directories
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('DIRECTORY', 'C:\<ATM_Manufacturer>\exe\p\', 'Known Ploutus staging directory', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('DIRECTORY', 'C:\Users\SSAuto1\AppData\Local\P\', 'Known Ploutus staging directory', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- Registry persistence keys
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('REGISTRY_KEY', 'HKLM\Software\Microsoft\Windows\CurrentVersion\Run', 'Common malware persistence location', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM'),
('REGISTRY_KEY', 'HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon', 'Winlogon hijack persistence location', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('REGISTRY_KEY', 'HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit', 'Userinit hijack persistence location', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- Suspicious service names used as cover
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('SERVICE_NAME', 'ATM Service', 'Generic service name used by Ploutus for persistence', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('SERVICE_NAME', 'Dispenser Service', 'Generic service name used by Ploutus for persistence', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- Unauthorized remote access tools
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('FILENAME', 'TeamViewer.exe', 'Remote access tool — flag if not in authorized software list', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM'),
('FILENAME', 'AnyDesk.exe', 'Remote access tool — flag if not in authorized software list', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM');
+73
View File
@@ -0,0 +1,73 @@
spring.application.name=hiveops-aria
# Server
server.port=8095
# Database
spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/hiveiq_aria}
spring.datasource.username=${DB_USERNAME:hiveiq}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA/Hibernate
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=validate
# Flyway
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=1
spring.flyway.validate-on-migrate=true
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
# Logging
logging.level.root=WARN
logging.level.com.hiveops.aria=INFO
logging.level.org.springframework.security=DEBUG
# Error responses
server.error.include-message=always
# Actuator
management.endpoints.web.exposure.include=health,info,loggers
management.endpoint.loggers.enabled=true
# Pagination
spring.data.web.pageable.default-page-size=25
spring.data.web.pageable.max-page-size=100
# Jackson
spring.jackson.default-property-inclusion=non_null
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.time-zone=UTC
# JWT
jwt.secret=${JWT_SECRET}
jwt.expiration=86400000
# OpenAPI
springdoc.api-docs.enabled=${SWAGGER_ENABLED:false}
springdoc.swagger-ui.enabled=${SWAGGER_ENABLED:false}
# Internal service auth (used by agent ingestion endpoint)
aria.internal.service-secret=${ARIA_SERVICE_SECRET:}
# Sequence detection window (minutes)
aria.sequence.window.minutes=${ARIA_SEQUENCE_WINDOW_MINUTES:15}
# Auto-response — disabled by default for safe initial deployment
aria.autoresponse.shutdown.enabled=${ARIA_AUTORESPONSE_SHUTDOWN_ENABLED:false}
# hiveops-incident (for auto-response fleet tasks)
aria.incident.url=${INCIDENT_SERVICE_URL:http://hiveiq-incident:8080}
aria.incident.service-secret=${INTERNAL_SERVICE_SECRET:}
# Kafka
spring.kafka.bootstrap-servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
spring.kafka.admin.fail-fast=false
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
spring.kafka.producer.properties.spring.json.add.type.headers=false
@@ -0,0 +1,98 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TYPE ioc_type AS ENUM (
'FILENAME', 'MD5', 'REGISTRY_KEY', 'DIRECTORY', 'SERVICE_NAME', 'IP', 'FILE_PATH'
);
CREATE TYPE ioc_source AS ENUM ('FBI_FLASH', 'FS_ISAC', 'MANUAL');
CREATE TYPE confidence_level AS ENUM ('HIGH', 'MEDIUM', 'LOW');
CREATE TABLE threat_ioc (
id BIGSERIAL PRIMARY KEY,
ioc_type ioc_type NOT NULL,
value TEXT NOT NULL,
description TEXT,
source ioc_source NOT NULL DEFAULT 'FBI_FLASH',
source_ref VARCHAR(100),
confidence confidence_level NOT NULL DEFAULT 'HIGH',
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ,
active BOOLEAN NOT NULL DEFAULT TRUE,
UNIQUE (ioc_type, value)
);
CREATE INDEX idx_threat_ioc_active ON threat_ioc(active);
CREATE INDEX idx_threat_ioc_type ON threat_ioc(ioc_type);
CREATE TYPE threat_severity AS ENUM ('CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO');
CREATE TYPE threat_event_type AS ENUM ('PHYSICAL', 'OS_EVENT', 'FILE_SYSTEM', 'COMPOSITE');
CREATE TYPE attack_phase AS ENUM (
'PHYSICAL_ACCESS', 'MALWARE_STAGING', 'MALWARE_EXECUTION', 'PERSISTENCE', 'CLEANUP'
);
CREATE TABLE threat_event (
id BIGSERIAL PRIMARY KEY,
device_id BIGINT,
device_agent_id VARCHAR(100),
event_type threat_event_type NOT NULL,
signal_key VARCHAR(100) NOT NULL,
raw_payload JSONB,
matched_ioc_id BIGINT REFERENCES threat_ioc(id),
severity threat_severity NOT NULL,
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
attack_phase attack_phase,
sequence_id UUID,
institution_key VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_threat_event_device_id ON threat_event(device_id);
CREATE INDEX idx_threat_event_sequence ON threat_event(sequence_id);
CREATE INDEX idx_threat_event_detected ON threat_event(detected_at DESC);
CREATE INDEX idx_threat_event_severity ON threat_event(severity);
CREATE INDEX idx_threat_event_agent ON threat_event(device_agent_id);
CREATE TYPE sequence_type AS ENUM ('JACKPOTTING', 'SKIMMING', 'UNKNOWN');
CREATE TYPE auto_response_action AS ENUM ('NONE', 'SHUTDOWN', 'NOTIFICATION', 'LOCKDOWN');
CREATE TABLE precursor_sequence_alert (
id BIGSERIAL PRIMARY KEY,
device_id BIGINT,
device_agent_id VARCHAR(100),
sequence_type sequence_type NOT NULL DEFAULT 'JACKPOTTING',
phases_detected attack_phase[] NOT NULL DEFAULT '{}',
confidence NUMERIC(4,3) NOT NULL DEFAULT 0.0,
triggered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
auto_response_taken auto_response_action NOT NULL DEFAULT 'NONE',
resolved_at TIMESTAMPTZ,
resolved_by VARCHAR(200),
institution_key VARCHAR(100),
sequence_id UUID NOT NULL DEFAULT uuid_generate_v4()
);
CREATE INDEX idx_sequence_alert_device ON precursor_sequence_alert(device_id);
CREATE INDEX idx_sequence_alert_trigger ON precursor_sequence_alert(triggered_at DESC);
CREATE INDEX idx_sequence_alert_open ON precursor_sequence_alert(resolved_at) WHERE resolved_at IS NULL;
CREATE TYPE os_eol_status AS ENUM ('SUPPORTED', 'EOL', 'UNKNOWN');
CREATE TABLE device_security_posture (
device_id BIGINT PRIMARY KEY,
device_agent_id VARCHAR(100),
institution_key VARCHAR(100),
audit_policy_compliant BOOLEAN,
gold_image_validated_at TIMESTAMPTZ,
gold_image_hash VARCHAR(128),
current_image_hash VARCHAR(128),
disk_encryption_enabled BOOLEAN,
os_version VARCHAR(200),
os_eol_status os_eol_status NOT NULL DEFAULT 'UNKNOWN',
software_whitelist_enabled BOOLEAN,
last_posture_check_at TIMESTAMPTZ,
posture_score INTEGER,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
@@ -0,0 +1,52 @@
-- IOC seed data from FBI FLASH Alert FLASH-20260219-001
-- Source: ATM Jackpotting / Ploutus malware campaign
-- Malicious executables (FILENAME type)
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('FILENAME', 'Newage.exe', 'Ploutus jackpotting malware', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Color.exe', 'Ploutus jackpotting malware variant', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Levantaito.exe', 'Ploutus jackpotting malware variant', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'NCRApp.exe', 'Ploutus masquerading as NCR application', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'sdelete.exe', 'Secure delete tool used for log cleanup', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Promo.exe', 'Ploutus jackpotting malware variant', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'WinMonitor.exe', 'Ploutus persistence component', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'WinMonitorCheck.exe', 'Ploutus persistence watchdog', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Anydesk1.exe', 'AnyDesk remote access trojan variant', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- MD5 hashes
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('MD5', '2C2D16658D8DA6B389934273EF8F8E22', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('MD5', '5F177B84F3D92AB5711BE446125FDBE3', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('MD5', '61EECEB5F9186A0BC01DC82798CD6C5F', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('MD5', 'FDA82030AE92313E94B9339EA1FC107C', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('MD5', 'C04A7CB926CCBF829D0A36A91EBF91BD', 'Ploutus malware hash', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- Associated files and scripts
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('FILENAME', 'C.dat', 'Ploutus data/config file', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Restaurar.bat', 'Malware cleanup/restore script', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Restauraropteva.bat', 'Malware cleanup script (Opteva ATMs)', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('FILENAME', 'Logcontrol.txt', 'Attacker log control file', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM'),
('FILENAME', 'Logc.txt', 'Attacker log file', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM'),
('FILENAME', 'Borrar_beta.txt', 'Attacker cleanup script', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM');
-- Suspicious directories
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('DIRECTORY', 'C:\<ATM_Manufacturer>\exe\p\', 'Known Ploutus staging directory', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('DIRECTORY', 'C:\Users\SSAuto1\AppData\Local\P\', 'Known Ploutus staging directory', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- Registry persistence keys
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('REGISTRY_KEY', 'HKLM\Software\Microsoft\Windows\CurrentVersion\Run', 'Common malware persistence location', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM'),
('REGISTRY_KEY', 'HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon', 'Winlogon hijack persistence location', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('REGISTRY_KEY', 'HKLM\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit', 'Userinit hijack persistence location', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- Suspicious service names used as cover
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('SERVICE_NAME', 'ATM Service', 'Generic service name used by Ploutus for persistence', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH'),
('SERVICE_NAME', 'Dispenser Service', 'Generic service name used by Ploutus for persistence', 'FBI_FLASH', 'FLASH-20260219-001', 'HIGH');
-- Unauthorized remote access tools
INSERT INTO threat_ioc (ioc_type, value, description, source, source_ref, confidence) VALUES
('FILENAME', 'TeamViewer.exe', 'Remote access tool — flag if not in authorized software list', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM'),
('FILENAME', 'AnyDesk.exe', 'Remote access tool — flag if not in authorized software list', 'FBI_FLASH', 'FLASH-20260219-001', 'MEDIUM');
Binary file not shown.
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
artifactId=hiveops-aria
groupId=com.hiveops
version=1.0.0
@@ -0,0 +1,37 @@
com/hiveops/aria/dto/ThreatEventIngestionRequest.class
com/hiveops/aria/entity/ThreatIoc$ThreatIocBuilder.class
com/hiveops/aria/entity/PrecursorSequenceAlert$AutoResponseAction.class
com/hiveops/aria/entity/ThreatIoc$IocType.class
com/hiveops/aria/controller/ThreatIocController.class
com/hiveops/aria/kafka/AriaThreatEvent.class
com/hiveops/aria/repository/DeviceSecurityPostureRepository.class
com/hiveops/aria/entity/DeviceSecurityPosture.class
com/hiveops/aria/service/ThreatEventClassifier.class
com/hiveops/aria/entity/ThreatEvent.class
com/hiveops/aria/kafka/AriaThreatEvent$AriaThreatEventBuilder.class
com/hiveops/aria/entity/DeviceSecurityPosture$DeviceSecurityPostureBuilder.class
com/hiveops/aria/controller/ThreatIocController$CreateIocRequest.class
com/hiveops/aria/kafka/AriaKafkaConfig.class
com/hiveops/aria/entity/ThreatIoc.class
com/hiveops/aria/security/AriaPrincipal.class
com/hiveops/aria/entity/PrecursorSequenceAlert$SequenceType.class
com/hiveops/aria/service/ThreatEventClassifier$ClassificationResult.class
com/hiveops/aria/repository/ThreatIocRepository.class
com/hiveops/aria/entity/ThreatIoc$IocSource.class
com/hiveops/aria/entity/ThreatEvent$ThreatEventBuilder.class
com/hiveops/aria/entity/ThreatEvent$AttackPhase.class
com/hiveops/aria/repository/ThreatEventRepository.class
com/hiveops/aria/repository/PrecursorSequenceAlertRepository.class
com/hiveops/aria/AriaApplication.class
com/hiveops/aria/dto/ThreatEventItem.class
com/hiveops/aria/entity/ThreatEvent$EventType.class
com/hiveops/aria/service/ThreatEventIngestionService.class
com/hiveops/aria/entity/PrecursorSequenceAlert$PrecursorSequenceAlertBuilder.class
com/hiveops/aria/controller/ThreatEventIngestionController.class
com/hiveops/aria/security/JwtAuthenticationFilter.class
com/hiveops/aria/entity/ThreatEvent$ThreatSeverity.class
com/hiveops/aria/config/SecurityConfig.class
com/hiveops/aria/entity/DeviceSecurityPosture$OsEolStatus.class
com/hiveops/aria/kafka/AriaThreatProducer.class
com/hiveops/aria/entity/PrecursorSequenceAlert.class
com/hiveops/aria/entity/ThreatIoc$ConfidenceLevel.class
@@ -0,0 +1,21 @@
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/AriaApplication.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/config/SecurityConfig.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/controller/ThreatEventIngestionController.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/controller/ThreatIocController.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/dto/ThreatEventIngestionRequest.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/dto/ThreatEventItem.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/entity/DeviceSecurityPosture.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/entity/PrecursorSequenceAlert.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/entity/ThreatEvent.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/entity/ThreatIoc.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/kafka/AriaKafkaConfig.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/kafka/AriaThreatEvent.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/kafka/AriaThreatProducer.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/repository/DeviceSecurityPostureRepository.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/repository/PrecursorSequenceAlertRepository.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/repository/ThreatEventRepository.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/repository/ThreatIocRepository.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/security/AriaPrincipal.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/security/JwtAuthenticationFilter.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/service/ThreatEventClassifier.java
/source/hiveops-src/hiveops-aria/src/main/java/com/hiveops/aria/service/ThreatEventIngestionService.java