package com.aluer.security; import com.aluer.config.ServerGuardConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; @Service public class FileIntegrityMonitorService { private final ServerGuardConfig config; private final Map baselineHashes = new ConcurrentHashMap<>(); private final ConcurrentLinkedQueue alerts = new ConcurrentLinkedQueue<>(); private volatile long lastScanTime = 0L; public FileIntegrityMonitorService() { this(new ServerGuardConfig()); } @Autowired public FileIntegrityMonitorService(ServerGuardConfig config) { this.config = config; } public synchronized Map scanNow() { if (!config.getSecurity().getAntiIntrusion().getFileIntegrity().isEnabled()) { return Map.of("enabled", false, "trackedFiles", baselineHashes.size(), "alerts", 0); } Map currentHashes = collectCurrentHashes(); int newAlerts = 0; if (baselineHashes.isEmpty()) { baselineHashes.putAll(currentHashes); } else { for (Map.Entry entry : currentHashes.entrySet()) { String oldHash = baselineHashes.get(entry.getKey()); if (oldHash == null) { alerts.offer(new IntegrityAlert("CREATED", entry.getKey(), "New monitored file discovered", Instant.now().toEpochMilli())); newAlerts++; } else if (!oldHash.equals(entry.getValue())) { alerts.offer(new IntegrityAlert("MODIFIED", entry.getKey(), "File hash changed", Instant.now().toEpochMilli())); newAlerts++; } } for (String tracked : new ArrayList<>(baselineHashes.keySet())) { if (!currentHashes.containsKey(tracked)) { alerts.offer(new IntegrityAlert("DELETED", tracked, "Monitored file disappeared", Instant.now().toEpochMilli())); newAlerts++; } } baselineHashes.clear(); baselineHashes.putAll(currentHashes); } trimAlerts(); lastScanTime = System.currentTimeMillis(); Map result = new LinkedHashMap<>(); result.put("enabled", true); result.put("trackedFiles", baselineHashes.size()); result.put("newAlerts", newAlerts); result.put("lastScanTime", lastScanTime); return result; } public synchronized Map rebuildBaseline() { baselineHashes.clear(); Map hashes = collectCurrentHashes(); baselineHashes.putAll(hashes); lastScanTime = System.currentTimeMillis(); Map result = new LinkedHashMap<>(); result.put("baselineFiles", baselineHashes.size()); result.put("lastScanTime", lastScanTime); return result; } public Map getBaselineStatus() { Map result = new LinkedHashMap<>(); result.put("enabled", config.getSecurity().getAntiIntrusion().getFileIntegrity().isEnabled()); result.put("trackedFiles", baselineHashes.size()); result.put("alerts", alerts.size()); result.put("lastScanTime", lastScanTime); result.put("paths", config.getSecurity().getAntiIntrusion().getFileIntegrity().getMonitoredPaths()); return result; } public List getRecentAlerts(int limit) { List result = new ArrayList<>(); int count = 0; for (IntegrityAlert alert : alerts) { if (count++ >= limit) { break; } result.add(alert); } result.sort(Comparator.comparingLong(IntegrityAlert::getTimestamp).reversed()); return result; } private Map collectCurrentHashes() { Map hashes = new HashMap<>(); for (String configuredPath : config.getSecurity().getAntiIntrusion().getFileIntegrity().getMonitoredPaths()) { hashes.putAll(resolvePathHashes(configuredPath)); } return hashes; } private Map resolvePathHashes(String configuredPath) { Map hashes = new HashMap<>(); Path path = Paths.get(configuredPath); try { if (configuredPath.contains("*")) { Path parent = path.getParent(); if (parent != null && Files.isDirectory(parent)) { PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:" + configuredPath); try (var stream = Files.walk(parent, 1)) { stream.filter(Files::isRegularFile) .filter(matcher::matches) .forEach(file -> hashes.put(file.toString(), sha256(file))); } } return hashes; } if (Files.isDirectory(path)) { int maxDepth = config.getSecurity().getAntiIntrusion().getFileIntegrity().getMaxDepth(); try (var stream = Files.walk(path, maxDepth)) { stream.filter(Files::isRegularFile) .forEach(file -> hashes.put(file.toString(), sha256(file))); } } else if (Files.isRegularFile(path)) { hashes.put(path.toString(), sha256(path)); } } catch (IOException ignored) { } return hashes; } private String sha256(Path file) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] bytes = Files.readAllBytes(file); byte[] hash = digest.digest(bytes); StringBuilder hex = new StringBuilder(); for (byte b : hash) { hex.append(String.format("%02x", b)); } return hex.toString(); } catch (IOException | NoSuchAlgorithmException e) { return "unavailable"; } } private void trimAlerts() { while (alerts.size() > 1000) { alerts.poll(); } } public static class IntegrityAlert { private final String type; private final String path; private final String details; private final long timestamp; public IntegrityAlert(String type, String path, String details, long timestamp) { this.type = type; this.path = path; this.details = details; this.timestamp = timestamp; } public String getType() { return type; } public String getPath() { return path; } public String getDetails() { return details; } public long getTimestamp() { return timestamp; } } }