imDrizzle commited on
Commit
cd91aab
·
1 Parent(s): 08e7bbd

Implement Threat Intelligence dashboard and capture session/client identity

Browse files
src/api/routes/check.py CHANGED
@@ -45,6 +45,10 @@ async def check_prompt(
45
  app_ctx = body.app_context if body.app_context != "general" else key_doc.get("app_context", "general")
46
  canary_token = body.custom_canary or key_doc.get("custom_canary", None)
47
 
 
 
 
 
48
  # Pre-pipeline check: Graph Replay Cache
49
  normalized_hash = hash_normalized_prompt(body.prompt)
50
  raw_hash = hash_prompt(body.prompt)
@@ -117,6 +121,8 @@ async def check_prompt(
117
  "request_id": result.request_id,
118
  "api_key_id": str(key_doc["_id"]),
119
  "user_id": key_doc.get("user_id"),
 
 
120
  "timestamp": datetime.now(timezone.utc),
121
  "prompt_hash": raw_hash,
122
  "prompt_length": len(body.prompt),
 
45
  app_ctx = body.app_context if body.app_context != "general" else key_doc.get("app_context", "general")
46
  canary_token = body.custom_canary or key_doc.get("custom_canary", None)
47
 
48
+ # Capture session and client identity (never store raw prompt)
49
+ session_id = request.headers.get("X-Session-ID") or str(uuid.uuid4())
50
+ client_ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else "unknown").split(",")[0].strip()
51
+
52
  # Pre-pipeline check: Graph Replay Cache
53
  normalized_hash = hash_normalized_prompt(body.prompt)
54
  raw_hash = hash_prompt(body.prompt)
 
121
  "request_id": result.request_id,
122
  "api_key_id": str(key_doc["_id"]),
123
  "user_id": key_doc.get("user_id"),
124
+ "session_id": session_id,
125
+ "client_ip": client_ip,
126
  "timestamp": datetime.now(timezone.utc),
127
  "prompt_hash": raw_hash,
128
  "prompt_length": len(body.prompt),
src/api/routes/graph.py CHANGED
@@ -10,6 +10,8 @@ from fastapi import APIRouter, Depends, HTTPException
10
 
11
  from src.api.auth_middleware import validate_user_token
12
  from src.db.neo4j_client import get_driver, is_connected
 
 
13
 
14
  logger = logging.getLogger("llm_firewall.routes.graph")
15
 
@@ -38,12 +40,12 @@ async def get_graph_stats(current_user: dict = Depends(validate_user_token)):
38
 
39
  try:
40
  async with driver.session() as session:
41
- # Query 1: Attack Co-occurrence Matrix
42
  q1 = """
43
- MATCH (a1:AttackType)<-[:TRIGGERED]-(k:ApiKey)-[:TRIGGERED]->(a2:AttackType)
44
- WHERE a1.name < a2.name
45
- RETURN a1.name AS source, a2.name AS target, COUNT(k) AS weight
46
- ORDER BY weight DESC LIMIT 20
47
  """
48
  result1 = await session.run(q1)
49
  async for record in result1:
@@ -83,17 +85,18 @@ async def get_graph_stats(current_user: dict = Depends(validate_user_token)):
83
  "times_seen": record["times_seen"]
84
  })
85
 
86
- # Query 4: Provider Targeting
87
  q4 = """
88
- MATCH (k:ApiKey)-[:TARGETS]->(p:Provider)
89
- RETURN p.name AS provider, COUNT(k) AS api_keys_targeting
90
- ORDER BY api_keys_targeting DESC
91
  """
92
  result4 = await session.run(q4)
93
  async for record in result4:
94
  provider_targeting.append({
95
- "provider": record["provider"],
96
- "api_keys_targeting": record["api_keys_targeting"]
 
97
  })
98
 
99
  except Exception as e:
