Skip to content

Commit 0989ef3

Browse files
author
Raj
committed
feat: implement examination identity audit logs for GATEKEEPER
- Add AuditLogService for comprehensive identity event logging - Log verification attempts, face matches/mismatches, suspicious events - Log admin override actions and examination access decisions - Add get_timeline() for roll-number or session filtered audit trails - Add search() for querying audit log content - Add 15 tests covering all audit logging functionality
1 parent 8e3bfdf commit 0989ef3

2 files changed

Lines changed: 399 additions & 0 deletions

File tree

backend/src/services/audit_log.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
from typing import Optional, List, Dict, Any
2+
from dataclasses import dataclass, field
3+
from datetime import datetime, timezone
4+
import hashlib
5+
import json
6+
7+
8+
@dataclass
9+
class AuditEvent:
10+
event_id: str
11+
timestamp: datetime
12+
event_type: str
13+
roll_number: str
14+
session_id: Optional[str]
15+
payload: Dict[str, Any]
16+
metadata: Optional[Dict[str, Any]] = None
17+
18+
def to_dict(self) -> Dict[str, Any]:
19+
return {
20+
"event_id": self.event_id,
21+
"timestamp": self.timestamp.isoformat(),
22+
"event_type": self.event_type,
23+
"roll_number": self.roll_number,
24+
"session_id": self.session_id,
25+
"payload": self.payload,
26+
"metadata": self.metadata,
27+
}
28+
29+
30+
class AuditLogService:
31+
def __init__(self):
32+
self._logs: List[AuditEvent] = []
33+
34+
def _generate_event_id(self, roll_number: str, event_type: str, timestamp: datetime) -> str:
35+
raw = f"{timestamp.isoformat()}:{roll_number}:{event_type}"
36+
return hashlib.sha256(raw.encode()).hexdigest()[:16]
37+
38+
def log_verification_attempt(
39+
self,
40+
roll_number: str,
41+
session_id: Optional[str],
42+
capture_info: Dict[str, Any],
43+
result: str,
44+
confidence: float,
45+
) -> AuditEvent:
46+
event = AuditEvent(
47+
event_id=self._generate_event_id(roll_number, "verification_attempt", datetime.now(timezone.utc)),
48+
timestamp=datetime.now(timezone.utc),
49+
event_type="verification_attempt",
50+
roll_number=roll_number,
51+
session_id=session_id,
52+
payload={
53+
"result": result,
54+
"confidence": confidence,
55+
"capture": capture_info,
56+
},
57+
)
58+
self._logs.append(event)
59+
return event
60+
61+
def log_match(
62+
self,
63+
roll_number: str,
64+
session_id: Optional[str],
65+
similarity: float,
66+
confidence: float,
67+
) -> AuditEvent:
68+
event = AuditEvent(
69+
event_id=self._generate_event_id(roll_number, "face_match", datetime.now(timezone.utc)),
70+
timestamp=datetime.now(timezone.utc),
71+
event_type="face_match",
72+
roll_number=roll_number,
73+
session_id=session_id,
74+
payload={
75+
"similarity": similarity,
76+
"confidence": confidence,
77+
},
78+
)
79+
self._logs.append(event)
80+
return event
81+
82+
def log_mismatch(
83+
self,
84+
roll_number: str,
85+
session_id: Optional[str],
86+
similarity: float,
87+
confidence: float,
88+
threshold: float,
89+
) -> AuditEvent:
90+
event = AuditEvent(
91+
event_id=self._generate_event_id(roll_number, "face_mismatch", datetime.now(timezone.utc)),
92+
timestamp=datetime.now(timezone.utc),
93+
event_type="face_mismatch",
94+
roll_number=roll_number,
95+
session_id=session_id,
96+
payload={
97+
"similarity": similarity,
98+
"confidence": confidence,
99+
"threshold": threshold,
100+
},
101+
)
102+
self._logs.append(event)
103+
return event
104+
105+
def log_suspicious_event(
106+
self,
107+
roll_number: str,
108+
session_id: Optional[str],
109+
reason: str,
110+
conflict_id: Optional[str] = None,
111+
matched_rolls: Optional[List[str]] = None,
112+
) -> AuditEvent:
113+
event = AuditEvent(
114+
event_id=self._generate_event_id(roll_number, "suspicious_identity", datetime.now(timezone.utc)),
115+
timestamp=datetime.now(timezone.utc),
116+
event_type="suspicious_identity",
117+
roll_number=roll_number,
118+
session_id=session_id,
119+
payload={
120+
"reason": reason,
121+
"conflict_id": conflict_id,
122+
"matched_rolls": matched_rolls or [],
123+
},
124+
)
125+
self._logs.append(event)
126+
return event
127+
128+
def log_admin_override(
129+
self,
130+
roll_number: str,
131+
session_id: Optional[str],
132+
conflict_id: str,
133+
reviewer_id: str,
134+
approved: bool,
135+
reason: str,
136+
) -> AuditEvent:
137+
event = AuditEvent(
138+
event_id=self._generate_event_id(roll_number, "admin_override", datetime.now(timezone.utc)),
139+
timestamp=datetime.now(timezone.utc),
140+
event_type="admin_override",
141+
roll_number=roll_number,
142+
session_id=session_id,
143+
payload={
144+
"conflict_id": conflict_id,
145+
"reviewer_id": reviewer_id,
146+
"approved": approved,
147+
"reason": reason,
148+
},
149+
)
150+
self._logs.append(event)
151+
return event
152+
153+
def log_examination_access(
154+
self,
155+
roll_number: str,
156+
session_id: str,
157+
granted: bool,
158+
reason: Optional[str] = None,
159+
) -> AuditEvent:
160+
event = AuditEvent(
161+
event_id=self._generate_event_id(roll_number, "examination_access", datetime.now(timezone.utc)),
162+
timestamp=datetime.now(timezone.utc),
163+
event_type="examination_access",
164+
roll_number=roll_number,
165+
session_id=session_id,
166+
payload={
167+
"granted": granted,
168+
"reason": reason,
169+
},
170+
)
171+
self._logs.append(event)
172+
return event
173+
174+
def get_timeline(self, roll_number: Optional[str] = None) -> List[Dict[str, Any]]:
175+
events = self._logs
176+
if roll_number:
177+
events = [e for e in events if e.roll_number == roll_number]
178+
return [e.to_dict() for e in sorted(events, key=lambda x: x.timestamp)]
179+
180+
def get_events_by_type(self, event_type: str) -> List[Dict[str, Any]]:
181+
events = [e for e in self._logs if e.event_type == event_type]
182+
return [e.to_dict() for e in sorted(events, key=lambda x: x.timestamp)]
183+
184+
def get_session_timeline(self, session_id: str) -> List[Dict[str, Any]]:
185+
events = [e for e in self._logs if e.session_id == session_id]
186+
return [e.to_dict() for e in sorted(events, key=lambda x: x.timestamp)]
187+
188+
def search(self, query: str) -> List[Dict[str, Any]]:
189+
query_lower = query.lower()
190+
results = []
191+
for event in self._logs:
192+
payload_str = json.dumps(event.payload).lower()
193+
if query_lower in payload_str or query_lower in event.roll_number.lower():
194+
results.append(event.to_dict())
195+
return results
196+
197+
def clear(self):
198+
self._logs.clear()
199+
200+
def count(self) -> int:
201+
return len(self._logs)

0 commit comments

Comments
 (0)