Ani14 commited on
Commit
b98b0d0
·
verified ·
1 Parent(s): 42608e5

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +100 -270
app.py CHANGED
@@ -2,24 +2,20 @@ import os
2
  import time
3
  import logging
4
  import json
5
- import uuid
6
- import hashlib
7
- from datetime import datetime
8
- from typing import Dict, Any, Optional, List
9
-
10
  from fastapi import FastAPI, Request, HTTPException, Depends, status
11
  from fastapi.exceptions import RequestValidationError
12
  from fastapi.responses import JSONResponse
13
  from fastapi.security import APIKeyHeader
14
- from starlette.responses import Response
 
15
 
16
  # LangGraph and Model Imports
17
  from langgraph.checkpoint.memory import MemorySaver
18
  from langgraph.checkpoint.base import BaseCheckpointSaver
19
  from agent import create_honeypot_graph
20
  from models import (
21
- HoneypotRequest, HoneypotResponse,
22
- AgentState, ExtractedIntelligence
23
  )
24
 
25
  # --- Logging Configuration ---
@@ -27,293 +23,134 @@ logging.basicConfig(level=logging.INFO)
27
  logger = logging.getLogger(__name__)
28
 
29
  # --- Global Debug Store ---
30
- # Stores the last N request/response summaries in memory for easy debugging
31
  DEBUG_LOGS: List[Dict[str, Any]] = []
32
- MAX_DEBUG_LOGS = int(os.environ.get("MAX_DEBUG_LOGS", "50"))
33
-
34
- # How much of a non-JSON body to preview (chars). Kept small to avoid log bloat.
35
- MAX_BODY_PREVIEW_CHARS = int(os.environ.get("MAX_BODY_PREVIEW_CHARS", "4000"))
36
-
37
- # --- Configuration ---
38
- API_KEY_NAME = "x-api-key"
39
- API_KEY = os.environ.get("HONEYPOT_API_KEY", "sk_test_123456789")
40
- api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
41
-
42
- # --- Helpers (safe logging) ---
43
- def mask_api_key(value: Optional[str]) -> str:
44
- """Mask API key while still letting us debug if it was sent."""
45
- if not value:
46
- return ""
47
- v = value.strip()
48
- if len(v) <= 8:
49
- return "***"
50
- return f"{v[:3]}...{v[-4:]}"
51
-
52
- def safe_headers(headers: Dict[str, str]) -> Dict[str, str]:
53
- """Return headers with sensitive values masked."""
54
- out: Dict[str, str] = {}
55
- for k, v in headers.items():
56
- if k.lower() == API_KEY_NAME:
57
- out[k] = mask_api_key(v)
58
- else:
59
- out[k] = v
60
- return out
61
-
62
- def summarize_body(raw: bytes, content_type: str) -> Dict[str, Any]:
63
- """
64
- Safe body summary for debugging endpoint testers:
65
- - parsed JSON if possible
66
- - else a small preview + sha256 hash + length
67
- """
68
- if not raw:
69
- return {"raw_len": 0, "sha256": None, "parsed": None, "preview": None}
70
-
71
- sha = hashlib.sha256(raw).hexdigest()
72
- length = len(raw)
73
-
74
- parsed = None
75
- ct = (content_type or "").lower()
76
- if "application/json" in ct:
77
- try:
78
- parsed = json.loads(raw.decode("utf-8", errors="replace"))
79
- except Exception:
80
- parsed = None
81
-
82
- # For text-only testers, preview is often the fastest way to spot issues.
83
- preview = raw.decode("utf-8", errors="replace")
84
- if len(preview) > MAX_BODY_PREVIEW_CHARS:
85
- preview = preview[:MAX_BODY_PREVIEW_CHARS] + "...(truncated)"
86
-
87
- return {
88
- "raw_len": length,
89
- "sha256": sha,
90
- "parsed": parsed,
91
- # If parsed JSON exists, preview is usually redundant.
92
- "preview": None if parsed is not None else preview,
93
- }
94
-
95
- def add_debug_log(
96
- direction: str,
97
- path: str,
98
- method: str,
99
- headers: Dict[str, str],
100
- body: Any,
101
- *,
102
- request_id: Optional[str] = None,
103
- status_code: Optional[int] = None,
104
- query_params: Optional[Dict[str, str]] = None,
105
- client: Optional[Dict[str, Any]] = None,
106
- extra: Optional[Dict[str, Any]] = None
107
- ) -> None:
108
  log_entry = {
109
  "timestamp": datetime.now().isoformat(),
110
- "request_id": request_id,
111
  "direction": direction,
112
  "path": path,
113
  "method": method,
114
- "status_code": status_code,
115
- "query_params": query_params or {},
116
- "client": client or {},
117
- "headers": safe_headers(headers),
118
- "body": body,
119
- "extra": extra or {},
120
  }
