Files
Aluer/src/main/java/com/aluer/security/SecurityBaselineHardeningService.java
T
2026-05-10 12:59:00 +08:00

266 lines
11 KiB
Java

package com.aluer.security;
import com.aluer.config.ServerGuardConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class SecurityBaselineHardeningService {
private static final Pattern INLINE_SECRET_PATTERN = Pattern.compile(
"(?im)^\\s*(api-key|password|secret|token|apiKey):\\s*(?!\\$\\{)([^\\s#]+)"
);
private final ServerGuardConfig config;
private final ConcurrentLinkedDeque<HardeningFinding> findingLog = new ConcurrentLinkedDeque<>();
private final Path applicationConfigPath = Paths.get("src", "main", "resources", "application.yml");
public SecurityBaselineHardeningService() {
this(new ServerGuardConfig());
}
@Autowired
public SecurityBaselineHardeningService(ServerGuardConfig config) {
this.config = config;
}
public HardeningReport assessCurrentBaseline() {
List<HardeningFinding> findings = new ArrayList<>();
scanRuntimeConfiguration(findings);
scanConfigFile(findings);
record(findings);
return new HardeningReport(findings);
}
public List<HardeningFinding> getRecentFindings(int limit) {
List<HardeningFinding> result = new ArrayList<>();
int count = 0;
for (HardeningFinding finding : findingLog) {
if (count++ >= limit) {
break;
}
result.add(finding);
}
result.sort(Comparator.comparingLong(HardeningFinding::getTimestamp).reversed());
return result;
}
public Map<String, Object> getSummary() {
return assessCurrentBaseline().toMap();
}
private void scanRuntimeConfiguration(List<HardeningFinding> findings) {
ServerGuardConfig.RconConfig rcon = config.getMinecraft().getRcon();
if (rcon.isEnabled() && isBlank(rcon.getPassword())) {
findings.add(new HardeningFinding(
"critical",
"RCON",
"RCON 已启用但未配置口令",
"远程控制接口暴露时,空口令会直接放大入侵面。",
"为 RCON 设置强随机密码,并限制来源 IP。"
));
} else if (rcon.isEnabled() && rcon.getPassword().length() < 12) {
findings.add(new HardeningFinding(
"high",
"RCON",
"RCON 口令强度偏弱",
"当前口令长度过短,容易被暴力猜解或撞库命中。",
"将 RCON 口令提升到 16 位以上并只通过环境变量注入。"
));
}
if (!config.getSecurity().getAntiIntrusion().getFileIntegrity().isEnabled()) {
findings.add(new HardeningFinding(
"high",
"INTEGRITY",
"文件完整性监控已关闭",
"插件篡改、脚本替换和 systemd 劫持将更难被发现。",
"启用 file-integrity,并覆盖核心 jar、插件目录和启动脚本。"
));
}
if (!config.getSecurity().getHostEnforcement().isEnabled()) {
findings.add(new HardeningFinding(
"medium",
"HOST_ENFORCEMENT",
"主机侧封禁未启用",
"系统当前只能在应用层做隔离,真正的源地址拦截无法落到主机网络层。",
"启用 host-enforcement,至少保持 dry-run 观察,再逐步切到真实执行。"
));
} else if (config.getSecurity().getHostEnforcement().isDryRun()) {
findings.add(new HardeningFinding(
"low",
"HOST_ENFORCEMENT",
"主机侧封禁仍处于 dry-run",
"当前只生成阻断预览,不会真正下发防火墙规则。",
"验证命令预览无误后,把 dry-run 切换为 false。"
));
}
if (config.getAi().getDeepseek().getAutoExecute().isEnabled()
&& config.getAi().getDeepseek().getAutoExecute().getMinConfidence() < 85) {
findings.add(new HardeningFinding(
"medium",
"AUTONOMY",
"AI 自动执行置信度阈值过低",
"过低阈值会放大误封禁、误踢人和错误白名单切换的概率。",
"将 min-confidence 提升到 85 以上,并保留工作流冷却。"
));
}
if (config.getSecurity().getCloudEdge().isEnabled()
&& (isBlank(config.getSecurity().getCloudEdge().getZoneId())
|| isBlank(config.getSecurity().getCloudEdge().getApiKey()))) {
findings.add(new HardeningFinding(
"high",
"EDGE",
"云边协同已启用但凭据不完整",
"边缘拦截配置不完整会导致自治编排在关键阶段失效。",
"补齐 Cloudflare zone/API 凭据,或显式关闭 cloud-edge。"
));
}
}
private void scanConfigFile(List<HardeningFinding> findings) {
if (!Files.exists(applicationConfigPath)) {
return;
}
try {
String content = Files.readString(applicationConfigPath, StandardCharsets.UTF_8);
Matcher matcher = INLINE_SECRET_PATTERN.matcher(content);
while (matcher.find()) {
String key = matcher.group(1);
String value = matcher.group(2);
if (value != null && !value.isBlank()) {
findings.add(new HardeningFinding(
"critical",
"CONFIG_SECRET",
"配置文件中存在明文敏感项: " + key,
"明文凭据一旦进仓库、日志或备份,泄露面会持续扩大。",
"改为 `${ENV_VAR:}` 形式,并轮换已暴露的真实凭据。"
));
}
}
} catch (IOException ignored) {
}
}
private void record(List<HardeningFinding> findings) {
for (HardeningFinding finding : findings) {
findingLog.offer(finding);
}
while (findingLog.size() > 500) {
findingLog.poll();
}
}
private boolean isBlank(String value) {
return value == null || value.isBlank();
}
public static class HardeningReport {
private final List<HardeningFinding> findings;
private final int criticalCount;
private final int highCount;
private final int mediumCount;
private final int lowCount;
private final int score;
public HardeningReport(List<HardeningFinding> findings) {
this.findings = findings.stream()
.sorted(Comparator.comparing((HardeningFinding finding) -> severityWeight(finding.severity)).reversed())
.toList();
this.criticalCount = (int) this.findings.stream().filter(finding -> "critical".equals(finding.severity)).count();
this.highCount = (int) this.findings.stream().filter(finding -> "high".equals(finding.severity)).count();
this.mediumCount = (int) this.findings.stream().filter(finding -> "medium".equals(finding.severity)).count();
this.lowCount = (int) this.findings.stream().filter(finding -> "low".equals(finding.severity)).count();
this.score = Math.max(0, 100 - (criticalCount * 25 + highCount * 15 + mediumCount * 8 + lowCount * 3));
}
public List<HardeningFinding> getFindings() { return findings; }
public int getCriticalCount() { return criticalCount; }
public int getHighCount() { return highCount; }
public int getMediumCount() { return mediumCount; }
public int getLowCount() { return lowCount; }
public int getScore() { return score; }
public Map<String, Object> toMap() {
Map<String, Object> result = new LinkedHashMap<>();
result.put("score", score);
result.put("exposureLevel", score >= 85 ? "stable" : score >= 65 ? "guarded" : score >= 45 ? "exposed" : "critical");
result.put("critical", criticalCount);
result.put("high", highCount);
result.put("medium", mediumCount);
result.put("low", lowCount);
result.put("findings", findings.stream().limit(6).map(HardeningFinding::toMap).toList());
result.put("recommendations", findings.stream()
.map(HardeningFinding::getRecommendation)
.distinct()
.limit(5)
.toList());
return result;
}
private static int severityWeight(String severity) {
return switch (severity.toLowerCase(Locale.ROOT)) {
case "critical" -> 4;
case "high" -> 3;
case "medium" -> 2;
default -> 1;
};
}
}
public static class HardeningFinding {
private final String severity;
private final String category;
private final String title;
private final String detail;
private final String recommendation;
private final long timestamp;
public HardeningFinding(String severity, String category, String title, String detail, String recommendation) {
this.severity = severity;
this.category = category;
this.title = title;
this.detail = detail;
this.recommendation = recommendation;
this.timestamp = Instant.now().toEpochMilli();
}
public String getSeverity() { return severity; }
public String getCategory() { return category; }
public String getTitle() { return title; }
public String getDetail() { return detail; }
public String getRecommendation() { return recommendation; }
public long getTimestamp() { return timestamp; }
public Map<String, Object> toMap() {
Map<String, Object> result = new LinkedHashMap<>();
result.put("severity", severity);
result.put("category", category);
result.put("title", title);
result.put("detail", detail);
result.put("recommendation", recommendation);
result.put("timestamp", timestamp);
return result;
}
}
}