Skip to content

Commit f1e4cde

Browse files
Copilotphrocker
andauthored
Add trust evaluation and scoring system for agents and users with ATPL integration (#75)
* Initial plan * Add trust evaluation backend infrastructure - Created AgentTrustScoreHistory entity for storing trust scores - Created AgentTrustScoreHistoryRepository for data access - Created AgentTrustScoreService for managing trust scores - Created TrustEvaluationService in analytics agent for scheduled evaluations - Created TrustScoreApiController for REST API endpoints - Created TrustScoreViewController for UI rendering - Made TrustScoreCalculator public for analytics module access - All builds and tests passing Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> * Add trust scores UI pages and navigation - Created trust_scores.html for viewing all agent trust scores - Created agent_trust_score.html for detailed agent trust score history - Added navigation link to sidebar for Trust Scores page - Created unit tests for AgentTrustScoreService - All tests passing, full build successful Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> * Enhanced behavior evaluation and added comprehensive documentation - Improved AgentContext.evaluateBehavior() with granular scoring based on prior runs and incidents - Added TRUST_EVALUATION.md with complete system documentation - Documented architecture, API endpoints, configuration, and usage examples - Included troubleshooting guide and future enhancement ideas - All builds passing Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> * Extend trust evaluation to include human users - Modified TrustEvaluationService to evaluate both agents and human users - Added SessionLogRepository to track human user session activity - Created buildHumanUserContext() to evaluate users based on session history - Renamed evaluateAllAgents() to evaluateAllAgentsAndUsers() - Added evaluateEntity() method that handles both USER and NON_PERSON_ENTITY types - Updated UI labels from "Agent Trust Scores" to "Trust Scores - Agents & Users" - Updated documentation to reflect support for human users - Human users are evaluated based on session activity (prior sessions vs prior runs) - All builds and tests passing Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> * Add database migration for agent_trust_score_history table - Created V34__create_agent_trust_score_history.sql Flyway migration - Defines table schema with all required columns (id, agent_id, trust_score, component scores, etc.) - Creates indexes for optimal query performance on agent_id, timestamp, and evaluation_result - Fixes PostgreSQL error: "relation agent_trust_score_history does not exist" - Migration will run automatically on application startup Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: phrocker <1781585+phrocker@users.noreply.github.com>
1 parent 8765c03 commit f1e4cde

15 files changed

Lines changed: 1781 additions & 3 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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

Comments
 (0)