121
  DEBUG_LOGS.insert(0, log_entry)
122
  if len(DEBUG_LOGS) > MAX_DEBUG_LOGS:
123
  DEBUG_LOGS.pop()
124
 
 
 
 
 
 
125
  # --- Initialization ---
126
  app = FastAPI(
127
  title="Agentic Honey-Pot API - Super Debug Mode",
128
- description="REST API for Scam Detection with enhanced request/response logging for endpoint testers.",
129
- version="1.4.0"
130
  )
131
 
132
  # Initialize LangGraph Checkpointer
133
  checkpointer: BaseCheckpointSaver = MemorySaver()
134
  honeypot_app = create_honeypot_graph(checkpointer)
135
 
136
- # --- Middleware for Global Request/Response Forensics ---
137
  @app.middleware("http")
138
  async def log_requests(request: Request, call_next):
139
- # Correlation ID (use tester-provided one if present)
140
- request_id = request.headers.get("x-request-id") or str(uuid.uuid4())
141
-
142
  path = request.url.path
143
  method = request.method
144
  headers = dict(request.headers)
145
- query_params = dict(request.query_params)
146
-
147
- client_host = request.client.host if request.client else None
148
- client_port = request.client.port if request.client else None
149
-
150
- content_type = headers.get("content-type", "")
151
- content_length = headers.get("content-length")
152
-
153
- # Read raw body (FastAPI caches it, so downstream can still access it)
154
- raw_body = await request.body()
155
- body_summary = summarize_body(raw_body, content_type)
156
-
157
- incoming_payload = {
158
- "content_type": content_type,
159
- "content_length": content_length,
160
- "body_summary": body_summary,
161
- }
162
-
163
- add_debug_log(
164
- "INCOMING",
165
- path,
166
- method,
167
- headers,
168
- incoming_payload,
169
- request_id=request_id,
170
- query_params=query_params,
171
- client={"host": client_host, "port": client_port},
172
- )
173
-
174
- logger.info(
175
- f"[{request_id}] INCOMING {method} {path} "
176
- f"qp={query_params} ct={content_type} cl={content_length} "
177
- f"client={client_host}:{client_port} "
178
- f"api_key_present={API_KEY_NAME in {k.lower() for k in headers.keys()}} "
179
- f"api_key_masked={mask_api_key(headers.get(API_KEY_NAME))}"
180
- )
181
 
182
  start_time = time.time()
183
-
184
- # Call downstream
185
  response = await call_next(request)
186
  process_time = time.time() - start_time
187
 
188
- # Capture response body (consume iterator, then rebuild response)
189
- resp_body_bytes = b""
190
- async for chunk in response.body_iterator:
191
- resp_body_bytes += chunk
192
-
193
- new_response = Response(
194
- content=resp_body_bytes,
195
- status_code=response.status_code,
196
- headers=dict(response.headers),
197
- media_type=response.media_type,
198
- )
199
-
200
- resp_content_type = new_response.headers.get("content-type", "")
201
- resp_summary = summarize_body(resp_body_bytes, resp_content_type)
202
-
203
- outgoing_payload = {
204
- "content_type": resp_content_type,
205
- "body_summary": resp_summary,
206
- "time_seconds": round(process_time, 4),
207
- }
208
 
