Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from functools import wraps
from flask import request, jsonify

def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if api_key != 'your-secret-key': # Replace with actual key or env var
Comment on lines +3 to +8

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded API key creates a security vulnerability. Load the key from environment variables or secure configuration.

Suggested change
def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if api_key != 'your-secret-key': # Replace with actual key or env var
import os
def require_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
expected_api_key = os.environ.get('API_KEY')
if api_key != expected_api_key:

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Hardcoding secrets is a critical security vulnerability. The API key 'your-secret-key' should not be in the source code. It should be loaded from a secure source, such as an environment variable or a secret management service, especially for a production environment.

Suggested change
if api_key != 'your-secret-key': # Replace with actual key or env var
if api_key != os.environ.get('API_SECRET_KEY'): # Replace with actual key or env var

return jsonify({'error': 'API key required'}), 401
return f(*args, **kwargs)
Comment on lines +7 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Eliminate hardcoded API key; use env var + constant-time compare.

Hardcoding secrets and using plain equality is insecure. Read from env and use hmac.compare_digest.

Apply within this block:

-        api_key = request.headers.get('X-API-Key')
-        if api_key != 'your-secret-key':  # Replace with actual key or env var
-            return jsonify({'error': 'API key required'}), 401
+        api_key = request.headers.get('X-API-Key', '')
+        expected = os.getenv('API_KEY', '')
+        if not expected or not hmac.compare_digest(api_key, expected):
+            return jsonify({'error': 'invalid or missing API key'}), 401

Add imports at top:

import os, hmac
πŸ€– Prompt for AI Agents
In src/auth.py around lines 7 to 10, the code currently uses a hardcoded API key
and plain equality; replace this by reading the expected key from an environment
variable (e.g., os.getenv('API_KEY', '')) and perform the comparison using
hmac.compare_digest to avoid timing attacks; also ensure you import os and hmac
at the top of the file, treat missing env var as empty string, and return the
same 401 response when the constant-time comparison fails.

return decorated_function
83 changes: 83 additions & 0 deletions src/health_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from flask import Blueprint, jsonify, request
from health_monitor import health_monitor
import logging

logger = logging.getLogger(__name__)

# Create health endpoints blueprint
health_bp = Blueprint('health', __name__, url_prefix='/api/health')

@health_bp.route('/', methods=['GET'])
def health_check():
"""Basic health check endpoint."""
try:
summary = health_monitor.get_health_summary()
status_code = 200 if summary["status"] in ["healthy", "warning"] else 503
return jsonify(summary), status_code
except Exception as e:
logger.error(f"Health check failed: {e}")
return jsonify({
"status": "error",
"message": "Health check failed"
}), 500

@health_bp.route('/detailed', methods=['GET'])
def detailed_health():
"""Detailed health check with system metrics."""
try:
health_data = health_monitor.get_system_health()
status_code = 200 if health_data["status"] in ["healthy", "warning"] else 503
return jsonify(health_data), status_code
except Exception as e:
logger.error(f"Detailed health check failed: {e}")
return jsonify({
"status": "error",
"message": "Detailed health check failed"
}), 500

@health_bp.route('/ready', methods=['GET'])
def readiness_check():
"""Kubernetes readiness probe endpoint."""
try:
health_data = health_monitor.get_system_health()
if health_data["status"] in ["healthy", "warning"]:
return jsonify({"ready": True}), 200
else:
return jsonify({"ready": False, "reason": health_data["status"]}), 503
except Exception as e:
logger.error(f"Readiness check failed: {e}")
return jsonify({"ready": False, "reason": "error"}), 503

@health_bp.route('/live', methods=['GET'])
def liveness_check():
"""Kubernetes liveness probe endpoint."""
try:
# Simple liveness check - just verify the service is responding
return jsonify({"alive": True}), 200
except Exception as e:
logger.error(f"Liveness check failed: {e}")
return jsonify({"alive": False}), 500

