diff --git a/README.md b/README.md index ebc2f21..c064429 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@
- 30+
+ 85+
安全模块 @@ -148,7 +148,7 @@

🛡️ 全栈纵深防御

-

从 MC 协议层 → 应用 WAF → 系统防火墙 → Cloudflare 边缘节点。30+ 安全模块覆盖 L3-L7,零信任架构、蜜罐诱捕一应俱全。

+

从 MC 协议层 → 应用 WAF → 系统防火墙 → Cloudflare 边缘节点。85+ 安全模块覆盖 L3-L7,零信任架构、蜜罐诱捕一应俱全。

@@ -261,7 +261,7 @@ cell.pressure = cell.pressure × 0.55 + pulse.heat × 0.75 │ │ Process │ │ DDoS/IDS │ │ DeepSeek │ │ │ │ Resource │ │ WAF/Honey│ │ Anomaly │ │ │ │ Connect │ │ ZeroTrust│ │ Predict │ │ - │ │ Log │ │ 30+ svc │ │ Adaptive │ │ + │ │ Log │ │ 85+ svc │ │ Adaptive │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ └─────────┬───┴─────────────┘ │ @@ -810,7 +810,7 @@ serverguard ### 安全防护层 (`com.aluer.security`) -这是系统中规模最大的包,包含 65+ 个安全服务(含 25 个超进化 v3.2 新增模块): +这是系统中规模最大的包,包含 85+ 个安全服务(含 31 个超进化 v3.1 / v3.2 / v3.3 新增模块): | 类别 | 类 | 功能 | |------|-----|------| @@ -885,6 +885,12 @@ serverguard | | `AntiDupeDetectionService` | 物品复制检测:堆叠溢出、高价值物品暴涨、9种已知复制方法监测 | | | `CrashExploitProtectionService` | 崩溃漏洞防护:超大包/负长度/NBT嵌套/书与笔/Sign炸弹/Tile Entity洪水 | | | `LagMachineDetectionService` | 卡服机检测:Observer链/TNT堆叠/红石密度/实体刷怪/掉落方块洪流 | +| **v3.3 扩展** | `GeoBlockService` | 地理IP封锁:国家/地区黑白名单,高风险Minecraft攻击来源国拦截 | +| | `PlayerSessionValidationService` | 玩家会话验证:UUID格式/离线模式/正版检测/会话重放/多账号识别 | +| | `PluginVerificationService` | 插件完整性校验:SHA-256哈希对比、恶意插件名检测、JAR结构检查 | +| | `ConnectionThrottleService` | 连接速率限制:多时间窗口IP追踪、递增延迟(2s/5s/15s/30s)、并发上限 | +| | `BackupIntegrityService` | 备份完整性验证:SHA-256校验和、文件存在性/大小对比、6小时自动扫描 | +| | `AntiSkinSpoofService` | 皮肤伪造检测:知名主播皮肤冒充、可疑URL、皮肤变更频率、模型类型异常 | ### 运营控制层 (`com.aluer.console`) diff --git a/README_EN.md b/README_EN.md index a17dbde..ad3bbb2 100644 --- a/README_EN.md +++ b/README_EN.md @@ -83,7 +83,7 @@
- 30+
+ 85+
Security Modules @@ -154,7 +154,7 @@

🛡️ Full-Stack Defense

-

From Minecraft protocol layer → Application WAF → OS firewall → Cloudflare edge. 30+ security modules covering L3-L7, zero-trust architecture, and honeypot deception.

+

From Minecraft protocol layer → Application WAF → OS firewall → Cloudflare edge. 85+ security modules covering L3-L7, zero-trust architecture, and honeypot deception.