209
- add_debug_log(
210
- "OUTGOING",
211
- path,
212
- method,
213
- headers={}, # response headers usually not needed in debug store
214
- body=outgoing_payload,
215
- request_id=request_id,
216
- status_code=new_response.status_code,
217
- query_params=query_params,
218
- client={"host": client_host, "port": client_port},
219
- )
220
-
221
- logger.info(
222
- f"[{request_id}] OUTGOING {method} {path} "
223
- f"status={new_response.status_code} time={process_time:.4f}s"
224
- )
225
-
226
- return new_response
227
-
228
- # --- Exception Handlers for Diagnostic Logging ---
229
  @app.exception_handler(RequestValidationError)
230
  async def validation_exception_handler(request: Request, exc: RequestValidationError):
231
- headers = dict(request.headers)
232
- raw_body = await request.body()
233
- content_type = headers.get("content-type", "")
234
- body_summary = summarize_body(raw_body, content_type)
235
-
 
236
  error_detail = exc.errors()
237
- logger.error(
238
- f"422 Validation Error! path={request.url.path} errors={error_detail} "
239
- f"received_body_len={body_summary.get('raw_len')}"
240
- )
241
-
242
  response_body = {
243
  "detail": error_detail,
244
- "received_body": body_summary.get("parsed") or body_summary.get("preview"),
245
- "message": "Validation failed. Check /debug/logs for request_id-correlated details."
246
  }
247
-
248
- add_debug_log(
249
- "OUTGOING_ERROR",
250
- request.url.path,
251
- request.method,
252
- headers,
253
- {"error": response_body, "received_body_summary": body_summary},
254
- request_id=request.headers.get("x-request-id"),
255
- status_code=422,
256
- query_params=dict(request.query_params),
257
- client={"host": request.client.host if request.client else None},
258
- )
259
-
260
  return JSONResponse(status_code=422, content=response_body)
261
 
262
- @app.exception_handler(HTTPException)
263
- async def http_exception_handler(request: Request, exc: HTTPException):
264
- # Log 401/404/etc with enough context to debug endpoint testers.
265
- headers = dict(request.headers)
266
- raw_body = await request.body()
267
- content_type = headers.get("content-type", "")
268
- body_summary = summarize_body(raw_body, content_type)
269
-
270
- response_body = {"status": "error", "message": exc.detail}
271
-
272
- logger.warning(
273
- f"HTTPException status={exc.status_code} path={request.url.path} "
274
- f"api_key_masked={mask_api_key(headers.get(API_KEY_NAME))}"
275
- )
276
-
277
- add_debug_log(
278
- "OUTGOING_ERROR",
279
- request.url.path,
280
- request.method,
281
- headers,
282
- {"error": response_body, "received_body_summary": body_summary},
283
- request_id=request.headers.get("x-request-id"),
284
- status_code=exc.status_code,
285
- query_params=dict(request.query_params),
286
- client={"host": request.client.host if request.client else None},
287
- )
288
-
289
- return JSONResponse(status_code=exc.status_code, content=response_body)
290
-
291
- # --- Dependency for API Key Validation (with explicit logs) ---
292
- async def get_api_key(api_key_value: Optional[str] = Depends(api_key_header)):
293
- if not api_key_value:
294
- logger.warning("AUTH FAIL: missing x-api-key header")
295
  raise HTTPException(
296
  status_code=status.HTTP_401_UNAUTHORIZED,
297
- detail="Missing 'x-api-key' header.",
298
  )
299
-
300
- if api_key_value != API_KEY:
301
- logger.warning(
302
- f"AUTH FAIL: invalid x-api-key provided={mask_api_key(api_key_value)} "
303
- f"expected={mask_api_key(API_KEY)}"
304
- )
305
- raise HTTPException(
306
- status_code=status.HTTP_401_UNAUTHORIZED,
307
- detail="Invalid API Key.",
308
- )
309
-
310
- return api_key_value
311
 
