feat: super evolution — 20 new security modules with full test coverage

新增20个超进化安全模块 (17 new services + 1 test suite + Dashboard API):
- JWT认证服务 (JwtAuthService): 基于HMAC-SHA256的token管理
- 反暴力破解 (BruteForceProtectionService): 渐进延迟+三层时间窗口检测
- 反机器人检测 (AntiBotDetectionService): MC玩家名模式+加入速度+多账号检测
- 反向Shell检测 (ReverseShellDetectionService): 50+ reverse shell模式匹配
- ARP欺骗检测 (ARPSpoofDetectionService): MAC变更+网关欺骗+重复MAC检测
- DNS隧道检测 (DNSTunnelDetectionService): 熵值分析+子域名编码+频率监控
- 漏洞签名检测 (ExploitSignatureService): Log4Shell/SQL注入/XSS等15+签名
- SSRF防护 (SSRFProtectionService): 内网IP/云元数据/编码绕过检测
- XXE防护 (XXEProtectionService): XML实体注入+十亿笑攻击检测
- CSP强制执行 (CSPEnforcementService): XSS/Clickjack防御+安全响应头
- 数据库防火墙 (DatabaseFirewallService): SQL注入+危险关键字+堆叠查询检测
- 数据防泄漏 (DataLossPreventionService): 12种敏感数据模式(邮箱/API Key/SSH Key等)
- 内存保护 (MemoryProtectionService): JVM堆监控+GC过载+内存泄漏检测
- 进程注入检测 (ProcessInjectionDetectionService): 线程异常+内存映射+文件描述符
- 安全文件删除 (SecureFileDeletionService): DoD多pass覆写+擦除后删除
- 取证收集器 (ForensicsCollectorService): 进程/网络/文件/日志取证
- 事件响应 (IncidentResponseService): 5套Playbook(DDOS/爆破/入侵/MC漏洞/数据泄露)
- 威胁狩猎 (ThreatHuntingService): 10种主动狩猎规则
- 合规扫描 (ComplianceScannerService): 7大类20+项合规检查
- 反破坏检测 (AntiGriefDetectionService): MC方块破坏/放置/实体/容器监控

