提交Git
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
package com.aluer.console;
|
||||
|
||||
import com.aluer.ai.AluerSovereignEngine;
|
||||
import com.aluer.audit.SecurityAuditService;
|
||||
import com.aluer.config.ServerGuardConfig;
|
||||
import com.aluer.kernel.AluerKernelEngine;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
@Service
|
||||
public class AluerEngineHandshakeService {
|
||||
private final ServerGuardConfig config;
|
||||
private final AluerSovereignEngine aluerSovereignEngine;
|
||||
private final AluerKernelEngine aluerKernelEngine;
|
||||
private final AluerMirageShieldService aluerMirageShieldService;
|
||||
private final SecurityAuditService securityAuditService;
|
||||
|
||||
private final Map<String, HandshakeGrant> grants = new ConcurrentHashMap<>();
|
||||
private final ConcurrentLinkedDeque<HandshakeRecord> history = new ConcurrentLinkedDeque<>();
|
||||
|
||||
public AluerEngineHandshakeService(ServerGuardConfig config,
|
||||
AluerSovereignEngine aluerSovereignEngine,
|
||||
AluerKernelEngine aluerKernelEngine,
|
||||
AluerMirageShieldService aluerMirageShieldService,
|
||||
SecurityAuditService securityAuditService) {
|
||||
this.config = config;
|
||||
this.aluerSovereignEngine = aluerSovereignEngine;
|
||||
this.aluerKernelEngine = aluerKernelEngine;
|
||||
this.aluerMirageShieldService = aluerMirageShieldService;
|
||||
this.securityAuditService = securityAuditService;
|
||||
}
|
||||
|
||||
public HandshakeResult requestSshHandshake(HandshakeIntent intent) {
|
||||
String host = requireText(intent.host(), "host");
|
||||
String username = requireText(intent.username(), "username");
|
||||
int port = intent.port() > 0 ? intent.port() : 22;
|
||||
String purpose = intent.purpose() == null || intent.purpose().isBlank() ? "ssh-connect" : intent.purpose().trim();
|
||||
|
||||
cleanupExpiredGrants();
|
||||
|
||||
Map<String, Object> engineStatus = aluerSovereignEngine.getEngineStatus();
|
||||
AluerKernelEngine.KernelPulse pulse = aluerKernelEngine.runKernelPulse("ssh-handshake:" + host + ":" + username);
|
||||
Map<String, Object> shieldStatus = aluerMirageShieldService.getShieldStatus();
|
||||
|
||||
int kernelHeat = (int) Math.round(pulse.getHeat());
|
||||
int resonance = (int) Math.round(pulse.getResonance());
|
||||
int riskScore = toInt(shieldStatus.get("riskScore"));
|
||||
String shieldMode = String.valueOf(shieldStatus.getOrDefault("currentMode", "OBSERVE"));
|
||||
String workflow = pulse.getDirective().getWorkflow();
|
||||
boolean handshakeRequired = config.getDashboard().getSshGateway().isRequireEngineHandshake();
|
||||
boolean approved = !handshakeRequired || (kernelHeat < 96 && riskScore < 97);
|
||||
String token = approved ? UUID.randomUUID().toString() : "";
|
||||
long ttlSeconds = Math.max(10, config.getDashboard().getSshGateway().getHandshakeTtlSeconds());
|
||||
long expiresAt = System.currentTimeMillis() + ttlSeconds * 1000L;
|
||||
String message = approved
|
||||
? "Sovereign handshake approved. Session may proceed."
|
||||
: "Main engine declined SSH session while the node is under critical defensive pressure.";
|
||||
|
||||
if (approved && handshakeRequired) {
|
||||
grants.put(token, new HandshakeGrant(token, host, port, username, expiresAt, purpose, shieldMode, kernelHeat, riskScore));
|
||||
}
|
||||
|
||||
HandshakeRecord record = new HandshakeRecord(host, port, username, purpose, approved, shieldMode, kernelHeat, riskScore, workflow, expiresAt);
|
||||
rememberRecord(record);
|
||||
securityAuditService.logEvent("ALUER_HANDSHAKE", username + "@" + host, approved ? "APPROVED" : "DENIED", purpose);
|
||||
|
||||
return new HandshakeResult(
|
||||
approved,
|
||||
token,
|
||||
message,
|
||||
shieldMode,
|
||||
workflow,
|
||||
kernelHeat,
|
||||
resonance,
|
||||
riskScore,
|
||||
expiresAt,
|
||||
engineStatus.getOrDefault("engine", "ALUER_SOVEREIGN_LOOP").toString()
|
||||
);
|
||||
}
|
||||
|
||||
public HandshakeGrant consumeSshGrant(String token, String host, int port, String username) {
|
||||
cleanupExpiredGrants();
|
||||
if (!config.getDashboard().getSshGateway().isRequireEngineHandshake()) {
|
||||
return new HandshakeGrant("", host, port, username, 0, "ssh-connect", "BYPASS", 0, 0);
|
||||
}
|
||||
String normalizedToken = requireText(token, "handshakeToken");
|
||||
HandshakeGrant grant = grants.remove(normalizedToken);
|
||||
if (grant == null) {
|
||||
throw new IllegalArgumentException("主引擎握手令牌不存在或已过期");
|
||||
}
|
||||
if (grant.expiresAt < System.currentTimeMillis()) {
|
||||
throw new IllegalArgumentException("主引擎握手令牌已过期");
|
||||
}
|
||||
if (!grant.host.equals(host) || grant.port != port || !grant.username.equals(username)) {
|
||||
throw new IllegalArgumentException("主引擎握手令牌与目标节点不匹配");
|
||||
}
|
||||
return grant;
|
||||
}
|
||||
|
||||
public Map<String, Object> getStatus() {
|
||||
cleanupExpiredGrants();
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("required", config.getDashboard().getSshGateway().isRequireEngineHandshake());
|
||||
result.put("ttlSeconds", config.getDashboard().getSshGateway().getHandshakeTtlSeconds());
|
||||
result.put("activeGrants", grants.size());
|
||||
if (!history.isEmpty()) {
|
||||
result.put("lastHandshake", history.peekFirst().toMap());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void cleanupExpiredGrants() {
|
||||
long now = System.currentTimeMillis();
|
||||
grants.entrySet().removeIf(entry -> entry.getValue().expiresAt < now);
|
||||
}
|
||||
|
||||
private void rememberRecord(HandshakeRecord record) {
|
||||
history.offerFirst(record);
|
||||
while (history.size() > 120) {
|
||||
history.pollLast();
|
||||
}
|
||||
}
|
||||
|
||||
private int toInt(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return value == null ? 0 : Integer.parseInt(String.valueOf(value));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private String requireText(String value, String field) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(field + " 不能为空");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
public record HandshakeIntent(
|
||||
String host,
|
||||
int port,
|
||||
String username,
|
||||
String purpose
|
||||
) {
|
||||
}
|
||||
|
||||
public static class HandshakeResult {
|
||||
private final boolean approved;
|
||||
private final String token;
|
||||
private final String message;
|
||||
private final String shieldMode;
|
||||
private final String workflow;
|
||||
private final int kernelHeat;
|
||||
private final int resonance;
|
||||
private final int riskScore;
|
||||
private final long expiresAt;
|
||||
private final String engine;
|
||||
|
||||
public HandshakeResult(boolean approved,
|
||||
String token,
|
||||
String message,
|
||||
String shieldMode,
|
||||
String workflow,
|
||||
int kernelHeat,
|
||||
int resonance,
|
||||
int riskScore,
|
||||
long expiresAt,
|
||||
String engine) {
|
||||
this.approved = approved;
|
||||
this.token = token;
|
||||
this.message = message;
|
||||
this.shieldMode = shieldMode;
|
||||
this.workflow = workflow;
|
||||
this.kernelHeat = kernelHeat;
|
||||
this.resonance = resonance;
|
||||
this.riskScore = riskScore;
|
||||
this.expiresAt = expiresAt;
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
public boolean isApproved() { return approved; }
|
||||
public String getToken() { return token; }
|
||||
public String getMessage() { return message; }
|
||||
public String getShieldMode() { return shieldMode; }
|
||||
public String getWorkflow() { return workflow; }
|
||||
public int getKernelHeat() { return kernelHeat; }
|
||||
public int getResonance() { return resonance; }
|
||||
public int getRiskScore() { return riskScore; }
|
||||
public long getExpiresAt() { return expiresAt; }
|
||||
public String getEngine() { return engine; }
|
||||
}
|
||||
|
||||
public static class HandshakeGrant {
|
||||
private final String token;
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final String username;
|
||||
private final long expiresAt;
|
||||
private final String purpose;
|
||||
private final String shieldMode;
|
||||
private final int kernelHeat;
|
||||
private final int riskScore;
|
||||
|
||||
public HandshakeGrant(String token,
|
||||
String host,
|
||||
int port,
|
||||
String username,
|
||||
long expiresAt,
|
||||
String purpose,
|
||||
String shieldMode,
|
||||
int kernelHeat,
|
||||
int riskScore) {
|
||||
this.token = token;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.expiresAt = expiresAt;
|
||||
this.purpose = purpose;
|
||||
this.shieldMode = shieldMode;
|
||||
this.kernelHeat = kernelHeat;
|
||||
this.riskScore = riskScore;
|
||||
}
|
||||
|
||||
public String getToken() { return token; }
|
||||
public String getHost() { return host; }
|
||||
public int getPort() { return port; }
|
||||
public String getUsername() { return username; }
|
||||
public long getExpiresAt() { return expiresAt; }
|
||||
public String getPurpose() { return purpose; }
|
||||
public String getShieldMode() { return shieldMode; }
|
||||
public int getKernelHeat() { return kernelHeat; }
|
||||
public int getRiskScore() { return riskScore; }
|
||||
}
|
||||
|
||||
public static class HandshakeRecord {
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final String username;
|
||||
private final String purpose;
|
||||
private final boolean approved;
|
||||
private final String shieldMode;
|
||||
private final int kernelHeat;
|
||||
private final int riskScore;
|
||||
private final String workflow;
|
||||
private final long expiresAt;
|
||||
private final long timestamp = Instant.now().toEpochMilli();
|
||||
|
||||
public HandshakeRecord(String host,
|
||||
int port,
|
||||
String username,
|
||||
String purpose,
|
||||
boolean approved,
|
||||
String shieldMode,
|
||||
int kernelHeat,
|
||||
int riskScore,
|
||||
String workflow,
|
||||
long expiresAt) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.purpose = purpose;
|
||||
this.approved = approved;
|
||||
this.shieldMode = shieldMode;
|
||||
this.kernelHeat = kernelHeat;
|
||||
this.riskScore = riskScore;
|
||||
this.workflow = workflow;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("host", host);
|
||||
result.put("port", port);
|
||||
result.put("username", username);
|
||||
result.put("purpose", purpose);
|
||||
result.put("approved", approved);
|
||||
result.put("shieldMode", shieldMode);
|
||||
result.put("kernelHeat", kernelHeat);
|
||||
result.put("riskScore", riskScore);
|
||||
result.put("workflow", workflow);
|
||||
result.put("expiresAt", expiresAt);
|
||||
result.put("timestamp", timestamp);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
package com.aluer.console;
|
||||
|
||||
import com.aluer.audit.SecurityAuditService;
|
||||
import com.aluer.config.ServerGuardConfig;
|
||||
import com.aluer.kernel.AluerKernelEngine;
|
||||
import com.aluer.kernel.AluerKernelTaskBus;
|
||||
import com.aluer.kernel.AluerSelfHealingOrchestrator;
|
||||
import com.aluer.security.CloudflareIntegrationService;
|
||||
import com.aluer.security.DDoSDefenseCoordinator;
|
||||
import com.aluer.security.DistributedAttackMitigationService;
|
||||
import com.aluer.security.HostEnforcementService;
|
||||
import com.aluer.security.LoadBalancerService;
|
||||
import com.aluer.security.NetworkThreatFusionService;
|
||||
import com.aluer.security.SecurityBaselineHardeningService;
|
||||
import com.aluer.security.CommandExecutionGuardService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
|
||||
@Service
|
||||
public class AluerMirageShieldService {
|
||||
private final ServerGuardConfig config;
|
||||
private final NetworkThreatFusionService networkThreatFusionService;
|
||||
private final DDoSDefenseCoordinator ddosDefenseCoordinator;
|
||||
private final DistributedAttackMitigationService distributedAttackMitigationService;
|
||||
private final CloudflareIntegrationService cloudflareIntegrationService;
|
||||
private final HostEnforcementService hostEnforcementService;
|
||||
private final LoadBalancerService loadBalancerService;
|
||||
private final SecurityBaselineHardeningService securityBaselineHardeningService;
|
||||
private final CommandExecutionGuardService commandExecutionGuardService;
|
||||
private final AluerKernelEngine aluerKernelEngine;
|
||||
private final AluerKernelTaskBus aluerKernelTaskBus;
|
||||
private final AluerSelfHealingOrchestrator aluerSelfHealingOrchestrator;
|
||||
private final SecurityAuditService securityAuditService;
|
||||
private final ConcurrentLinkedDeque<ShieldTransition> transitions = new ConcurrentLinkedDeque<>();
|
||||
|
||||
private volatile ShieldMode currentMode = ShieldMode.OBSERVE;
|
||||
|
||||
public AluerMirageShieldService(ServerGuardConfig config,
|
||||
NetworkThreatFusionService networkThreatFusionService,
|
||||
DDoSDefenseCoordinator ddosDefenseCoordinator,
|
||||
DistributedAttackMitigationService distributedAttackMitigationService,
|
||||
CloudflareIntegrationService cloudflareIntegrationService,
|
||||
HostEnforcementService hostEnforcementService,
|
||||
LoadBalancerService loadBalancerService,
|
||||
SecurityBaselineHardeningService securityBaselineHardeningService,
|
||||
CommandExecutionGuardService commandExecutionGuardService,
|
||||
AluerKernelEngine aluerKernelEngine,
|
||||
AluerKernelTaskBus aluerKernelTaskBus,
|
||||
AluerSelfHealingOrchestrator aluerSelfHealingOrchestrator,
|
||||
SecurityAuditService securityAuditService) {
|
||||
this.config = config;
|
||||
this.networkThreatFusionService = networkThreatFusionService;
|
||||
this.ddosDefenseCoordinator = ddosDefenseCoordinator;
|
||||
this.distributedAttackMitigationService = distributedAttackMitigationService;
|
||||
this.cloudflareIntegrationService = cloudflareIntegrationService;
|
||||
this.hostEnforcementService = hostEnforcementService;
|
||||
this.loadBalancerService = loadBalancerService;
|
||||
this.securityBaselineHardeningService = securityBaselineHardeningService;
|
||||
this.commandExecutionGuardService = commandExecutionGuardService;
|
||||
this.aluerKernelEngine = aluerKernelEngine;
|
||||
this.aluerKernelTaskBus = aluerKernelTaskBus;
|
||||
this.aluerSelfHealingOrchestrator = aluerSelfHealingOrchestrator;
|
||||
this.securityAuditService = securityAuditService;
|
||||
}
|
||||
|
||||
public Map<String, Object> getShieldStatus() {
|
||||
ShieldSnapshot snapshot = buildSnapshot("status");
|
||||
Map<String, Object> result = snapshot.toMap();
|
||||
result.put("enabled", config.getSecurity().getShield().isEnabled());
|
||||
result.put("currentMode", currentMode.name());
|
||||
result.put("recommendedMode", recommendMode(snapshot).name());
|
||||
result.put("deterrenceMessage", composeDeterrenceNotice(snapshot.primaryOffenderIp, "access denied"));
|
||||
if (!transitions.isEmpty()) {
|
||||
result.put("lastTransition", transitions.peekFirst().toMap());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Map<String, Object> engage(String requestedMode, String reason) {
|
||||
if (!config.getSecurity().getShield().isEnabled()) {
|
||||
return Map.of("enabled", false, "status", "disabled");
|
||||
}
|
||||
|
||||
ShieldSnapshot snapshot = buildSnapshot("engage");
|
||||
ShieldMode targetMode = parseMode(requestedMode, recommendMode(snapshot));
|
||||
List<Map<String, Object>> actions = new ArrayList<>();
|
||||
String actionReason = reason == null || reason.isBlank() ? "manual-console-engage" : reason.trim();
|
||||
List<Map<String, Object>> topOffenders = networkThreatFusionService.getTopRiskIPs(
|
||||
Math.max(1, config.getSecurity().getShield().getEdgeChallengeOffenderLimit())
|
||||
);
|
||||
String notice = composeDeterrenceNotice(snapshot.primaryOffenderIp, actionReason);
|
||||
|
||||
switch (targetMode) {
|
||||
case OBSERVE -> {
|
||||
distributedAttackMitigationService.setDefenseLevel("LOW");
|
||||
loadBalancerService.setAlgorithm("ROUND_ROBIN");
|
||||
actions.add(action("defense-level", "LOW", true));
|
||||
actions.add(action("load-balancer", "ROUND_ROBIN", true));
|
||||
}
|
||||
case FORTIFY -> {
|
||||
distributedAttackMitigationService.setDefenseLevel("MEDIUM");
|
||||
cloudflareIntegrationService.setSecurityLevel(resolveZoneId(), "high");
|
||||
actions.add(action("defense-level", "MEDIUM", true));
|
||||
actions.add(action("edge-security-level", "high", true));
|
||||
submitTask(actions, AluerKernelTaskBus.TaskType.SNAPSHOT_STATE, 74, Map.of("reason", actionReason));
|
||||
submitTask(actions, AluerKernelTaskBus.TaskType.SYNC_DEFENSE_LEVEL, 72, Map.of("defenseLevel", "ELEVATED"));
|
||||
}
|
||||
case MIRAGE -> {
|
||||
distributedAttackMitigationService.setDefenseLevel("HIGH");
|
||||
loadBalancerService.setAlgorithm("LEAST_CONNECTIONS");
|
||||
actions.add(action("defense-level", "HIGH", true));
|
||||
actions.add(action("load-balancer", "LEAST_CONNECTIONS", true));
|
||||
challengeTopOffenders(actions, topOffenders, notice);
|
||||
rateLimitTopOffenders(actions, topOffenders, "mirage-shield");
|
||||
submitTask(actions, AluerKernelTaskBus.TaskType.RELIEVE_PRESSURE, 84, Map.of("reason", actionReason));
|
||||
submitTask(actions, AluerKernelTaskBus.TaskType.SNAPSHOT_STATE, 76, Map.of("reason", "mirage-forensics"));
|
||||
}
|
||||
case SHELTER -> {
|
||||
distributedAttackMitigationService.setDefenseLevel("HIGH");
|
||||
loadBalancerService.setAlgorithm("RESPONSE_TIME");
|
||||
actions.add(action("defense-level", "HIGH", true));
|
||||
actions.add(action("load-balancer", "RESPONSE_TIME", true));
|
||||
if (config.getSecurity().getShield().isAutoEnableUnderAttack()) {
|
||||
CloudflareIntegrationService.EdgeActionResult edgeResult =
|
||||
cloudflareIntegrationService.setUnderAttackMode(true, actionReason);
|
||||
actions.add(edgeAction("under-attack", edgeResult));
|
||||
}
|
||||
challengeTopOffenders(actions, topOffenders, notice);
|
||||
blockTopOffenders(actions, topOffenders, "shelter-mode");
|
||||
submitTask(actions, AluerKernelTaskBus.TaskType.WHITELIST_LOCKDOWN, 95, Map.of(
|
||||
"reason", actionReason,
|
||||
"notice", notice
|
||||
));
|
||||
submitTask(actions, AluerKernelTaskBus.TaskType.PREPARE_BACKUP, 92, Map.of("reason", "shield-shelter"));
|
||||
submitTask(actions, AluerKernelTaskBus.TaskType.SNAPSHOT_STATE, 80, Map.of("reason", "shield-shelter"));
|
||||
actions.add(action("self-healing", aluerSelfHealingOrchestrator.runHealingCycle("mirage-shield").getSummary(), true));
|
||||
}
|
||||
case RECOVERY -> {
|
||||
distributedAttackMitigationService.setDefenseLevel("MEDIUM");
|
||||
loadBalancerService.setAlgorithm("RESPONSE_TIME");
|
||||
actions.add(action("defense-level", "MEDIUM", true));
|
||||
actions.add(action("load-balancer", "RESPONSE_TIME", true));
|
||||
actions.add(action("self-healing", aluerSelfHealingOrchestrator.runHealingCycle("mirage-recovery").getSummary(), true));
|
||||
submitTask(actions, AluerKernelTaskBus.TaskType.SYNC_DEFENSE_LEVEL, 72, Map.of("defenseLevel", "ELEVATED"));
|
||||
}
|
||||
}
|
||||
|
||||
List<AluerKernelTaskBus.KernelTaskResult> dispatched = aluerKernelTaskBus.dispatchQueuedTasks(4);
|
||||
if (!dispatched.isEmpty()) {
|
||||
actions.add(action("task-bus-dispatch", dispatched.stream().map(AluerKernelTaskBus.KernelTaskResult::getSummary).toList(), true));
|
||||
}
|
||||
|
||||
currentMode = targetMode;
|
||||
ShieldTransition transition = new ShieldTransition(targetMode.name(), actionReason, snapshot.riskScore, snapshot.kernelHeat, actions);
|
||||
rememberTransition(transition);
|
||||
securityAuditService.logEvent("ALUER_MIRAGE_SHIELD", targetMode.name(), "ENGAGE", actionReason);
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("status", "engaged");
|
||||
result.put("mode", targetMode.name());
|
||||
result.put("reason", actionReason);
|
||||
result.put("riskScore", snapshot.riskScore);
|
||||
result.put("deterrenceMessage", notice);
|
||||
result.put("actions", actions);
|
||||
result.put("topOffenders", topOffenders);
|
||||
result.put("transition", transition.toMap());
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<ShieldTransition> getRecentTransitions(int limit) {
|
||||
List<ShieldTransition> result = new ArrayList<>();
|
||||
int count = 0;
|
||||
for (ShieldTransition transition : transitions) {
|
||||
if (count++ >= limit) {
|
||||
break;
|
||||
}
|
||||
result.add(transition);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String composeDeterrenceNotice(String ip, String reason) {
|
||||
if (!config.getSecurity().getShield().isAttackerNoticeEnabled()) {
|
||||
return "";
|
||||
}
|
||||
String source = ip == null || ip.isBlank() ? "unknown-source" : ip;
|
||||
String details = reason == null || reason.isBlank() ? "policy enforcement" : reason;
|
||||
return "ALUER NOTICE: source " + source + " has been identified, recorded, and isolated. " + details + ".";
|
||||
}
|
||||
|
||||
private ShieldSnapshot buildSnapshot(String trigger) {
|
||||
Map<String, Object> posture = networkThreatFusionService.getPosture();
|
||||
Map<String, Object> ddosPosture = ddosDefenseCoordinator.getPosture();
|
||||
SecurityBaselineHardeningService.HardeningReport hardening = securityBaselineHardeningService.assessCurrentBaseline();
|
||||
Map<String, Object> kernel = aluerKernelEngine.getKernelStatus();
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> lastPulse = (Map<String, Object>) kernel.getOrDefault("lastPulse", Collections.emptyMap());
|
||||
int postureScore = asInt(posture.get("postureScore"));
|
||||
int kernelHeat = asInt(lastPulse.get("heat"));
|
||||
int kernelResonance = asInt(lastPulse.get("resonance"));
|
||||
int criticalRiskIps = asInt(posture.get("criticalRiskIPs"));
|
||||
int activeIncidents = asInt(ddosPosture.get("activeIncidents"));
|
||||
int commandCritical = asInt(commandExecutionGuardService.getStats().get("criticalIncidents"));
|
||||
int hardeningCritical = hardening.getCriticalCount();
|
||||
int riskScore = Math.min(100,
|
||||
Math.max(0, 100 - postureScore)
|
||||
+ kernelHeat / 2
|
||||
+ kernelResonance / 3
|
||||
+ criticalRiskIps * 9
|
||||
+ activeIncidents * 4
|
||||
+ commandCritical * 12
|
||||
+ hardeningCritical * 10
|
||||
);
|
||||
List<Map<String, Object>> topOffenders = networkThreatFusionService.getTopRiskIPs(5);
|
||||
String primaryOffenderIp = topOffenders.isEmpty()
|
||||
? ""
|
||||
: String.valueOf(topOffenders.get(0).getOrDefault("ip", ""));
|
||||
return new ShieldSnapshot(
|
||||
trigger,
|
||||
posture,
|
||||
ddosPosture,
|
||||
hardening,
|
||||
kernelHeat,
|
||||
kernelResonance,
|
||||
riskScore,
|
||||
primaryOffenderIp,
|
||||
topOffenders
|
||||
);
|
||||
}
|
||||
|
||||
private ShieldMode recommendMode(ShieldSnapshot snapshot) {
|
||||
if (snapshot.riskScore >= config.getSecurity().getShield().getThreatScoreTrigger()
|
||||
|| snapshot.kernelHeat >= config.getSecurity().getKernel().getLockdownHeatThreshold()) {
|
||||
return ShieldMode.SHELTER;
|
||||
}
|
||||
if (snapshot.kernelHeat >= config.getSecurity().getShield().getHeatTrigger()
|
||||
|| snapshot.kernelResonance >= config.getSecurity().getShield().getResonanceTrigger()) {
|
||||
return ShieldMode.MIRAGE;
|
||||
}
|
||||
if (snapshot.riskScore >= config.getSecurity().getShield().getThreatScoreTrigger() - 20) {
|
||||
return ShieldMode.FORTIFY;
|
||||
}
|
||||
return ShieldMode.OBSERVE;
|
||||
}
|
||||
|
||||
private ShieldMode parseMode(String requestedMode, ShieldMode fallback) {
|
||||
if (requestedMode == null || requestedMode.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return ShieldMode.valueOf(requestedMode.trim().toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private void submitTask(List<Map<String, Object>> actions,
|
||||
AluerKernelTaskBus.TaskType type,
|
||||
int priority,
|
||||
Map<String, Object> payload) {
|
||||
String taskId = aluerKernelTaskBus.submitTask(type, "mirage-shield", priority, payload);
|
||||
actions.add(action("task:" + type.name(), taskId, true));
|
||||
}
|
||||
|
||||
private void challengeTopOffenders(List<Map<String, Object>> actions,
|
||||
List<Map<String, Object>> topOffenders,
|
||||
String notice) {
|
||||
for (Map<String, Object> offender : topOffenders) {
|
||||
String ip = String.valueOf(offender.getOrDefault("ip", ""));
|
||||
if (ip.isBlank() || asInt(offender.get("riskScore")) < 55) {
|
||||
continue;
|
||||
}
|
||||
CloudflareIntegrationService.EdgeActionResult result = cloudflareIntegrationService.applyChallenge(ip, notice);
|
||||
actions.add(edgeAction("challenge:" + ip, result));
|
||||
}
|
||||
}
|
||||
|
||||
private void rateLimitTopOffenders(List<Map<String, Object>> actions,
|
||||
List<Map<String, Object>> topOffenders,
|
||||
String reason) {
|
||||
for (Map<String, Object> offender : topOffenders) {
|
||||
String ip = String.valueOf(offender.getOrDefault("ip", ""));
|
||||
if (ip.isBlank() || asInt(offender.get("riskScore")) < 60) {
|
||||
continue;
|
||||
}
|
||||
HostEnforcementService.EnforcementActionResult result = hostEnforcementService.rateLimitIp(
|
||||
ip,
|
||||
config.getSecurity().getShield().getShelterRateLimitPerMinute(),
|
||||
reason
|
||||
);
|
||||
actions.add(hostAction("rate-limit:" + ip, result));
|
||||
}
|
||||
}
|
||||
|
||||
private void blockTopOffenders(List<Map<String, Object>> actions,
|
||||
List<Map<String, Object>> topOffenders,
|
||||
String reason) {
|
||||
for (Map<String, Object> offender : topOffenders) {
|
||||
String ip = String.valueOf(offender.getOrDefault("ip", ""));
|
||||
if (ip.isBlank() || asInt(offender.get("riskScore")) < 70) {
|
||||
continue;
|
||||
}
|
||||
HostEnforcementService.EnforcementActionResult result = hostEnforcementService.blockIp(
|
||||
ip,
|
||||
reason,
|
||||
config.getSecurity().getHostEnforcement().getDefaultBlockMinutes()
|
||||
);
|
||||
actions.add(hostAction("block:" + ip, result));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> action(String type, Object details, boolean success) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("type", type);
|
||||
result.put("details", details);
|
||||
result.put("success", success);
|
||||
result.put("timestamp", Instant.now().toEpochMilli());
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> edgeAction(String type, CloudflareIntegrationService.EdgeActionResult result) {
|
||||
return action(type, Map.<String, Object>of(
|
||||
"success", result.isSuccess(),
|
||||
"dryRun", result.isDryRun(),
|
||||
"mode", result.getAction(),
|
||||
"ip", result.getIp(),
|
||||
"reason", result.getReason()
|
||||
), result.isSuccess());
|
||||
}
|
||||
|
||||
private Map<String, Object> hostAction(String type, HostEnforcementService.EnforcementActionResult result) {
|
||||
return action(type, Map.of(
|
||||
"success", result.isSuccess(),
|
||||
"dryRun", result.isDryRun(),
|
||||
"backend", result.getBackend(),
|
||||
"status", result.getStatus(),
|
||||
"ip", result.getIp()
|
||||
), result.isSuccess());
|
||||
}
|
||||
|
||||
private void rememberTransition(ShieldTransition transition) {
|
||||
transitions.offerFirst(transition);
|
||||
while (transitions.size() > 120) {
|
||||
transitions.pollLast();
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveZoneId() {
|
||||
Object zoneId = cloudflareIntegrationService.getStats().get("zoneId");
|
||||
return zoneId == null ? "" : String.valueOf(zoneId);
|
||||
}
|
||||
|
||||
private int asInt(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
if (value instanceof Collection<?> collection) {
|
||||
return collection.size();
|
||||
}
|
||||
try {
|
||||
return value == null ? 0 : Integer.parseInt(String.valueOf(value));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private enum ShieldMode {
|
||||
OBSERVE,
|
||||
FORTIFY,
|
||||
MIRAGE,
|
||||
SHELTER,
|
||||
RECOVERY
|
||||
}
|
||||
|
||||
private record ShieldSnapshot(
|
||||
String trigger,
|
||||
Map<String, Object> posture,
|
||||
Map<String, Object> ddosPosture,
|
||||
SecurityBaselineHardeningService.HardeningReport hardening,
|
||||
int kernelHeat,
|
||||
int kernelResonance,
|
||||
int riskScore,
|
||||
String primaryOffenderIp,
|
||||
List<Map<String, Object>> topOffenders
|
||||
) {
|
||||
private Map<String, Object> toMap() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("trigger", trigger);
|
||||
result.put("riskScore", riskScore);
|
||||
result.put("kernelHeat", kernelHeat);
|
||||
result.put("kernelResonance", kernelResonance);
|
||||
result.put("primaryOffenderIp", primaryOffenderIp);
|
||||
result.put("topOffenders", topOffenders);
|
||||
result.put("posture", posture);
|
||||
result.put("ddos", ddosPosture);
|
||||
result.put("hardening", hardening.toMap());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShieldTransition {
|
||||
private final String mode;
|
||||
private final String reason;
|
||||
private final int riskScore;
|
||||
private final int kernelHeat;
|
||||
private final List<Map<String, Object>> actions;
|
||||
private final long timestamp;
|
||||
|
||||
public ShieldTransition(String mode,
|
||||
String reason,
|
||||
int riskScore,
|
||||
int kernelHeat,
|
||||
List<Map<String, Object>> actions) {
|
||||
this.mode = mode;
|
||||
this.reason = reason;
|
||||
this.riskScore = riskScore;
|
||||
this.kernelHeat = kernelHeat;
|
||||
this.actions = actions == null ? List.of() : new ArrayList<>(actions);
|
||||
this.timestamp = Instant.now().toEpochMilli();
|
||||
}
|
||||
|
||||
public String getMode() { return mode; }
|
||||
public String getReason() { return reason; }
|
||||
public int getRiskScore() { return riskScore; }
|
||||
public int getKernelHeat() { return kernelHeat; }
|
||||
public List<Map<String, Object>> getActions() { return actions; }
|
||||
public long getTimestamp() { return timestamp; }
|
||||
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("mode", mode);
|
||||
result.put("reason", reason);
|
||||
result.put("riskScore", riskScore);
|
||||
result.put("kernelHeat", kernelHeat);
|
||||
result.put("actions", actions);
|
||||
result.put("timestamp", timestamp);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package com.aluer.console;
|
||||
|
||||
import com.aluer.ai.AIAutonomousService;
|
||||
import com.aluer.ai.AluerSovereignEngine;
|
||||
import com.aluer.audit.SecurityAuditService;
|
||||
import com.aluer.backup.BackupService;
|
||||
import com.aluer.config.ServerGuardConfig;
|
||||
import com.aluer.kernel.AluerKernelEngine;
|
||||
import com.aluer.kernel.AluerKernelTaskBus;
|
||||
import com.aluer.kernel.AluerSelfHealingOrchestrator;
|
||||
import com.aluer.model.MetricsData;
|
||||
import com.aluer.monitor.ProcessMonitor;
|
||||
import com.aluer.monitor.ResourceMonitor;
|
||||
import com.aluer.security.CloudflareIntegrationService;
|
||||
import com.aluer.security.CommandExecutionGuardService;
|
||||
import com.aluer.security.DDoSDefenseCoordinator;
|
||||
import com.aluer.security.DistributedAttackMitigationService;
|
||||
import com.aluer.security.HostEnforcementService;
|
||||
import com.aluer.security.LoadBalancerService;
|
||||
import com.aluer.security.NetworkThreatFusionService;
|
||||
import com.aluer.security.SecurityBaselineHardeningService;
|
||||
import com.aluer.service.RconClient;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class AluerOperationsCenterService {
|
||||
private final ServerGuardConfig config;
|
||||
private final ResourceMonitor resourceMonitor;
|
||||
private final ProcessMonitor processMonitor;
|
||||
private final RconClient rconClient;
|
||||
private final BackupService backupService;
|
||||
private final NetworkThreatFusionService networkThreatFusionService;
|
||||
private final DDoSDefenseCoordinator ddosDefenseCoordinator;
|
||||
private final DistributedAttackMitigationService distributedAttackMitigationService;
|
||||
private final SecurityBaselineHardeningService securityBaselineHardeningService;
|
||||
private final SecurityAuditService securityAuditService;
|
||||
private final AluerSovereignEngine aluerSovereignEngine;
|
||||
private final AIAutonomousService aiAutonomousService;
|
||||
private final AluerKernelEngine aluerKernelEngine;
|
||||
private final AluerKernelTaskBus aluerKernelTaskBus;
|
||||
private final AluerSelfHealingOrchestrator aluerSelfHealingOrchestrator;
|
||||
private final HostEnforcementService hostEnforcementService;
|
||||
private final CloudflareIntegrationService cloudflareIntegrationService;
|
||||
private final LoadBalancerService loadBalancerService;
|
||||
private final CommandExecutionGuardService commandExecutionGuardService;
|
||||
private final AluerMirageShieldService aluerMirageShieldService;
|
||||
private final RemoteSshGatewayService remoteSshGatewayService;
|
||||
|
||||
public AluerOperationsCenterService(ServerGuardConfig config,
|
||||
ResourceMonitor resourceMonitor,
|
||||
ProcessMonitor processMonitor,
|
||||
RconClient rconClient,
|
||||
BackupService backupService,
|
||||
NetworkThreatFusionService networkThreatFusionService,
|
||||
DDoSDefenseCoordinator ddosDefenseCoordinator,
|
||||
DistributedAttackMitigationService distributedAttackMitigationService,
|
||||
SecurityBaselineHardeningService securityBaselineHardeningService,
|
||||
SecurityAuditService securityAuditService,
|
||||
AluerSovereignEngine aluerSovereignEngine,
|
||||
AIAutonomousService aiAutonomousService,
|
||||
AluerKernelEngine aluerKernelEngine,
|
||||
AluerKernelTaskBus aluerKernelTaskBus,
|
||||
AluerSelfHealingOrchestrator aluerSelfHealingOrchestrator,
|
||||
HostEnforcementService hostEnforcementService,
|
||||
CloudflareIntegrationService cloudflareIntegrationService,
|
||||
LoadBalancerService loadBalancerService,
|
||||
CommandExecutionGuardService commandExecutionGuardService,
|
||||
AluerMirageShieldService aluerMirageShieldService,
|
||||
RemoteSshGatewayService remoteSshGatewayService) {
|
||||
this.config = config;
|
||||
this.resourceMonitor = resourceMonitor;
|
||||
this.processMonitor = processMonitor;
|
||||
this.rconClient = rconClient;
|
||||
this.backupService = backupService;
|
||||
this.networkThreatFusionService = networkThreatFusionService;
|
||||
this.ddosDefenseCoordinator = ddosDefenseCoordinator;
|
||||
this.distributedAttackMitigationService = distributedAttackMitigationService;
|
||||
this.securityBaselineHardeningService = securityBaselineHardeningService;
|
||||
this.securityAuditService = securityAuditService;
|
||||
this.aluerSovereignEngine = aluerSovereignEngine;
|
||||
this.aiAutonomousService = aiAutonomousService;
|
||||
this.aluerKernelEngine = aluerKernelEngine;
|
||||
this.aluerKernelTaskBus = aluerKernelTaskBus;
|
||||
this.aluerSelfHealingOrchestrator = aluerSelfHealingOrchestrator;
|
||||
this.hostEnforcementService = hostEnforcementService;
|
||||
this.cloudflareIntegrationService = cloudflareIntegrationService;
|
||||
this.loadBalancerService = loadBalancerService;
|
||||
this.commandExecutionGuardService = commandExecutionGuardService;
|
||||
this.aluerMirageShieldService = aluerMirageShieldService;
|
||||
this.remoteSshGatewayService = remoteSshGatewayService;
|
||||
}
|
||||
|
||||
public Map<String, Object> buildOverview() {
|
||||
MetricsData metrics = resourceMonitor.collectMetrics();
|
||||
Map<String, Object> network = networkThreatFusionService.getPosture();
|
||||
Map<String, Object> ddos = ddosDefenseCoordinator.getPosture();
|
||||
Map<String, Object> sovereign = aluerSovereignEngine.getEngineStatus();
|
||||
Map<String, Object> hardening = securityBaselineHardeningService.getSummary();
|
||||
Map<String, Object> shield = aluerMirageShieldService.getShieldStatus();
|
||||
Map<String, Object> ssh = remoteSshGatewayService.getGatewayStatus();
|
||||
|
||||
Map<String, Object> server = new LinkedHashMap<>();
|
||||
server.put("serviceName", config.getMinecraft().getServiceName());
|
||||
server.put("workingDir", config.getMinecraft().getWorkingDir());
|
||||
server.put("processRunning", processMonitor.isProcessRunning());
|
||||
server.put("rconConnected", rconClient.isConnected());
|
||||
server.put("metrics", Map.of(
|
||||
"tps", metrics.getTps(),
|
||||
"cpuUsage", metrics.getCpuUsage(),
|
||||
"memoryUsage", metrics.getMemoryUsage(),
|
||||
"onlinePlayers", metrics.getOnlinePlayers(),
|
||||
"connections", metrics.getConnections(),
|
||||
"tickTime", metrics.getTickTime()
|
||||
));
|
||||
|
||||
Map<String, Object> defense = new LinkedHashMap<>();
|
||||
defense.put("network", network);
|
||||
defense.put("ddos", ddos);
|
||||
defense.put("sovereign", sovereign);
|
||||
defense.put("shield", shield);
|
||||
defense.put("hardening", hardening);
|
||||
defense.put("cloudEdge", cloudflareIntegrationService.getStats());
|
||||
defense.put("hostEnforcement", hostEnforcementService.getStats());
|
||||
defense.put("distributedMitigation", distributedAttackMitigationService.getStats());
|
||||
defense.put("loadBalancer", loadBalancerService.getStats());
|
||||
defense.put("commandGuard", commandExecutionGuardService.getStats());
|
||||
|
||||
Map<String, Object> kernel = new LinkedHashMap<>();
|
||||
kernel.put("engine", aluerKernelEngine.getKernelStatus());
|
||||
kernel.put("taskBus", aluerKernelTaskBus.getBusStatus());
|
||||
kernel.put("selfHealing", aluerSelfHealingOrchestrator.getStatus());
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("title", config.getDashboard().getTitle());
|
||||
result.put("subtitle", config.getDashboard().getSubtitle());
|
||||
result.put("refreshIntervalSeconds", config.getDashboard().getRefreshIntervalSeconds());
|
||||
result.put("timestamp", Instant.now().toEpochMilli());
|
||||
result.put("server", server);
|
||||
result.put("defense", defense);
|
||||
result.put("ai", aiAutonomousService.getAutonomousStats());
|
||||
result.put("kernel", kernel);
|
||||
result.put("backups", Map.of("count", backupService.getBackupHistory().size()));
|
||||
result.put("ssh", ssh);
|
||||
result.put("modules", buildModuleCatalog());
|
||||
result.put("audit", buildAuditFeed(12));
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> buildModuleCatalog() {
|
||||
List<Map<String, Object>> modules = new ArrayList<>();
|
||||
Map<String, Object> shield = aluerMirageShieldService.getShieldStatus();
|
||||
Map<String, Object> sovereign = aluerSovereignEngine.getEngineStatus();
|
||||
Map<String, Object> kernel = aluerKernelEngine.getKernelStatus();
|
||||
Map<String, Object> taskBus = aluerKernelTaskBus.getBusStatus();
|
||||
Map<String, Object> selfHealing = aluerSelfHealingOrchestrator.getStatus();
|
||||
Map<String, Object> ssh = remoteSshGatewayService.getGatewayStatus();
|
||||
|
||||
modules.add(module("mirage-shield", "Mirage Shield", String.valueOf(shield.get("currentMode")),
|
||||
"偏转护盾 / DDoS 防线 / 攻击者告警", intOf(shield.get("riskScore"))));
|
||||
modules.add(module("sovereign-engine", "Sovereign Loop", String.valueOf(sovereign.get("engine")),
|
||||
"主控决策环与 DeepSeek 协同", intOf(sovereign.get("kernelHeat"))));
|
||||
modules.add(module("kernel-engine", "Aluer Kernel", "ACTIVE",
|
||||
"Threat Mesh / Echo Grid / Strain Matrix", lastPulseValue(kernel, "heat")));
|
||||
modules.add(module("task-bus", "Kernel Task Bus", "QUEUE=" + taskBus.get("queuedTasks"),
|
||||
"插件协议与优先级派发", intOf(taskBus.get("executedTasks"))));
|
||||
modules.add(module("self-healing", "Self-Healing", String.valueOf(selfHealing.get("dryRun")),
|
||||
"恢复编排与软重启护栏", intOf(selfHealing.get("cycles"))));
|
||||
modules.add(module("ssh-gateway", "Remote SSH", String.valueOf(ssh.get("activeSessions")),
|
||||
"远程连接、命令执行、审计联动", intOf(ssh.get("activeSessions"))));
|
||||
modules.add(module("cloud-edge", "Cloud Edge", String.valueOf(cloudflareIntegrationService.getStats().get("enabled")),
|
||||
"边缘挑战、Under Attack、缓存侧协同", intOf(cloudflareIntegrationService.getStats().get("blockedRequests"))));
|
||||
modules.add(module("host-enforcement", "Host Enforcement", String.valueOf(hostEnforcementService.getStats().get("backend")),
|
||||
"本机封禁与速率限制", intOf(hostEnforcementService.getStats().get("managedRules"))));
|
||||
return modules;
|
||||
}
|
||||
|
||||
public Map<String, Object> buildAuditFeed(int limit) {
|
||||
List<Map<String, Object>> auditEvents = securityAuditService.getRecentEvents(limit).stream()
|
||||
.map(event -> Map.<String, Object>of(
|
||||
"category", event.getCategory(),
|
||||
"player", event.getPlayer(),
|
||||
"action", event.getAction(),
|
||||
"details", event.getDetails(),
|
||||
"timestamp", String.valueOf(event.getTimestamp())
|
||||
))
|
||||
.toList();
|
||||
List<Map<String, Object>> networkIncidents = networkThreatFusionService.getRecentIncidents(Math.max(4, limit / 2));
|
||||
List<Map<String, Object>> shieldTransitions = aluerMirageShieldService.getRecentTransitions(5).stream()
|
||||
.map(AluerMirageShieldService.ShieldTransition::toMap)
|
||||
.toList();
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("events", auditEvents);
|
||||
result.put("networkIncidents", networkIncidents);
|
||||
result.put("shieldTransitions", shieldTransitions);
|
||||
result.put("activeAlerts", securityAuditService.getActiveAlerts());
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> module(String id, String name, String status, String summary, int signal) {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("id", id);
|
||||
result.put("name", name);
|
||||
result.put("status", status);
|
||||
result.put("summary", summary);
|
||||
result.put("signal", signal);
|
||||
return result;
|
||||
}
|
||||
|
||||
private int lastPulseValue(Map<String, Object> kernelStatus, String field) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> lastPulse = (Map<String, Object>) kernelStatus.get("lastPulse");
|
||||
return lastPulse == null ? 0 : intOf(lastPulse.get(field));
|
||||
}
|
||||
|
||||
private int intOf(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
}
|
||||
try {
|
||||
return value == null ? 0 : Integer.parseInt(String.valueOf(value));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
package com.aluer.console;
|
||||
|
||||
import com.aluer.audit.SecurityAuditService;
|
||||
import com.aluer.config.ServerGuardConfig;
|
||||
import com.aluer.security.CommandExecutionGuardService;
|
||||
import com.jcraft.jsch.ChannelExec;
|
||||
import com.jcraft.jsch.HostKey;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
import com.jcraft.jsch.Session;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class RemoteSshGatewayService {
|
||||
private static final int CONNECT_TIMEOUT_MS = 12_000;
|
||||
private static final int MAX_OUTPUT_CHARS = 16_000;
|
||||
|
||||
private final ServerGuardConfig config;
|
||||
private final CommandExecutionGuardService commandExecutionGuardService;
|
||||
private final SecurityAuditService securityAuditService;
|
||||
private final AluerEngineHandshakeService aluerEngineHandshakeService;
|
||||
private final Map<String, ActiveSession> sessions = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "aluer-ssh-gateway");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
|
||||
@Autowired
|
||||
public RemoteSshGatewayService(ServerGuardConfig config,
|
||||
CommandExecutionGuardService commandExecutionGuardService,
|
||||
SecurityAuditService securityAuditService,
|
||||
AluerEngineHandshakeService aluerEngineHandshakeService) {
|
||||
this.config = config;
|
||||
this.commandExecutionGuardService = commandExecutionGuardService;
|
||||
this.securityAuditService = securityAuditService;
|
||||
this.aluerEngineHandshakeService = aluerEngineHandshakeService;
|
||||
startCleanupLoop();
|
||||
}
|
||||
|
||||
public RemoteSshGatewayService(ServerGuardConfig config,
|
||||
CommandExecutionGuardService commandExecutionGuardService,
|
||||
SecurityAuditService securityAuditService) {
|
||||
this(config, commandExecutionGuardService, securityAuditService, null);
|
||||
}
|
||||
|
||||
public AluerEngineHandshakeService.HandshakeResult requestHandshake(HandshakeRequest request) {
|
||||
ensureEnabled();
|
||||
if (aluerEngineHandshakeService == null) {
|
||||
throw new IllegalStateException("主引擎握手服务不可用");
|
||||
}
|
||||
return aluerEngineHandshakeService.requestSshHandshake(
|
||||
new AluerEngineHandshakeService.HandshakeIntent(
|
||||
request.host(),
|
||||
request.port(),
|
||||
request.username(),
|
||||
request.purpose()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public SessionView connect(ConnectRequest request) {
|
||||
ensureEnabled();
|
||||
cleanupExpiredSessions();
|
||||
if (sessions.size() >= Math.max(1, config.getDashboard().getSshGateway().getMaxSessions())) {
|
||||
throw new IllegalStateException("SSH 会话已达到上限");
|
||||
}
|
||||
|
||||
String host = requireText(request.host(), "host");
|
||||
String username = requireText(request.username(), "username");
|
||||
int port = request.port() > 0 ? request.port() : 22;
|
||||
boolean hasPassword = request.password() != null && !request.password().isBlank();
|
||||
boolean hasInlineKey = request.privateKey() != null && !request.privateKey().isBlank();
|
||||
boolean hasKeyPath = request.privateKeyPath() != null && !request.privateKeyPath().isBlank();
|
||||
if (!hasPassword && !hasInlineKey && !hasKeyPath) {
|
||||
throw new IllegalArgumentException("请提供 SSH 密码或私钥");
|
||||
}
|
||||
if (hasInlineKey && !config.getDashboard().getSshGateway().isAllowPrivateKeyPaste()) {
|
||||
throw new IllegalArgumentException("当前配置不允许粘贴私钥");
|
||||
}
|
||||
|
||||
String alias = request.alias() == null || request.alias().isBlank()
|
||||
? username + "@" + host
|
||||
: request.alias().trim();
|
||||
|
||||
validateHandshake(request, host, port, username);
|
||||
|
||||
try {
|
||||
JSch jsch = new JSch();
|
||||
if (hasInlineKey) {
|
||||
byte[] passphrase = toBytes(request.passphrase());
|
||||
jsch.addIdentity(alias, request.privateKey().getBytes(StandardCharsets.UTF_8), null, passphrase);
|
||||
} else if (hasKeyPath) {
|
||||
if (request.passphrase() != null && !request.passphrase().isBlank()) {
|
||||
jsch.addIdentity(request.privateKeyPath(), request.passphrase());
|
||||
} else {
|
||||
jsch.addIdentity(request.privateKeyPath());
|
||||
}
|
||||
}
|
||||
|
||||
Session session = jsch.getSession(username, host, port);
|
||||
if (hasPassword) {
|
||||
session.setPassword(request.password());
|
||||
}
|
||||
session.setConfig("StrictHostKeyChecking",
|
||||
config.getDashboard().getSshGateway().isStrictHostKeyChecking() ? "yes" : "no");
|
||||
session.connect(CONNECT_TIMEOUT_MS);
|
||||
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
HostKey hostKey = session.getHostKey();
|
||||
ActiveSession activeSession = new ActiveSession(
|
||||
sessionId,
|
||||
alias,
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
hostKey == null ? "" : hostKey.getFingerPrint(jsch),
|
||||
session
|
||||
);
|
||||
sessions.put(sessionId, activeSession);
|
||||
securityAuditService.logEvent("SSH_GATEWAY", alias, "CONNECT", host + ":" + port);
|
||||
return activeSession.toView();
|
||||
} catch (JSchException e) {
|
||||
throw new IllegalStateException("SSH 连接失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public SshCommandResult execute(String sessionId, String command) {
|
||||
ensureEnabled();
|
||||
ActiveSession activeSession = requireSession(sessionId);
|
||||
String normalizedCommand = requireText(command, "command");
|
||||
activeSession.touch();
|
||||
|
||||
CommandExecutionGuardService.AntiIntrusionIncident guardIncident =
|
||||
commandExecutionGuardService.analyzeCommand("ssh:" + activeSession.username, normalizedCommand, activeSession.host);
|
||||
|
||||
ChannelExec channel = null;
|
||||
long started = System.currentTimeMillis();
|
||||
try {
|
||||
channel = (ChannelExec) activeSession.session.openChannel("exec");
|
||||
channel.setCommand(normalizedCommand);
|
||||
channel.setInputStream(null);
|
||||
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
|
||||
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
|
||||
channel.setOutputStream(stdout);
|
||||
channel.setErrStream(stderr);
|
||||
channel.connect(CONNECT_TIMEOUT_MS);
|
||||
|
||||
long timeoutAt = started + Math.max(5, config.getDashboard().getSshGateway().getCommandTimeoutSeconds()) * 1000L;
|
||||
while (!channel.isClosed() && System.currentTimeMillis() < timeoutAt) {
|
||||
Thread.sleep(120);
|
||||
}
|
||||
|
||||
boolean timedOut = !channel.isClosed();
|
||||
if (timedOut) {
|
||||
channel.disconnect();
|
||||
}
|
||||
|
||||
int exitStatus = timedOut ? -1 : channel.getExitStatus();
|
||||
long durationMs = System.currentTimeMillis() - started;
|
||||
String stdoutText = trimOutput(stdout.toString(StandardCharsets.UTF_8));
|
||||
String stderrText = trimOutput(stderr.toString(StandardCharsets.UTF_8));
|
||||
securityAuditService.logEvent(
|
||||
"SSH_GATEWAY",
|
||||
activeSession.alias,
|
||||
"EXECUTE",
|
||||
normalizedCommand + " [" + exitStatus + "]"
|
||||
);
|
||||
|
||||
return new SshCommandResult(
|
||||
sessionId,
|
||||
normalizedCommand,
|
||||
stdoutText,
|
||||
stderrText,
|
||||
exitStatus,
|
||||
durationMs,
|
||||
timedOut,
|
||||
guardIncident != null,
|
||||
guardIncident == null ? "" : guardIncident.getType(),
|
||||
guardIncident == null ? 0 : guardIncident.getSeverity(),
|
||||
Instant.now().toEpochMilli()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("SSH 命令执行失败: " + e.getMessage(), e);
|
||||
} finally {
|
||||
if (channel != null && channel.isConnected()) {
|
||||
channel.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SessionView disconnect(String sessionId) {
|
||||
ActiveSession activeSession = requireSession(sessionId);
|
||||
activeSession.session.disconnect();
|
||||
sessions.remove(sessionId);
|
||||
securityAuditService.logEvent("SSH_GATEWAY", activeSession.alias, "DISCONNECT", activeSession.host);
|
||||
return activeSession.toView();
|
||||
}
|
||||
|
||||
public Map<String, Object> getGatewayStatus() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("enabled", config.getDashboard().getSshGateway().isEnabled());
|
||||
result.put("activeSessions", sessions.size());
|
||||
result.put("sessionTimeoutMinutes", config.getDashboard().getSshGateway().getSessionTimeoutMinutes());
|
||||
result.put("maxSessions", config.getDashboard().getSshGateway().getMaxSessions());
|
||||
result.put("strictHostKeyChecking", config.getDashboard().getSshGateway().isStrictHostKeyChecking());
|
||||
result.put("handshakeRequired", config.getDashboard().getSshGateway().isRequireEngineHandshake());
|
||||
if (aluerEngineHandshakeService != null) {
|
||||
result.put("engineHandshake", aluerEngineHandshakeService.getStatus());
|
||||
}
|
||||
result.put("sessions", listSessions());
|
||||
return result;
|
||||
}
|
||||
|
||||
public List<SessionView> listSessions() {
|
||||
cleanupExpiredSessions();
|
||||
return sessions.values().stream()
|
||||
.sorted(Comparator.comparingLong(ActiveSession::getLastUsedAt).reversed())
|
||||
.map(ActiveSession::toView)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void startCleanupLoop() {
|
||||
scheduler.scheduleAtFixedRate(this::cleanupExpiredSessions, 60, 60, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void cleanupExpiredSessions() {
|
||||
long cutoff = System.currentTimeMillis()
|
||||
- Math.max(5, config.getDashboard().getSshGateway().getSessionTimeoutMinutes()) * 60_000L;
|
||||
List<String> expired = new ArrayList<>();
|
||||
for (ActiveSession activeSession : sessions.values()) {
|
||||
if (activeSession.lastUsedAt < cutoff || !activeSession.session.isConnected()) {
|
||||
expired.add(activeSession.sessionId);
|
||||
}
|
||||
}
|
||||
for (String sessionId : expired) {
|
||||
ActiveSession activeSession = sessions.remove(sessionId);
|
||||
if (activeSession != null && activeSession.session.isConnected()) {
|
||||
activeSession.session.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureEnabled() {
|
||||
if (!config.getDashboard().getSshGateway().isEnabled()) {
|
||||
throw new IllegalStateException("SSH Gateway 已禁用");
|
||||
}
|
||||
}
|
||||
|
||||
private ActiveSession requireSession(String sessionId) {
|
||||
String key = requireText(sessionId, "sessionId");
|
||||
ActiveSession activeSession = sessions.get(key);
|
||||
if (activeSession == null || !activeSession.session.isConnected()) {
|
||||
sessions.remove(key);
|
||||
throw new IllegalArgumentException("SSH 会话不存在或已断开");
|
||||
}
|
||||
return activeSession;
|
||||
}
|
||||
|
||||
private String requireText(String value, String field) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(field + " 不能为空");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private void validateHandshake(ConnectRequest request, String host, int port, String username) {
|
||||
if (!config.getDashboard().getSshGateway().isRequireEngineHandshake()) {
|
||||
return;
|
||||
}
|
||||
if (aluerEngineHandshakeService == null) {
|
||||
throw new IllegalStateException("主引擎握手服务不可用");
|
||||
}
|
||||
aluerEngineHandshakeService.consumeSshGrant(request.handshakeToken(), host, port, username);
|
||||
}
|
||||
|
||||
private byte[] toBytes(String value) {
|
||||
return value == null || value.isBlank() ? null : value.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String trimOutput(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
String normalized = value.trim();
|
||||
if (normalized.length() <= MAX_OUTPUT_CHARS) {
|
||||
return normalized;
|
||||
}
|
||||
return normalized.substring(0, MAX_OUTPUT_CHARS) + "\n...[output truncated by Aluer]";
|
||||
}
|
||||
|
||||
public record ConnectRequest(
|
||||
String alias,
|
||||
String host,
|
||||
int port,
|
||||
String username,
|
||||
String password,
|
||||
String privateKey,
|
||||
String privateKeyPath,
|
||||
String passphrase,
|
||||
String handshakeToken
|
||||
) {
|
||||
}
|
||||
|
||||
public record HandshakeRequest(
|
||||
String host,
|
||||
int port,
|
||||
String username,
|
||||
String purpose
|
||||
) {
|
||||
}
|
||||
|
||||
public static class SessionView {
|
||||
private final String sessionId;
|
||||
private final String alias;
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final String username;
|
||||
private final String fingerprint;
|
||||
private final long connectedAt;
|
||||
private final long lastUsedAt;
|
||||
|
||||
public SessionView(String sessionId,
|
||||
String alias,
|
||||
String host,
|
||||
int port,
|
||||
String username,
|
||||
String fingerprint,
|
||||
long connectedAt,
|
||||
long lastUsedAt) {
|
||||
this.sessionId = sessionId;
|
||||
this.alias = alias;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.fingerprint = fingerprint;
|
||||
this.connectedAt = connectedAt;
|
||||
this.lastUsedAt = lastUsedAt;
|
||||
}
|
||||
|
||||
public String getSessionId() { return sessionId; }
|
||||
public String getAlias() { return alias; }
|
||||
public String getHost() { return host; }
|
||||
public int getPort() { return port; }
|
||||
public String getUsername() { return username; }
|
||||
public String getFingerprint() { return fingerprint; }
|
||||
public long getConnectedAt() { return connectedAt; }
|
||||
public long getLastUsedAt() { return lastUsedAt; }
|
||||
}
|
||||
|
||||
public static class SshCommandResult {
|
||||
private final String sessionId;
|
||||
private final String command;
|
||||
private final String stdout;
|
||||
private final String stderr;
|
||||
private final int exitStatus;
|
||||
private final long durationMs;
|
||||
private final boolean timedOut;
|
||||
private final boolean flagged;
|
||||
private final String guardType;
|
||||
private final int guardSeverity;
|
||||
private final long timestamp;
|
||||
|
||||
public SshCommandResult(String sessionId,
|
||||
String command,
|
||||
String stdout,
|
||||
String stderr,
|
||||
int exitStatus,
|
||||
long durationMs,
|
||||
boolean timedOut,
|
||||
boolean flagged,
|
||||
String guardType,
|
||||
int guardSeverity,
|
||||
long timestamp) {
|
||||
this.sessionId = sessionId;
|
||||
this.command = command;
|
||||
this.stdout = stdout;
|
||||
this.stderr = stderr;
|
||||
this.exitStatus = exitStatus;
|
||||
this.durationMs = durationMs;
|
||||
this.timedOut = timedOut;
|
||||
this.flagged = flagged;
|
||||
this.guardType = guardType;
|
||||
this.guardSeverity = guardSeverity;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getSessionId() { return sessionId; }
|
||||
public String getCommand() { return command; }
|
||||
public String getStdout() { return stdout; }
|
||||
public String getStderr() { return stderr; }
|
||||
public int getExitStatus() { return exitStatus; }
|
||||
public long getDurationMs() { return durationMs; }
|
||||
public boolean isTimedOut() { return timedOut; }
|
||||
public boolean isFlagged() { return flagged; }
|
||||
public String getGuardType() { return guardType; }
|
||||
public int getGuardSeverity() { return guardSeverity; }
|
||||
public long getTimestamp() { return timestamp; }
|
||||
}
|
||||
|
||||
private static final class ActiveSession {
|
||||
private final String sessionId;
|
||||
private final String alias;
|
||||
private final String host;
|
||||
private final int port;
|
||||
private final String username;
|
||||
private final String fingerprint;
|
||||
private final Session session;
|
||||
private final long connectedAt;
|
||||
private volatile long lastUsedAt;
|
||||
|
||||
private ActiveSession(String sessionId,
|
||||
String alias,
|
||||
String host,
|
||||
int port,
|
||||
String username,
|
||||
String fingerprint,
|
||||
Session session) {
|
||||
this.sessionId = sessionId;
|
||||
this.alias = alias;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.fingerprint = fingerprint;
|
||||
this.session = session;
|
||||
this.connectedAt = System.currentTimeMillis();
|
||||
this.lastUsedAt = this.connectedAt;
|
||||
}
|
||||
|
||||
private long getLastUsedAt() {
|
||||
return lastUsedAt;
|
||||
}
|
||||
|
||||
private void touch() {
|
||||
this.lastUsedAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private SessionView toView() {
|
||||
return new SessionView(sessionId, alias, host, port, username, fingerprint, connectedAt, lastUsedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user