312
  # --- API Endpoints ---
 
313
  @app.get("/debug/logs")
314
  async def get_debug_logs():
315
- """Endpoint to view the last N request/response summaries."""
316
- return {"count": len(DEBUG_LOGS), "logs": DEBUG_LOGS}
 
 
 
317
 
318
  @app.post("/api/honeypot-detection", response_model=HoneypotResponse)
319
  async def honeypot_detection(
@@ -322,7 +159,7 @@ async def honeypot_detection(
322
  ) -> Dict[str, Any]:
323
  session_id = request_data.sessionId
324
  config = {"configurable": {"thread_id": session_id}}
325
-
326
  checkpoint = honeypot_app.get_state(config)
327
  start_time = time.time()
328
 
@@ -330,11 +167,11 @@ async def honeypot_detection(
330
  current_state_dict = checkpoint.values
331
  current_state_dict.setdefault("callbackSent", False)
332
  current_state_dict.setdefault("agentNotes", "")
333
- current_state_dict.setdefault("extractedIntelligence", ExtractedIntelligence())
334
- current_state_dict.setdefault("conversationHistory", [])
335
  current_state_dict.setdefault("totalMessagesExchanged", 0)
336
  current_state_dict.setdefault("sessionId", session_id)
337
-
338
  current_state = AgentState(**current_state_dict)
339
  current_state["conversationHistory"].append(request_data.message)
340
  current_state["totalMessagesExchanged"] += 1
@@ -357,7 +194,7 @@ async def honeypot_detection(
357
  final_state_dict = honeypot_app.invoke(input_state, config=config)
358
  final_state = AgentState(**final_state_dict)
359
  engagement_duration = int(time.time() - start_time)
360
-
361
  response_content = {
362
  "status": "success",
363
  "scamDetected": final_state["scamDetected"],
@@ -368,31 +205,24 @@ async def honeypot_detection(
368
  "extractedIntelligence": final_state["extractedIntelligence"].model_dump(),
369
  "agentNotes": final_state["agentNotes"]
370
  }
371
-
372
- # Keep endpoint-level success log too (useful if middleware is disabled later)
373
- add_debug_log(
374
- "OUTGOING_SUCCESS_ENDPOINT",
375
- "/api/honeypot-detection",
376
- "POST",
377
- headers={},
378
- body=response_content
379
- )
380
-
381
  return response_content
382
 
383
  except Exception as e:
384
  error_msg = f"Internal Error: {str(e)}"
385
- logger.exception(error_msg)
386
- add_debug_log("OUTGOING_ERROR_ENDPOINT", "/api/honeypot-detection", "POST", {}, {"error": error_msg})
387
  raise HTTPException(status_code=500, detail=error_msg)
388
 
389
  @app.get("/")
390
  async def root():
391
  return {
392
  "message": "Agentic Honey-Pot API is running.",
393
- "endpoints": {"detection": "/api/honeypot-detection", "debug_logs": "/debug/logs"},
394
- "debug": {
395
- "max_debug_logs": MAX_DEBUG_LOGS,
396
- "max_body_preview_chars": MAX_BODY_PREVIEW_CHARS
397
  }
398
  }
 
2
  import time
3
  import logging
4
  import json
 
 
 
 
 
5
  from fastapi import FastAPI, Request, HTTPException, Depends, status
6
  from fastapi.exceptions import RequestValidationError
7
  from fastapi.responses import JSONResponse
8
  from fastapi.security import APIKeyHeader
9
+ from typing import Dict, Any, Optional, List
10
+ from datetime import datetime
11
 
12
  # LangGraph and Model Imports
13
  from langgraph.checkpoint.memory import MemorySaver
14
  from langgraph.checkpoint.base import BaseCheckpointSaver
15
  from agent import create_honeypot_graph
16
  from models import (
17
+ HoneypotRequest, HoneypotResponse,
18
+ AgentState, ExtractedIntelligence, Message, EngagementMetrics
19
  )
20
 
21
  # --- Logging Configuration ---
 
23
  logger = logging.getLogger(__name__)
24
 
25
  # --- Global Debug Store ---
26
+ # This will store the last 50 requests and responses in memory for easy debugging
27
  DEBUG_LOGS: List[Dict[str, Any]] = []
28
+ MAX_DEBUG_LOGS = 50
29
+
30
+ # --- Helpers (Strict Message Objects) ---
31
+ def _ensure_message(obj: Any) -> Message:
32
+ """Ensure conversation items are Message objects (as required by the tester contract)."""
33
+ if isinstance(obj, Message):
34
+ return obj
35
+ if isinstance(obj, dict):
36
+ return Message(**obj)
37
+ raise TypeError(f"Invalid message type in conversationHistory: {type(obj)}")
38
+
39
+ def _ensure_intelligence(obj: Any) -> ExtractedIntelligence:
40
+ """Ensure extractedIntelligence is an ExtractedIntelligence model."""
41
+ if isinstance(obj, ExtractedIntelligence):
42
+ return obj
43
+ if isinstance(obj, dict):
44
+ return ExtractedIntelligence(**obj)
45
+ # If missing/None, return empty model
46
+ return ExtractedIntelligence()
47
+
48
+ def _normalize_history(history: Any) -> List[Message]:
49
+ """Normalize conversationHistory to a list[Message]."""
50
+ if history is None:
51
+ return []
52
+ if not isinstance(history, list):
53
+ raise TypeError(f"conversationHistory must be a list, got: {type(history)}")
54
+ return [_ensure_message(m) for m in history]
55
+
56
+
57
+ def add_debug_log(direction: str, path: str, method: str, headers: Dict[str, str], body: Any):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  log_entry = {
59
  "timestamp": datetime.now().isoformat(),
 
60
  "direction": direction,
61
  "path": path,
62
  "method": method,
63
+ "headers": {k: v for k, v in headers.items() if k.lower() != "x-api-key"}, # Hide key for safety
64
+ "body": body
 
 
 
 
65
  }
66
  DEBUG_LOGS.insert(0, log_entry)
67
  if len(DEBUG_LOGS) > MAX_DEBUG_LOGS:
68
  DEBUG_LOGS.pop()
69
 
70
+ # --- Configuration ---
71
+ API_KEY_NAME = "x-api-key"
72
+ API_KEY = os.environ.get("HONEYPOT_API_KEY", "sk_test_123456789")
73
+ api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
74
+
75
  # --- Initialization ---
76
  app = FastAPI(
77
  title="Agentic Honey-Pot API - Super Debug Mode",
78
+ description="REST API for Scam Detection with enhanced logging.",
79
+ version="1.3.0"
80
  )
81
 
82
  # Initialize LangGraph Checkpointer
83
  checkpointer: BaseCheckpointSaver = MemorySaver()
84
  honeypot_app = create_honeypot_graph(checkpointer)
85
 
86
+ # --- Middleware for Global Logging ---
87
  @app.middleware("http")
88
  async def log_requests(request: Request, call_next):
 
 
 
89
  path = request.url.path
90
  method = request.method
91
  headers = dict(request.headers)
92
+
93
+ # Capture request body
94
+ body = None
95
+ if method == "POST":
96
+ try:
97
+ raw_body = await request.body()
98
+ body = json.loads(raw_body)
99
+ except:
100
+ body = "Could not parse body as JSON"
101
+
102
+ add_debug_log("INCOMING", path, method, headers, body)
103
+ logger.info(f"INCOMING {method} {path} | Body: {body}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
 
105
  start_time = time.time()
 
 
106
  response = await call_next(request)
107
  process_time = time.time() - start_time
108
 
109
+ # Capture response body (this is a bit tricky in FastAPI middleware)
110
+ # For simplicity, we'll log the status code here and log the actual body in the endpoint
111
+ logger.info(f"OUTGOING {method} {path} | Status: {response.status_code} | Time: {process_time:.4f}s")
112
+
113
+ return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ # --- Exception Handler for Diagnostic Logging ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  @app.exception_handler(RequestValidationError)
117
  async def validation_exception_handler(request: Request, exc: RequestValidationError):
118
+ body = await request.body()
119
+ try:
120
+ payload = json.loads(body)
121
+ except:
122
+ payload = body.decode()
123
+
124
  error_detail = exc.errors()
125
+ logger.error(f"422 Unprocessable Entity Error! Payload: {payload} | Errors: {error_detail}")
126
+
 
 
 
127
  response_body = {
128
  "detail": error_detail,
129
+ "received_body": payload,
130
+ "message": "Validation failed. Check /debug/logs for details."
131
  }
132
+ add_debug_log("OUTGOING_ERROR", request.url.path, request.method, {}, response_body)
133
+
 
 
 
 
 
 
 
 
 
 
 
134
  return JSONResponse(status_code=422, content=response_body)
135
 
136
+ # --- Dependency for API Key Validation ---
137
+ async def get_api_key(api_key_header: str = Depends(api_key_header)):
138
+ if api_key_header is None or api_key_header != API_KEY:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  raise HTTPException(
140
  status_code=status.HTTP_401_UNAUTHORIZED,
141
+ detail="Invalid API Key or missing 'x-api-key' header.",
142
  )
143
+ return api_key_header
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  # --- API Endpoints ---
146
+
147
  @app.get("/debug/logs")
148
  async def get_debug_logs():
149
+ """Endpoint to view the last 50 requests and responses."""
150
+ return {
151
+ "count": len(DEBUG_LOGS),
152
+ "logs": DEBUG_LOGS
153
+ }
154
 
155
  @app.post("/api/honeypot-detection", response_model=HoneypotResponse)
156
  async def honeypot_detection(
 
159
  ) -> Dict[str, Any]:
160
  session_id = request_data.sessionId
161
  config = {"configurable": {"thread_id": session_id}}
162
+
163
  checkpoint = honeypot_app.get_state(config)
164
  start_time = time.time()
165
 
 
167
  current_state_dict = checkpoint.values
168
  current_state_dict.setdefault("callbackSent", False)
169
  current_state_dict.setdefault("agentNotes", "")
170
+ current_state_dict["extractedIntelligence"] = _ensure_intelligence(current_state_dict.get("extractedIntelligence"))
171
+ current_state_dict["conversationHistory"] = _normalize_history(current_state_dict.get("conversationHistory"))
172
  current_state_dict.setdefault("totalMessagesExchanged", 0)
173
  current_state_dict.setdefault("sessionId", session_id)
174
+
175
  current_state = AgentState(**current_state_dict)
176
  current_state["conversationHistory"].append(request_data.message)
177
  current_state["totalMessagesExchanged"] += 1
 
194
  final_state_dict = honeypot_app.invoke(input_state, config=config)
195
  final_state = AgentState(**final_state_dict)
196
  engagement_duration = int(time.time() - start_time)
197
+
198
  response_content = {
199
  "status": "success",
200
  "scamDetected": final_state["scamDetected"],
 
205
  "extractedIntelligence": final_state["extractedIntelligence"].model_dump(),
206
  "agentNotes": final_state["agentNotes"]
207
  }
208
+
209
+ # Log the successful response body
210
+ add_debug_log("OUTGOING_SUCCESS", "/api/honeypot-detection", "POST", {}, response_content)
211
+
 
 
 
 
 
 
212
  return response_content
213
 
214
  except Exception as e:
215
  error_msg = f"Internal Error: {str(e)}"
216
+ logger.error(error_msg)
217
+ add_debug_log("OUTGOING_ERROR", "/api/honeypot-detection", "POST", {}, {"error": error_msg})
218
  raise HTTPException(status_code=500, detail=error_msg)
219
 
220
  @app.get("/")
221
  async def root():
222
  return {
223
  "message": "Agentic Honey-Pot API is running.",
224
+ "endpoints": {
225
+ "detection": "/api/honeypot-detection",
226
+ "debug_logs": "/debug/logs"
 
227
  }
228
  }