|
| 1 | +package io.sentrius.agent.analysis.agents.trust; |
| 2 | + |
| 3 | +import io.sentrius.sso.core.model.AgentHeartbeat; |
| 4 | +import io.sentrius.sso.core.model.sessions.SessionLog; |
| 5 | +import io.sentrius.sso.core.model.trust.AgentTrustScoreHistory; |
| 6 | +import io.sentrius.sso.core.model.users.User; |
| 7 | +import io.sentrius.sso.core.model.security.enums.IdentityType; |
| 8 | +import io.sentrius.sso.core.repository.AgentCommunicationRepository; |
| 9 | +import io.sentrius.sso.core.repository.AgentHeartbeatRepository; |
| 10 | +import io.sentrius.sso.core.repository.SessionLogRepository; |
| 11 | +import io.sentrius.sso.core.services.ATPLPolicyService; |
| 12 | +import io.sentrius.sso.core.services.UserService; |
| 13 | +import io.sentrius.sso.core.services.trust.AgentTrustScoreService; |
| 14 | +import io.sentrius.sso.core.trust.*; |
| 15 | +import io.sentrius.sso.provenance.ProvenanceEvent; |
| 16 | +import lombok.extern.slf4j.Slf4j; |
| 17 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| 18 | +import org.springframework.scheduling.annotation.Scheduled; |
| 19 | +import org.springframework.stereotype.Service; |
| 20 | + |
| 21 | +import java.sql.Timestamp; |
| 22 | +import java.time.LocalDateTime; |
| 23 | +import java.util.*; |
| 24 | +import java.util.concurrent.ConcurrentHashMap; |
| 25 | +import java.util.stream.Collectors; |
| 26 | + |
| 27 | +@Service |
| 28 | +@Slf4j |
| 29 | +@ConditionalOnProperty(name = "sentrius.trust.evaluation.enabled", havingValue = "true", matchIfMissing = true) |
| 30 | +public class TrustEvaluationService { |
| 31 | + |
| 32 | + private final AgentHeartbeatRepository heartbeatRepository; |
| 33 | + private final AgentCommunicationRepository communicationRepository; |
| 34 | + private final SessionLogRepository sessionLogRepository; |
| 35 | + private final AgentTrustScoreService trustScoreService; |
| 36 | + private final ATPLPolicyService atplPolicyService; |
| 37 | + private final UserService userService; |
| 38 | + |
| 39 | + private final Map<String, List<ProvenanceEvent>> provenanceCache = new ConcurrentHashMap<>(); |
| 40 | + private final Map<String, Integer> incidentTracker = new ConcurrentHashMap<>(); |
| 41 | + |
| 42 | + public TrustEvaluationService( |
| 43 | + AgentHeartbeatRepository heartbeatRepository, |
| 44 | + AgentCommunicationRepository communicationRepository, |
| 45 | + SessionLogRepository sessionLogRepository, |
| 46 | + AgentTrustScoreService trustScoreService, |
| 47 | + ATPLPolicyService atplPolicyService, |
| 48 | + UserService userService) { |
| 49 | + this.heartbeatRepository = heartbeatRepository; |
| 50 | + this.communicationRepository = communicationRepository; |
| 51 | + this.sessionLogRepository = sessionLogRepository; |
| 52 | + this.trustScoreService = trustScoreService; |
| 53 | + this.atplPolicyService = atplPolicyService; |
| 54 | + this.userService = userService; |
| 55 | + } |
| 56 | + |
| 57 | + @Scheduled(fixedRate = 300000, initialDelay = 60000) |
| 58 | + public void evaluateAllAgentsAndUsers() { |
| 59 | + log.info("Starting scheduled trust evaluation for all agents and users"); |
| 60 | + |
| 61 | + // Evaluate agents (NON_PERSON_ENTITY) |
| 62 | + List<AgentHeartbeat> activeAgents = heartbeatRepository.findAll().stream() |
| 63 | + .filter(hb -> hb.getLastHeartbeat() != null && |
| 64 | + hb.getLastHeartbeat().isAfter(LocalDateTime.now().minusMinutes(30))) |
| 65 | + .collect(Collectors.toList()); |
| 66 | + |
| 67 | + log.info("Found {} active agents to evaluate", activeAgents.size()); |
| 68 | + |
| 69 | + for (AgentHeartbeat heartbeat : activeAgents) { |
| 70 | + try { |
| 71 | + evaluateEntity(heartbeat.getAgentId(), heartbeat.getAgentName(), IdentityType.NON_PERSON_ENTITY); |
| 72 | + } catch (Exception e) { |
| 73 | + log.error("Error evaluating agent {}: {}", heartbeat.getAgentId(), e.getMessage(), e); |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + // Evaluate human users (USER) |
| 78 | + List<User> humanUsers = userService.getAllUsers("USER").stream() |
| 79 | + .map(dto -> userService.getUserByUserid(dto.getId().toString())) |
| 80 | + .filter(Objects::nonNull) |
| 81 | + .collect(Collectors.toList()); |
| 82 | + |
| 83 | + log.info("Found {} human users to evaluate", humanUsers.size()); |
| 84 | + |
| 85 | + for (User user : humanUsers) { |
| 86 | + try { |
| 87 | + evaluateEntity(user.getUserId(), user.getUsername(), IdentityType.USER); |
| 88 | + } catch (Exception e) { |
| 89 | + log.error("Error evaluating user {}: {}", user.getUserId(), e.getMessage(), e); |
| 90 | + } |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + /** |
| 95 | + * Evaluate trust score for a specific entity (agent or user). |
| 96 | + * @deprecated Use evaluateEntity instead |
| 97 | + */ |
| 98 | + @Deprecated |
| 99 | + public AgentTrustScoreHistory evaluateAgent(String agentId, String agentName) { |
| 100 | + return evaluateEntity(agentId, agentName, IdentityType.NON_PERSON_ENTITY); |
| 101 | + } |
| 102 | + |
| 103 | + public AgentTrustScoreHistory evaluateEntity(String entityId, String entityName, IdentityType identityType) { |
| 104 | + log.debug("Evaluating trust score for entity: {} ({}) type: {}", entityName, entityId, identityType); |
| 105 | + |
| 106 | + User user = userService.getUserByUserid(entityId); |
| 107 | + if (user == null) { |
| 108 | + log.warn("No user found for entity ID: {}", entityId); |
| 109 | + return null; |
| 110 | + } |
| 111 | + |
| 112 | + Optional<ATPLPolicy> policyOpt = atplPolicyService.getPolicy(user); |
| 113 | + if (policyOpt.isEmpty()) { |
| 114 | + log.debug("No ATPL policy found for entity: {}", entityId); |
| 115 | + return null; |
| 116 | + } |
| 117 | + |
| 118 | + ATPLPolicy policy = policyOpt.get(); |
| 119 | + AgentContext context = buildEntityContext(entityId, entityName, identityType); |
| 120 | + |
| 121 | + TrustScoreCalculator calculator = new TrustScoreCalculator(); |
| 122 | + int trustScore = calculator.calculate(context, policy); |
| 123 | + |
| 124 | + TrustScoreResult result = determineResult(trustScore, policy.getTrustScore()); |
| 125 | + |
| 126 | + AgentTrustScoreHistory history = AgentTrustScoreHistory.builder() |
| 127 | + .agentId(entityId) |
| 128 | + .agentName(entityName) |
| 129 | + .trustScore(trustScore) |
| 130 | + .identityScore(context.evaluateIdentity()) |
| 131 | + .provenanceScore(context.evaluateProvenance()) |
| 132 | + .runtimeScore(context.evaluateRuntime()) |
| 133 | + .behaviorScore(context.evaluateBehavior()) |
| 134 | + .evaluationResult(result.name()) |
| 135 | + .policyId(policy.getPolicyId()) |
| 136 | + .timestamp(LocalDateTime.now()) |
| 137 | + .priorRuns(context.getPriorRuns()) |
| 138 | + .incidentCount(context.getIncidentCount()) |
| 139 | + .enclaveVerified(context.isEnclaveVerified()) |
| 140 | + .evaluationNotes(generateEvaluationNotes(context, trustScore, result, identityType)) |
| 141 | + .build(); |
| 142 | + |
| 143 | + AgentTrustScoreHistory saved = trustScoreService.recordTrustScore(history); |
| 144 | + log.info("Trust score evaluated for {} {}: score={}, result={}", |
| 145 | + identityType == IdentityType.USER ? "user" : "agent", entityName, trustScore, result); |
| 146 | + |
| 147 | + return saved; |
| 148 | + } |
| 149 | + |
| 150 | + private AgentContext buildEntityContext(String entityId, String entityName, IdentityType identityType) { |
| 151 | + if (identityType == IdentityType.USER) { |
| 152 | + return buildHumanUserContext(entityId, entityName); |
| 153 | + } else { |
| 154 | + return buildAgentContext(entityId, entityName); |
| 155 | + } |
| 156 | + } |
| 157 | + |
| 158 | + private AgentContext buildHumanUserContext(String userId, String username) { |
| 159 | + // For human users, we track sessions instead of heartbeats |
| 160 | + List<SessionLog> userSessions = sessionLogRepository.findByUsername(username); |
| 161 | + |
| 162 | + int priorRuns = calculatePriorSessions(userSessions); |
| 163 | + int incidentCount = incidentTracker.getOrDefault(userId, 0); |
| 164 | + |
| 165 | + // Human users are verified through Keycloak authentication |
| 166 | + List<ProvenanceEvent> events = provenanceCache.getOrDefault(userId, Collections.emptyList()); |
| 167 | + String identityIssuer = "keycloak"; // Always verified for authenticated users |
| 168 | + |
| 169 | + // Enclave verification doesn't apply to human users, but we can consider |
| 170 | + // if they're accessing from a secure/verified location |
| 171 | + boolean enclaveVerified = false; // Could be enhanced with IP/location verification |
| 172 | + |
| 173 | + return AgentContext.builder() |
| 174 | + .agentId(userId) |
| 175 | + .tags(extractUserTags(username)) |
| 176 | + .identityIssuer(identityIssuer) |
| 177 | + .enclaveVerified(enclaveVerified) |
| 178 | + .priorRuns(priorRuns) |
| 179 | + .incidentCount(incidentCount) |
| 180 | + .build(); |
| 181 | + } |
| 182 | + |
| 183 | + private int calculatePriorSessions(List<SessionLog> sessions) { |
| 184 | + // Count sessions in the last 30 days |
| 185 | + LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30); |
| 186 | + Timestamp thirtyDaysAgoTimestamp = Timestamp.valueOf(thirtyDaysAgo); |
| 187 | + |
| 188 | + return (int) sessions.stream() |
| 189 | + .filter(session -> session.getSessionTm() != null && |
| 190 | + session.getSessionTm().after(thirtyDaysAgoTimestamp)) |
| 191 | + .count(); |
| 192 | + } |
| 193 | + |
| 194 | + private Set<String> extractUserTags(String username) { |
| 195 | + Set<String> tags = new HashSet<>(); |
| 196 | + if (username != null) { |
| 197 | + tags.add("human-user"); |
| 198 | + // Could add role-based tags here if needed |
| 199 | + } |
| 200 | + return tags; |
| 201 | + } |
| 202 | + |
| 203 | + private AgentContext buildAgentContext(String agentId, String agentName) { |
| 204 | + Optional<AgentHeartbeat> heartbeatOpt = heartbeatRepository.findByAgentId(agentId); |
| 205 | + int priorRuns = calculatePriorRuns(agentId); |
| 206 | + int incidentCount = incidentTracker.getOrDefault(agentId, 0); |
| 207 | + |
| 208 | + boolean enclaveVerified = heartbeatOpt |
| 209 | + .map(hb -> hb.getStatus() != null && hb.getStatus().contains("verified")) |
| 210 | + .orElse(false); |
| 211 | + |
| 212 | + List<ProvenanceEvent> events = provenanceCache.getOrDefault(agentId, Collections.emptyList()); |
| 213 | + String identityIssuer = events.isEmpty() ? null : "keycloak"; |
| 214 | + |
| 215 | + return AgentContext.builder() |
| 216 | + .agentId(agentId) |
| 217 | + .tags(extractTags(agentName)) |
| 218 | + .identityIssuer(identityIssuer) |
| 219 | + .enclaveVerified(enclaveVerified) |
| 220 | + .priorRuns(priorRuns) |
| 221 | + .incidentCount(incidentCount) |
| 222 | + .build(); |
| 223 | + } |
| 224 | + |
| 225 | + private int calculatePriorRuns(String agentId) { |
| 226 | + LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30); |
| 227 | + return (int) heartbeatRepository.findAll().stream() |
| 228 | + .filter(hb -> hb.getAgentId().equals(agentId)) |
| 229 | + .filter(hb -> hb.getLastHeartbeat() != null && hb.getLastHeartbeat().isAfter(thirtyDaysAgo)) |
| 230 | + .count(); |
| 231 | + } |
| 232 | + |
| 233 | + private Set<String> extractTags(String agentName) { |
| 234 | + Set<String> tags = new HashSet<>(); |
| 235 | + if (agentName != null) { |
| 236 | + if (agentName.contains("analytics")) tags.add("analytics"); |
| 237 | + if (agentName.contains("ai")) tags.add("ai"); |
| 238 | + if (agentName.contains("chat")) tags.add("chat"); |
| 239 | + if (agentName.contains("monitor")) tags.add("monitor"); |
| 240 | + } |
| 241 | + return tags; |
| 242 | + } |
| 243 | + |
| 244 | + private TrustScoreResult determineResult(int trustScore, TrustScore config) { |
| 245 | + if (trustScore >= config.getMinimum()) { |
| 246 | + return TrustScoreResult.SUCCESS; |
| 247 | + } else if (trustScore >= config.getMarginalThreshold()) { |
| 248 | + return TrustScoreResult.MARGINAL; |
| 249 | + } else { |
| 250 | + return TrustScoreResult.FAILURE; |
| 251 | + } |
| 252 | + } |
| 253 | + |
| 254 | + private String generateEvaluationNotes(AgentContext context, int score, TrustScoreResult result, IdentityType identityType) { |
| 255 | + StringBuilder notes = new StringBuilder(); |
| 256 | + notes.append("Trust evaluation completed for "); |
| 257 | + notes.append(identityType == IdentityType.USER ? "human user" : "agent"); |
| 258 | + notes.append(". "); |
| 259 | + notes.append("Identity: ").append(context.getIdentityIssuer() != null ? "verified" : "unverified").append(". "); |
| 260 | + if (identityType != IdentityType.USER) { |
| 261 | + notes.append("Enclave: ").append(context.isEnclaveVerified() ? "verified" : "not verified").append(". "); |
| 262 | + } |
| 263 | + notes.append("Prior ").append(identityType == IdentityType.USER ? "sessions" : "runs").append(": ").append(context.getPriorRuns()).append(". "); |
| 264 | + notes.append("Incidents: ").append(context.getIncidentCount()).append("."); |
| 265 | + return notes.toString(); |
| 266 | + } |
| 267 | + |
| 268 | + public void cacheProvenanceEvent(ProvenanceEvent event) { |
| 269 | + if (event.getActor() != null) { |
| 270 | + provenanceCache.computeIfAbsent(event.getActor(), k -> new ArrayList<>()).add(event); |
| 271 | + |
| 272 | + List<ProvenanceEvent> events = provenanceCache.get(event.getActor()); |
| 273 | + if (events.size() > 100) { |
| 274 | + events.remove(0); |
| 275 | + } |
| 276 | + } |
| 277 | + } |
| 278 | + |
| 279 | + public void recordIncident(String agentId) { |
| 280 | + incidentTracker.merge(agentId, 1, Integer::sum); |
| 281 | + log.warn("Incident recorded for agent: {}. Total incidents: {}", |
| 282 | + agentId, incidentTracker.get(agentId)); |
| 283 | + } |
| 284 | + |
| 285 | + public void clearIncidents(String agentId) { |
| 286 | + incidentTracker.put(agentId, 0); |
| 287 | + log.info("Incidents cleared for agent: {}", agentId); |
| 288 | + } |
| 289 | +} |
0 commit comments