@@ -103,9 +106,105 @@ async def get_graph_stats(current_user: dict = Depends(validate_user_token)):
103
  return {
104
  "status": "ok",
105
  "data": {
106
- "co_occurrence": co_occurrence,
107
  "layer_bypass": layer_bypass,
108
  "top_replayed": top_replayed,
109
- "provider_targeting": provider_targeting
110
  }
111
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  from src.api.auth_middleware import validate_user_token
12
  from src.db.neo4j_client import get_driver, is_connected
13
+ from src.db import mongo
14
+ from datetime import datetime, timezone, timedelta
15
 
16
  logger = logging.getLogger("llm_firewall.routes.graph")
17
 
 
40
 
41
  try:
42
  async with driver.session() as session:
43
+ # Query 1: Force Graph Data (API Key -> Attack Type)
44
  q1 = """
45
+ MATCH (k:ApiKey)-[:TRIGGERED]->(a:AttackType)
46
+ WITH k, a, COUNT(*) as weight
47
+ RETURN k.key_id AS source, a.name AS target, weight
48
+ ORDER BY weight DESC LIMIT 50
49
  """
50
  result1 = await session.run(q1)
51
  async for record in result1:
 
85
  "times_seen": record["times_seen"]
86
  })
87
 
88
+ # Query 4: API Key Breakdown
89
  q4 = """
90
+ MATCH (k:ApiKey)-[:TRIGGERED]->(a:AttackType)
91
+ RETURN k.key_id AS key_id, a.name AS attack_type, COUNT(a) AS attack_count
92
+ ORDER BY attack_count DESC
93
  """
94
  result4 = await session.run(q4)
95
  async for record in result4:
96
  provider_targeting.append({
97
+ "key_id": record["key_id"],
98
+ "attack_type": record["attack_type"],
99
+ "attack_count": record["attack_count"]
100
  })
101
 
102
  except Exception as e:
 
106
  return {
107
  "status": "ok",
108
  "data": {
109
+ "force_graph": co_occurrence,
110
  "layer_bypass": layer_bypass,
111
  "top_replayed": top_replayed,
112
+ "api_key_breakdown": provider_targeting
113
  }
114
  }