@health_bp.route('/metrics', methods=['GET'])
def health_metrics():
"""Health metrics endpoint for monitoring systems."""
try:
health_data = health_monitor.get_system_health()
metrics = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Uptime calculation in metrics endpoint may lose precision.

Tracking uptime in seconds instead of converting from hours will improve metric accuracy.

Suggested implementation:

            "uptime_seconds": health_data["uptime_seconds"]

Ensure that the health_monitor.get_system_health() method returns an uptime_seconds field in its result dictionary. You may need to update the health monitor implementation to track uptime in seconds and include it in the returned data.

"api_requests_total": health_data["process"]["request_count"],
"api_errors_total": health_data["process"]["error_count"],
"api_error_rate_percent": health_data["process"]["error_rate"],
"system_cpu_percent": health_data["system"]["cpu_percent"],
"system_memory_percent": health_data["system"]["memory_percent"],
"system_disk_percent": health_data["system"]["disk_percent"],
"uptime_seconds": health_data["uptime_hours"] * 3600
}
return jsonify(metrics), 200
Comment on lines +65 to +75

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard metrics extraction when health_data indicates error.

If get_system_health() returns {"status":"error",...}, indexing process/system will KeyError.

-        health_data = health_monitor.get_system_health()
-        metrics = {
+        health_data = health_monitor.get_system_health()
+        if health_data.get("status") == "error":
+            return jsonify({"error": "Metrics unavailable"}), 503
+        metrics = {
             "api_requests_total": health_data["process"]["request_count"],
             "api_errors_total": health_data["process"]["error_count"],
             "api_error_rate_percent": health_data["process"]["error_rate"],
             "system_cpu_percent": health_data["system"]["cpu_percent"],
             "system_memory_percent": health_data["system"]["memory_percent"],
             "system_disk_percent": health_data["system"]["disk_percent"],
             "uptime_seconds": health_data["uptime_hours"] * 3600
         }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
health_data = health_monitor.get_system_health()
metrics = {
"api_requests_total": health_data["process"]["request_count"],
"api_errors_total": health_data["process"]["error_count"],
"api_error_rate_percent": health_data["process"]["error_rate"],
"system_cpu_percent": health_data["system"]["cpu_percent"],
"system_memory_percent": health_data["system"]["memory_percent"],
"system_disk_percent": health_data["system"]["disk_percent"],
"uptime_seconds": health_data["uptime_hours"] * 3600
}
return jsonify(metrics), 200
health_data = health_monitor.get_system_health()
if health_data.get("status") == "error":
return jsonify({"error": "Metrics unavailable"}), 503
metrics = {
"api_requests_total": health_data["process"]["request_count"],
"api_errors_total": health_data["process"]["error_count"],
"api_error_rate_percent": health_data["process"]["error_rate"],
"system_cpu_percent": health_data["system"]["cpu_percent"],
"system_memory_percent": health_data["system"]["memory_percent"],
"system_disk_percent": health_data["system"]["disk_percent"],
"uptime_seconds": health_data["uptime_hours"] * 3600
}
return jsonify(metrics), 200
πŸ€– Prompt for AI Agents
In src/health_endpoints.py around lines 65 to 75, the code assumes health_data
contains "process" and "system" and will KeyError if get_system_health() returns
{"status": "error", ...}; guard the extraction by first checking
health_data.get("status") == "ok" (or validate presence of "process" and
"system") and if not return an appropriate error response (e.g.,
jsonify({"error":"health unavailable"}), 500) or populate safe default metric
values, and optionally log the health error; implement this check before
building the metrics dict to avoid KeyError.

except Exception as e:
logger.error(f"Metrics collection failed: {e}")
return jsonify({"error": "Metrics collection failed"}), 500

def register_health_endpoints(app):
"""Register health endpoints with the Flask app."""
app.register_blueprint(health_bp)
logger.info("Health endpoints registered: /api/health/*")
90 changes: 90 additions & 0 deletions src/health_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import time
import psutil
from datetime import datetime
from typing import Dict, Any, Optional
import logging

logger = logging.getLogger(__name__)

class HealthMonitor:
"""Health monitoring system for API endpoints and system resources."""

def __init__(self):
self.start_time = time.time()
self.request_count = 0
self.error_count = 0
self.last_health_check = None

def get_system_health(self) -> Dict[str, Any]:
"""Get comprehensive system health metrics."""
try:
# System resource usage
cpu_percent = psutil.cpu_percent(interval=1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using psutil.cpu_percent(interval=1) creates a 1-second blocking call. This will make your health check endpoints slow to respond, which can cause problems for services that rely on them (like Kubernetes liveness/readiness probes). For a responsive API, consider using a non-blocking call with interval=None (after an initial call to start measurement) or a much smaller interval like 0.1.

Suggested change
cpu_percent = psutil.cpu_percent(interval=1)
cpu_percent = psutil.cpu_percent(interval=0.1)

memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')

# Process information
process = psutil.Process()
process_memory = process.memory_info().rss / 1024 / 1024 # MB

# Uptime calculation
uptime_seconds = time.time() - self.start_time
uptime_hours = uptime_seconds / 3600

health_data = {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"uptime_hours": round(uptime_hours, 2),
"system": {
"cpu_percent": cpu_percent,
"memory_percent": memory.percent,
"memory_available_gb": round(memory.available / 1024**3, 2),
"disk_percent": disk.percent,
"disk_free_gb": round(disk.free / 1024**3, 2)
},
"process": {
"memory_mb": round(process_memory, 2),
"request_count": self.request_count,
"error_count": self.error_count,
"error_rate": round(self.error_count / max(self.request_count, 1) * 100, 2)
},
"last_health_check": self.last_health_check
}
Comment on lines +34 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve efficiency and precision, consider adding uptime_seconds directly to the health_data dictionary. Currently, consumers like the /metrics endpoint have to recalculate it from uptime_hours, which involves floating-point arithmetic and can lead to precision loss. Providing the raw seconds value is cleaner.

Suggested change
health_data = {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"uptime_hours": round(uptime_hours, 2),
"system": {
"cpu_percent": cpu_percent,
"memory_percent": memory.percent,
"memory_available_gb": round(memory.available / 1024**3, 2),
"disk_percent": disk.percent,
"disk_free_gb": round(disk.free / 1024**3, 2)
},
"process": {
"memory_mb": round(process_memory, 2),
"request_count": self.request_count,
"error_count": self.error_count,
"error_rate": round(self.error_count / max(self.request_count, 1) * 100, 2)
},
"last_health_check": self.last_health_check
}
health_data = {
"status": "healthy",
"timestamp": datetime.utcnow().isoformat(),
"uptime_hours": round(uptime_hours, 2),
"uptime_seconds": int(uptime_seconds),
"system": {
"cpu_percent": cpu_percent,
"memory_percent": memory.percent,
"memory_available_gb": round(memory.available / 1024**3, 2),
"disk_percent": disk.percent,
"disk_free_gb": round(disk.free / 1024**3, 2)
},
"process": {
"memory_mb": round(process_memory, 2),
"request_count": self.request_count,
"error_count": self.error_count,
"error_rate": round(self.error_count / max(self.request_count, 1) * 100, 2)
},
"last_health_check": self.last_health_check
}


# Determine overall health status
if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
health_data["status"] = "warning"
if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
health_data["status"] = "critical"
Comment on lines +55 to +58

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Health threshold values (90, 95) are hardcoded magic numbers. Consider making these configurable constants or parameters to improve maintainability.

Copilot uses AI. Check for mistakes.
if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1:

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error rate threshold (0.1) is a hardcoded magic number. Consider defining this as a configurable constant.

Copilot uses AI. Check for mistakes.
health_data["status"] = "degraded"
Comment on lines +55 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The logic for determining the overall health status is flawed. The sequence of if statements can lead to a more severe status (like critical) being overwritten by a less severe one (like degraded). You should use an if/elif structure to ensure the most severe status is reported. The order should be from most to least severe.

Suggested change
if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
health_data["status"] = "warning"
if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
health_data["status"] = "critical"
if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1:
health_data["status"] = "degraded"
# Determine overall health status
if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
health_data["status"] = "critical"
elif self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1:
health_data["status"] = "degraded"
elif cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
health_data["status"] = "warning"


Comment on lines +55 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Status precedence bug: β€œdegraded” overrides β€œcritical”.

If CPU/mem/disk >95%, status becomes critical, then is overwritten to degraded on error_rate >10%. Preserve highest severity.

-            if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
-                health_data["status"] = "warning"
-            if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
-                health_data["status"] = "critical"
-            if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1:
-                health_data["status"] = "degraded"
+            # Determine overall health with clear precedence: critical > degraded > warning > healthy
+            status = "healthy"
+            if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
+                status = "warning"
+            if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
+                status = "critical"
+            err_rate = self.error_count / max(self.request_count, 1)
+            if status != "critical" and self.error_count > 0 and err_rate > 0.10:
+                status = "degraded"
+            health_data["status"] = status
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
health_data["status"] = "warning"
if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
health_data["status"] = "critical"
if self.error_count > 0 and self.error_count / max(self.request_count, 1) > 0.1:
health_data["status"] = "degraded"
# Determine overall health with clear precedence: critical > degraded > warning > healthy
status = "healthy"
if cpu_percent > 90 or memory.percent > 90 or disk.percent > 90:
status = "warning"
if cpu_percent > 95 or memory.percent > 95 or disk.percent > 95:
status = "critical"
err_rate = self.error_count / max(self.request_count, 1)
if status != "critical" and self.error_count > 0 and err_rate > 0.10:
status = "degraded"
health_data["status"] = status
πŸ€– Prompt for AI Agents
In src/health_monitor.py around lines 55 to 61, the status assignment logic
allows "degraded" (error rate >10%) to overwrite a previously set
higher-severity "critical" state; change the logic to enforce severity
precedence so the final status is the highest-severity condition met (e.g.,
critical > degraded > warning > healthy). Implement this by computing each
condition but only upgrading status when the new severity is higher than the
current one (or map statuses to numeric severity levels and pick the maximum),
so critical is never overridden by degraded or warning.

self.last_health_check = health_data["timestamp"]
return health_data

except Exception as e:
logger.error(f"Health check failed: {e}")
return {
"status": "error",
"timestamp": datetime.utcnow().isoformat(),
"error": str(e)
}

def record_request(self, success: bool = True):
"""Record a request for health monitoring."""
self.request_count += 1
if not success:
self.error_count += 1
Comment on lines +73 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The record_request method is not thread-safe. In a multi-threaded environment (like a production WSGI server), incrementing self.request_count and self.error_count without a lock will lead to a race condition, resulting in inaccurate metrics. You should use a threading.Lock to protect access to these shared counters. You'll need to add self.lock = threading.Lock() in the __init__ method and import the threading module.

Suggested change
def record_request(self, success: bool = True):
"""Record a request for health monitoring."""
self.request_count += 1
if not success:
self.error_count += 1
def record_request(self, success: bool = True):
"""Record a request for health monitoring."""
with self.lock:
self.request_count += 1
if not success:
self.error_count += 1


def get_health_summary(self) -> Dict[str, Any]:
"""Get a simplified health summary for quick checks."""
health = self.get_system_health()
return {
"status": health["status"],
"uptime_hours": health["uptime_hours"],
"request_count": health["process"]["request_count"],
"error_rate": health["process"]["error_rate"]
}

# Global health monitor instance
health_monitor = HealthMonitor()
21 changes: 21 additions & 0 deletions src/rate_limiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from collections import defaultdict
from datetime import datetime, timedelta
from flask import abort, current_app

# Simple rate limiter using memory (use Redis for production)
rate_limit = defaultdict(list)

def rate_limit(max_requests=100, window_minutes=1):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Decorator name 'rate_limit' conflicts with global variable.

Consider renaming either the decorator or the global variable to avoid confusion and potential bugs.

Comment on lines +6 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a name collision between the global variable rate_limit and the function rate_limit. This will cause the function to overwrite the defaultdict variable, leading to a TypeError at runtime on line 15 when you try to access rate_limit[client_ip]. Please rename the global variable to avoid this critical bug.

Suggested change
rate_limit = defaultdict(list)
def rate_limit(max_requests=100, window_minutes=1):
# Simple rate limiter using memory (use Redis for production)
request_timestamps = defaultdict(list)
def rate_limit(max_requests=100, window_minutes=1):

def decorator(f):
@wraps(f)

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing import for wraps function. Add from functools import wraps at the top of the file.

Copilot uses AI. Check for mistakes.
def decorated_function(*args, **kwargs):
client_ip = request.remote_addr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Rate limiting by client IP may not be reliable behind proxies.

Behind proxies or load balancers, 'request.remote_addr' may not provide the actual client IP. Use 'X-Forwarded-For' or configure trusted proxies to ensure accurate rate limiting.

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing import for request object. Add from flask import request to the imports.

Copilot uses AI. Check for mistakes.
now = datetime.utcnow()
window_start = now - timedelta(minutes=window_minutes)
rate_limit[client_ip] = [req_time for req_time in rate_limit[client_ip] if req_time > window_start]
if len(rate_limit[client_ip]) >= max_requests:
abort(429, description="Rate limit exceeded")
rate_limit[client_ip].append(now)
return f(*args, **kwargs)
Comment on lines +11 to +19

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The rate limiting logic is not thread-safe. The global rate_limit dictionary (assuming the naming collision is fixed) is a shared resource that is modified by concurrent requests without any synchronization. This will cause race conditions in a multi-threaded server. You need to protect access to this dictionary with a threading.Lock.

return decorated_function
return decorator
Comment on lines +1 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix name shadowing and missing imports causing runtime failures.

rate_limit function shadows the dict rate_limit, and wraps/request aren’t imported. This will raise TypeError and NameError.

Apply:

-from collections import defaultdict
-from datetime import datetime, timedelta
-from flask import abort, current_app
+from collections import defaultdict
+from datetime import datetime, timedelta
+from functools import wraps
+from flask import abort, request
@@
-# Simple rate limiter using memory (use Redis for production)
-rate_limit = defaultdict(list)
+# Simple rate limiter using memory (use Redis for production)
+_RATE_LIMIT_STORE = defaultdict(list)
@@
-def rate_limit(max_requests=100, window_minutes=1):
+def rate_limit(max_requests=100, window_minutes=1):
     def decorator(f):
         @wraps(f)
         def decorated_function(*args, **kwargs):
             client_ip = request.remote_addr
             now = datetime.utcnow()
             window_start = now - timedelta(minutes=window_minutes)
-            rate_limit[client_ip] = [req_time for req_time in rate_limit[client_ip] if req_time > window_start]
-            if len(rate_limit[client_ip]) >= max_requests:
+            _RATE_LIMIT_STORE[client_ip] = [t for t in _RATE_LIMIT_STORE[client_ip] if t > window_start]
+            if len(_RATE_LIMIT_STORE[client_ip]) >= max_requests:
                 abort(429, description="Rate limit exceeded")
-            rate_limit[client_ip].append(now)
+            _RATE_LIMIT_STORE[client_ip].append(now)
             return f(*args, **kwargs)
         return decorated_function
     return decorator

Optional hardening:

  • Use X-Forwarded-For when behind proxies.
  • Add a lock if running multi-threaded.
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from collections import defaultdict
from datetime import datetime, timedelta
from flask import abort, current_app
# Simple rate limiter using memory (use Redis for production)
rate_limit = defaultdict(list)
def rate_limit(max_requests=100, window_minutes=1):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
client_ip = request.remote_addr
now = datetime.utcnow()
window_start = now - timedelta(minutes=window_minutes)
rate_limit[client_ip] = [req_time for req_time in rate_limit[client_ip] if req_time > window_start]
if len(rate_limit[client_ip]) >= max_requests:
abort(429, description="Rate limit exceeded")
rate_limit[client_ip].append(now)
return f(*args, **kwargs)
return decorated_function
return decorator
from collections import defaultdict
from datetime import datetime, timedelta
from functools import wraps
from flask import abort, request
# Simple rate limiter using memory (use Redis for production)
_RATE_LIMIT_STORE = defaultdict(list)
def rate_limit(max_requests=100, window_minutes=1):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
client_ip = request.remote_addr
now = datetime.utcnow()
window_start = now - timedelta(minutes=window_minutes)
_RATE_LIMIT_STORE[client_ip] = [
t for t in _RATE_LIMIT_STORE[client_ip]
if t > window_start
]
if len(_RATE_LIMIT_STORE[client_ip]) >= max_requests:
abort(429, description="Rate limit exceeded")
_RATE_LIMIT_STORE[client_ip].append(now)
return f(*args, **kwargs)
return decorated_function
return decorator
🧰 Tools
πŸͺ› Ruff (0.12.2)

8-8: Redefinition of unused rate_limit from line 6

(F811)


10-10: Undefined name wraps

(F821)


12-12: Undefined name request

(F821)

πŸ€– Prompt for AI Agents
In src/rate_limiter.py lines 1-21 the top-level dict rate_limit is being
shadowed by the decorator function rate_limit and wraps and request are not
imported causing TypeError/NameError; rename the dict (e.g., rate_limit_store or
rate_limit_map) to avoid the name collision, add the missing imports from
functools import wraps and from flask import request, update all references in
the decorator to use the new dict name, and keep the decorator name rate_limit
(or rename the function if you prefer) so runtime errors are resolved;
optionally consider using X-Forwarded-For for proxied clients and protecting the
dict with a threading.Lock for multi-threaded environments.

45 changes: 45 additions & 0 deletions src/security/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Optional

# Security settings
SECRET_KEY = "your-secret-key" # Should be loaded from config

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): Hardcoded secret key poses a security risk.

Consider loading the secret key from environment variables or a secure config system instead of hardcoding it.

Comment on lines +7 to +9

Copilot AI Sep 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded secret key poses a security risk. This should be loaded from environment variables or secure configuration.

Suggested change
# Security settings
SECRET_KEY = "your-secret-key" # Should be loaded from config
import os
# Security settings
SECRET_KEY = os.environ["SECRET_KEY"] # Must be set in environment

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

Hardcoding a secret key is a critical security vulnerability. The SECRET_KEY should be loaded from environment variables or a secure secret management system, not committed to the repository.

Suggested change
SECRET_KEY = "your-secret-key" # Should be loaded from config
SECRET_KEY = os.environ.get("JWT_SECRET_KEY") # Should be loaded from config

ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

Comment on lines +8 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Replace hardcoded JWT secret and align expiry default.

Load secret from env/config and use ACCESS_TOKEN_EXPIRE_MINUTES consistently.

-SECRET_KEY = "your-secret-key"  # Should be loaded from config
+import os
+SECRET_KEY = os.getenv("JWT_SECRET_KEY", "")
@@
-ACCESS_TOKEN_EXPIRE_MINUTES = 30
+ACCESS_TOKEN_EXPIRE_MINUTES = 30

Ensure startup fails if SECRET_KEY is empty, or handle gracefully.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Security settings
SECRET_KEY = "your-secret-key" # Should be loaded from config
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Security settings
import os
SECRET_KEY = os.getenv("JWT_SECRET_KEY", "")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
🧰 Tools
πŸͺ› Ruff (0.12.2)

9-9: Possible hardcoded password assigned to: "SECRET_KEY"

(S105)

πŸ€– Prompt for AI Agents
In src/security/auth.py around lines 8 to 12, the JWT secret is hardcoded and
the expiry constant may be inconsistently used; replace the literal SECRET_KEY
with a value read from configuration or environment (e.g., os.environ or your
app config loader), validate that the loaded SECRET_KEY is non-empty at startup
and raise/exit with a clear error if it is missing, and ensure
ACCESS_TOKEN_EXPIRE_MINUTES is defined as the single canonical constant used
throughout the codebase (remove duplicate literals and reference this constant
wherever token expiry is set) so expiry behavior is consistent.

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer()

def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password):
return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
Comment on lines +28 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): We've found these issues:

Suggested change
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
to_encode["exp"] = expire
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)


async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
Comment on lines +43 to +44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): Explicitly raise from a previous error (raise-from-previous-error)

Suggested change
except JWTError:
raise credentials_exception
except JWTError as e:
raise credentials_exception from e

return username
31 changes: 31 additions & 0 deletions src/security/rate_limiter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Optional
import time

class RateLimiter:
def __init__(self, max_requests: int = 100, window_seconds: int = 3600):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = defaultdict(list)

def is_allowed(self, identifier: str) -> bool:
now = time.time()
window_start = now - self.window_seconds
self.requests[identifier] = [
timestamp for timestamp in self.requests[identifier]
if timestamp > window_start
]
if len(self.requests[identifier]) < self.max_requests:
self.requests[identifier].append(now)
return True
return False
Comment on lines +12 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The is_allowed method is not thread-safe. It modifies the shared self.requests dictionary. In a multi-threaded application, this can lead to race conditions where multiple threads read and write to the list for the same identifier concurrently, resulting in incorrect rate limiting. You should use a threading.Lock to protect modifications to self.requests.


def get_remaining_requests(self, identifier: str) -> int:
now = time.time()
window_start = now - self.window_seconds
self.requests[identifier] = [
timestamp for timestamp in self.requests[identifier]
if timestamp > window_start
]
Comment on lines +15 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There is code duplication between is_allowed and get_remaining_requests. Both methods contain the same logic to filter out old timestamps. This logic should be extracted into a private helper method to improve maintainability and reduce redundancy.

