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

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +267 -70
app.py CHANGED
@@ -2,20 +2,24 @@ import os
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,107 +27,293 @@ logging.basicConfig(level=logging.INFO)
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
- def add_debug_log(direction: str, path: str, method: str, headers: Dict[str, str], body: Any):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  log_entry = {
32
  "timestamp": datetime.now().isoformat(),
 
33
  "direction": direction,
34
  "path": path,
35
  "method": method,
36
- "headers": {k: v for k, v in headers.items() if k.lower() != "x-api-key"}, # Hide key for safety
37
- "body": body
 
 
 
 
38
  }
39
  DEBUG_LOGS.insert(0, log_entry)
40
  if len(DEBUG_LOGS) > MAX_DEBUG_LOGS:
41
  DEBUG_LOGS.pop()
42
 
43
- # --- Configuration ---
44
- API_KEY_NAME = "x-api-key"
45
- API_KEY = os.environ.get("HONEYPOT_API_KEY", "sk_test_123456789")
46
- api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
47
-
48
  # --- Initialization ---
49
  app = FastAPI(
50
  title="Agentic Honey-Pot API - Super Debug Mode",
51
- description="REST API for Scam Detection with enhanced logging.",
52
- version="1.3.0"
53
  )
54
 
55
  # Initialize LangGraph Checkpointer
56
  checkpointer: BaseCheckpointSaver = MemorySaver()
57
  honeypot_app = create_honeypot_graph(checkpointer)
58
 
59
- # --- Middleware for Global Logging ---
60
  @app.middleware("http")
61
  async def log_requests(request: Request, call_next):
 
 
 
62
  path = request.url.path
63
  method = request.method
64
  headers = dict(request.headers)
65
-
66
- # Capture request body
67
- body = None
68
- if method == "POST":
69
- try:
70
- raw_body = await request.body()
71
- body = json.loads(raw_body)
72
- except:
73
- body = "Could not parse body as JSON"
74
-
75
- add_debug_log("INCOMING", path, method, headers, body)
76
- logger.info(f"INCOMING {method} {path} | Body: {body}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  start_time = time.time()
 
 
79
  response = await call_next(request)
80
  process_time = time.time() - start_time
81
 
82
- # Capture response body (this is a bit tricky in FastAPI middleware)
83
- # For simplicity, we'll log the status code here and log the actual body in the endpoint
84
- logger.info(f"OUTGOING {method} {path} | Status: {response.status_code} | Time: {process_time:.4f}s")
85
-
86
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- # --- Exception Handler for Diagnostic Logging ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  @app.exception_handler(RequestValidationError)
90
  async def validation_exception_handler(request: Request, exc: RequestValidationError):
91
- body = await request.body()
92
- try:
93
- payload = json.loads(body)
94
- except:
95
- payload = body.decode()
96
-
97
  error_detail = exc.errors()
98
- logger.error(f"422 Unprocessable Entity Error! Payload: {payload} | Errors: {error_detail}")
99
-
 
 
 
100
  response_body = {
101
  "detail": error_detail,
102
- "received_body": payload,
103
- "message": "Validation failed. Check /debug/logs for details."
104
  }
105
- add_debug_log("OUTGOING_ERROR", request.url.path, request.method, {}, response_body)
106
-
 
 
 
 
 
 
 
 
 
 
 
107
  return JSONResponse(status_code=422, content=response_body)
108
 
109
- # --- Dependency for API Key Validation ---
110
- async def get_api_key(api_key_header: str = Depends(api_key_header)):
111
- if api_key_header is None or api_key_header != API_KEY:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  raise HTTPException(
113
  status_code=status.HTTP_401_UNAUTHORIZED,
114
- detail="Invalid API Key or missing 'x-api-key' header.",
115
  )
116
- return api_key_header
117
 
118
- # --- API Endpoints ---
 
 
 
 
 
 
 
 
119
 
 
 
 
120
  @app.get("/debug/logs")
121
  async def get_debug_logs():
122
- """Endpoint to view the last 50 requests and responses."""
123
- return {
124
- "count": len(DEBUG_LOGS),
125
- "logs": DEBUG_LOGS
126
- }
127
 
128
  @app.post("/api/honeypot-detection", response_model=HoneypotResponse)
129
  async def honeypot_detection(
@@ -132,7 +322,7 @@ async def honeypot_detection(
132
  ) -> Dict[str, Any]:
133
  session_id = request_data.sessionId
134
  config = {"configurable": {"thread_id": session_id}}
135
-
136
  checkpoint = honeypot_app.get_state(config)
137
  start_time = time.time()
138
 
@@ -144,7 +334,7 @@ async def honeypot_detection(
144
  current_state_dict.setdefault("conversationHistory", [])
145
  current_state_dict.setdefault("totalMessagesExchanged", 0)
146
  current_state_dict.setdefault("sessionId", session_id)
147
-
148
  current_state = AgentState(**current_state_dict)
149
  current_state["conversationHistory"].append(request_data.message)
150
  current_state["totalMessagesExchanged"] += 1
@@ -167,7 +357,7 @@ async def honeypot_detection(
167
  final_state_dict = honeypot_app.invoke(input_state, config=config)
168
  final_state = AgentState(**final_state_dict)
169
  engagement_duration = int(time.time() - start_time)
170
-
171
  response_content = {
172
  "status": "success",
173
  "scamDetected": final_state["scamDetected"],
@@ -178,24 +368,31 @@ async def honeypot_detection(
178
  "extractedIntelligence": final_state["extractedIntelligence"].model_dump(),
179
  "agentNotes": final_state["agentNotes"]
180
  }
181
-
182
- # Log the successful response body
183
- add_debug_log("OUTGOING_SUCCESS", "/api/honeypot-detection", "POST", {}, response_content)
184
-
 
 
 
 
 
 
185
  return response_content
186
 
187
  except Exception as e:
188
  error_msg = f"Internal Error: {str(e)}"
189
- logger.error(error_msg)
190
- add_debug_log("OUTGOING_ERROR", "/api/honeypot-detection", "POST", {}, {"error": error_msg})
191
  raise HTTPException(status_code=500, detail=error_msg)
192
 
193
  @app.get("/")
194
  async def root():
195
  return {
196
  "message": "Agentic Honey-Pot API is running.",
197
- "endpoints": {
198
- "detection": "/api/honeypot-detection",
199
- "debug_logs": "/debug/logs"
 
200
  }
201
  }
 
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
  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
  ) -> 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
 
 
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
  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
  "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
  }