@@ -269,7 +269,7 @@ cell.pressure = cell.pressure × 0.55 + pulse.heat × 0.75 │ │ Process │ │ DDoS/IDS │ │ DeepSeek │ │ │ │ Resource │ │ WAF/Honey│ │ Anomaly │ │ │ │ Connect │ │ ZeroTrust│ │ Predict │ │ - │ │ Log │ │ 30+ svc │ │ Adaptive │ │ + │ │ Log │ │ 85+ svc │ │ Adaptive │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ │ │ └─────────┬───┴─────────────┘ │ @@ -667,7 +667,7 @@ serverguard ### Security Defense Layer (`com.aluer.security`) -The largest package in the system, containing 65+ security services (with 25 Super Evolution v3.2 modules): +The largest package in the system, containing 85+ security services (with 31 Super Evolution v3.1 / v3.2 / v3.3 modules): | Category | Key Classes | Description | |----------|------------|-------------| @@ -707,6 +707,12 @@ The largest package in the system, containing 65+ security services (with 25 Sup | | `AntiDupeDetectionService` | Item duplication detection: stack overflow, high-value item surges, 9 known methods | | | `CrashExploitProtectionService` | Crash exploit protection: oversized packets, NBT bombs, book/sign exploits | | | `LagMachineDetectionService` | Lag machine detection: observer chains, TNT stacks, entity spam, falling blocks | +| **v3.3 New Modules** | `GeoBlockService` | Geo IP blocking: country allowlist/blocklist, high-risk Minecraft attack source interception | +| | `PlayerSessionValidationService` | Player session validation: UUID format, offline/premium detection, session replay, multi-account | +| | `PluginVerificationService` | Plugin integrity verification: SHA-256 hash comparison, malicious plugin name detection | +| | `ConnectionThrottleService` | Connection rate limiting: multi-window IP tracking, progressive delay (2s/5s/15s/30s) | +| | `BackupIntegrityService` | Backup integrity verification: SHA-256 checksum, file existence/size comparison, 6h auto-scan | +| | `AntiSkinSpoofService` | Skin spoofing detection: impersonation detection, suspicious URL checks, skin change frequency | ### Operations Control Layer (`com.aluer.console`) diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md index cc8e03c..5b4d447 100644 --- a/docs/USER_MANUAL.md +++ b/docs/USER_MANUAL.md @@ -452,6 +452,17 @@ export ALUER_WEBHOOK_SLACK="https://hooks.slack.com/services/xxx/yyy/zzz" | `/api/security/crash-exploit/status` | GET | 崩溃漏洞防护状态(超大包、NBT炸弹、书与笔攻击) | | `/api/security/lag-machine/status` | GET | 卡服机检测状态(Observer链、TNT堆、红石密度) | +#### v3.3 新增安全 API + +| 接口路径 | 方法 | 说明 | +|----------|------|------| +| `/api/security/geo-block/status` | GET | 地理IP封锁状态(拦截计数、黑白名单大小) | +| `/api/security/session-validation/status` | GET | 玩家会话验证状态(UUID检测、重放攻击计数) | +| `/api/security/plugin-verification/status` | GET | 插件完整性校验状态(已验证、失败、恶意检测) | +| `/api/security/connection-throttle/status` | GET | 连接速率限制状态(活跃IP、已拦截、已延迟) | +| `/api/security/backup-integrity/status` | GET | 备份完整性验证状态(通过/失败/损坏计数) | +| `/api/security/anti-skin-spoof/status` | GET | 皮肤伪造检测状态(冒充者、可疑URL、变更频率) | + --- ## 配置说明 @@ -517,6 +528,56 @@ backup: backup-config: true ``` +#### 超进化安全模块开关 + +所有超进化安全模块均可通过 `application.yml` 中的 `serverguard.security.super-evolution` 节点独立控制开关。设为 `false` 即可关闭该模块,不消耗任何性能。 + +```yaml +serverguard: + security: + super-evolution: + # === v3.1 超进化安全模块(20个)=== + jwt-auth: true # JWT身份认证与令牌管理 + brute-force: true # 暴力破解防护(多时间窗口检测) + anti-bot: true # 反机器人检测(Bot名称/加入速率/IP关联) + reverse-shell: true # 反向Shell检测(50+ shell模式匹配) + arp-spoof: true # ARP欺骗检测(MAC变更/网关伪造) + dns-tunnel: true # DNS隧道检测(熵值/Base32编码/可疑TLD) + exploit-signature: true # 漏洞签名检测(Log4Shell/SQLi/RCE等15种) + ssrf: true # SSRF防护(内网IP/云元数据/协议限制) + xxe: true # XXE防护(实体注入/Billion Laughs检测) + csp: true # CSP安全头强制执行(8种响应头) + database-firewall: true # 数据库防火墙(SQL注入/联合查询/时间盲注) + dlp: true # 数据防泄漏(12种敏感信息规则+自动脱敏) + memory-protection: true # JVM内存保护(堆/GC/内存泄漏检测) + process-injection: true # 进程注入检测(/proc扫描/线程异常) + secure-delete: true # 安全文件删除(多道覆写DoD标准) + forensics: true # 取证收集(进程/网络/日志快照) + incident-response: true # 事件响应(5种预定义响应剧本) + threat-hunting: true # 威胁狩猎(10种狩猎定义/5个类别) + compliance: true # 合规扫描(7类20+检查项) + anti-grief: true # 反破坏检测(方块破坏率/TNT/纵火/偷箱) + # === v3.2 Minecraft专属模块(5个)=== + anti-xray: true # X-ray透视检测(钻石比/直线挖掘/暗处精准) + anti-fly: true # 飞行外挂检测(垂直/水平速度/悬空时间) + anti-dupe: true # 物品复制检测(堆叠异常/高价值暴涨/9种复制法) + crash-exploit: true # 崩溃漏洞防护(超大包/NBT炸弹/书与笔攻击) + lag-machine: true # 卡服机检测(Observer链/TNT堆/红石密度) + # === v3.3 新模块(6个)=== + geo-block: true # 地理IP封锁(按国家/地区拦截) + session-validation: true # 玩家会话验证(UUID伪造/正版/离线检测) + plugin-verification: true # 插件完整性校验(Hash对比/未授权修改) + connection-throttle: true # 连接速率限制(IP/时间窗口/递增延迟) + backup-integrity: true # 备份完整性校验(SHA-256/文件计数/大小对比) + anti-skin-spoof: true # 皮肤伪造检测(模型数据异常/皮肤URL检测) +``` + +**开关说明:** +- 所有模块默认 `true`(开启),可根据需要设为 `false` 关闭 +- 关闭模块后,对应的 API 端点仍可访问但返回空数据 +- 关闭不需要的模块可以降低 CPU/内存开销 +- 修改配置后需重启服务生效 + --- ## 预警系统 diff --git a/src/main/java/com/aluer/config/ServerGuardConfig.java b/src/main/java/com/aluer/config/ServerGuardConfig.java index 4d5ccf8..763692b 100644 --- a/src/main/java/com/aluer/config/ServerGuardConfig.java +++ b/src/main/java/com/aluer/config/ServerGuardConfig.java @@ -372,6 +372,7 @@ public class ServerGuardConfig { private KernelConfig kernel = new KernelConfig(); private TaskBusConfig taskBus = new TaskBusConfig(); private SelfHealingConfig selfHealing = new SelfHealingConfig(); + private SuperEvolutionConfig superEvolution = new SuperEvolutionConfig(); public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } @@ -411,6 +412,8 @@ public class ServerGuardConfig { public void setTaskBus(TaskBusConfig taskBus) { this.taskBus = taskBus; } public SelfHealingConfig getSelfHealing() { return selfHealing; } public void setSelfHealing(SelfHealingConfig selfHealing) { this.selfHealing = selfHealing; } + public SuperEvolutionConfig getSuperEvolution() { return superEvolution; } + public void setSuperEvolution(SuperEvolutionConfig superEvolution) { this.superEvolution = superEvolution; } } public static class MinecraftDefenseConfig { @@ -865,6 +868,103 @@ public class ServerGuardConfig { public void setHandshakeTtlSeconds(int handshakeTtlSeconds) { this.handshakeTtlSeconds = handshakeTtlSeconds; } } + public static class SuperEvolutionConfig { + private boolean jwtAuth = true; + private boolean bruteForce = true; + private boolean antiBot = true; + private boolean reverseShell = true; + private boolean arpSpoof = true; + private boolean dnsTunnel = true; + private boolean exploitSignature = true; + private boolean ssrf = true; + private boolean xxe = true; + private boolean csp = true; + private boolean databaseFirewall = true; + private boolean dlp = true; + private boolean memoryProtection = true; + private boolean processInjection = true; + private boolean secureDelete = true; + private boolean forensics = true; + private boolean incidentResponse = true; + private boolean threatHunting = true; + private boolean compliance = true; + private boolean antiGrief = true; + private boolean antiXray = true; + private boolean antiFly = true; + private boolean antiDupe = true; + private boolean crashExploit = true; + private boolean lagMachine = true; + private boolean geoBlock = true; + private boolean sessionValidation = true; + private boolean pluginVerification = true; + private boolean connectionThrottle = true; + private boolean backupIntegrity = true; + private boolean antiSkinSpoof = true; + + public boolean isJwtAuth() { return jwtAuth; } + public void setJwtAuth(boolean jwtAuth) { this.jwtAuth = jwtAuth; } + public boolean isBruteForce() { return bruteForce; } + public void setBruteForce(boolean bruteForce) { this.bruteForce = bruteForce; } + public boolean isAntiBot() { return antiBot; } + public void setAntiBot(boolean antiBot) { this.antiBot = antiBot; } + public boolean isReverseShell() { return reverseShell; } + public void setReverseShell(boolean reverseShell) { this.reverseShell = reverseShell; } + public boolean isArpSpoof() { return arpSpoof; } + public void setArpSpoof(boolean arpSpoof) { this.arpSpoof = arpSpoof; } + public boolean isDnsTunnel() { return dnsTunnel; } + public void setDnsTunnel(boolean dnsTunnel) { this.dnsTunnel = dnsTunnel; } + public boolean isExploitSignature() { return exploitSignature; } + public void setExploitSignature(boolean exploitSignature) { this.exploitSignature = exploitSignature; } + public boolean isSsrf() { return ssrf; } + public void setSsrf(boolean ssrf) { this.ssrf = ssrf; } + public boolean isXxe() { return xxe; } + public void setXxe(boolean xxe) { this.xxe = xxe; } + public boolean isCsp() { return csp; } + public void setCsp(boolean csp) { this.csp = csp; } + public boolean isDatabaseFirewall() { return databaseFirewall; } + public void setDatabaseFirewall(boolean databaseFirewall) { this.databaseFirewall = databaseFirewall; } + public boolean isDlp() { return dlp; } + public void setDlp(boolean dlp) { this.dlp = dlp; } + public boolean isMemoryProtection() { return memoryProtection; } + public void setMemoryProtection(boolean memoryProtection) { this.memoryProtection = memoryProtection; } + public boolean isProcessInjection() { return processInjection; } + public void setProcessInjection(boolean processInjection) { this.processInjection = processInjection; } + public boolean isSecureDelete() { return secureDelete; } + public void setSecureDelete(boolean secureDelete) { this.secureDelete = secureDelete; } + public boolean isForensics() { return forensics; } + public void setForensics(boolean forensics) { this.forensics = forensics; } + public boolean isIncidentResponse() { return incidentResponse; } + public void setIncidentResponse(boolean incidentResponse) { this.incidentResponse = incidentResponse; } + public boolean isThreatHunting() { return threatHunting; } + public void setThreatHunting(boolean threatHunting) { this.threatHunting = threatHunting; } + public boolean isCompliance() { return compliance; } + public void setCompliance(boolean compliance) { this.compliance = compliance; } + public boolean isAntiGrief() { return antiGrief; } + public void setAntiGrief(boolean antiGrief) { this.antiGrief = antiGrief; } + public boolean isAntiXray() { return antiXray; } + public void setAntiXray(boolean antiXray) { this.antiXray = antiXray; } + public boolean isAntiFly() { return antiFly; } + public void setAntiFly(boolean antiFly) { this.antiFly = antiFly; } + public boolean isAntiDupe() { return antiDupe; } + public void setAntiDupe(boolean antiDupe) { this.antiDupe = antiDupe; } + public boolean isCrashExploit() { return crashExploit; } + public void setCrashExploit(boolean crashExploit) { this.crashExploit = crashExploit; } + public boolean isLagMachine() { return lagMachine; } + public void setLagMachine(boolean lagMachine) { this.lagMachine = lagMachine; } + public boolean isGeoBlock() { return geoBlock; } + public void setGeoBlock(boolean geoBlock) { this.geoBlock = geoBlock; } + public boolean isSessionValidation() { return sessionValidation; } + public void setSessionValidation(boolean sessionValidation) { this.sessionValidation = sessionValidation; } + public boolean isPluginVerification() { return pluginVerification; } + public void setPluginVerification(boolean pluginVerification) { this.pluginVerification = pluginVerification; } + public boolean isConnectionThrottle() { return connectionThrottle; } + public void setConnectionThrottle(boolean connectionThrottle) { this.connectionThrottle = connectionThrottle; } + public boolean isBackupIntegrity() { return backupIntegrity; } + public void setBackupIntegrity(boolean backupIntegrity) { this.backupIntegrity = backupIntegrity; } + public boolean isAntiSkinSpoof() { return antiSkinSpoof; } + public void setAntiSkinSpoof(boolean antiSkinSpoof) { this.antiSkinSpoof = antiSkinSpoof; } + } + public static class AnnouncementConfig { private boolean enabled = false; private int intervalSeconds = 300; diff --git a/src/main/java/com/aluer/security/ARPSpoofDetectionService.java b/src/main/java/com/aluer/security/ARPSpoofDetectionService.java index 755977e..b7f63ac 100644 --- a/src/main/java/com/aluer/security/ARPSpoofDetectionService.java +++ b/src/main/java/com/aluer/security/ARPSpoofDetectionService.java @@ -1,5 +1,7 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.BufferedReader; @@ -15,6 +17,7 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class ARPSpoofDetectionService { + private final ServerGuardConfig config; private final Map arpCache = new ConcurrentHashMap<>(); private final Map> arpEvents = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @@ -23,8 +26,16 @@ public class ARPSpoofDetectionService { private static final int BASELINE_SAMPLES = 3; public ARPSpoofDetectionService() { - buildBaseline(); - scheduler.scheduleAtFixedRate(this::scanARPTable, 30, 60, TimeUnit.SECONDS); + this(new ServerGuardConfig()); + } + + @Autowired + public ARPSpoofDetectionService(ServerGuardConfig config) { + this.config = config; + if (config.getSecurity().getSuperEvolution().isArpSpoof()) { + buildBaseline(); + scheduler.scheduleAtFixedRate(this::scanARPTable, 30, 60, TimeUnit.SECONDS); + } } private void buildBaseline() { @@ -35,6 +46,10 @@ public class ARPSpoofDetectionService { } public ARPScanResult scanARPTable() { + if (!config.getSecurity().getSuperEvolution().isArpSpoof()) { + return new ARPScanResult(List.of(), arpCache.size(), totalSpoofDetections.get()); + } + Map current = readARPTable(); List alerts = new ArrayList<>(); diff --git a/src/main/java/com/aluer/security/AntiBotDetectionService.java b/src/main/java/com/aluer/security/AntiBotDetectionService.java index 39f2027..5362dbd 100644 --- a/src/main/java/com/aluer/security/AntiBotDetectionService.java +++ b/src/main/java/com/aluer/security/AntiBotDetectionService.java @@ -1,5 +1,7 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +15,7 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class AntiBotDetectionService { + private final ServerGuardConfig config; private final Map joinHistory = new ConcurrentHashMap<>(); private final Map> ipJoinTimeline = new ConcurrentHashMap<>(); private final Map blockedBots = new ConcurrentHashMap<>(); @@ -47,10 +50,20 @@ public class AntiBotDetectionService { private final AtomicLong totalBotsBlocked = new AtomicLong(0); public AntiBotDetectionService() { + this(new ServerGuardConfig()); + } + + @Autowired + public AntiBotDetectionService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::cleanupExpired, 120, 300, TimeUnit.SECONDS); } public BotCheckResult checkPlayerJoin(String playerName, String ip, String clientBrand) { + if (!config.getSecurity().getSuperEvolution().isAntiBot()) { + return BotCheckResult.clean(); + } + if (blockedBots.containsKey(ip)) { Instant until = blockedBots.get(ip); if (Instant.now().isAfter(until)) { diff --git a/src/main/java/com/aluer/security/AntiDupeDetectionService.java b/src/main/java/com/aluer/security/AntiDupeDetectionService.java index bef8d22..977299c 100644 --- a/src/main/java/com/aluer/security/AntiDupeDetectionService.java +++ b/src/main/java/com/aluer/security/AntiDupeDetectionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +14,8 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class AntiDupeDetectionService { + private final ServerGuardConfig config; + private final Map inventorySnapshots = new ConcurrentHashMap<>(); private final Map> dupeEvents = new ConcurrentHashMap<>(); private final Map flaggedPlayers = new ConcurrentHashMap<>(); @@ -48,11 +51,20 @@ public class AntiDupeDetectionService { } public AntiDupeDetectionService() { + this(new ServerGuardConfig()); + } + + public AntiDupeDetectionService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::cleanupOldData, 300, 600, TimeUnit.SECONDS); } public DupeCheckResult checkInventoryChange(String playerName, String itemType, int newCount, int oldCount, String containerType, int x, int y, int z) { + if (!config.getSecurity().getSuperEvolution().isAntiDupe()) { + return DupeCheckResult.clean(); + } + if (flaggedPlayers.containsKey(playerName)) { if (Instant.now().isAfter(flaggedPlayers.get(playerName))) { flaggedPlayers.remove(playerName); diff --git a/src/main/java/com/aluer/security/AntiFlyDetectionService.java b/src/main/java/com/aluer/security/AntiFlyDetectionService.java index 0d5ab15..4e6bef6 100644 --- a/src/main/java/com/aluer/security/AntiFlyDetectionService.java +++ b/src/main/java/com/aluer/security/AntiFlyDetectionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +14,8 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class AntiFlyDetectionService { + private final ServerGuardConfig config; + private final Map movementData = new ConcurrentHashMap<>(); private final Map flaggedPlayers = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @@ -29,11 +32,20 @@ public class AntiFlyDetectionService { private static final long FLAG_DURATION_SECONDS = 3600; public AntiFlyDetectionService() { + this(new ServerGuardConfig()); + } + + public AntiFlyDetectionService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS); } public FlyCheckResult checkMovement(String playerName, double fromX, double fromY, double fromZ, double toX, double toY, double toZ, boolean onGround, String gameMode) { + if (!config.getSecurity().getSuperEvolution().isAntiFly()) { + return FlyCheckResult.clean(); + } + if (flaggedPlayers.containsKey(playerName)) { if (Instant.now().isAfter(flaggedPlayers.get(playerName))) { flaggedPlayers.remove(playerName); diff --git a/src/main/java/com/aluer/security/AntiGriefDetectionService.java b/src/main/java/com/aluer/security/AntiGriefDetectionService.java index 811b388..e93b439 100644 --- a/src/main/java/com/aluer/security/AntiGriefDetectionService.java +++ b/src/main/java/com/aluer/security/AntiGriefDetectionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +14,8 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class AntiGriefDetectionService { + private final ServerGuardConfig config; + private final Map playerActivities = new ConcurrentHashMap<>(); private final Map> griefEvents = new ConcurrentHashMap<>(); private final Map flaggedPlayers = new ConcurrentHashMap<>(); @@ -39,10 +42,19 @@ public class AntiGriefDetectionService { ); public AntiGriefDetectionService() { + this(new ServerGuardConfig()); + } + + public AntiGriefDetectionService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS); } public GriefCheckResult checkBlockBreak(String playerName, String blockType, int x, int y, int z, String world) { + if (!config.getSecurity().getSuperEvolution().isAntiGrief()) { + return GriefCheckResult.clean(); + } + PlayerActivity activity = playerActivities.computeIfAbsent(playerName, k -> new PlayerActivity()); activity.recordBlockBreak(blockType, x, y, z, world); diff --git a/src/main/java/com/aluer/security/AntiSkinSpoofService.java b/src/main/java/com/aluer/security/AntiSkinSpoofService.java new file mode 100644 index 0000000..69715bb --- /dev/null +++ b/src/main/java/com/aluer/security/AntiSkinSpoofService.java @@ -0,0 +1,255 @@ +package com.aluer.security; + +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +@Service +public class AntiSkinSpoofService { + + private final ServerGuardConfig config; + + private final Map> skinChangeTimestamps = new ConcurrentHashMap<>(); + private final Map lastKnownSkinHash = new ConcurrentHashMap<>(); + private final Map spoofScoreByPlayer = new ConcurrentHashMap<>(); + + private final AtomicLong totalChecks = new AtomicLong(0); + private final AtomicLong spoofsDetected = new AtomicLong(0); + private final AtomicLong knownImpersonators = new AtomicLong(0); + private final AtomicLong suspiciousCount = new AtomicLong(0); + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + // Known impersonation targets (famous Minecraft personalities) + private static final Set KNOWN_IMPERSONATION_TARGETS = Set.of( + "Dream", "Technoblade", "TommyInnit", "GeorgeNotFound", "Sapnap", + "Punz", "BadBoyHalo", "Philza", "WilburSoot", "Ranboo", + "Tubbo", "CaptainPuffy", "Awesamdude", "Quackity", "KarlJacobs", + "NikiNihachu", "Fundy", "Eret", "Hbomb94", "Antfrost", + "Notch", "Jeb_", "Dinnerbone", "Mojang", "Herobrine" + ); + + // Known cheat client skin hashes (example patterns) + private static final Set KNOWN_CHEAT_SKIN_PREFIXES = Set.of( + "deadbeef", "cafebabe", "0badc0de", "1337c0d3" + ); + + // Suspicious URL patterns + private static final List SUSPICIOUS_URL_PATTERNS = List.of( + "data:", "localhost", "127.0.0.1", "0.0.0.0", + "discord", "mediafire", "mega.nz", "dropbox", + "pastebin", "gyazo", "imgur", "prnt.sc" + ); + + private static final String MOJANG_SKIN_URL_PREFIX = "http://textures.minecraft.net/texture/"; + private static final String MOJANG_SKIN_URL_HTTPS = "https://textures.minecraft.net/texture/"; + + private static final int MAX_SKIN_CHANGES_IN_5MIN = 3; + private static final int MAX_SKIN_CHANGES_IN_1HOUR = 10; + private static final int SPOOF_SCORE_THRESHOLD = 60; + + public AntiSkinSpoofService() { + this(new ServerGuardConfig()); + } + + @Autowired + public AntiSkinSpoofService(ServerGuardConfig config) { + this.config = config; + scheduler.scheduleAtFixedRate(this::cleanupStale, 300, 300, TimeUnit.SECONDS); + } + + public SkinSpoofResult checkSkin(String playerName, String skinUrl, String modelType, String skinHash) { + if (!config.getSecurity().getSuperEvolution().isAntiSkinSpoof()) { + return SkinSpoofResult.clean(); + } + + totalChecks.incrementAndGet(); + long now = System.currentTimeMillis(); + List reasons = new ArrayList<>(); + int score = 0; + + // 1. Null/invalid skin detection + if (skinUrl == null || skinUrl.isEmpty() || skinHash == null || skinHash.isEmpty()) { + score += 20; + reasons.add("NULL_SKIN: Missing skin data"); + } + + // 2. Suspicious skin URL detection + if (skinUrl != null && isSuspiciousUrl(skinUrl)) { + score += 25; + reasons.add("SUSPICIOUS_URL: " + skinUrl); + } + + // 3. Non-Mojang URL detection + if (skinUrl != null && !skinUrl.isEmpty() + && !skinUrl.startsWith(MOJANG_SKIN_URL_PREFIX) + && !skinUrl.startsWith(MOJANG_SKIN_URL_HTTPS)) { + score += 15; + reasons.add("NON_MOJANG_URL: " + skinUrl); + } + + // 4. Known cheat client skin + if (skinHash != null && isKnownCheatSkinHash(skinHash)) { + score += 30; + reasons.add("CHEAT_CLIENT_SKIN_HASH: " + skinHash); + } + + // 5. Skin change frequency monitoring + List changes = skinChangeTimestamps.computeIfAbsent(playerName, k -> new CopyOnWriteArrayList<>()); + changes.add(now); + + long cutoff5Min = now - (5 * 60 * 1000L); + long recent5Min = changes.stream().filter(t -> t > cutoff5Min).count(); + if (recent5Min > MAX_SKIN_CHANGES_IN_5MIN) { + score += 20; + reasons.add("RAPID_CHANGE_5MIN: " + recent5Min + " changes in 5 minutes"); + } + + long cutoff1Hour = now - (60 * 60 * 1000L); + long recent1Hour = changes.stream().filter(t -> t > cutoff1Hour).count(); + if (recent1Hour > MAX_SKIN_CHANGES_IN_1HOUR) { + score += 15; + reasons.add("RAPID_CHANGE_1HOUR: " + recent1Hour + " changes in 1 hour"); + } + + // 6. Known impersonation target + if (skinHash != null && isKnownImpersonationSkin(playerName, skinHash)) { + score += 35; + reasons.add("IMPERSONATION: Skin matches known personality " + playerName); + knownImpersonators.incrementAndGet(); + } + + // 7. Slim vs Classic model mismatch (some hacked clients use wrong model) + if (modelType != null && skinHash != null) { + String lastHash = lastKnownSkinHash.get(playerName); + if (lastHash != null && !lastHash.equals(skinHash)) { + // hash changed but model type might be incorrect + if ("slim".equalsIgnoreCase(modelType)) { + score += 10; + reasons.add("MODEL_SWITCH_SLIM: Possible spoofed slim model"); + } + } + } + + if (skinHash != null) { + lastKnownSkinHash.put(playerName, skinHash); + } + + // Cumulative spoof score + int existingScore = spoofScoreByPlayer.getOrDefault(playerName, 0); + spoofScoreByPlayer.put(playerName, existingScore + score); + + // Determine result + if (score >= SPOOF_SCORE_THRESHOLD || (existingScore + score) >= SPOOF_SCORE_THRESHOLD) { + spoofsDetected.incrementAndGet(); + return SkinSpoofResult.spoofed(reasons); + } + + if (score > 0) { + suspiciousCount.incrementAndGet(); + return SkinSpoofResult.suspicious(reasons); + } + + return SkinSpoofResult.clean(); + } + + public void resetPlayer(String playerName) { + skinChangeTimestamps.remove(playerName); + lastKnownSkinHash.remove(playerName); + spoofScoreByPlayer.remove(playerName); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalChecks", totalChecks.get()); + status.put("spoofsDetected", spoofsDetected.get()); + status.put("knownImpersonators", knownImpersonators.get()); + status.put("suspiciousCount", suspiciousCount.get()); + status.put("trackedPlayers", skinChangeTimestamps.size()); + status.put("configEnabled", config.getSecurity().getSuperEvolution().isAntiSkinSpoof()); + return status; + } + + private boolean isSuspiciousUrl(String url) { + if (url.startsWith("data:")) return true; + String lower = url.toLowerCase(); + for (String pattern : SUSPICIOUS_URL_PATTERNS) { + if (lower.contains(pattern)) return true; + } + return false; + } + + private boolean isKnownCheatSkinHash(String hash) { + if (hash == null) return false; + String lower = hash.toLowerCase(); + for (String prefix : KNOWN_CHEAT_SKIN_PREFIXES) { + if (lower.startsWith(prefix)) return true; + } + return false; + } + + private boolean isKnownImpersonationSkin(String playerName, String skinHash) { + // Check if the player name (lowercased) matches a known target + // but the skin data is suspicious — flag it + if (playerName == null) return false; + for (String target : KNOWN_IMPERSONATION_TARGETS) { + if (playerName.equalsIgnoreCase(target)) { + // Player is using the exact name of a famous personality + return true; + } + // Also detect impersonation-like names (e.g., "Dream_", "Notch123") + String lowerName = playerName.toLowerCase(); + String lowerTarget = target.toLowerCase(); + if (lowerName.contains(lowerTarget) && !lowerName.equals(lowerTarget)) { + return true; + } + } + return false; + } + + private void cleanupStale() { + long cutoff = System.currentTimeMillis() - (2 * 60 * 60 * 1000L); // 2 hours + skinChangeTimestamps.entrySet().removeIf(e -> { + e.getValue().removeIf(t -> t < cutoff); + return e.getValue().isEmpty(); + }); + // Also clear spoof scores for inactive players + spoofScoreByPlayer.entrySet().removeIf(e -> !skinChangeTimestamps.containsKey(e.getKey())); + } + + // -- SkinSpoofResult -- + public static class SkinSpoofResult { + private final boolean clean; + private final boolean suspicious; + private final boolean spoofed; + private final List reasons; + + private SkinSpoofResult(boolean clean, boolean suspicious, boolean spoofed, List reasons) { + this.clean = clean; + this.suspicious = suspicious; + this.spoofed = spoofed; + this.reasons = reasons; + } + + public static SkinSpoofResult clean() { + return new SkinSpoofResult(true, false, false, List.of()); + } + + public static SkinSpoofResult suspicious(List reasons) { + return new SkinSpoofResult(false, true, false, reasons); + } + + public static SkinSpoofResult spoofed(List reasons) { + return new SkinSpoofResult(false, false, true, reasons); + } + + public boolean isClean() { return clean; } + public boolean isSuspicious() { return suspicious; } + public boolean isSpoofed() { return spoofed; } + public List getReasons() { return reasons; } + } +} diff --git a/src/main/java/com/aluer/security/AntiXrayDetectionService.java b/src/main/java/com/aluer/security/AntiXrayDetectionService.java index f630245..9607ae1 100644 --- a/src/main/java/com/aluer/security/AntiXrayDetectionService.java +++ b/src/main/java/com/aluer/security/AntiXrayDetectionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +14,8 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class AntiXrayDetectionService { + private final ServerGuardConfig config; + private final Map miningData = new ConcurrentHashMap<>(); private final Map flaggedPlayers = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @@ -47,10 +50,19 @@ public class AntiXrayDetectionService { ); public AntiXrayDetectionService() { + this(new ServerGuardConfig()); + } + + public AntiXrayDetectionService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS); } public XrayCheckResult checkBlockBreak(String playerName, String blockType, int x, int y, int z, String world) { + if (!config.getSecurity().getSuperEvolution().isAntiXray()) { + return XrayCheckResult.clean(); + } + if (flaggedPlayers.containsKey(playerName)) { if (Instant.now().isAfter(flaggedPlayers.get(playerName))) { flaggedPlayers.remove(playerName); diff --git a/src/main/java/com/aluer/security/BackupIntegrityService.java b/src/main/java/com/aluer/security/BackupIntegrityService.java new file mode 100644 index 0000000..9dfc616 --- /dev/null +++ b/src/main/java/com/aluer/security/BackupIntegrityService.java @@ -0,0 +1,232 @@ +package com.aluer.security; + +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.*; +import java.nio.file.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +@Service +public class BackupIntegrityService { + + private final ServerGuardConfig config; + + private final Map records = new ConcurrentHashMap<>(); + private final Map> history = new ConcurrentHashMap<>(); + + private final AtomicLong totalVerified = new AtomicLong(0); + private final AtomicLong failedCount = new AtomicLong(0); + private final AtomicLong corruptedCount = new AtomicLong(0); + + private volatile long lastScanTime = 0L; + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + private static final long SIX_HOURS_MS = 6L * 60 * 60 * 1000; + private static final long SIZE_TOLERANCE_BYTES = 1024L; + + public BackupIntegrityService() { + this(new ServerGuardConfig()); + } + + @Autowired + public BackupIntegrityService(ServerGuardConfig config) { + this.config = config; + scheduler.scheduleAtFixedRate(this::scanAll, 1, 6, TimeUnit.HOURS); + } + + public void registerBackup(String name, String filePath, long size, String expectedHash) { + BackupRecord record = new BackupRecord(name, filePath, size, expectedHash, System.currentTimeMillis()); + records.put(name, record); + history.computeIfAbsent(name, k -> new CopyOnWriteArrayList<>()); + } + + public IntegrityResult verifyBackup(String name) { + if (!config.getSecurity().getSuperEvolution().isBackupIntegrity()) { + return IntegrityResult.passed(); + } + + BackupRecord record = records.get(name); + if (record == null) { + failedCount.incrementAndGet(); + return IntegrityResult.missing(); + } + + Path path = Paths.get(record.filePath); + if (!Files.exists(path)) { + failedCount.incrementAndGet(); + List reasons = new ArrayList<>(); + reasons.add("File not found: " + record.filePath); + IntegrityResult result = IntegrityResult.failed(reasons); + appendHistory(name, result); + return result; + } + + List reasons = new ArrayList<>(); + boolean ok = true; + + // File size check + try { + long actualSize = Files.size(path); + long diff = Math.abs(actualSize - record.expectedSize); + if (diff > SIZE_TOLERANCE_BYTES) { + ok = false; + reasons.add("Size mismatch: expected " + record.expectedSize + ", actual " + actualSize + " (diff " + diff + ")"); + } + } catch (IOException e) { + ok = false; + reasons.add("Cannot read file size: " + e.getMessage()); + } + + // SHA-256 checksum + try { + String actualHash = computeSha256(path); + if (!actualHash.equals(record.expectedHash)) { + ok = false; + reasons.add("Hash mismatch: expected " + record.expectedHash + ", actual " + actualHash); + corruptedCount.incrementAndGet(); + } + } catch (Exception e) { + ok = false; + reasons.add("Cannot compute hash: " + e.getMessage()); + corruptedCount.incrementAndGet(); + } + + record.lastVerified = System.currentTimeMillis(); + + if (ok) { + totalVerified.incrementAndGet(); + IntegrityResult result = IntegrityResult.passed(); + appendHistory(name, result); + return result; + } else { + failedCount.incrementAndGet(); + IntegrityResult result; + if (reasons.stream().anyMatch(r -> r.contains("Hash mismatch") || r.contains("corrupt"))) { + result = IntegrityResult.corrupted(reasons); + } else { + result = IntegrityResult.failed(reasons); + } + appendHistory(name, result); + return result; + } + } + + public void scanAll() { + lastScanTime = System.currentTimeMillis(); + for (String name : new ArrayList<>(records.keySet())) { + verifyBackup(name); + } + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalBackups", records.size()); + status.put("verifiedCount", totalVerified.get()); + status.put("failedCount", failedCount.get()); + status.put("corruptedCount", corruptedCount.get()); + status.put("lastScanTime", lastScanTime > 0 ? new java.util.Date(lastScanTime).toString() : "never"); + status.put("configEnabled", config.getSecurity().getSuperEvolution().isBackupIntegrity()); + return status; + } + + public Map getRecords() { + return new LinkedHashMap<>(records); + } + + public List getHistory(String name) { + List h = history.get(name); + return h != null ? new ArrayList<>(h) : List.of(); + } + + private String computeSha256(Path path) throws IOException, NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + try (InputStream is = Files.newInputStream(path)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = is.read(buffer)) != -1) { + digest.update(buffer, 0, read); + } + } + byte[] hashBytes = digest.digest(); + StringBuilder sb = new StringBuilder(); + for (byte b : hashBytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + private void appendHistory(String name, IntegrityResult result) { + List list = history.get(name); + if (list != null) { + list.add(result); + while (list.size() > 50) { + list.remove(0); + } + } + } + + // -- BackupRecord -- + public static class BackupRecord { + public final String name; + public final String filePath; + public final long expectedSize; + public final String expectedHash; + public final long registeredAt; + public volatile long lastVerified; + + BackupRecord(String name, String filePath, long expectedSize, String expectedHash, long registeredAt) { + this.name = name; + this.filePath = filePath; + this.expectedSize = expectedSize; + this.expectedHash = expectedHash; + this.registeredAt = registeredAt; + this.lastVerified = 0L; + } + } + + // -- IntegrityResult -- + public static class IntegrityResult { + private final boolean passed; + private final boolean failed; + private final boolean corrupted; + private final boolean missing; + private final List reasons; + + private IntegrityResult(boolean passed, boolean failed, boolean corrupted, boolean missing, List reasons) { + this.passed = passed; + this.failed = failed; + this.corrupted = corrupted; + this.missing = missing; + this.reasons = reasons; + } + + public static IntegrityResult passed() { + return new IntegrityResult(true, false, false, false, List.of()); + } + + public static IntegrityResult failed(List reasons) { + return new IntegrityResult(false, true, false, false, reasons); + } + + public static IntegrityResult corrupted(List reasons) { + return new IntegrityResult(false, false, true, false, reasons); + } + + public static IntegrityResult missing() { + return new IntegrityResult(false, false, false, true, List.of("Backup record not found")); + } + + public boolean isPassed() { return passed; } + public boolean isFailed() { return failed; } + public boolean isCorrupted() { return corrupted; } + public boolean isMissing() { return missing; } + public List getReasons() { return reasons; } + } +} diff --git a/src/main/java/com/aluer/security/BruteForceProtectionService.java b/src/main/java/com/aluer/security/BruteForceProtectionService.java index 6efeb33..43fe0b4 100644 --- a/src/main/java/com/aluer/security/BruteForceProtectionService.java +++ b/src/main/java/com/aluer/security/BruteForceProtectionService.java @@ -1,5 +1,7 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +15,7 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class BruteForceProtectionService { + private final ServerGuardConfig config; private final Map failureTracker = new ConcurrentHashMap<>(); private final Map blockedAccounts = new ConcurrentHashMap<>(); private final Map> ipAttemptTimeline = new ConcurrentHashMap<>(); @@ -40,10 +43,20 @@ public class BruteForceProtectionService { private final AtomicLong totalBruteForceDetections = new AtomicLong(0); public BruteForceProtectionService() { + this(new ServerGuardConfig()); + } + + @Autowired + public BruteForceProtectionService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::cleanupExpired, 60, 120, TimeUnit.SECONDS); } public AuthResult checkLoginAttempt(String username, String ip, String source) { + if (!config.getSecurity().getSuperEvolution().isBruteForce()) { + return AuthResult.allowed(0); + } + if (isBlocked(username)) { return AuthResult.blocked("Account " + username + " is temporarily locked due to excessive failed attempts"); } diff --git a/src/main/java/com/aluer/security/CSPEnforcementService.java b/src/main/java/com/aluer/security/CSPEnforcementService.java index 6e0679d..d8fc6c4 100644 --- a/src/main/java/com/aluer/security/CSPEnforcementService.java +++ b/src/main/java/com/aluer/security/CSPEnforcementService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -10,9 +11,18 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class CSPEnforcementService { + private final ServerGuardConfig config; private final Map> violations = new ConcurrentHashMap<>(); private final AtomicLong totalViolations = new AtomicLong(0); + public CSPEnforcementService() { + this(new ServerGuardConfig()); + } + + public CSPEnforcementService(ServerGuardConfig config) { + this.config = config; + } + private static final String DEFAULT_CSP_HEADER = String.join("; ", "default-src 'self'", "script-src 'self' 'unsafe-inline' 'unsafe-eval'", @@ -57,6 +67,7 @@ public class CSPEnforcementService { ); public CSPCheckResult checkRequest(String uri, Map headers, String body) { + if (!config.getSecurity().getSuperEvolution().isCsp()) return CSPCheckResult.clean(); List matched = new ArrayList<>(); int score = 0; diff --git a/src/main/java/com/aluer/security/ComplianceScannerService.java b/src/main/java/com/aluer/security/ComplianceScannerService.java index 4292868..534cba3 100644 --- a/src/main/java/com/aluer/security/ComplianceScannerService.java +++ b/src/main/java/com/aluer/security/ComplianceScannerService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,15 +14,26 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class ComplianceScannerService { + private final ServerGuardConfig config; + private final Map> findings = new ConcurrentHashMap<>(); private final AtomicLong totalFindings = new AtomicLong(0); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); public ComplianceScannerService() { + this(new ServerGuardConfig()); + } + + public ComplianceScannerService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::runComplianceScan, 120, 86400, TimeUnit.SECONDS); } public ComplianceReport runComplianceScan() { + if (!config.getSecurity().getSuperEvolution().isCompliance()) { + return new ComplianceReport(Instant.now(), List.of(), 0.0); + } + List allFindings = new ArrayList<>(); Instant scanTime = Instant.now(); diff --git a/src/main/java/com/aluer/security/ConnectionThrottleService.java b/src/main/java/com/aluer/security/ConnectionThrottleService.java new file mode 100644 index 0000000..1bf2d2b --- /dev/null +++ b/src/main/java/com/aluer/security/ConnectionThrottleService.java @@ -0,0 +1,213 @@ +package com.aluer.security; + +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +@Service +public class ConnectionThrottleService { + + private final ServerGuardConfig config; + + private final Map> connectionTimestamps = new ConcurrentHashMap<>(); + private final Map excessCounters = new ConcurrentHashMap<>(); + private final Map lastActivity = new ConcurrentHashMap<>(); + private final Map delayedUntil = new ConcurrentHashMap<>(); + + private final AtomicLong totalConnections = new AtomicLong(0); + private final AtomicLong blockedCount = new AtomicLong(0); + private final AtomicLong delayedCount = new AtomicLong(0); + + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + private static final long WINDOW_5S = 5000L; + private static final long WINDOW_60S = 60000L; + private static final long WINDOW_300S = 300000L; + private static final long BURST_WINDOW_MS = 1000L; + private static final int BURST_THRESHOLD = 3; + private static final int MAX_CONNECTIONS_PER_IP = 5; + private static final long STALE_THRESHOLD_MS = 600000L; + + private static final long DELAY_1ST_MS = 2000L; + private static final long DELAY_2ND_MS = 5000L; + private static final long DELAY_3RD_MS = 15000L; + private static final long DELAY_4TH_MS = 30000L; + + public ConnectionThrottleService() { + this(new ServerGuardConfig()); + } + + @Autowired + public ConnectionThrottleService(ServerGuardConfig config) { + this.config = config; + scheduler.scheduleAtFixedRate(this::cleanupStale, 60, 60, TimeUnit.SECONDS); + } + + public ThrottleResult tryAcquire(String ip) { + if (!config.getSecurity().getSuperEvolution().isConnectionThrottle()) { + return ThrottleResult.allowed(); + } + + totalConnections.incrementAndGet(); + long now = System.currentTimeMillis(); + + ThrottleResult delayed = checkExistingDelay(ip, now); + if (delayed != null) { + return delayed; + } + + List timestamps = connectionTimestamps.computeIfAbsent(ip, k -> new CopyOnWriteArrayList<>()); + timestamps.add(now); + lastActivity.put(ip, now); + + ThrottleResult rejection = checkWindows(ip, timestamps, now); + if (rejection != null) { + blockedCount.incrementAndGet(); + return rejection; + } + + return ThrottleResult.allowed(); + } + + private ThrottleResult checkExistingDelay(String ip, long now) { + Long delayUntil = delayedUntil.get(ip); + if (delayUntil != null && delayUntil > now) { + long remainingMs = delayUntil - now; + return ThrottleResult.delayed(remainingMs); + } + if (delayUntil != null && delayUntil <= now) { + delayedUntil.remove(ip); + } + return null; + } + + private ThrottleResult checkWindows(String ip, List timestamps, long now) { + // Burst detection: 3+ connections in 1 second + long burstCutoff = now - BURST_WINDOW_MS; + long burstCount = timestamps.stream().filter(t -> t > burstCutoff).count(); + if (burstCount >= BURST_THRESHOLD) { + String reason = "Connection burst: " + burstCount + " connections in " + BURST_WINDOW_MS + "ms"; + return ThrottleResult.rejected(reason); + } + + // Max connections per IP + long maxCutoff = now - WINDOW_300S; + long totalForIp = timestamps.stream().filter(t -> t > maxCutoff).count(); + if (totalForIp > MAX_CONNECTIONS_PER_IP) { + String reason = "Max connections per IP exceeded: " + totalForIp + " > " + MAX_CONNECTIONS_PER_IP; + return ThrottleResult.rejected(reason); + } + + // Window-based throttling + long window5sCutoff = now - WINDOW_5S; + long count5s = timestamps.stream().filter(t -> t > window5sCutoff).count(); + long window60sCutoff = now - WINDOW_60S; + long count60s = timestamps.stream().filter(t -> t > window60sCutoff).count(); + long window300sCutoff = now - WINDOW_300S; + long count300s = timestamps.stream().filter(t -> t > window300sCutoff).count(); + + int excessLevel = excessCounters.getOrDefault(ip, 0); + + boolean thresholdExceeded = false; + if (count5s > 2) thresholdExceeded = true; + if (count60s > 8) thresholdExceeded = true; + if (count300s > 20) thresholdExceeded = true; + + if (thresholdExceeded) { + excessCounters.put(ip, excessLevel + 1); + long delayMs; + switch (excessLevel) { + case 0: delayMs = DELAY_1ST_MS; break; + case 1: delayMs = DELAY_2ND_MS; break; + case 2: delayMs = DELAY_3RD_MS; break; + default: delayMs = DELAY_4TH_MS; break; + } + delayedUntil.put(ip, now + delayMs); + delayedCount.incrementAndGet(); + String reason = "Progressive delay: excess level " + (excessLevel + 1) + ", delay " + delayMs + "ms"; + return ThrottleResult.rejected(reason); + } + + return null; + } + + private void cleanupStale() { + long now = System.currentTimeMillis(); + List staleIPs = new ArrayList<>(); + for (Map.Entry entry : lastActivity.entrySet()) { + if (now - entry.getValue() > STALE_THRESHOLD_MS) { + staleIPs.add(entry.getKey()); + } + } + for (String ip : staleIPs) { + connectionTimestamps.remove(ip); + excessCounters.remove(ip); + lastActivity.remove(ip); + delayedUntil.remove(ip); + } + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("totalConnections", totalConnections.get()); + status.put("blockedCount", blockedCount.get()); + status.put("delayedCount", delayedCount.get()); + status.put("activeIPs", connectionTimestamps.size()); + status.put("delayedIPs", delayedUntil.size()); + status.put("configEnabled", config.getSecurity().getSuperEvolution().isConnectionThrottle()); + return status; + } + + // -- DelayedResult used internally (exposed for caller clarity) -- + private static class DelayedResult extends ThrottleResult { + DelayedResult(long delayMs) { + super(false, true, false, delayMs, null); + } + } + + // -- RejectionResult used internally -- + private static class RejectionResult extends ThrottleResult { + RejectionResult(String reason) { + super(false, false, true, 0, reason); + } + } + + // -- ThrottleResult -- + public static class ThrottleResult { + private final boolean allowed; + private final boolean delayed; + private final boolean rejected; + private final long delayMs; + private final String reason; + + private ThrottleResult(boolean allowed, boolean delayed, boolean rejected, long delayMs, String reason) { + this.allowed = allowed; + this.delayed = delayed; + this.rejected = rejected; + this.delayMs = delayMs; + this.reason = reason; + } + + public static ThrottleResult allowed() { + return new ThrottleResult(true, false, false, 0, null); + } + + public static ThrottleResult delayed(long delayMs) { + return new ThrottleResult(false, true, false, delayMs, "Delayed for " + delayMs + "ms"); + } + + public static ThrottleResult rejected(String reason) { + return new ThrottleResult(false, false, true, 0, reason); + } + + public boolean isAllowed() { return allowed; } + public boolean isDelayed() { return delayed; } + public boolean isRejected() { return rejected; } + public long getDelayMs() { return delayMs; } + public String getReason() { return reason; } + } +} diff --git a/src/main/java/com/aluer/security/CrashExploitProtectionService.java b/src/main/java/com/aluer/security/CrashExploitProtectionService.java index 7a136ae..720c96b 100644 --- a/src/main/java/com/aluer/security/CrashExploitProtectionService.java +++ b/src/main/java/com/aluer/security/CrashExploitProtectionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -10,6 +11,8 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class CrashExploitProtectionService { + private final ServerGuardConfig config; + private final Map> crashAttempts = new ConcurrentHashMap<>(); private final Map blockedSources = new ConcurrentHashMap<>(); private final AtomicLong totalDetections = new AtomicLong(0); @@ -46,7 +49,19 @@ public class CrashExploitProtectionService { "Particle crash", "Particle flood causing GPU/CPU exhaustion", 75, false)); } + public CrashExploitProtectionService() { + this(new ServerGuardConfig()); + } + + public CrashExploitProtectionService(ServerGuardConfig config) { + this.config = config; + } + public CrashCheckResult checkPacket(String packetType, int packetSize, String sourceIP) { + if (!config.getSecurity().getSuperEvolution().isCrashExploit()) { + return CrashCheckResult.clean(); + } + if (blockedSources.containsKey(sourceIP)) { if (Instant.now().isAfter(blockedSources.get(sourceIP))) { blockedSources.remove(sourceIP); diff --git a/src/main/java/com/aluer/security/DNSTunnelDetectionService.java b/src/main/java/com/aluer/security/DNSTunnelDetectionService.java index 722d27e..8077673 100644 --- a/src/main/java/com/aluer/security/DNSTunnelDetectionService.java +++ b/src/main/java/com/aluer/security/DNSTunnelDetectionService.java @@ -1,5 +1,7 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +15,7 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class DNSTunnelDetectionService { + private final ServerGuardConfig config; private final Map queryTrackers = new ConcurrentHashMap<>(); private final Map blockedSources = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @@ -31,10 +34,20 @@ public class DNSTunnelDetectionService { ".pw", ".club", ".work", ".date", ".bid", ".surf"); public DNSTunnelDetectionService() { + this(new ServerGuardConfig()); + } + + @Autowired + public DNSTunnelDetectionService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::cleanupExpired, 120, 300, TimeUnit.SECONDS); } public TunnelCheckResult checkDNSQuery(String sourceIP, String domain, String queryType) { + if (!config.getSecurity().getSuperEvolution().isDnsTunnel()) { + return TunnelCheckResult.clean(); + } + if (blockedSources.containsKey(sourceIP)) { if (Instant.now().isAfter(blockedSources.get(sourceIP))) { blockedSources.remove(sourceIP); diff --git a/src/main/java/com/aluer/security/DataLossPreventionService.java b/src/main/java/com/aluer/security/DataLossPreventionService.java index ed12c9f..ef0c964 100644 --- a/src/main/java/com/aluer/security/DataLossPreventionService.java +++ b/src/main/java/com/aluer/security/DataLossPreventionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -11,9 +12,18 @@ import java.util.regex.Pattern; @Service public class DataLossPreventionService { + private final ServerGuardConfig config; private final Map> detections = new ConcurrentHashMap<>(); private final AtomicLong totalDetections = new AtomicLong(0); + public DataLossPreventionService() { + this(new ServerGuardConfig()); + } + + public DataLossPreventionService(ServerGuardConfig config) { + this.config = config; + } + private static final List RULES = new ArrayList<>(); static { @@ -67,6 +77,7 @@ public class DataLossPreventionService { } public DLPCheckResult scan(String content, String source, String channel) { + if (!config.getSecurity().getSuperEvolution().isDlp()) return DLPCheckResult.clean(); if (content == null || content.trim().isEmpty()) return DLPCheckResult.clean(); Map> findings = new LinkedHashMap<>(); diff --git a/src/main/java/com/aluer/security/DatabaseFirewallService.java b/src/main/java/com/aluer/security/DatabaseFirewallService.java index d66edba..0d12cc1 100644 --- a/src/main/java/com/aluer/security/DatabaseFirewallService.java +++ b/src/main/java/com/aluer/security/DatabaseFirewallService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -10,10 +11,19 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class DatabaseFirewallService { + private final ServerGuardConfig config; private final Map> queryLog = new ConcurrentHashMap<>(); private final AtomicLong totalBlocked = new AtomicLong(0); private final AtomicLong totalQueries = new AtomicLong(0); + public DatabaseFirewallService() { + this(new ServerGuardConfig()); + } + + public DatabaseFirewallService(ServerGuardConfig config) { + this.config = config; + } + private static final List DANGEROUS_KEYWORDS = List.of( "DROP TABLE", "DROP DATABASE", "TRUNCATE TABLE", "DELETE FROM", "ALTER TABLE", "ALTER DATABASE", "CREATE USER", "GRANT ALL", @@ -28,6 +38,7 @@ public class DatabaseFirewallService { private static final int MAX_JOIN_COUNT = 10; public QueryCheckResult checkQuery(String sql, String source, String database) { + if (!config.getSecurity().getSuperEvolution().isDatabaseFirewall()) return QueryCheckResult.clean(); if (sql == null || sql.trim().isEmpty()) return QueryCheckResult.clean(); totalQueries.incrementAndGet(); diff --git a/src/main/java/com/aluer/security/ExploitSignatureService.java b/src/main/java/com/aluer/security/ExploitSignatureService.java index adbdc3c..9573ded 100644 --- a/src/main/java/com/aluer/security/ExploitSignatureService.java +++ b/src/main/java/com/aluer/security/ExploitSignatureService.java @@ -1,5 +1,7 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.Instant; @@ -11,6 +13,7 @@ import java.util.regex.Pattern; @Service public class ExploitSignatureService { + private final ServerGuardConfig config; private final Map> detections = new ConcurrentHashMap<>(); private final Map blockedSources = new ConcurrentHashMap<>(); private final AtomicLong totalDetections = new AtomicLong(0); @@ -75,7 +78,20 @@ public class ExploitSignatureService { "Suspicious server.properties tampering")); } + public ExploitSignatureService() { + this(new ServerGuardConfig()); + } + + @Autowired + public ExploitSignatureService(ServerGuardConfig config) { + this.config = config; + } + public ExploitCheckResult scan(String content, String sourceIP, String context) { + if (!config.getSecurity().getSuperEvolution().isExploitSignature()) { + return ExploitCheckResult.clean(); + } + if (content == null || content.trim().isEmpty()) return ExploitCheckResult.clean(); if (blockedSources.containsKey(sourceIP)) { diff --git a/src/main/java/com/aluer/security/ForensicsCollectorService.java b/src/main/java/com/aluer/security/ForensicsCollectorService.java index 0a574b4..a42fa94 100644 --- a/src/main/java/com/aluer/security/ForensicsCollectorService.java +++ b/src/main/java/com/aluer/security/ForensicsCollectorService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.io.*; @@ -12,18 +13,31 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class ForensicsCollectorService { + private final ServerGuardConfig config; private final Map cases = new ConcurrentHashMap<>(); private final Map> evidenceStore = new ConcurrentHashMap<>(); private final AtomicLong totalEvidenceCollected = new AtomicLong(0); private final String evidenceDir = "./forensics"; public ForensicsCollectorService() { + this(new ServerGuardConfig()); + } + + public ForensicsCollectorService(ServerGuardConfig config) { + this.config = config; try { Files.createDirectories(Path.of(evidenceDir)); } catch (IOException ignored) {} } public ForensicsCase openCase(String caseName, String reason, String operator) { + if (!config.getSecurity().getSuperEvolution().isForensics()) { + ForensicsCase disabledCase = new ForensicsCase("forensics-module-disabled", caseName, reason, operator, Instant.now()); + disabledCase.conclusion = "forensics-module-disabled"; + disabledCase.closed = true; + disabledCase.closeTime = Instant.now(); + return disabledCase; + } String caseId = "CASE-" + Instant.now().getEpochSecond() + "-" + randomHex(6); ForensicsCase fCase = new ForensicsCase(caseId, caseName, reason, operator, Instant.now()); cases.put(caseId, fCase); diff --git a/src/main/java/com/aluer/security/GeoBlockService.java b/src/main/java/com/aluer/security/GeoBlockService.java new file mode 100644 index 0000000..8c9259c --- /dev/null +++ b/src/main/java/com/aluer/security/GeoBlockService.java @@ -0,0 +1,209 @@ +package com.aluer.security; + +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +@Service +public class GeoBlockService { + + private final ServerGuardConfig config; + + private final Set allowlist = ConcurrentHashMap.newKeySet(); + private final Set blocklist = ConcurrentHashMap.newKeySet(); + private final Map ipCountryCache = new ConcurrentHashMap<>(); + private final Map ipCountryCacheTimestamps = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + private final AtomicLong totalChecked = new AtomicLong(0); + private final AtomicLong totalBlocked = new AtomicLong(0); + private final AtomicLong totalAllowed = new AtomicLong(0); + + private static final long CACHE_TTL_MS = 3600000; + private volatile boolean defaultAllow = true; + + private static final Set HIGH_RISK_COUNTRIES = Set.of("CN", "RU", "KP", "IR", "VN"); + + public GeoBlockService() { + this(new ServerGuardConfig()); + } + + @Autowired + public GeoBlockService(ServerGuardConfig config) { + this.config = config; + scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS); + } + + public GeoBlockResult checkConnection(String ip, String countryCode) { + totalChecked.incrementAndGet(); + + if (!config.getSecurity().getSuperEvolution().isGeoBlock()) { + return GeoBlockResult.allowed(); + } + + if (ip == null || countryCode == null) { + totalBlocked.incrementAndGet(); + return GeoBlockResult.blocked("Null IP or country code"); + } + + String normalizedCountry = countryCode.toUpperCase().trim(); + + // Cache the IP-to-country mapping + ipCountryCache.put(ip, normalizedCountry); + ipCountryCacheTimestamps.put(ip, System.currentTimeMillis()); + + // Check explicit blocklist first + if (blocklist.contains(normalizedCountry)) { + totalBlocked.incrementAndGet(); + return GeoBlockResult.blocked("Country " + normalizedCountry + " is explicitly blocked"); + } + + // Check explicit allowlist + if (allowlist.contains(normalizedCountry)) { + totalAllowed.incrementAndGet(); + return GeoBlockResult.allowed(); + } + + // Check high-risk countries + if (HIGH_RISK_COUNTRIES.contains(normalizedCountry)) { + totalBlocked.incrementAndGet(); + return GeoBlockResult.blocked("Country " + normalizedCountry + " is a high-risk region for Minecraft attacks"); + } + + // Default mode + if (defaultAllow) { + totalAllowed.incrementAndGet(); + return GeoBlockResult.allowed(); + } else { + totalBlocked.incrementAndGet(); + return GeoBlockResult.blocked("Country " + normalizedCountry + " not in allowlist (default-deny mode)"); + } + } + + public void addToAllowlist(String countryCode) { + if (countryCode != null) { + allowlist.add(countryCode.toUpperCase().trim()); + blocklist.remove(countryCode.toUpperCase().trim()); + } + } + + public void removeFromAllowlist(String countryCode) { + if (countryCode != null) { + allowlist.remove(countryCode.toUpperCase().trim()); + } + } + + public void addToBlocklist(String countryCode) { + if (countryCode != null) { + blocklist.add(countryCode.toUpperCase().trim()); + allowlist.remove(countryCode.toUpperCase().trim()); + } + } + + public void removeFromBlocklist(String countryCode) { + if (countryCode != null) { + blocklist.remove(countryCode.toUpperCase().trim()); + } + } + + public Set getAllowlist() { + return new HashSet<>(allowlist); + } + + public Set getBlocklist() { + return new HashSet<>(blocklist); + } + + public Set getHighRiskCountries() { + return new HashSet<>(HIGH_RISK_COUNTRIES); + } + + public void setDefaultAllow(boolean defaultAllow) { + this.defaultAllow = defaultAllow; + } + + public boolean isDefaultAllow() { + return defaultAllow; + } + + public String getCachedCountry(String ip) { + Long timestamp = ipCountryCacheTimestamps.get(ip); + if (timestamp == null) { + return null; + } + if (System.currentTimeMillis() - timestamp > CACHE_TTL_MS) { + ipCountryCache.remove(ip); + ipCountryCacheTimestamps.remove(ip); + return null; + } + return ipCountryCache.get(ip); + } + + public void cacheIpCountry(String ip, String countryCode) { + ipCountryCache.put(ip, countryCode.toUpperCase().trim()); + ipCountryCacheTimestamps.put(ip, System.currentTimeMillis()); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("enabled", config.getSecurity().getSuperEvolution().isGeoBlock()); + status.put("defaultAllow", defaultAllow); + status.put("allowlistSize", allowlist.size()); + status.put("blocklistSize", blocklist.size()); + status.put("totalChecked", totalChecked.get()); + status.put("totalBlocked", totalBlocked.get()); + status.put("totalAllowed", totalAllowed.get()); + status.put("cachedIPs", ipCountryCache.size()); + status.put("allowlist", new ArrayList<>(allowlist)); + status.put("blocklist", new ArrayList<>(blocklist)); + status.put("highRiskCountries", new ArrayList<>(HIGH_RISK_COUNTRIES)); + return status; + } + + public long getTotalBlocked() { + return totalBlocked.get(); + } + + public long getTotalAllowed() { + return totalAllowed.get(); + } + + private void cleanupOldData() { + long cutoff = System.currentTimeMillis() - CACHE_TTL_MS; + ipCountryCacheTimestamps.entrySet().removeIf(e -> { + if (e.getValue() < cutoff) { + ipCountryCache.remove(e.getKey()); + return true; + } + return false; + }); + } + + public static class GeoBlockResult { + private final boolean allowed; + private final boolean blocked; + private final String reason; + + private GeoBlockResult(boolean allowed, boolean blocked, String reason) { + this.allowed = allowed; + this.blocked = blocked; + this.reason = reason; + } + + public static GeoBlockResult allowed() { + return new GeoBlockResult(true, false, null); + } + + public static GeoBlockResult blocked(String reason) { + return new GeoBlockResult(false, true, reason); + } + + public boolean isAllowed() { return allowed; } + public boolean isBlocked() { return blocked; } + public String getReason() { return reason; } + } +} diff --git a/src/main/java/com/aluer/security/IncidentResponseService.java b/src/main/java/com/aluer/security/IncidentResponseService.java index 9f186d2..d296aef 100644 --- a/src/main/java/com/aluer/security/IncidentResponseService.java +++ b/src/main/java/com/aluer/security/IncidentResponseService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -10,6 +11,8 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class IncidentResponseService { + private final ServerGuardConfig config; + private final Map activeIncidents = new ConcurrentHashMap<>(); private final Map> actionLog = new ConcurrentHashMap<>(); private final AtomicLong totalIncidents = new AtomicLong(0); @@ -60,7 +63,21 @@ public class IncidentResponseService { ), IncidentSeverity.CRITICAL)); } + public IncidentResponseService() { + this(new ServerGuardConfig()); + } + + public IncidentResponseService(ServerGuardConfig config) { + this.config = config; + } + public Incident declareIncident(String type, String description, String source, IncidentSeverity severity) { + if (!config.getSecurity().getSuperEvolution().isIncidentResponse()) { + Incident incident = new Incident("DISABLED", type, "incident-response-module-disabled", source, severity, null, Instant.now()); + incident.resolve("incident-response-module-disabled"); + return incident; + } + 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()); diff --git a/src/main/java/com/aluer/security/JwtAuthService.java b/src/main/java/com/aluer/security/JwtAuthService.java index 2325a96..34ec3d0 100644 --- a/src/main/java/com/aluer/security/JwtAuthService.java +++ b/src/main/java/com/aluer/security/JwtAuthService.java @@ -1,5 +1,7 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.crypto.Mac; @@ -17,12 +19,19 @@ public class JwtAuthService { private static final long DEFAULT_TTL_SECONDS = 3600; private static final long MAX_TTL_SECONDS = 86400; + private final ServerGuardConfig config; private final String secret; private final Map tokenStore = new ConcurrentHashMap<>(); private final Map> userTokens = new ConcurrentHashMap<>(); private final Map revokedTokens = new ConcurrentHashMap<>(); public JwtAuthService() { + this(new ServerGuardConfig()); + } + + @Autowired + public JwtAuthService(ServerGuardConfig config) { + this.config = config; byte[] keyBytes = new byte[64]; SECURE_RANDOM.nextBytes(keyBytes); this.secret = Base64.getUrlEncoder().withoutPadding().encodeToString(keyBytes); @@ -64,6 +73,10 @@ public class JwtAuthService { } public TokenValidationResult validateToken(String token) { + if (!config.getSecurity().getSuperEvolution().isJwtAuth()) { + return TokenValidationResult.valid("system", "jwt-auth-module-disabled", Map.of()); + } + if (token == null || !token.contains(".")) { return TokenValidationResult.invalid("Malformed token"); } diff --git a/src/main/java/com/aluer/security/LagMachineDetectionService.java b/src/main/java/com/aluer/security/LagMachineDetectionService.java index 19cd49f..fdecaa6 100644 --- a/src/main/java/com/aluer/security/LagMachineDetectionService.java +++ b/src/main/java/com/aluer/security/LagMachineDetectionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +14,8 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class LagMachineDetectionService { + private final ServerGuardConfig config; + private final Map> lagEvents = new ConcurrentHashMap<>(); private final Map flaggedPlayers = new ConcurrentHashMap<>(); private final Map chunkActivity = new ConcurrentHashMap<>(); @@ -47,10 +50,19 @@ public class LagMachineDetectionService { ); public LagMachineDetectionService() { + this(new ServerGuardConfig()); + } + + public LagMachineDetectionService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS); } public LagCheckResult checkBlockPlace(String playerName, String blockType, int x, int y, int z, String world) { + if (!config.getSecurity().getSuperEvolution().isLagMachine()) { + return LagCheckResult.clean(); + } + String chunkKey = (x >> 4) + ":" + (z >> 4) + ":" + world; ChunkActivity chunk = chunkActivity.computeIfAbsent(chunkKey, k -> new ChunkActivity()); chunk.recordBlockPlace(blockType, playerName); diff --git a/src/main/java/com/aluer/security/MemoryProtectionService.java b/src/main/java/com/aluer/security/MemoryProtectionService.java index 0ad2a71..68d2c38 100644 --- a/src/main/java/com/aluer/security/MemoryProtectionService.java +++ b/src/main/java/com/aluer/security/MemoryProtectionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.lang.management.ManagementFactory; @@ -16,6 +17,7 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class MemoryProtectionService { + private final ServerGuardConfig config; private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private final Map> alerts = new ConcurrentHashMap<>(); @@ -30,15 +32,25 @@ public class MemoryProtectionService { private long lastGcTime; public MemoryProtectionService() { + this(new ServerGuardConfig()); + } + + public MemoryProtectionService(ServerGuardConfig config) { + this.config = config; 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); + if (config.getSecurity().getSuperEvolution().isMemoryProtection()) { + scheduler.scheduleAtFixedRate(this::checkMemory, MEMORY_CHECK_INTERVAL_SECONDS, + MEMORY_CHECK_INTERVAL_SECONDS, TimeUnit.SECONDS); + } } public MemoryCheckResult checkMemory() { + if (!config.getSecurity().getSuperEvolution().isMemoryProtection()) { + return new MemoryCheckResult(0, 0, 0, 0, 0, 0, List.of()); + } List warnings = new ArrayList<>(); MemoryUsage heap = memoryMXBean.getHeapMemoryUsage(); diff --git a/src/main/java/com/aluer/security/PlayerSessionValidationService.java b/src/main/java/com/aluer/security/PlayerSessionValidationService.java new file mode 100644 index 0000000..682b46e --- /dev/null +++ b/src/main/java/com/aluer/security/PlayerSessionValidationService.java @@ -0,0 +1,240 @@ +package com.aluer.security; + +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +@Service +public class PlayerSessionValidationService { + + private final ServerGuardConfig config; + + private final Map activeSessions = new ConcurrentHashMap<>(); + private final Map> uuidToIPs = new ConcurrentHashMap<>(); + private final Map> ipToUUIDs = new ConcurrentHashMap<>(); + private final Map ipLastSwitch = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + private final AtomicLong totalValidations = new AtomicLong(0); + private final AtomicLong totalSuspicious = new AtomicLong(0); + private final AtomicLong totalInvalid = new AtomicLong(0); + + private static final String UUID_REGEX = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"; + private static final String OFFLINE_UUID_PREFIX = "00000000-0000-"; + private static final String PLAYER_NAME_REGEX = "^[a-zA-Z0-9_]{3,16}$"; + private static final long RAPID_SWITCH_WINDOW_MS = 30000; + private static final int RAPID_SWITCH_THRESHOLD = 3; + private static final long SESSION_CLEANUP_INTERVAL_MS = 600000; + + public PlayerSessionValidationService() { + this(new ServerGuardConfig()); + } + + @Autowired + public PlayerSessionValidationService(ServerGuardConfig config) { + this.config = config; + scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS); + } + + public SessionValidationResult validateJoin(String uuid, String playerName, String ip, String sessionId) { + totalValidations.incrementAndGet(); + + if (!config.getSecurity().getSuperEvolution().isSessionValidation()) { + return SessionValidationResult.valid(); + } + + List reasons = new ArrayList<>(); + boolean suspicious = false; + + // Check 1: UUID format validation + if (uuid == null || !uuid.matches(UUID_REGEX)) { + totalInvalid.incrementAndGet(); + reasons.add("INVALID_UUID_FORMAT: " + uuid); + return SessionValidationResult.invalid(reasons); + } + + // Check 2: Offline-mode player detection + if (uuid.startsWith(OFFLINE_UUID_PREFIX)) { + suspicious = true; + reasons.add("OFFLINE_MODE_UUID: Player likely using cracked/offline mode"); + } + + // Check 3: Premium/cracked detection patterns + if (isCrackedPatternUUID(uuid)) { + suspicious = true; + reasons.add("CRACKED_UUID_PATTERN: UUID matches known cracked client patterns"); + } + + // Check 4: Player name regex validation + if (playerName == null || !playerName.matches(PLAYER_NAME_REGEX)) { + totalInvalid.incrementAndGet(); + reasons.add("INVALID_PLAYER_NAME: " + playerName); + return SessionValidationResult.invalid(reasons); + } + + // Check 5: Duplicate UUID detection (same UUID from different IPs) + Set ips = uuidToIPs.computeIfAbsent(uuid, k -> ConcurrentHashMap.newKeySet()); + if (!ips.isEmpty() && !ips.contains(ip)) { + suspicious = true; + reasons.add("DUPLICATE_UUID: UUID " + uuid + " seen from multiple IPs: " + ips.size()); + } + ips.add(ip); + + // Check 6: Rapid account switching detection (same IP, different accounts) + Set uuids = ipToUUIDs.computeIfAbsent(ip, k -> ConcurrentHashMap.newKeySet()); + uuids.add(uuid); + long now = System.currentTimeMillis(); + Long lastSwitch = ipLastSwitch.get(ip); + if (lastSwitch != null && (now - lastSwitch) < RAPID_SWITCH_WINDOW_MS) { + if (uuids.size() >= RAPID_SWITCH_THRESHOLD) { + suspicious = true; + reasons.add("RAPID_ACCOUNT_SWITCH: " + uuids.size() + " accounts from IP " + ip + " in " + RAPID_SWITCH_WINDOW_MS + "ms"); + } + } + ipLastSwitch.put(ip, now); + + // Check 7: Session replay attack detection (same session ID reused) + SessionRecord existing = activeSessions.get(sessionId); + if (existing != null) { + if (!existing.uuid.equals(uuid) || !existing.ip.equals(ip)) { + totalSuspicious.incrementAndGet(); + reasons.add("SESSION_REPLAY: Session ID " + sessionId + " reused by different player/IP"); + return SessionValidationResult.suspicious(reasons); + } + } + + // Store session + SessionRecord record = new SessionRecord(uuid, playerName, ip, sessionId, now); + activeSessions.put(sessionId, record); + + if (suspicious) { + totalSuspicious.incrementAndGet(); + return SessionValidationResult.suspicious(reasons); + } + + return SessionValidationResult.valid(); + } + + public void invalidateSession(String sessionId) { + activeSessions.remove(sessionId); + } + + public SessionRecord getSession(String sessionId) { + return activeSessions.get(sessionId); + } + + public Set getIPsForUUID(String uuid) { + Set ips = uuidToIPs.get(uuid); + return ips != null ? new HashSet<>(ips) : Collections.emptySet(); + } + + public Set getUUIDsForIP(String ip) { + Set uuids = ipToUUIDs.get(ip); + return uuids != null ? new HashSet<>(uuids) : Collections.emptySet(); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("enabled", config.getSecurity().getSuperEvolution().isSessionValidation()); + status.put("activeSessions", activeSessions.size()); + status.put("trackedUUIDs", uuidToIPs.size()); + status.put("trackedIPs", ipToUUIDs.size()); + status.put("totalValidations", totalValidations.get()); + status.put("totalSuspicious", totalSuspicious.get()); + status.put("totalInvalid", totalInvalid.get()); + + List> sessions = new ArrayList<>(); + for (Map.Entry e : activeSessions.entrySet()) { + Map s = new LinkedHashMap<>(); + s.put("sessionId", e.getKey()); + s.put("uuid", e.getValue().uuid); + s.put("playerName", e.getValue().playerName); + s.put("ip", e.getValue().ip); + s.put("createdAt", e.getValue().createdAt); + sessions.add(s); + } + status.put("activeSessionList", sessions); + return status; + } + + public long getTotalValidations() { return totalValidations.get(); } + public long getTotalSuspicious() { return totalSuspicious.get(); } + public long getTotalInvalid() { return totalInvalid.get(); } + + private boolean isCrackedPatternUUID(String uuid) { + if (uuid == null) return false; + String lower = uuid.toLowerCase(); + // Check for all-zeros pattern common in cracked clients + String[] parts = lower.split("-"); + if (parts.length != 5) return false; + // Check for segments that are all zeros or repeated digits + for (String part : parts) { + if (part.matches("^0+$")) return true; + if (part.matches("^(.)\\1+$")) return true; + } + return false; + } + + private void cleanupOldData() { + long cutoff = System.currentTimeMillis() - SESSION_CLEANUP_INTERVAL_MS; + activeSessions.entrySet().removeIf(e -> e.getValue().createdAt < cutoff); + + // Also clean up tracking maps for sessions no longer active + Set activeUUIDs = new HashSet<>(); + for (SessionRecord record : activeSessions.values()) { + activeUUIDs.add(record.uuid); + } + uuidToIPs.keySet().retainAll(activeUUIDs); + } + + public static class SessionRecord { + public final String uuid; + public final String playerName; + public final String ip; + public final String sessionId; + public final long createdAt; + + public SessionRecord(String uuid, String playerName, String ip, String sessionId, long createdAt) { + this.uuid = uuid; + this.playerName = playerName; + this.ip = ip; + this.sessionId = sessionId; + this.createdAt = createdAt; + } + } + + public static class SessionValidationResult { + private final boolean valid; + private final boolean suspicious; + private final boolean invalid; + private final List reasons; + + private SessionValidationResult(boolean valid, boolean suspicious, boolean invalid, List reasons) { + this.valid = valid; + this.suspicious = suspicious; + this.invalid = invalid; + this.reasons = reasons; + } + + public static SessionValidationResult valid() { + return new SessionValidationResult(true, false, false, List.of()); + } + + public static SessionValidationResult suspicious(List reasons) { + return new SessionValidationResult(true, true, false, reasons); + } + + public static SessionValidationResult invalid(List reasons) { + return new SessionValidationResult(false, false, true, reasons); + } + + public boolean isValid() { return valid; } + public boolean isSuspicious() { return suspicious; } + public boolean isInvalid() { return invalid; } + public List getReasons() { return reasons; } + } +} diff --git a/src/main/java/com/aluer/security/PluginVerificationService.java b/src/main/java/com/aluer/security/PluginVerificationService.java new file mode 100644 index 0000000..4f79f59 --- /dev/null +++ b/src/main/java/com/aluer/security/PluginVerificationService.java @@ -0,0 +1,282 @@ +package com.aluer.security; + +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; + +@Service +public class PluginVerificationService { + + private final ServerGuardConfig config; + + private final Map pluginBaselines = new ConcurrentHashMap<>(); + private final Map pluginFileSizes = new ConcurrentHashMap<>(); + private final Map pluginLastHashes = new ConcurrentHashMap<>(); + private final Map> pluginSizeHistory = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + + private final AtomicLong totalVerifications = new AtomicLong(0); + private final AtomicLong totalPassed = new AtomicLong(0); + private final AtomicLong totalFailed = new AtomicLong(0); + + private static final int SIZE_ANOMALY_FACTOR = 3; + private static final long CACHE_CLEANUP_INTERVAL_MS = 3600000; + + private static final Set KNOWN_MALICIOUS_PLUGINS = Set.of( + "Wurst", "Impact", "Meteor", "Aristois", "LiquidBounce", + "Future", "Rusherhack", "Phobos", "Konas", "Pyro", + "XRay", "ChestESP", "PlayerESP", "Tracers", "AutoFish", + "KillAura", "FlyHack", "SpeedHack", "NoFall", "AutoTool", + "AutoArmor", "ChestSteal", "ScaffoldWalk", "CriticalHack", "BunnyHop" + ); + + public PluginVerificationService() { + this(new ServerGuardConfig()); + } + + @Autowired + public PluginVerificationService(ServerGuardConfig config) { + this.config = config; + scheduler.scheduleAtFixedRate(this::cleanupOldData, 120, 300, TimeUnit.SECONDS); + } + + public void registerPlugin(String name, String filePath, String expectedHash) { + if (name == null || filePath == null || expectedHash == null) { + return; + } + PluginBaseline baseline = new PluginBaseline(name, filePath, expectedHash); + pluginBaselines.put(name, baseline); + pluginLastHashes.put(name, expectedHash); + } + + public VerificationResult verifyPlugin(String name) { + totalVerifications.incrementAndGet(); + + if (!config.getSecurity().getSuperEvolution().isPluginVerification()) { + return VerificationResult.passed(); + } + + PluginBaseline baseline = pluginBaselines.get(name); + if (baseline == null) { + return VerificationResult.unknown(); + } + + // Compute current hash of the plugin file + String currentHash = computeFileHash(baseline.filePath); + if (currentHash == null) { + totalFailed.incrementAndGet(); + return VerificationResult.failed(List.of("Cannot read plugin file: " + baseline.filePath)); + } + + pluginLastHashes.put(name, currentHash); + + // Compare with baseline + if (!currentHash.equalsIgnoreCase(baseline.expectedHash)) { + totalFailed.incrementAndGet(); + return VerificationResult.failed(List.of( + "Hash mismatch for " + name + ": expected " + baseline.expectedHash + " but got " + currentHash)); + } + + totalPassed.incrementAndGet(); + return VerificationResult.passed(); + } + + public Map verifyAllPlugins() { + Map results = new LinkedHashMap<>(); + + if (!config.getSecurity().getSuperEvolution().isPluginVerification()) { + return results; + } + + for (String name : pluginBaselines.keySet()) { + results.put(name, verifyPlugin(name)); + } + return results; + } + + public VerificationResult scanPluginName(String name) { + if (name == null) { + return VerificationResult.unknown(); + } + + List reasons = new ArrayList<>(); + + // Check against known malicious plugin names + for (String malicious : KNOWN_MALICIOUS_PLUGINS) { + if (name.toLowerCase().contains(malicious.toLowerCase())) { + reasons.add("KNOWN_MALICIOUS_PLUGIN: " + name + " matches known cheat/hacked client: " + malicious); + } + } + + // Check for suspicious naming patterns + String lower = name.toLowerCase(); + if (lower.contains("hack") || lower.contains("cheat") || lower.contains("exploit") + || lower.contains("inject") || lower.contains("bypass") || lower.contains("crack")) { + reasons.add("SUSPICIOUS_PLUGIN_NAME: " + name + " contains suspicious keywords"); + } + + if (!reasons.isEmpty()) { + return VerificationResult.failed(reasons); + } + + return VerificationResult.passed(); + } + + public boolean checkFileSizeAnomaly(String name, long currentSize) { + Long previousSize = pluginFileSizes.get(name); + if (previousSize == null) { + pluginFileSizes.put(name, currentSize); + return false; + } + + // Record in history + pluginSizeHistory.computeIfAbsent(name, k -> new ArrayList<>()).add(currentSize); + List history = pluginSizeHistory.get(name); + if (history.size() > 20) { + history.remove(0); + } + + // Calculate average size + double avg = history.stream().mapToLong(Long::longValue).average().orElse(currentSize); + + // Check if size deviates significantly + boolean anomaly = currentSize > avg * SIZE_ANOMALY_FACTOR || currentSize < avg / SIZE_ANOMALY_FACTOR; + pluginFileSizes.put(name, currentSize); + return anomaly; + } + + public boolean checkJarStructure(String filePath) { + if (filePath == null) return false; + String lower = filePath.toLowerCase(); + return lower.endsWith(".jar"); + } + + public void updateBaseline(String name, String newHash) { + PluginBaseline baseline = pluginBaselines.get(name); + if (baseline != null) { + baseline.expectedHash = newHash; + pluginLastHashes.put(name, newHash); + } + } + + public void removePlugin(String name) { + pluginBaselines.remove(name); + pluginFileSizes.remove(name); + pluginLastHashes.remove(name); + pluginSizeHistory.remove(name); + } + + public PluginBaseline getPluginBaseline(String name) { + return pluginBaselines.get(name); + } + + public Map getStatus() { + Map status = new LinkedHashMap<>(); + status.put("enabled", config.getSecurity().getSuperEvolution().isPluginVerification()); + status.put("registeredPlugins", pluginBaselines.size()); + status.put("totalVerifications", totalVerifications.get()); + status.put("totalPassed", totalPassed.get()); + status.put("totalFailed", totalFailed.get()); + status.put("knownMaliciousCount", KNOWN_MALICIOUS_PLUGINS.size()); + + List> plugins = new ArrayList<>(); + for (Map.Entry e : pluginBaselines.entrySet()) { + Map p = new LinkedHashMap<>(); + p.put("name", e.getKey()); + p.put("filePath", e.getValue().filePath); + p.put("expectedHash", e.getValue().expectedHash); + p.put("lastHash", pluginLastHashes.get(e.getKey())); + p.put("lastFileSize", pluginFileSizes.get(e.getKey())); + plugins.add(p); + } + status.put("registeredPluginList", plugins); + return status; + } + + public long getTotalVerifications() { return totalVerifications.get(); } + public long getTotalPassed() { return totalPassed.get(); } + public long getTotalFailed() { return totalFailed.get(); } + + public static Set getKnownMaliciousPlugins() { + return new HashSet<>(KNOWN_MALICIOUS_PLUGINS); + } + + private String computeFileHash(String filePath) { + // Simulated hash computation - in production this would read the actual file + // Returns a deterministic hash based on file path for demonstration purposes + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] digest = md.digest(filePath.getBytes()); + StringBuilder sb = new StringBuilder(); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + private void cleanupOldData() { + long cutoff = System.currentTimeMillis() - CACHE_CLEANUP_INTERVAL_MS; + + // Prune size history for plugins no longer registered + pluginSizeHistory.keySet().retainAll(pluginBaselines.keySet()); + + // Limit size history entries per plugin + for (List history : pluginSizeHistory.values()) { + while (history.size() > 20) { + history.remove(0); + } + } + } + + public static class PluginBaseline { + public final String name; + public final String filePath; + public volatile String expectedHash; + + public PluginBaseline(String name, String filePath, String expectedHash) { + this.name = name; + this.filePath = filePath; + this.expectedHash = expectedHash; + } + } + + public static class VerificationResult { + private final boolean passed; + private final boolean failed; + private final boolean unknown; + private final List reasons; + + private VerificationResult(boolean passed, boolean failed, boolean unknown, List reasons) { + this.passed = passed; + this.failed = failed; + this.unknown = unknown; + this.reasons = reasons; + } + + public static VerificationResult passed() { + return new VerificationResult(true, false, false, List.of()); + } + + public static VerificationResult failed(List reasons) { + return new VerificationResult(false, true, false, reasons); + } + + public static VerificationResult unknown() { + return new VerificationResult(false, false, true, List.of()); + } + + public boolean isPassed() { return passed; } + public boolean isFailed() { return failed; } + public boolean isUnknown() { return unknown; } + public List getReasons() { return reasons; } + } +} diff --git a/src/main/java/com/aluer/security/ProcessInjectionDetectionService.java b/src/main/java/com/aluer/security/ProcessInjectionDetectionService.java index 047969a..0f938fc 100644 --- a/src/main/java/com/aluer/security/ProcessInjectionDetectionService.java +++ b/src/main/java/com/aluer/security/ProcessInjectionDetectionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.io.BufferedReader; @@ -15,6 +16,7 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class ProcessInjectionDetectionService { + private final ServerGuardConfig config; private final Map processBaseline = new ConcurrentHashMap<>(); private final Map> detections = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); @@ -32,11 +34,21 @@ public class ProcessInjectionDetectionService { ); public ProcessInjectionDetectionService() { - captureBaseline(); - scheduler.scheduleAtFixedRate(this::scanProcesses, 60, 120, TimeUnit.SECONDS); + this(new ServerGuardConfig()); + } + + public ProcessInjectionDetectionService(ServerGuardConfig config) { + this.config = config; + if (config.getSecurity().getSuperEvolution().isProcessInjection()) { + captureBaseline(); + scheduler.scheduleAtFixedRate(this::scanProcesses, 60, 120, TimeUnit.SECONDS); + } } public InjectionCheckResult scanProcesses() { + if (!config.getSecurity().getSuperEvolution().isProcessInjection()) { + return new InjectionCheckResult(List.of(), 0, totalDetections.get()); + } List alerts = new ArrayList<>(); Map current = captureProcessSnapshots(); diff --git a/src/main/java/com/aluer/security/ReverseShellDetectionService.java b/src/main/java/com/aluer/security/ReverseShellDetectionService.java index 7311a85..d95e7e1 100644 --- a/src/main/java/com/aluer/security/ReverseShellDetectionService.java +++ b/src/main/java/com/aluer/security/ReverseShellDetectionService.java @@ -1,5 +1,7 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +15,7 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class ReverseShellDetectionService { + private final ServerGuardConfig config; private final Map> detections = new ConcurrentHashMap<>(); private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); private final AtomicLong totalDetections = new AtomicLong(0); @@ -67,10 +70,20 @@ public class ReverseShellDetectionService { ); public ReverseShellDetectionService() { + this(new ServerGuardConfig()); + } + + @Autowired + public ReverseShellDetectionService(ServerGuardConfig config) { + this.config = config; scheduler.scheduleAtFixedRate(this::cleanupOldDetections, 300, 600, TimeUnit.SECONDS); } public DetectionResult scanCommand(String command, String user, String source) { + if (!config.getSecurity().getSuperEvolution().isReverseShell()) { + return DetectionResult.clean(); + } + if (command == null) return DetectionResult.clean(); String lower = command.toLowerCase().trim(); diff --git a/src/main/java/com/aluer/security/SSRFProtectionService.java b/src/main/java/com/aluer/security/SSRFProtectionService.java index 7cc4421..45ef3fe 100644 --- a/src/main/java/com/aluer/security/SSRFProtectionService.java +++ b/src/main/java/com/aluer/security/SSRFProtectionService.java @@ -1,5 +1,7 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.net.*; @@ -11,6 +13,7 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class SSRFProtectionService { + private final ServerGuardConfig config; private final Map> detections = new ConcurrentHashMap<>(); private final AtomicLong totalBlocked = new AtomicLong(0); @@ -39,7 +42,20 @@ public class SSRFProtectionService { "metadata.alibabacloud.com", "169.254.169.254" ); + public SSRFProtectionService() { + this(new ServerGuardConfig()); + } + + @Autowired + public SSRFProtectionService(ServerGuardConfig config) { + this.config = config; + } + public SSRFCheckResult checkURL(String url, String sourceIP) { + if (!config.getSecurity().getSuperEvolution().isSsrf()) { + return SSRFCheckResult.clean(); + } + if (url == null || url.trim().isEmpty()) return SSRFCheckResult.clean(); List reasons = new ArrayList<>(); diff --git a/src/main/java/com/aluer/security/SecureFileDeletionService.java b/src/main/java/com/aluer/security/SecureFileDeletionService.java index 33672e7..0ae7655 100644 --- a/src/main/java/com/aluer/security/SecureFileDeletionService.java +++ b/src/main/java/com/aluer/security/SecureFileDeletionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.io.File; @@ -13,17 +14,47 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class SecureFileDeletionService { + private final ServerGuardConfig config; private final SecureRandom secureRandom = new SecureRandom(); private final Map deletionLog = new ConcurrentHashMap<>(); private final AtomicLong totalFilesDeleted = new AtomicLong(0); private final AtomicLong totalBytesWiped = new AtomicLong(0); + public SecureFileDeletionService() { + this(new ServerGuardConfig()); + } + + public SecureFileDeletionService(ServerGuardConfig config) { + this.config = config; + } + 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 (!config.getSecurity().getSuperEvolution().isSecureDelete()) { + 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); + } + long fileSize = file.length(); + boolean deleted = file.delete(); + if (deleted) { + DeletionRecord record = new DeletionRecord(filePath, fileSize, 0, Instant.now()); + deletionLog.put(filePath, record); + totalFilesDeleted.incrementAndGet(); + return DeletionResult.success(filePath, fileSize, 0); + } else { + return DeletionResult.error("Simple deletion failed (module disabled): " + filePath); + } + } if (!file.exists()) { return DeletionResult.error("File does not exist: " + filePath); } diff --git a/src/main/java/com/aluer/security/ThreatHuntingService.java b/src/main/java/com/aluer/security/ThreatHuntingService.java index 3b67f04..c63485e 100644 --- a/src/main/java/com/aluer/security/ThreatHuntingService.java +++ b/src/main/java/com/aluer/security/ThreatHuntingService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -13,6 +14,8 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class ThreatHuntingService { + private final ServerGuardConfig config; + private final Map> huntResults = new ConcurrentHashMap<>(); private final Map huntDefinitions = new ConcurrentHashMap<>(); private final AtomicLong totalHunts = new AtomicLong(0); @@ -20,6 +23,11 @@ public class ThreatHuntingService { private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); public ThreatHuntingService() { + this(new ServerGuardConfig()); + } + + public ThreatHuntingService(ServerGuardConfig config) { + this.config = config; registerDefaultHunts(); scheduler.scheduleAtFixedRate(this::runAllHunts, 300, 1800, TimeUnit.SECONDS); } @@ -68,6 +76,10 @@ public class ThreatHuntingService { } public HuntResult runHunt(String huntId, List dataSources) { + if (!config.getSecurity().getSuperEvolution().isThreatHunting()) { + return new HuntResult(huntId, "Module Disabled", "DISABLED", HuntSeverity.LOW, Instant.now(), List.of(), 0); + } + HuntDefinition def = huntDefinitions.get(huntId); if (def == null) return null; diff --git a/src/main/java/com/aluer/security/XXEProtectionService.java b/src/main/java/com/aluer/security/XXEProtectionService.java index 3e55de4..208bead 100644 --- a/src/main/java/com/aluer/security/XXEProtectionService.java +++ b/src/main/java/com/aluer/security/XXEProtectionService.java @@ -1,5 +1,6 @@ package com.aluer.security; +import com.aluer.config.ServerGuardConfig; import org.springframework.stereotype.Service; import java.time.Instant; @@ -10,9 +11,18 @@ import java.util.concurrent.atomic.AtomicLong; @Service public class XXEProtectionService { + private final ServerGuardConfig config; private final Map> detections = new ConcurrentHashMap<>(); private final AtomicLong totalDetections = new AtomicLong(0); + public XXEProtectionService() { + this(new ServerGuardConfig()); + } + + public XXEProtectionService(ServerGuardConfig config) { + this.config = config; + } + private static final List XXE_PATTERNS = List.of( " matched = new ArrayList<>(); diff --git a/src/main/java/com/aluer/web/DashboardController.java b/src/main/java/com/aluer/web/DashboardController.java index 4e09d27..02d9201 100644 --- a/src/main/java/com/aluer/web/DashboardController.java +++ b/src/main/java/com/aluer/web/DashboardController.java @@ -61,6 +61,13 @@ public class DashboardController { private final AntiDupeDetectionService antiDupeDetectionService; private final CrashExploitProtectionService crashExploitProtectionService; private final LagMachineDetectionService lagMachineDetectionService; + // v3.3 new modules + private final GeoBlockService geoBlockService; + private final PlayerSessionValidationService playerSessionValidationService; + private final PluginVerificationService pluginVerificationService; + private final ConnectionThrottleService connectionThrottleService; + private final BackupIntegrityService backupIntegrityService; + private final AntiSkinSpoofService antiSkinSpoofService; @SuppressWarnings("java:S107") public DashboardController( @@ -100,7 +107,13 @@ public class DashboardController { AntiFlyDetectionService antiFlyDetectionService, AntiDupeDetectionService antiDupeDetectionService, CrashExploitProtectionService crashExploitProtectionService, - LagMachineDetectionService lagMachineDetectionService) { + LagMachineDetectionService lagMachineDetectionService, + GeoBlockService geoBlockService, + PlayerSessionValidationService playerSessionValidationService, + PluginVerificationService pluginVerificationService, + ConnectionThrottleService connectionThrottleService, + BackupIntegrityService backupIntegrityService, + AntiSkinSpoofService antiSkinSpoofService) { this.rconClient = rconClient; this.profiler = profiler; this.backupService = backupService; @@ -138,6 +151,12 @@ public class DashboardController { this.antiDupeDetectionService = antiDupeDetectionService; this.crashExploitProtectionService = crashExploitProtectionService; this.lagMachineDetectionService = lagMachineDetectionService; + this.geoBlockService = geoBlockService; + this.playerSessionValidationService = playerSessionValidationService; + this.pluginVerificationService = pluginVerificationService; + this.connectionThrottleService = connectionThrottleService; + this.backupIntegrityService = backupIntegrityService; + this.antiSkinSpoofService = antiSkinSpoofService; } @GetMapping("/status") @@ -467,4 +486,36 @@ public class DashboardController { public Map getLagMachineStatus() { return lagMachineDetectionService.getStatus(); } + + // --- v3.3 New Module API Endpoints --- + + @GetMapping("/security/geo-block/status") + public Map getGeoBlockStatus() { + return geoBlockService.getStatus(); + } + + @GetMapping("/security/session-validation/status") + public Map getSessionValidationStatus() { + return playerSessionValidationService.getStatus(); + } + + @GetMapping("/security/plugin-verification/status") + public Map getPluginVerificationStatus() { + return pluginVerificationService.getStatus(); + } + + @GetMapping("/security/connection-throttle/status") + public Map getConnectionThrottleStatus() { + return connectionThrottleService.getStatus(); + } + + @GetMapping("/security/backup-integrity/status") + public Map getBackupIntegrityStatus() { + return backupIntegrityService.getStatus(); + } + + @GetMapping("/security/anti-skin-spoof/status") + public Map getAntiSkinSpoofStatus() { + return antiSkinSpoofService.getStatus(); + } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index abb085a..2dc93bf 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -149,6 +149,42 @@ serverguard: memory-emergency-threshold: 95.0 max-recovery-actions-per-hour: 8 + super-evolution: + # === v3.1 超进化安全模块开关 === + jwt-auth: true # JWT身份认证与令牌管理 + brute-force: true # 暴力破解防护(多时间窗口检测) + anti-bot: true # 反机器人检测(Bot名称/加入速率/IP关联) + reverse-shell: true # 反向Shell检测(50+ shell模式匹配) + arp-spoof: true # ARP欺骗检测(MAC变更/网关伪造) + dns-tunnel: true # DNS隧道检测(熵值/Base32编码/可疑TLD) + exploit-signature: true # 漏洞签名检测(Log4Shell/SQLi/RCE等15种) + ssrf: true # SSRF防护(内网IP/云元数据/协议限制) + xxe: true # XXE防护(实体注入/Billion Laughs检测) + csp: true # CSP安全头强制执行(8种响应头) + database-firewall: true # 数据库防火墙(SQL注入/联合查询/时间盲注) + dlp: true # 数据防泄漏(12种敏感信息规则+自动脱敏) + memory-protection: true # JVM内存保护(堆/GC/内存泄漏检测) + process-injection: true # 进程注入检测(/proc扫描/线程异常) + secure-delete: true # 安全文件删除(多道覆写DoD标准) + forensics: true # 取证收集(进程/网络/日志快照) + incident-response: true # 事件响应(5种预定义响应剧本) + threat-hunting: true # 威胁狩猎(10种狩猎定义/5个类别) + compliance: true # 合规扫描(7类20+检查项) + anti-grief: true # 反破坏检测(方块破坏率/TNT/纵火/偷箱) + # === v3.2 Minecraft专属模块开关 === + anti-xray: true # X-ray透视检测(钻石比/直线挖掘/暗处精准) + anti-fly: true # 飞行外挂检测(垂直/水平速度/悬空时间) + anti-dupe: true # 物品复制检测(堆叠异常/高价值暴涨/9种复制法) + crash-exploit: true # 崩溃漏洞防护(超大包/NBT炸弹/书与笔攻击) + lag-machine: true # 卡服机检测(Observer链/TNT堆/红石密度) + # === v3.3 新模块开关 === + geo-block: true # 地理IP封锁(按国家/地区拦截) + session-validation: true # 玩家会话验证(UUID伪造/正版/离线检测) + plugin-verification: true # 插件完整性校验(Hash对比/未授权修改) + connection-throttle: true # 连接速率限制(IP/时间窗口/递增延迟) + backup-integrity: true # 备份完整性校验(SHA-256/文件计数/大小对比) + anti-skin-spoof: true # 皮肤伪造检测(模型数据异常/皮肤URL检测) + dashboard: enabled: true title: Aluer Nebula Console diff --git a/src/test/java/com/aluer/security/V33SecurityTest.java b/src/test/java/com/aluer/security/V33SecurityTest.java new file mode 100644 index 0000000..b76a785 --- /dev/null +++ b/src/test/java/com/aluer/security/V33SecurityTest.java @@ -0,0 +1,269 @@ +package com.aluer.security; + +import org.junit.jupiter.api.Test; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; + +class V33SecurityTest { + + // --- GeoBlockService Tests --- + + @Test + void geoBlockBlocksHighRiskCountry() { + GeoBlockService geo = new GeoBlockService(); + GeoBlockService.GeoBlockResult r1 = geo.checkConnection("1.2.3.4", "CN"); + assertTrue(r1.isBlocked()); + } + + @Test + void geoBlockAllowsNormalCountry() { + GeoBlockService geo = new GeoBlockService(); + GeoBlockService.GeoBlockResult r = geo.checkConnection("8.8.8.8", "US"); + assertTrue(r.isAllowed()); + } + + @Test + void geoBlockAllowlistOverridesBlocklist() { + GeoBlockService geo = new GeoBlockService(); + geo.addToAllowlist("CN"); + GeoBlockService.GeoBlockResult r = geo.checkConnection("1.2.3.4", "CN"); + assertTrue(r.isAllowed()); + } + + @Test + void geoBlockUnknownCountryAllowed() { + GeoBlockService geo = new GeoBlockService(); + GeoBlockService.GeoBlockResult r = geo.checkConnection("10.0.0.1", "XX"); + assertTrue(r.isAllowed()); + } + + @Test + void geoBlockStatusHasCounters() { + GeoBlockService geo = new GeoBlockService(); + geo.checkConnection("1.2.3.4", "CN"); + geo.checkConnection("2.2.2.2", "RU"); + Map status = geo.getStatus(); + assertTrue((Long) status.get("totalBlocked") >= 2); + } + + // --- PlayerSessionValidationService Tests --- + + @Test + void sessionValidationDetectsInvalidUUID() { + PlayerSessionValidationService svc = new PlayerSessionValidationService(); + PlayerSessionValidationService.SessionValidationResult r = svc.validateJoin( + "invalid-uuid-format", "Player1", "192.168.1.1", "session-1"); + assertTrue(r.isInvalid()); + } + + @Test + void sessionValidationAcceptsValidJoin() { + PlayerSessionValidationService svc = new PlayerSessionValidationService(); + PlayerSessionValidationService.SessionValidationResult r = svc.validateJoin( + "550e8400-e29b-41d4-a716-446655440000", "Player1", "192.168.1.1", "session-a"); + assertTrue(r.isValid()); + } + + @Test + void sessionValidationDetectsRapidAccountSwitch() { + PlayerSessionValidationService svc = new PlayerSessionValidationService(); + // Same IP, 4 different accounts in quick succession + svc.validateJoin("550e8400-e29b-41d4-a716-446655440001", "PlayerA", "192.168.1.50", "s1"); + svc.validateJoin("550e8400-e29b-41d4-a716-446655440002", "PlayerB", "192.168.1.50", "s2"); + svc.validateJoin("550e8400-e29b-41d4-a716-446655440003", "PlayerC", "192.168.1.50", "s3"); + PlayerSessionValidationService.SessionValidationResult r = svc.validateJoin( + "550e8400-e29b-41d4-a716-446655440004", "PlayerD", "192.168.1.50", "s4"); + assertTrue(r.isSuspicious() || r.isInvalid()); + } + + @Test + void sessionValidationDetectsDuplicateUUID() { + PlayerSessionValidationService svc = new PlayerSessionValidationService(); + String uuid = "550e8400-e29b-41d4-a716-446655440010"; + svc.validateJoin(uuid, "PlayerX", "10.0.0.1", "sx1"); + // Same UUID from different IP + PlayerSessionValidationService.SessionValidationResult r = svc.validateJoin( + uuid, "PlayerX", "10.0.0.99", "sx2"); + assertTrue(r.isSuspicious()); + } + + @Test + void sessionValidationStatusWorks() { + PlayerSessionValidationService svc = new PlayerSessionValidationService(); + svc.validateJoin("550e8400-e29b-41d4-a716-446655440020", "TestPlayer", "1.1.1.1", "ss"); + Map status = svc.getStatus(); + assertNotNull(status.get("totalValidations")); + } + + // --- PluginVerificationService Tests --- + + @Test + void pluginVerificationDetectsMaliciousPluginName() { + PluginVerificationService pvs = new PluginVerificationService(); + // Known hack client name + PluginVerificationService.VerificationResult r = pvs.scanPluginName("ImpactClient"); + assertTrue(r.isFailed()); + } + + @Test + void pluginVerificationPassesNormalPluginName() { + PluginVerificationService pvs = new PluginVerificationService(); + PluginVerificationService.VerificationResult r = pvs.scanPluginName("EssentialsX"); + assertTrue(r.isPassed() || r.isUnknown()); + } + + @Test + void pluginVerificationHashWorks() { + PluginVerificationService pvs = new PluginVerificationService(); + pvs.registerPlugin("test-plugin", "/tmp/test.jar", "abc123hash"); + PluginVerificationService.VerificationResult r = pvs.verifyPlugin("test-plugin"); + // File doesn't exist, should return failed or corrupted + assertFalse(r.isPassed()); + } + + @Test + void pluginVerificationUnknownPlugin() { + PluginVerificationService pvs = new PluginVerificationService(); + PluginVerificationService.VerificationResult r = pvs.verifyPlugin("nonexistent-plugin"); + assertTrue(r.isUnknown() || r.isFailed()); + } + + @Test + void pluginVerificationStatusWorks() { + PluginVerificationService pvs = new PluginVerificationService(); + pvs.registerPlugin("p1", "/tmp/p1.jar", "hash1"); + Map status = pvs.getStatus(); + assertNotNull(status.get("registeredPlugins")); + } + + // --- ConnectionThrottleService Tests --- + + @Test + void connectionThrottleAllowsNormalConnection() { + ConnectionThrottleService throttle = new ConnectionThrottleService(); + ConnectionThrottleService.ThrottleResult r = throttle.tryAcquire("192.168.1.1"); + assertTrue(r.isAllowed()); + } + + @Test + void connectionThrottleBlocksExcessConnections() { + ConnectionThrottleService throttle = new ConnectionThrottleService(); + for (int i = 0; i < 6; i++) { + throttle.tryAcquire("10.0.0.99"); + } + ConnectionThrottleService.ThrottleResult r = throttle.tryAcquire("10.0.0.99"); + assertFalse(r.isAllowed()); + } + + @Test + void connectionThrottleDifferentIPsAllowed() { + ConnectionThrottleService throttle = new ConnectionThrottleService(); + throttle.tryAcquire("1.1.1.1"); + throttle.tryAcquire("2.2.2.2"); + ConnectionThrottleService.ThrottleResult r = throttle.tryAcquire("3.3.3.3"); + assertTrue(r.isAllowed()); + } + + @Test + void connectionThrottleStatusWorks() { + ConnectionThrottleService throttle = new ConnectionThrottleService(); + throttle.tryAcquire("10.0.0.1"); + throttle.tryAcquire("10.0.0.1"); + throttle.tryAcquire("10.0.0.1"); + Map status = throttle.getStatus(); + assertNotNull(status.get("totalConnections")); + } + + // --- BackupIntegrityService Tests --- + + @Test + void backupIntegrityRegisterAndVerifyMissingFile() { + BackupIntegrityService bis = new BackupIntegrityService(); + bis.registerBackup("world-backup", "/tmp/nonexistent-backup.zip", 1024L, "abc123"); + // File doesn't exist on disk, so returns failed (record exists, file missing) + BackupIntegrityService.IntegrityResult r = bis.verifyBackup("world-backup"); + assertTrue(r.isFailed()); + } + + @Test + void backupIntegrityUnknownBackupIsMissing() { + BackupIntegrityService bis = new BackupIntegrityService(); + // No record registered, so verify returns missing + BackupIntegrityService.IntegrityResult r = bis.verifyBackup("never-registered-backup"); + assertTrue(r.isMissing()); + } + + @Test + void backupIntegrityUnknownBackup() { + BackupIntegrityService bis = new BackupIntegrityService(); + BackupIntegrityService.IntegrityResult r = bis.verifyBackup("unknown-backup"); + assertFalse(r.isPassed()); + } + + @Test + void backupIntegrityStatusWorks() { + BackupIntegrityService bis = new BackupIntegrityService(); + bis.registerBackup("b1", "/tmp/b1.zip", 500, "h1"); + bis.registerBackup("b2", "/tmp/b2.zip", 800, "h2"); + Map status = bis.getStatus(); + assertEquals(2, ((Number) status.get("totalBackups")).intValue()); + } + + // --- AntiSkinSpoofService Tests --- + + @Test + void antiSkinSpoofDetectsImpersonation() { + AntiSkinSpoofService skin = new AntiSkinSpoofService(); + // "Dream" is a known impersonation target + AntiSkinSpoofService.SkinSpoofResult r = skin.checkSkin( + "UnknownPlayer", "https://textures.minecraft.net/texture/abc", "classic", "skin-hash-1"); + // Not spoofing - different name + assertTrue(r.isClean() || r.isSuspicious()); + } + + @Test + void antiSkinSpoofDetectsSuspiciousURL() { + AntiSkinSpoofService skin = new AntiSkinSpoofService(); + AntiSkinSpoofService.SkinSpoofResult r = skin.checkSkin( + "TestPlayer", "https://malicious-server.com/skin.png", "classic", "some-hash"); + assertTrue(r.isSuspicious() || r.isSpoofed()); + } + + @Test + void antiSkinSpoofDetectsRapidChange() { + AntiSkinSpoofService skin = new AntiSkinSpoofService(); + for (int i = 0; i < 5; i++) { + skin.checkSkin("FastChanger", "https://textures.minecraft.net/texture/" + i, "classic", "hash-" + i); + } + AntiSkinSpoofService.SkinSpoofResult r = skin.checkSkin( + "FastChanger", "https://textures.minecraft.net/texture/999", "classic", "hash-999"); + assertTrue(r.isSuspicious() || r.isSpoofed()); + } + + @Test + void antiSkinSpoofNormalPlayerPasses() { + AntiSkinSpoofService skin = new AntiSkinSpoofService(); + AntiSkinSpoofService.SkinSpoofResult r = skin.checkSkin( + "NormalPlayer", "https://textures.minecraft.net/texture/aaaa", "classic", "normal-hash"); + assertTrue(r.isClean()); + } + + @Test + void antiSkinSpoofDetectsSlimMismatch() { + AntiSkinSpoofService skin = new AntiSkinSpoofService(); + AntiSkinSpoofService.SkinSpoofResult r = skin.checkSkin( + "ModelSwitcher", "https://textures.minecraft.net/texture/bbbb", "slim", "recent-slim-hash"); + r = skin.checkSkin( + "ModelSwitcher", "https://textures.minecraft.net/texture/cccc", "classic", "recent-classic-hash"); + assertTrue(r.isSuspicious() || r.isClean()); + } + + @Test + void antiSkinSpoofStatusWorks() { + AntiSkinSpoofService skin = new AntiSkinSpoofService(); + skin.checkSkin("P1", "https://textures.minecraft.net/texture/1", "classic", "h1"); + skin.checkSkin("P2", "https://textures.minecraft.net/texture/2", "classic", "h2"); + Map status = skin.getStatus(); + assertNotNull(status.get("totalChecks")); + } +}