115
+
116
+ @router.get("/velocity")
117
+ async def get_threat_velocity(current_user: dict = Depends(validate_user_token)):
118
+ """
119
+ Get attacks per minute for the last 60 minutes, split by API Key.
120
+ Queries MongoDB logs.
121
+ """
122
+ logs = mongo.get_logs_collection()
123
+ now = datetime.now(timezone.utc)
124
+ sixty_mins_ago = now - timedelta(minutes=60)
125
+
126
+ pipeline = [
127
+ {"$match": {"user_id": current_user["_id"], "timestamp": {"$gte": sixty_mins_ago}, "safe": False}},
128
+ {
129
+ "$group": {
130
+ "_id": {
131
+ "minute": {"$minute": "$timestamp"},
132
+ "hour": {"$hour": "$timestamp"},
133
+ "day": {"$dayOfMonth": "$timestamp"},
134
+ "api_key": "$api_key_id"
135
+ },
136
+ "count": {"$sum": 1}
137
+ }
138
+ },
139
+ {"$sort": {"_id.day": 1, "_id.hour": 1, "_id.minute": 1}}
140
+ ]
141
+
142
+ velocity_data = []
143
+ async for doc in logs.aggregate(pipeline):
144
+ # Format a simple HH:MM string for the frontend
145
+ time_str = f"{doc['_id']['hour']:02d}:{doc['_id']['minute']:02d}"
146
+ velocity_data.append({
147
+ "time": time_str,
148
+ "api_key": doc["_id"]["api_key"],
149
+ "count": doc["count"]
150
+ })
151
+
152
+ return {"status": "ok", "data": velocity_data}
153
+
154
+ @router.get("/session-chains")
155
+ async def get_session_chains(current_user: dict = Depends(validate_user_token)):
156
+ """
157
+ Get top 20 suspicious sessions and their request sequence (attack chains).
158
+ """
159
+ logs = mongo.get_logs_collection()
160
+
161
+ # We find sessions that have at least one blocked request, or sort by most requests
162
+ pipeline = [
163
+ {"$match": {"user_id": current_user["_id"]}},
164
+ {
165
+ "$group": {
166
+ "_id": "$session_id",
167
+ "total_requests": {"$sum": 1},
168
+ "blocked_count": {"$sum": {"$cond": [{"$eq": ["$safe", False]}, 1, 0]}},
169
+ "max_risk": {"$max": "$risk_score"},
170
+ "events": {
171
+ "$push": {
172
+ "safe": "$safe",
173
+ "risk_score": "$risk_score",
174
+ "attack_type": "$attack_type",
175
+ "flagged_layer": "$flagged_layer",
176
+ "timestamp": "$timestamp"
177
+ }
178
+ }
179
+ }
180
+ },
181
+ # Calculate threat score: (blocked / total) * max_risk
182
+ {
183
+ "$addFields": {
184
+ "threat_score": {
185
+ "$multiply": [
186
+ {"$divide": ["$blocked_count", "$total_requests"]},
187
+ "$max_risk"
188
+ ]
189
+ }
190
+ }
191
+ },
192
+ {"$sort": {"threat_score": -1}},
193
+ {"$limit": 20}
194
+ ]
195
+
196
+ sessions = []
197
+ async for doc in logs.aggregate(pipeline):
198
+ # Format timestamps
199
+ for ev in doc["events"]:
200
+ ev["timestamp"] = ev["timestamp"].isoformat()
201
+ sessions.append({
202
+ "session_id": str(doc["_id"]) if doc["_id"] else "unknown",
203
+ "total_requests": doc["total_requests"],
204
+ "blocked_count": doc["blocked_count"],
205
+ "max_risk": doc["max_risk"],
206
+ "threat_score": doc["threat_score"],
207
+ "events": doc["events"]
208
+ })
209
+
210
+ return {"status": "ok", "data": sessions}
src/api/routes/proxy.py CHANGED
@@ -77,7 +77,12 @@ async def proxy_llm_request(
77
  pipeline = request.app.state.pipeline
78
  app_ctx = key_doc.get("app_context", "general")
79
  canary_token = key_doc.get("custom_canary", None)
80
-
 
 
 
 
 
81
  normalized_hash = hash_normalized_prompt(prompt)
82
  raw_hash = hash_prompt(prompt)
83
  graph_replay_hit = False
@@ -150,6 +155,8 @@ async def proxy_llm_request(
150
  log_entry = {
151
  "request_id": result.request_id,
152
  "api_key_id": str(key_doc["_id"]),
 
 
153
  "timestamp": datetime.now(timezone.utc),
154
  "prompt_hash": raw_hash,
155
  "prompt_length": len(prompt),
 
77
  pipeline = request.app.state.pipeline
78
  app_ctx = key_doc.get("app_context", "general")
79
  canary_token = key_doc.get("custom_canary", None)
80
+
81
+ # Capture session and client identity (never store raw prompt)
82
+ import uuid as _uuid
83
+ session_id = request.headers.get("X-Session-ID") or str(_uuid.uuid4())
84
+ client_ip = request.headers.get("X-Forwarded-For", request.client.host if request.client else "unknown").split(",")[0].strip()
85
+
86
  normalized_hash = hash_normalized_prompt(prompt)
87
  raw_hash = hash_prompt(prompt)
88
  graph_replay_hit = False
 
155
  log_entry = {
156
  "request_id": result.request_id,
157
  "api_key_id": str(key_doc["_id"]),
158
+ "session_id": session_id,
159
+ "client_ip": client_ip,
160
  "timestamp": datetime.now(timezone.utc),
161
  "prompt_hash": raw_hash,
162
  "prompt_length": len(prompt),
src/db/mongo.py CHANGED
@@ -48,6 +48,8 @@ async def _create_indexes(db: AsyncIOMotorDatabase) -> None:
48
  await logs.create_index("timestamp")
49
  await logs.create_index("api_key_id")
50
  await logs.create_index("user_id")
 
 
51
  await logs.create_index("safe")
52
  await logs.create_index("attack_type")
53
  await logs.create_index("provider")
 
48
  await logs.create_index("timestamp")
49
  await logs.create_index("api_key_id")
50
  await logs.create_index("user_id")
51
+ await logs.create_index("session_id")
52
+ await logs.create_index("client_ip")
53
  await logs.create_index("safe")
54
  await logs.create_index("attack_type")
55
  await logs.create_index("provider")