diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 0578543..8ec7cbc 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -3,6 +3,7 @@ import ThreatEvents from './components/ThreatEvents.svelte'; import IocManagement from './components/IocManagement.svelte'; import PrecursorAlerts from './components/PrecursorAlerts.svelte'; + import DevicePosture from './components/DevicePosture.svelte'; import Toast from './components/common/Toast.svelte'; import { onMount } from 'svelte'; import { authApi } from './lib/api'; @@ -19,7 +20,7 @@ localStorage.setItem('ariaSidebarCollapsed', String(sidebarCollapsed)); } - type View = 'dashboard' | 'events' | 'ioc' | 'sequences'; + type View = 'dashboard' | 'events' | 'ioc' | 'sequences' | 'posture'; let currentView: View = 'dashboard'; let eventsInitialSeverity: ThreatSeverity | '' = ''; @@ -33,6 +34,7 @@ else if (e.detail.view === 'events-high') navigate('events', 'HIGH'); else if (e.detail.view === 'ioc') navigate('ioc'); else if (e.detail.view === 'sequences') navigate('sequences'); + else if (e.detail.view === 'posture') navigate('posture'); } onMount(() => { @@ -94,12 +96,9 @@ - {#if !sidebarCollapsed} - - {/if} - diff --git a/frontend/src/components/Dashboard.svelte b/frontend/src/components/Dashboard.svelte index 63ae8b3..6e54152 100644 --- a/frontend/src/components/Dashboard.svelte +++ b/frontend/src/components/Dashboard.svelte @@ -31,6 +31,7 @@ {#if loading}
Loading…
{:else if stats} +
Threat Activity
πŸ›‘οΈ
@@ -38,33 +39,56 @@
Total Events
- - - +
+ +
Compliance
+
+ - + + + +
+
πŸ“‹
+
{(stats.totalEvents - (stats.criticalCount ?? 0) - (stats.highCount ?? 0)).toLocaleString()}
+
Non-Critical Events
+
Phase Coverage
@@ -106,7 +130,7 @@ .stat-grid { display: grid; - grid-template-columns: repeat(5, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 32px; } @@ -142,6 +166,8 @@ .high-val { color: #d97706; } .ioc-val { color: #2563eb; } .seq-val { color: #7c3aed; } + .posture-val { color: #dc2626; } + .eol-val { color: #d97706; } .section-title { font-size: 0.78rem; diff --git a/frontend/src/components/DevicePosture.svelte b/frontend/src/components/DevicePosture.svelte new file mode 100644 index 0000000..48cff27 --- /dev/null +++ b/frontend/src/components/DevicePosture.svelte @@ -0,0 +1,532 @@ + + +
+
+
+

Device Posture

+

Per-device security compliance β€” disk encryption, image integrity, OS currency

+
+
+ {#if !loading} + {totalElements} device{totalElements !== 1 ? 's' : ''} + {/if} +
+
+ +
+
+
+ + +
+ +
+ + +
+
+ + +
+ + {#if loading} +
Loading…
+ {:else if postures.length === 0} +
+
πŸ”’
+
No posture data yet
+
Devices report posture via POST /api/aria/posture/report using the service secret
+
+ {:else} +
+ + + + + + + + + + + + + + + + + {#each postures as device (device.deviceId)} + + + + + + + + + + + + + {/each} + +
DeviceOS VersionOS StatusDisk Enc.Audit PolicyWhitelistImageScoreLast Check
{device.deviceAgentId ?? device.deviceId}{device.osVersion ?? 'β€”'} + + {eolLabel(device.osEolStatus)} + + {boolCell(device.diskEncryptionEnabled)}{boolCell(device.auditPolicyCompliant)}{boolCell(device.softwareWhitelistEnabled)} + {#if imageMismatch(device)} + Drift + {:else if device.goldImageHash} + Match + {:else} + β€” + {/if} + +
+
+
+
+ + {device.postureScore ?? 'β€”'} + +
+
{formatDate(device.lastPostureCheckAt)} + +
+
+ + {#if totalPages > 1} + + {/if} + {/if} +
+ +{#if panelOpen && panelDevice} + + +
{ panelOpen = false; panelDevice = null; }}>
+
+
+
+

{panelDevice.deviceAgentId ?? `Device ${panelDevice.deviceId}`}

+

Institution: {panelDevice.institutionKey ?? 'β€”'}

+
+ +
+ +
+
+ {panelDevice.postureScore ?? 'β€”'} +
+
Posture Score
+
+
+
+
+ +
+
+ Disk Encryption + + {boolCell(panelDevice.diskEncryptionEnabled)} + {#if panelDevice.diskEncryptionEnabled === false}βˆ’25{/if} + +
+
+ Audit Policy + + {boolCell(panelDevice.auditPolicyCompliant)} + {#if panelDevice.auditPolicyCompliant === false}βˆ’20{/if} + +
+
+ Software Whitelist + + {boolCell(panelDevice.softwareWhitelistEnabled)} + {#if panelDevice.softwareWhitelistEnabled === false}βˆ’10{/if} + +
+
+ OS Version + {panelDevice.osVersion ?? 'β€”'} +
+
+ OS EOL Status + + {eolLabel(panelDevice.osEolStatus)} + {#if panelDevice.osEolStatus === 'EOL'}βˆ’20{/if} + +
+ +
+ +
+ Gold Image Hash + {panelDevice.goldImageHash ? panelDevice.goldImageHash.substring(0, 20) + '…' : 'β€”'} +
+
+ Current Hash + + {panelDevice.currentImageHash ? panelDevice.currentImageHash.substring(0, 20) + '…' : 'β€”'} + {#if imageMismatch(panelDevice)}βˆ’20 drift{/if} + +
+ +
+ +
+ Last Posture Check + {formatDate(panelDevice.lastPostureCheckAt)} +
+
+ Gold Validated At + {formatDate(panelDevice.goldImageValidatedAt)} +
+
+ Record Updated + {formatDate(panelDevice.updatedAt)} +
+
+
+{/if} + + diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 443096f..a3b6ad1 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -61,12 +61,33 @@ export interface ThreatEvent { export type SequenceType = 'JACKPOTTING' | 'SKIMMING' | 'UNKNOWN'; +export type OsEolStatus = 'SUPPORTED' | 'EOL' | 'UNKNOWN'; + export interface AriaStats { totalEvents: number; criticalCount: number; highCount: number; activeIocCount: number; activeSequenceCount: number; + lowPostureCount: number; + eolDeviceCount: number; +} + +export interface DeviceSecurityPosture { + deviceId: number; + deviceAgentId?: string; + institutionKey?: string; + auditPolicyCompliant?: boolean; + goldImageValidatedAt?: string; + goldImageHash?: string; + currentImageHash?: string; + diskEncryptionEnabled?: boolean; + osVersion?: string; + osEolStatus: OsEolStatus; + softwareWhitelistEnabled?: boolean; + lastPostureCheckAt?: string; + postureScore?: number; + updatedAt: string; } export interface PrecursorSequenceAlert { @@ -131,4 +152,10 @@ export const ariaApi = { resolveSequence: (id: number) => api.post(`/api/aria/sequences/${id}/resolve`), + + getPostures: (params?: { osEolStatus?: OsEolStatus; scoreFilter?: string; page?: number; size?: number }) => + api.get>('/api/aria/posture', { params }), + + getPosture: (deviceId: number) => + api.get(`/api/aria/posture/${deviceId}`), }; diff --git a/src/main/java/com/hiveops/aria/config/SecurityConfig.java b/src/main/java/com/hiveops/aria/config/SecurityConfig.java index 62759cc..799e953 100644 --- a/src/main/java/com/hiveops/aria/config/SecurityConfig.java +++ b/src/main/java/com/hiveops/aria/config/SecurityConfig.java @@ -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); diff --git a/src/main/java/com/hiveops/aria/controller/DevicePostureController.java b/src/main/java/com/hiveops/aria/controller/DevicePostureController.java new file mode 100644 index 0000000..9b9e57d --- /dev/null +++ b/src/main/java/com/hiveops/aria/controller/DevicePostureController.java @@ -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 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 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 getPosture(@PathVariable Long deviceId) { + return postureRepository.findById(deviceId) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } +} diff --git a/src/main/java/com/hiveops/aria/controller/ThreatEventController.java b/src/main/java/com/hiveops/aria/controller/ThreatEventController.java index b87d2aa..605d8dd 100644 --- a/src/main/java/com/hiveops/aria/controller/ThreatEventController.java +++ b/src/main/java/com/hiveops/aria/controller/ThreatEventController.java @@ -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 )); } } diff --git a/src/main/java/com/hiveops/aria/dto/PostureReportRequest.java b/src/main/java/com/hiveops/aria/dto/PostureReportRequest.java new file mode 100644 index 0000000..2b0e171 --- /dev/null +++ b/src/main/java/com/hiveops/aria/dto/PostureReportRequest.java @@ -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; +} diff --git a/src/main/java/com/hiveops/aria/repository/DeviceSecurityPostureRepository.java b/src/main/java/com/hiveops/aria/repository/DeviceSecurityPostureRepository.java index cfdde3b..c96dd31 100644 --- a/src/main/java/com/hiveops/aria/repository/DeviceSecurityPostureRepository.java +++ b/src/main/java/com/hiveops/aria/repository/DeviceSecurityPostureRepository.java @@ -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 { Optional findByDeviceAgentId(String deviceAgentId); + + Page findAllByOrderByPostureScoreAsc(Pageable pageable); + + Page findByOsEolStatusOrderByPostureScoreAsc( + DeviceSecurityPosture.OsEolStatus osEolStatus, Pageable pageable); + + Page findByPostureScoreLessThanOrderByPostureScoreAsc( + int score, Pageable pageable); + + long countByPostureScoreLessThan(int score); + + long countByOsEolStatus(DeviceSecurityPosture.OsEolStatus osEolStatus); } diff --git a/src/main/java/com/hiveops/aria/service/DevicePostureService.java b/src/main/java/com/hiveops/aria/service/DevicePostureService.java new file mode 100644 index 0000000..1d54f60 --- /dev/null +++ b/src/main/java/com/hiveops/aria/service/DevicePostureService.java @@ -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); + } +}