feat(aria): Phase 3 — device security posture tracking
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:
2026-06-15 14:38:11 -04:00
parent aa9f8a10ef
commit 68cee9bd90
10 changed files with 810 additions and 24 deletions
@@ -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());
}
}