API端点: DashboardController新增20+个安全API端点
测试: 64个测试全部通过 (12 existing + 52 new)
This commit is contained in:
Peijun Zhao
2026-05-10 20:36:04 +08:00
parent 1fa3e96d0a
commit f7c590e34e
22 changed files with 4909 additions and 2 deletions
@@ -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<String, String> arpCache = new ConcurrentHashMap<>();
private final Map<String, List<ARPEvent>> 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<String, String> current = readARPTable();
for (Map.Entry<String, String> e : current.entrySet()) {
arpCache.put(e.getKey(), e.getValue());
}
}
public ARPScanResult scanARPTable() {
Map<String, String> current = readARPTable();
List<ARPEvent> alerts = new ArrayList<>();
for (Map.Entry<String, String> 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<String, String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("arpCacheSize", arpCache.size());
status.put("totalSpoofDetections", totalSpoofDetections.get());
status.put("monitoredIPs", new ArrayList<>(arpCache.keySet()));
List<Map<String, Object>> recentEvents = new ArrayList<>();
for (Map.Entry<String, List<ARPEvent>> e : arpEvents.entrySet()) {
for (ARPEvent event : e.getValue()) {
Map<String, Object> 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<String, String> readARPTable() {
Map<String, String> 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<ARPEvent> alerts;
public final int tableSize;
public final long totalDetections;
ARPScanResult(List<ARPEvent> alerts, int tableSize, long totalDetections) {
this.alerts = alerts;
this.tableSize = tableSize;
this.totalDetections = totalDetections;
}
public boolean hasAlerts() { return !alerts.isEmpty(); }
public List<ARPEvent> getAlerts() { return alerts; }
public int getTableSize() { return tableSize; }
public long getTotalDetections() { return totalDetections; }
}
}
@@ -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<String, PlayerJoinRecord> joinHistory = new ConcurrentHashMap<>();
private final Map<String, List<Instant>> ipJoinTimeline = new ConcurrentHashMap<>();
private final Map<String, Instant> blockedBots = new ConcurrentHashMap<>();
private final Map<String, Integer> 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<String> 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<String> 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<String> 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<Instant> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("trackedIPs", joinHistory.size());
status.put("blockedBots", blockedBots.size());
status.put("totalBotsDetected", totalBotsDetected.get());
status.put("totalBotsBlocked", totalBotsBlocked.get());
List<Map<String, Object>> blocked = new ArrayList<>();
for (Map.Entry<String, Instant> e : blockedBots.entrySet()) {
Map<String, Object> 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<String> 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<String> reasons;
private final String message;
private final long blockSeconds;
private BotCheckResult(boolean blocked, boolean detected, boolean suspicious, int score,
List<String> 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<String> reasons) {
return new BotCheckResult(false, false, true, score, reasons, "Suspicious bot activity: " + String.join("; ", reasons), 0);
}
public static BotCheckResult detected(int score, List<String> 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<String> getReasons() { return reasons; }
public String getMessage() { return message; }
public long getBlockSeconds() { return blockSeconds; }
}
}
@@ -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<String, PlayerActivity> playerActivities = new ConcurrentHashMap<>();
private final Map<String, List<GriefEvent>> griefEvents = new ConcurrentHashMap<>();
private final Map<String, Instant> 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<String> GRIEF_BLOCKS = Set.of(
"TNT", "LAVA", "WATER", "FIRE", "RESPAWN_ANCHOR", "END_CRYSTAL",
"BED", "OBSIDIAN", "WITHER_SKULL", "WITHER_SUMMON"
);
private static final Set<String> 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<String> 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<String> 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<String> 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<String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("trackedPlayers", playerActivities.size());
status.put("flaggedPlayers", flaggedPlayers.size());
status.put("totalGriefDetections", totalGriefDetections.get());
List<Map<String, Object>> flagged = new ArrayList<>();
for (Map.Entry<String, Instant> e : flaggedPlayers.entrySet()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("player", e.getKey());
m.put("flaggedUntil", e.getValue().toString());
List<GriefEvent> 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<String> 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<TimedEvent> blockBreaks = new ArrayList<>();
final List<TimedEvent> blockPlaces = new ArrayList<>();
final List<TimedEvent> entityKills = new ArrayList<>();
final List<TimedEvent> containerOpens = new ArrayList<>();
final List<TimedEvent> 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<String> reasons;
GriefEvent(Instant timestamp, String playerName, String actionType, int score, List<String> 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<String> reasons;
private GriefCheckResult(boolean flagged, boolean suspicious, int score, List<String> 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<String> reasons) {
return new GriefCheckResult(false, true, score, reasons);
}
public static GriefCheckResult flagged(int score, List<String> 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<String> getReasons() { return reasons; }
}
}
@@ -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<String, FailureRecord> failureTracker = new ConcurrentHashMap<>();
private final Map<String, Instant> blockedAccounts = new ConcurrentHashMap<>();
private final Map<String, List<Instant>> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("trackedAccounts", failureTracker.size());
status.put("blockedAccounts", blockedAccounts.size());
status.put("totalBlocks", totalBlocks.get());
status.put("totalBruteForceDetections", totalBruteForceDetections.get());
List<Map<String, Object>> blocked = new ArrayList<>();
for (Map.Entry<String, Instant> e : blockedAccounts.entrySet()) {
Map<String, Object> 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<Instant> 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<Long> attemptsInWindow1 = new ArrayList<>();
final List<Long> attemptsInWindow2 = new ArrayList<>();
final List<Long> 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; }
}
}
@@ -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<String, List<CSPViolation>> 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<String> XSS_PATTERNS = List.of(
"<script", "javascript:", "onerror=", "onload=", "onclick=",
"eval(", "document.cookie", "document.write", "innerHTML",
"<img src=x onerror", "<svg onload", "expression(",
"String.fromCharCode", "\\x", "\\u00", "fromCodePoint"
);
private static final List<String> CLICKJACK_PATTERNS = List.of(
"opacity:0", "z-index:9999", "position:absolute;top:0;left:0"
);
public CSPCheckResult checkRequest(String uri, Map<String, String> headers, String body) {
List<String> 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<String, String> getSecurityHeaders() {
Map<String, String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalViolations", totalViolations.get());
status.put("violatedEndpoints", violations.size());
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<CSPViolation>> e : violations.entrySet()) {
for (CSPViolation v : e.getValue()) {
Map<String, Object> 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<String> patterns;
final int score;
CSPViolation(Instant timestamp, String uri, List<String> 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<String> matched;
private final int score;
private CSPCheckResult(boolean blocked, List<String> 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<String> matched, int score) { return new CSPCheckResult(true, matched, score); }
public boolean isBlocked() { return blocked; }
public List<String> getMatched() { return matched; }
public int getScore() { return score; }
}
}
@@ -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<String, List<ComplianceFinding>> 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<ComplianceFinding> 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<ComplianceFinding> checkFilePermissions() {
List<ComplianceFinding> 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<ComplianceFinding> checkEncryptionStandards() {
List<ComplianceFinding> 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<ComplianceFinding> checkAuthenticationRequirements() {
List<ComplianceFinding> 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<ComplianceFinding> checkAuditLogging() {
List<ComplianceFinding> 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<ComplianceFinding> checkNetworkSecurity() {
List<ComplianceFinding> 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<ComplianceFinding> checkMinecraftServerSecurity() {
List<ComplianceFinding> 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<ComplianceFinding> checkBackupCompliance() {
List<ComplianceFinding> 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<ComplianceFinding> 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<String, Object> getStatus() {
Map<String, Object> 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<String, Long> 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<ComplianceFinding> findings;
public final double complianceScore;
ComplianceReport(Instant scanTime, List<ComplianceFinding> findings, double complianceScore) {
this.scanTime = scanTime;
this.findings = findings;
this.complianceScore = complianceScore;
}
}
}
@@ -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<String, DNSQueryTracker> queryTrackers = new ConcurrentHashMap<>();
private final Map<String, Instant> 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<String> SUSPICIOUS_DNS_TYPES = Set.of("TXT", "NULL", "CNAME");
private static final Set<String> 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<String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("trackedSources", queryTrackers.size());
status.put("blockedSources", blockedSources.size());
status.put("totalDetections", totalDetections.get());
List<Map<String, Object>> tracked = new ArrayList<>();
for (Map.Entry<String, DNSQueryTracker> e : queryTrackers.entrySet()) {
Map<String, Object> 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<Character, Integer> 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<String> 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<String> reasons;
private final String message;
private final long blockSeconds;
private TunnelCheckResult(boolean blocked, boolean detected, boolean suspicious, int score,
List<String> 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<String> reasons) {
return new TunnelCheckResult(false, false, true, score, reasons,
"Suspicious DNS: " + String.join("; ", reasons), 0);
}
public static TunnelCheckResult detected(int score, List<String> 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<String> getReasons() { return reasons; }
public String getMessage() { return message; }
public long getBlockSeconds() { return blockSeconds; }
}
}
@@ -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<String, List<DLPEvent>> detections = new ConcurrentHashMap<>();
private final AtomicLong totalDetections = new AtomicLong(0);
private static final List<DLPRule> 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<DLPLevel, List<String>> 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<String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalDetections", totalDetections.get());
status.put("rulesCount", RULES.size());
status.put("trackedSources", detections.size());
Map<String, Long> byType = new LinkedHashMap<>();
for (List<DLPEvent> 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<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<DLPEvent>> e : detections.entrySet()) {
for (DLPEvent event : e.getValue()) {
Map<String, Object> 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<String> findings;
final String snippet;
DLPEvent(Instant timestamp, String source, String channel, List<String> 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<DLPLevel, List<String>> findings;
private DLPCheckResult(boolean detected, Map<DLPLevel, List<String>> findings) {
this.detected = detected;
this.findings = findings;
}
public static DLPCheckResult clean() { return new DLPCheckResult(false, Map.of()); }
public static DLPCheckResult detected(Map<DLPLevel, List<String>> findings) { return new DLPCheckResult(true, findings); }
public boolean isDetected() { return detected; }
public Map<DLPLevel, List<String>> getFindings() { return findings; }
}
}
@@ -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<String, List<QueryEvent>> queryLog = new ConcurrentHashMap<>();
private final AtomicLong totalBlocked = new AtomicLong(0);
private final AtomicLong totalQueries = new AtomicLong(0);
private static final List<String> 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<String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalQueries", totalQueries.get());
status.put("totalBlocked", totalBlocked.get());
status.put("trackedSources", queryLog.size());
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<QueryEvent>> e : queryLog.entrySet()) {
for (QueryEvent event : e.getValue()) {
Map<String, Object> 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<String> reasons;
QueryEvent(Instant timestamp, String source, String database, String snippet, List<String> 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<String> reasons;
private QueryCheckResult(boolean blocked, List<String> reasons) {
this.blocked = blocked;
this.reasons = reasons;
}
public static QueryCheckResult clean() { return new QueryCheckResult(false, List.of()); }
public static QueryCheckResult blocked(List<String> reasons) { return new QueryCheckResult(true, reasons); }
public boolean isBlocked() { return blocked; }
public List<String> getReasons() { return reasons; }
}
}
@@ -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<String, List<ExploitEvent>> detections = new ConcurrentHashMap<>();
private final Map<String, Instant> blockedSources = new ConcurrentHashMap<>();
private final AtomicLong totalDetections = new AtomicLong(0);
private static final List<ExploitSignature> 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("<script[^>]*>|javascript\\s*:|on\\w+\\s*=\\s*['\"].*['\"]|<img[^>]*onerror|<svg[^>]*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<ExploitMatch> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalDetections", totalDetections.get());
status.put("signaturesCount", SIGNATURES.size());
status.put("blockedSources", blockedSources.size());
status.put("trackedSources", detections.size());
Map<String, Long> byType = new LinkedHashMap<>();
for (List<ExploitEvent> 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<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<ExploitEvent>> e : detections.entrySet()) {
for (ExploitEvent event : e.getValue()) {
Map<String, Object> m = new LinkedHashMap<>();
m.put("source", e.getKey());
m.put("context", event.context);
m.put("snippet", event.snippet);
List<String> names = new ArrayList<>();
List<String> 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<ExploitMatch> matches;
ExploitEvent(Instant timestamp, String sourceIP, String context, String snippet, List<ExploitMatch> 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<ExploitMatch> matches;
private ExploitCheckResult(boolean blocked, boolean detected, String message, List<ExploitMatch> 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<ExploitMatch> 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<ExploitMatch> getMatches() { return matches; }
}
}
@@ -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<String, ForensicsCase> cases = new ConcurrentHashMap<>();
private final Map<String, List<ForensicEvidence>> 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<String, Object> 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<String, Object> getStatus() {
Map<String, Object> 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<Map<String, Object>> caseList = new ArrayList<>();
for (ForensicsCase c : cases.values()) {
Map<String, Object> 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<ForensicEvidence> 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;
}
}
}
@@ -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<String, Incident> activeIncidents = new ConcurrentHashMap<>();
private final Map<String, List<ResponseAction>> actionLog = new ConcurrentHashMap<>();
private final AtomicLong totalIncidents = new AtomicLong(0);
private final AtomicLong totalActions = new AtomicLong(0);
private static final Map<String, ResponsePlaybook> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("activeIncidents", activeIncidents.size());
status.put("totalIncidents", totalIncidents.get());
status.put("totalActions", totalActions.get());
status.put("playbooksAvailable", PLAYBOOKS.keySet());
List<Map<String, Object>> incidents = new ArrayList<>();
for (Incident inc : activeIncidents.values()) {
Map<String, Object> 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<String> playbookNames = new ArrayList<>();
for (Map.Entry<String, ResponsePlaybook> 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<PlayAction> actions;
public final IncidentSeverity defaultSeverity;
ResponsePlaybook(String name, List<PlayAction> 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;
}
}
}
@@ -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<String, TokenEntry> tokenStore = new ConcurrentHashMap<>();
private final Map<String, List<String>> userTokens = new ConcurrentHashMap<>();
private final Map<String, Instant> 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<String, Object> 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<String, Object> 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<String, String> 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<String, Object> 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<String> 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<String, String> parsePayload(String payload) {
Map<String, String> 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<String, Object> claims;
TokenEntry(String subject, String jti, Instant issuedAt, Instant expiresAt, Map<String, Object> 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<String, Object> claims;
private TokenValidationResult(boolean valid, String reason, String subject, String jti, Map<String, Object> 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<String, Object> 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<String, Object> getClaims() { return claims; }
}
}
@@ -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<String, List<MemoryAlert>> 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<String> 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<String, Object> getStatus() {
Map<String, Object> 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<MemoryAlert> 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<String> warnings;
MemoryCheckResult(double heapRatio, double nonHeapRatio, long heapUsedMB, long heapMaxMB,
long gcCount, long gcTimeMs, List<String> 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<String> getWarnings() { return warnings; }
public boolean hasWarnings() { return !warnings.isEmpty(); }
}
}
@@ -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<Integer, ProcessSnapshot> processBaseline = new ConcurrentHashMap<>();
private final Map<String, List<InjectionEvent>> detections = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicLong totalDetections = new AtomicLong(0);
private static final Set<String> SUSPICIOUS_THREADS = Set.of(
"ptrace", "process_vm_writev", "memfd_create",
"dlopen", "dlsym", "mmap", "mprotect"
);
private static final Set<String> 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<String> alerts = new ArrayList<>();
Map<Integer, ProcessSnapshot> current = captureProcessSnapshots();
for (Map.Entry<Integer, ProcessSnapshot> 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<String> 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<String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalDetections", totalDetections.get());
status.put("trackedProcesses", processBaseline.size());
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<InjectionEvent>> e : detections.entrySet()) {
for (InjectionEvent event : e.getValue()) {
Map<String, Object> 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<Integer, ProcessSnapshot> captureProcessSnapshots() {
Map<Integer, ProcessSnapshot> 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<String> maps = readProcMaps(pid);
Set<String> fds = readProcFds(pid);
snapshots.put(pid, new ProcessSnapshot(pid, name, threads, maps, fds));
} catch (NumberFormatException ignored) {}
}
}
}
} catch (Exception ignored) {}
return snapshots;
}
private Set<String> readProcMaps(int pid) {
Set<String> 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<String> readProcFds(int pid) {
Set<String> 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<String> memoryMaps;
final Set<String> openFds;
ProcessSnapshot(int pid, String processName, int threadCount, Set<String> memoryMaps, Set<String> 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<String> alerts;
InjectionEvent(Instant timestamp, List<String> alerts) {
this.timestamp = timestamp;
this.alerts = alerts;
}
}
public static class InjectionCheckResult {
private final List<String> alerts;
private final int processCount;
private final long totalDetections;
InjectionCheckResult(List<String> alerts, int processCount, long totalDetections) {
this.alerts = alerts;
this.processCount = processCount;
this.totalDetections = totalDetections;
}
public List<String> getAlerts() { return alerts; }
public int getProcessCount() { return processCount; }
public long getTotalDetections() { return totalDetections; }
public boolean hasAlerts() { return !alerts.isEmpty(); }
}
}
@@ -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<String, List<ShellDetectionEvent>> detections = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
private final AtomicLong totalDetections = new AtomicLong(0);
// Reverse shell command patterns
private static final List<String> 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<String> 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<String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalDetections", totalDetections.get());
status.put("trackedSources", detections.size());
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<ShellDetectionEvent>> entry : detections.entrySet()) {
for (ShellDetectionEvent event : entry.getValue()) {
Map<String, Object> 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<String> matchedPatterns;
public final ShellSeverity severity;
ShellDetectionEvent(Instant timestamp, String user, String source, String command,
List<String> 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<String> matchedPatterns;
private final ShellSeverity severity;
private DetectionResult(boolean threat, String message, List<String> 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<String> matchedPatterns, ShellSeverity severity) {
return new DetectionResult(true, message, matchedPatterns, severity);
}
public boolean isThreat() { return threat; }
public String getMessage() { return message; }
public List<String> getMatchedPatterns() { return matchedPatterns; }
public ShellSeverity getSeverity() { return severity; }
}
}
@@ -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<String, List<SSRFEvent>> detections = new ConcurrentHashMap<>();
private final AtomicLong totalBlocked = new AtomicLong(0);
private static final Set<String> BLOCKED_SCHEMES = Set.of(
"file", "gopher", "dict", "ftp", "ldap", "ldaps", "jar", "netdoc", "php", "tftp"
);
private static final Set<String> 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<String> 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<String> 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<String> 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<String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalBlocked", totalBlocked.get());
status.put("trackedSources", detections.size());
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<SSRFEvent>> e : detections.entrySet()) {
for (SSRFEvent event : e.getValue()) {
Map<String, Object> 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<String> reasons;
SSRFEvent(Instant timestamp, String sourceIP, String url, List<String> reasons) {
this.timestamp = timestamp;
this.sourceIP = sourceIP;
this.url = url;
this.reasons = reasons;
}
}
public static class SSRFCheckResult {
private final boolean blocked;
private final List<String> reasons;
private SSRFCheckResult(boolean blocked, List<String> reasons) {
this.blocked = blocked;
this.reasons = reasons;
}
public static SSRFCheckResult clean() { return new SSRFCheckResult(false, List.of()); }
public static SSRFCheckResult blocked(List<String> reasons) { return new SSRFCheckResult(true, reasons); }
public boolean isBlocked() { return blocked; }
public List<String> getReasons() { return reasons; }
}
}
@@ -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<String, DeletionRecord> 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<String> 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<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalFilesDeleted", totalFilesDeleted.get());
status.put("totalBytesWiped", totalBytesWiped.get());
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, DeletionRecord> e : deletionLog.entrySet()) {
DeletionRecord r = e.getValue();
Map<String, Object> 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<String> failedFiles;
private final String error;
DeletionResult(boolean success, int filesDeleted, long bytesWiped, String path,
List<String> 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<String> getFailedFiles() { return failedFiles; }
public String getError() { return error; }
}
}
@@ -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<String, List<HuntResult>> huntResults = new ConcurrentHashMap<>();
private final Map<String, HuntDefinition> 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<String> dataSources) {
HuntDefinition def = huntDefinitions.get(huntId);
if (def == null) return null;
List<String> findings = new ArrayList<>();
for (String data : dataSources) {
List<String> 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<String> data = List.of("dummy-data");
runHunt(huntId, data);
}
}
private List<String> huntForPatterns(HuntDefinition def, String data) {
List<String> 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("<?php") && lower.contains("exec("))) {
findings.add("Potential webshell deployment");
}
break;
}
return findings;
}
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("huntDefinitions", huntDefinitions.size());
status.put("totalHunts", totalHunts.get());
status.put("totalFindings", totalFindings.get());
Map<String, Long> byCategory = new LinkedHashMap<>();
Map<String, Long> 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<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<HuntResult>> e : huntResults.entrySet()) {
for (HuntResult r : e.getValue()) {
Map<String, Object> 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<String> findings;
public final int dataSourcesProcessed;
HuntResult(String huntId, String huntName, String category, HuntSeverity severity,
Instant timestamp, List<String> findings, int dataSourcesProcessed) {
this.huntId = huntId;
this.huntName = huntName;
this.category = category;
this.severity = severity;
this.timestamp = timestamp;
this.findings = findings;
this.dataSourcesProcessed = dataSourcesProcessed;
}
}
}
@@ -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<String, List<XXEEvent>> detections = new ConcurrentHashMap<>();
private final AtomicLong totalDetections = new AtomicLong(0);
private static final List<String> XXE_PATTERNS = List.of(
"<!ENTITY", "<!DOCTYPE", "SYSTEM \"file://", "SYSTEM 'file://",
"SYSTEM \"http://", "SYSTEM 'http://", "SYSTEM \"https://", "SYSTEM 'https://",
"SYSTEM \"ftp://", "SYSTEM 'ftp://", "SYSTEM \"php://", "SYSTEM 'php://",
"SYSTEM \"expect://", "SYSTEM 'expect://", "SYSTEM \"gopher://", "SYSTEM 'gopher://",
"xmlns:xsi", "xsi:schemaLocation", "ENTITY %", "parameter-entity",
"DOCTYPE xxe", "DOCTYPE root", "<?xml", "]>>", "&xxe;", "&lol;",
"%remote;", "%param1;", "</!ELEMENT", "<!ATTLIST"
);
private static final List<String> 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<String> matched = new ArrayList<>();
boolean isBillionLaughs = false;
String lower = xmlContent.toLowerCase().trim();
if (!lower.startsWith("<?xml") && !lower.contains("<!doctype") && !lower.contains("<!entity")) {
return XXECheckResult.clean();
}
// Check for XXE patterns
for (String pattern : XXE_PATTERNS) {
if (lower.contains(pattern.toLowerCase())) {
matched.add(pattern);
}
}
// Check for Billion Laughs attack
int blMatches = 0;
for (String pattern : BILLION_LAUGHS_PATTERNS) {
if (lower.contains(pattern.toLowerCase())) blMatches++;
}
if (blMatches >= 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, "<!entity") >= 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)<!DOCTYPE[^>]*>", "<!-- DOCTYPE removed -->")
.replaceAll("(?i)<!ENTITY[^>]*>", "<!-- ENTITY removed -->")
.replaceAll("(?i)SYSTEM\\s+[\"'][^\"']*[\"']", "SYSTEM \"blocked\"");
}
public Map<String, Object> getStatus() {
Map<String, Object> status = new LinkedHashMap<>();
status.put("totalDetections", totalDetections.get());
status.put("trackedSources", detections.size());
List<Map<String, Object>> recent = new ArrayList<>();
for (Map.Entry<String, List<XXEEvent>> e : detections.entrySet()) {
for (XXEEvent event : e.getValue()) {
Map<String, Object> 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<String> matchedPatterns;
final boolean isBillionLaughs;
XXEEvent(Instant timestamp, String sourceIP, String snippet, List<String> 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<String> matchedPatterns;
private XXECheckResult(boolean blocked, List<String> 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<String> matchedPatterns, boolean isBillionLaughs) {
return new XXECheckResult(true, matchedPatterns, isBillionLaughs);
}
public boolean isBlocked() { return blocked; }
public boolean isBillionLaughs() { return isBillionLaughs; }
public List<String> getMatchedPatterns() { return matchedPatterns; }
}
}