feat(aria): Phase 3 — device security posture tracking
CD - Develop / build-and-deploy (push) Failing after 14m13s
CD - Develop / build-and-deploy (push) Failing after 14m13s
Adds per-device security posture reports and compliance dashboard. Agents POST
posture data (disk encryption, audit policy, image hashes, OS version/EOL) to
POST /api/aria/posture/report (service-secret authenticated). DevicePostureService
upserts the record and calculates a 0–100 posture score using a penalty model:
disk encryption off −25, audit policy off −20, OS EOL −20, image hash drift −20,
whitelist off −10, stale check −5/day (capped at −15). REST API adds GET /posture
(filterable by EOL status and score band) and GET /posture/{deviceId}. Stats
endpoint gains lowPostureCount (<50) and eolDeviceCount. Dashboard reorganised
into Threat Activity and Compliance rows. Device Posture SPA view enables the
previously-disabled nav item with score bar, EOL badges, and full detail panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,7 @@ public class SecurityConfig {
|
||||
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
|
||||
.requestMatchers("/error").permitAll()
|
||||
.requestMatchers("/api/aria/ingest/**").permitAll()
|
||||
.requestMatchers("/api/aria/posture/report").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
@@ -62,7 +63,7 @@ public class SecurityConfig {
|
||||
}
|
||||
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
|
||||
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With", "X-Service-Secret"));
|
||||
config.setAllowCredentials(true);
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.hiveops.aria.controller;
|
||||
|
||||
import com.hiveops.aria.dto.PostureReportRequest;
|
||||
import com.hiveops.aria.entity.DeviceSecurityPosture;
|
||||
import com.hiveops.aria.repository.DeviceSecurityPostureRepository;
|
||||
import com.hiveops.aria.service.DevicePostureService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/aria/posture")
|
||||
@RequiredArgsConstructor
|
||||
public class DevicePostureController {
|
||||
|
||||
private final DevicePostureService postureService;
|
||||
private final DeviceSecurityPostureRepository postureRepository;
|
||||
|
||||
@Value("${aria.internal.service-secret:}")
|
||||
private String serviceSecret;
|
||||
|
||||
@PostMapping("/report")
|
||||
public ResponseEntity<DeviceSecurityPosture> report(
|
||||
@RequestBody PostureReportRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
|
||||
String header = httpRequest.getHeader("X-Service-Secret");
|
||||
if (serviceSecret.isBlank() || !serviceSecret.equals(header)) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||
}
|
||||
if (request.getDeviceAgentId() == null || request.getDeviceAgentId().isBlank()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
return ResponseEntity.ok(postureService.report(request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")
|
||||
public Page<DeviceSecurityPosture> getPostures(
|
||||
@RequestParam(required = false) DeviceSecurityPosture.OsEolStatus osEolStatus,
|
||||
@RequestParam(required = false) String scoreFilter,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "25") int size) {
|
||||
|
||||
Pageable pageable = PageRequest.of(page, size);
|
||||
|
||||
if (osEolStatus != null) {
|
||||
return postureRepository.findByOsEolStatusOrderByPostureScoreAsc(osEolStatus, pageable);
|
||||
}
|
||||
if ("low".equals(scoreFilter)) {
|
||||
return postureRepository.findByPostureScoreLessThanOrderByPostureScoreAsc(50, pageable);
|
||||
}
|
||||
if ("medium".equals(scoreFilter)) {
|
||||
return postureRepository.findByPostureScoreLessThanOrderByPostureScoreAsc(80, pageable);
|
||||
}
|
||||
return postureRepository.findAllByOrderByPostureScoreAsc(pageable);
|
||||
}
|
||||
|
||||
@GetMapping("/{deviceId}")
|
||||
@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")
|
||||
public ResponseEntity<DeviceSecurityPosture> getPosture(@PathVariable Long deviceId) {
|
||||
return postureRepository.findById(deviceId)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.hiveops.aria.controller;
|
||||
|
||||
import com.hiveops.aria.entity.DeviceSecurityPosture;
|
||||
import com.hiveops.aria.entity.ThreatEvent;
|
||||
import com.hiveops.aria.repository.DeviceSecurityPostureRepository;
|
||||
import com.hiveops.aria.repository.PrecursorSequenceAlertRepository;
|
||||
import com.hiveops.aria.repository.ThreatEventRepository;
|
||||
import com.hiveops.aria.repository.ThreatIocRepository;
|
||||
@@ -24,6 +26,7 @@ public class ThreatEventController {
|
||||
private final ThreatEventRepository eventRepository;
|
||||
private final ThreatIocRepository iocRepository;
|
||||
private final PrecursorSequenceAlertRepository sequenceAlertRepository;
|
||||
private final DeviceSecurityPostureRepository postureRepository;
|
||||
|
||||
@GetMapping("/events")
|
||||
@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")
|
||||
@@ -60,13 +63,17 @@ public class ThreatEventController {
|
||||
long activeIocs = iocRepository.findByActiveTrue().size();
|
||||
|
||||
long activeSequences = sequenceAlertRepository.countByResolvedAtIsNull();
|
||||
long lowPostureCount = postureRepository.countByPostureScoreLessThan(50);
|
||||
long eolDeviceCount = postureRepository.countByOsEolStatus(DeviceSecurityPosture.OsEolStatus.EOL);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"totalEvents", totalEvents,
|
||||
"criticalCount", critical24h,
|
||||
"highCount", high24h,
|
||||
"activeIocCount", activeIocs,
|
||||
"activeSequenceCount", activeSequences
|
||||
"totalEvents", totalEvents,
|
||||
"criticalCount", critical24h,
|
||||
"highCount", high24h,
|
||||
"activeIocCount", activeIocs,
|
||||
"activeSequenceCount", activeSequences,
|
||||
"lowPostureCount", lowPostureCount,
|
||||
"eolDeviceCount", eolDeviceCount
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.hiveops.aria.dto;
|
||||
|
||||
import com.hiveops.aria.entity.DeviceSecurityPosture.OsEolStatus;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class PostureReportRequest {
|
||||
|
||||
private Long deviceId;
|
||||
private String deviceAgentId;
|
||||
private String institutionKey;
|
||||
|
||||
private Boolean diskEncryptionEnabled;
|
||||
private Boolean auditPolicyCompliant;
|
||||
private Boolean softwareWhitelistEnabled;
|
||||
|
||||
private String goldImageHash;
|
||||
private String currentImageHash;
|
||||
|
||||
private String osVersion;
|
||||
private OsEolStatus osEolStatus;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.hiveops.aria.repository;
|
||||
|
||||
import com.hiveops.aria.entity.DeviceSecurityPosture;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@@ -10,4 +12,16 @@ import java.util.Optional;
|
||||
public interface DeviceSecurityPostureRepository extends JpaRepository<DeviceSecurityPosture, Long> {
|
||||
|
||||
Optional<DeviceSecurityPosture> findByDeviceAgentId(String deviceAgentId);
|
||||
|
||||
Page<DeviceSecurityPosture> findAllByOrderByPostureScoreAsc(Pageable pageable);
|
||||
|
||||
Page<DeviceSecurityPosture> findByOsEolStatusOrderByPostureScoreAsc(
|
||||
DeviceSecurityPosture.OsEolStatus osEolStatus, Pageable pageable);
|
||||
|
||||
Page<DeviceSecurityPosture> findByPostureScoreLessThanOrderByPostureScoreAsc(
|
||||
int score, Pageable pageable);
|
||||
|
||||
long countByPostureScoreLessThan(int score);
|
||||
|
||||
long countByOsEolStatus(DeviceSecurityPosture.OsEolStatus osEolStatus);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.hiveops.aria.service;
|
||||
|
||||
import com.hiveops.aria.dto.PostureReportRequest;
|
||||
import com.hiveops.aria.entity.DeviceSecurityPosture;
|
||||
import com.hiveops.aria.entity.DeviceSecurityPosture.OsEolStatus;
|
||||
import com.hiveops.aria.repository.DeviceSecurityPostureRepository;
|
||||
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.time.temporal.ChronoUnit;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DevicePostureService {
|
||||
|
||||
private final DeviceSecurityPostureRepository postureRepository;
|
||||
|
||||
@Transactional
|
||||
public DeviceSecurityPosture report(PostureReportRequest req) {
|
||||
DeviceSecurityPosture posture = postureRepository
|
||||
.findByDeviceAgentId(req.getDeviceAgentId())
|
||||
.orElseGet(() -> DeviceSecurityPosture.builder()
|
||||
.deviceId(req.getDeviceId())
|
||||
.deviceAgentId(req.getDeviceAgentId())
|
||||
.institutionKey(req.getInstitutionKey())
|
||||
.build());
|
||||
|
||||
if (req.getDeviceId() != null) posture.setDeviceId(req.getDeviceId());
|
||||
if (req.getInstitutionKey() != null) posture.setInstitutionKey(req.getInstitutionKey());
|
||||
if (req.getDiskEncryptionEnabled() != null) posture.setDiskEncryptionEnabled(req.getDiskEncryptionEnabled());
|
||||
if (req.getAuditPolicyCompliant() != null) posture.setAuditPolicyCompliant(req.getAuditPolicyCompliant());
|
||||
if (req.getSoftwareWhitelistEnabled() != null) posture.setSoftwareWhitelistEnabled(req.getSoftwareWhitelistEnabled());
|
||||
if (req.getGoldImageHash() != null) posture.setGoldImageHash(req.getGoldImageHash());
|
||||
if (req.getCurrentImageHash() != null) posture.setCurrentImageHash(req.getCurrentImageHash());
|
||||
if (req.getOsVersion() != null) posture.setOsVersion(req.getOsVersion());
|
||||
if (req.getOsEolStatus() != null) posture.setOsEolStatus(req.getOsEolStatus());
|
||||
|
||||
posture.setLastPostureCheckAt(Instant.now());
|
||||
posture.setPostureScore(calculateScore(posture));
|
||||
posture.setUpdatedAt(Instant.now());
|
||||
|
||||
DeviceSecurityPosture saved = postureRepository.save(posture);
|
||||
|
||||
log.info("Posture report received: device={} score={}",
|
||||
req.getDeviceAgentId(), saved.getPostureScore());
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
private int calculateScore(DeviceSecurityPosture p) {
|
||||
int score = 100;
|
||||
|
||||
if (Boolean.FALSE.equals(p.getDiskEncryptionEnabled())) score -= 25;
|
||||
if (Boolean.FALSE.equals(p.getAuditPolicyCompliant())) score -= 20;
|
||||
if (p.getOsEolStatus() == OsEolStatus.EOL) score -= 20;
|
||||
if (Boolean.FALSE.equals(p.getSoftwareWhitelistEnabled())) score -= 10;
|
||||
|
||||
// Image hash drift — current diverged from gold
|
||||
if (p.getGoldImageHash() != null && p.getCurrentImageHash() != null
|
||||
&& !Objects.equals(p.getGoldImageHash(), p.getCurrentImageHash())) {
|
||||
score -= 20;
|
||||
}
|
||||
|
||||
// Stale posture check — penalise 5 points per day overdue beyond 7 days
|
||||
if (p.getLastPostureCheckAt() != null) {
|
||||
long daysOld = ChronoUnit.DAYS.between(p.getLastPostureCheckAt(), Instant.now());
|
||||
if (daysOld > 7) {
|
||||
score -= (int) Math.min(15, (daysOld - 7) * 5);
|
||||
}
|
||||
}
|
||||
|
||||
return Math.max(0, score);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user