return max(0, self.max_requests - len(self.requests[identifier]))
20 changes: 20 additions & 0 deletions src/unified_api_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from flask import Flask, jsonify
from flask_cors import CORS
from auth import require_api_key

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 issue (security): API key is hardcoded and not configurable.

Consider loading the API key from environment variables or a config file to improve security and flexibility.

from rate_limiter import rate_limit

app = Flask(__name__)
CORS(app) # Enable CORS for all routes

@app.route('/api/health')
def health():
return jsonify({'status': 'healthy'})

Comment on lines +9 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion

Route duplication with health blueprint.

This overlaps with health_bp at /api/health/. Prefer the blueprint-only version to avoid divergent responses.

-@app.route('/api/health')
-def health():
-    return jsonify({'status': 'healthy'})
+# Register health blueprint instead of a separate route
+from health_endpoints import register_health_endpoints  # at top-level imports
+register_health_endpoints(app)
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@app.route('/api/health')
def health():
return jsonify({'status': 'healthy'})
# Register health blueprint instead of a separate route
from health_endpoints import register_health_endpoints # at top-level imports
register_health_endpoints(app)
πŸ€– Prompt for AI Agents
In src/unified_api_server.py around lines 9 to 12, there's a duplicate
/api/health route defined directly on the app that conflicts with the health_bp
blueprint at /api/health/; remove this function-based route (the shown
@app.route('/api/health') health() handler) so only the blueprint-backed
endpoint remains, and verify health_bp is registered on the app with the
intended URL prefix.

@app.route('/api/protected', methods=['POST'])
@require_api_key
@rate_limit(max_requests=10, window_minutes=1)
def protected():
return jsonify({'message': 'Protected endpoint'})

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
Comment on lines +19 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using app.run() starts the Flask development server, which is not suitable for production use as it's not designed to be efficient, stable, or secure. For a production deployment, especially in a containerized environment like Kubernetes, you should use a production-grade WSGI server like Gunicorn or uWSGI to run the application.

Loading