# SneakyScanner Web API Reference **Version:** 2.0 (Phase 2) **Base URL:** `http://localhost:5000` **Authentication:** Session-based (Flask-Login) ## Table of Contents 1. [Authentication](#authentication) 2. [Scans API](#scans-api) 3. [Settings API](#settings-api) 4. [Error Handling](#error-handling) 5. [Status Codes](#status-codes) 6. [Request/Response Examples](#request-response-examples) --- ## Authentication SneakyScanner uses session-based authentication with Flask-Login. All API endpoints (except login) require authentication. ### Login Authenticate and create a session. **Endpoint:** `POST /auth/login` **Request Body:** ```json { "password": "your-password-here" } ``` **Success Response (200 OK):** ```json { "message": "Login successful", "redirect": "/dashboard" } ``` **Error Response (401 Unauthorized):** ```json { "error": "Invalid password" } ``` **Usage Example:** ```bash # Login and save session cookie curl -X POST http://localhost:5000/auth/login \ -H "Content-Type: application/json" \ -d '{"password":"yourpassword"}' \ -c cookies.txt # Use session cookie for subsequent requests curl -X GET http://localhost:5000/api/scans \ -b cookies.txt ``` ### Logout Destroy the current session. **Endpoint:** `GET /auth/logout` **Success Response:** Redirects to login page (302) --- ## Scans API Manage network scans: trigger, list, view, and delete. ### Trigger Scan Start a new background scan. **Endpoint:** `POST /api/scans` **Authentication:** Required **Request Body:** ```json { "config_file": "/app/configs/example-site.yaml" } ``` **Success Response (201 Created):** ```json { "scan_id": 42, "status": "running", "message": "Scan queued successfully" } ``` **Error Responses:** *400 Bad Request* - Invalid config file: ```json { "error": "Invalid config file", "message": "Config file does not exist or is not valid YAML" } ``` *500 Internal Server Error* - Scan queue failure: ```json { "error": "Failed to queue scan", "message": "Internal server error" } ``` **Usage Example:** ```bash curl -X POST http://localhost:5000/api/scans \ -H "Content-Type: application/json" \ -d '{"config_file":"/app/configs/production.yaml"}' \ -b cookies.txt ``` ### List Scans Retrieve a paginated list of scans with optional status filtering. **Endpoint:** `GET /api/scans` **Authentication:** Required **Query Parameters:** | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `page` | integer | No | 1 | Page number (1-indexed) | | `per_page` | integer | No | 20 | Items per page (1-100) | | `status` | string | No | - | Filter by status: `running`, `completed`, `failed` | **Success Response (200 OK):** ```json { "scans": [ { "id": 42, "timestamp": "2025-11-14T10:30:00Z", "duration": 125.5, "status": "completed", "title": "Production Network Scan", "config_file": "/app/configs/production.yaml", "triggered_by": "manual", "started_at": "2025-11-14T10:30:00Z", "completed_at": "2025-11-14T10:32:05Z" }, { "id": 41, "timestamp": "2025-11-13T15:00:00Z", "duration": 98.2, "status": "completed", "title": "Development Network Scan", "config_file": "/app/configs/dev.yaml", "triggered_by": "scheduled", "started_at": "2025-11-13T15:00:00Z", "completed_at": "2025-11-13T15:01:38Z" } ], "total": 42, "page": 1, "per_page": 20, "pages": 3 } ``` **Error Responses:** *400 Bad Request* - Invalid parameters: ```json { "error": "Invalid pagination parameters", "message": "Page and per_page must be positive integers" } ``` **Usage Examples:** ```bash # List first page (default 20 items) curl -X GET http://localhost:5000/api/scans \ -b cookies.txt # List page 2 with 50 items per page curl -X GET "http://localhost:5000/api/scans?page=2&per_page=50" \ -b cookies.txt # List only running scans curl -X GET "http://localhost:5000/api/scans?status=running" \ -b cookies.txt ``` ### Get Scan Details Retrieve complete details for a specific scan, including all sites, IPs, ports, services, certificates, and TLS versions. **Endpoint:** `GET /api/scans/{id}` **Authentication:** Required **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | Scan ID | **Success Response (200 OK):** ```json { "id": 42, "timestamp": "2025-11-14T10:30:00Z", "duration": 125.5, "status": "completed", "title": "Production Network Scan", "config_file": "/app/configs/production.yaml", "json_path": "/app/output/scan_report_20251114_103000.json", "html_path": "/app/output/scan_report_20251114_103000.html", "zip_path": "/app/output/scan_report_20251114_103000.zip", "screenshot_dir": "/app/output/scan_report_20251114_103000_screenshots", "triggered_by": "manual", "started_at": "2025-11-14T10:30:00Z", "completed_at": "2025-11-14T10:32:05Z", "error_message": null, "sites": [ { "id": 101, "site_name": "Production Web Servers", "ips": [ { "id": 201, "ip_address": "192.168.1.10", "ping_expected": true, "ping_actual": true, "ports": [ { "id": 301, "port": 443, "protocol": "tcp", "expected": true, "state": "open", "services": [ { "id": 401, "service_name": "https", "product": "nginx", "version": "1.24.0", "extrainfo": null, "ostype": "Linux", "http_protocol": "https", "screenshot_path": "scan_report_20251114_103000_screenshots/192_168_1_10_443.png", "certificates": [ { "id": 501, "subject": "CN=example.com", "issuer": "CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US", "serial_number": "123456789012345678901234567890", "not_valid_before": "2025-01-01T00:00:00+00:00", "not_valid_after": "2025-04-01T23:59:59+00:00", "days_until_expiry": 89, "sans": "[\"example.com\", \"www.example.com\"]", "is_self_signed": false, "tls_versions": [ { "id": 601, "tls_version": "TLS 1.2", "supported": true, "cipher_suites": "[\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\", \"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256\"]" }, { "id": 602, "tls_version": "TLS 1.3", "supported": true, "cipher_suites": "[\"TLS_AES_256_GCM_SHA384\", \"TLS_AES_128_GCM_SHA256\"]" } ] } ] } ] } ] } ] } ] } ``` **Error Responses:** *404 Not Found* - Scan doesn't exist: ```json { "error": "Scan not found", "message": "Scan with ID 42 does not exist" } ``` **Usage Example:** ```bash curl -X GET http://localhost:5000/api/scans/42 \ -b cookies.txt ``` ### Get Scan Status Poll the current status of a running scan. Use this endpoint to track scan progress. **Endpoint:** `GET /api/scans/{id}/status` **Authentication:** Required **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | Scan ID | **Success Response (200 OK):** *Running scan:* ```json { "scan_id": 42, "status": "running", "started_at": "2025-11-14T10:30:00Z", "completed_at": null, "error_message": null } ``` *Completed scan:* ```json { "scan_id": 42, "status": "completed", "started_at": "2025-11-14T10:30:00Z", "completed_at": "2025-11-14T10:32:05Z", "error_message": null } ``` *Failed scan:* ```json { "scan_id": 42, "status": "failed", "started_at": "2025-11-14T10:30:00Z", "completed_at": "2025-11-14T10:30:15Z", "error_message": "Config file not found: /app/configs/missing.yaml" } ``` **Error Responses:** *404 Not Found* - Scan doesn't exist: ```json { "error": "Scan not found", "message": "Scan with ID 42 does not exist" } ``` **Usage Example:** ```bash # Poll status every 5 seconds while true; do curl -X GET http://localhost:5000/api/scans/42/status -b cookies.txt sleep 5 done ``` ### Delete Scan Delete a scan and all associated files (JSON, HTML, ZIP, screenshots). **Endpoint:** `DELETE /api/scans/{id}` **Authentication:** Required **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | Scan ID | **Success Response (200 OK):** ```json { "message": "Scan 42 deleted successfully" } ``` **Error Responses:** *404 Not Found* - Scan doesn't exist: ```json { "error": "Scan not found", "message": "Scan with ID 42 does not exist" } ``` **Usage Example:** ```bash curl -X DELETE http://localhost:5000/api/scans/42 \ -b cookies.txt ``` --- ## Settings API Manage application settings including SMTP configuration, encryption keys, and preferences. ### Get All Settings Retrieve all application settings. Sensitive values (passwords, keys) are masked. **Endpoint:** `GET /api/settings` **Authentication:** Required **Success Response (200 OK):** ```json { "smtp_server": "smtp.gmail.com", "smtp_port": 587, "smtp_username": "alerts@example.com", "smtp_password": "********", "smtp_from_email": "alerts@example.com", "smtp_to_emails": "[\"admin@example.com\"]", "retention_days": 90, "app_password": "********" } ``` **Usage Example:** ```bash curl -X GET http://localhost:5000/api/settings \ -b cookies.txt ``` ### Update Setting Update a specific setting value. **Endpoint:** `PUT /api/settings/{key}` **Authentication:** Required **Path Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `key` | string | Yes | Setting key (e.g., `smtp_server`) | **Request Body:** ```json { "value": "smtp.example.com" } ``` **Success Response (200 OK):** ```json { "message": "Setting updated successfully", "key": "smtp_server", "value": "smtp.example.com" } ``` **Error Responses:** *400 Bad Request* - Missing value: ```json { "error": "Missing required field: value" } ``` **Usage Example:** ```bash curl -X PUT http://localhost:5000/api/settings/smtp_server \ -H "Content-Type: application/json" \ -d '{"value":"smtp.example.com"}' \ -b cookies.txt ``` ### Health Check Check if the API is running and database is accessible. **Endpoint:** `GET /api/settings/health` **Authentication:** Not required **Success Response (200 OK):** ```json { "status": "healthy", "database": "connected" } ``` **Error Response (500 Internal Server Error):** ```json { "status": "unhealthy", "database": "disconnected", "error": "Connection error details" } ``` **Usage Example:** ```bash curl -X GET http://localhost:5000/api/settings/health ``` --- ## Error Handling ### Error Response Format All error responses follow a consistent JSON format: ```json { "error": "Brief error type", "message": "Detailed error message for debugging" } ``` ### Content Negotiation The API supports content negotiation based on the request: - **API Requests** (Accept: application/json or /api/* path): Returns JSON errors - **Web Requests** (Accept: text/html): Returns HTML error pages **Example:** ```bash # JSON error response curl -X GET http://localhost:5000/api/scans/999 \ -H "Accept: application/json" \ -b cookies.txt # HTML error page curl -X GET http://localhost:5000/scans/999 \ -H "Accept: text/html" \ -b cookies.txt ``` ### Request ID Tracking Every request receives a unique request ID for tracking and debugging: **Response Headers:** ``` X-Request-ID: a1b2c3d4 X-Request-Duration-Ms: 125 ``` Check application logs for detailed error information using the request ID: ``` 2025-11-14 10:30:15 INFO [a1b2c3d4] GET /api/scans 200 125ms ``` --- ## Status Codes ### Success Codes | Code | Meaning | Usage | |------|---------|-------| | 200 | OK | Successful GET, PUT, DELETE requests | | 201 | Created | Successful POST request that creates a resource | | 302 | Found | Redirect (used for logout, login success) | ### Client Error Codes | Code | Meaning | Usage | |------|---------|-------| | 400 | Bad Request | Invalid request parameters or body | | 401 | Unauthorized | Authentication required or failed | | 403 | Forbidden | Authenticated but not authorized | | 404 | Not Found | Resource doesn't exist | | 405 | Method Not Allowed | HTTP method not supported for endpoint | ### Server Error Codes | Code | Meaning | Usage | |------|---------|-------| | 500 | Internal Server Error | Unexpected server error | --- ## Request/Response Examples ### Complete Workflow: Trigger and Monitor Scan ```bash #!/bin/bash # 1. Login and save session curl -X POST http://localhost:5000/auth/login \ -H "Content-Type: application/json" \ -d '{"password":"yourpassword"}' \ -c cookies.txt # 2. Trigger a new scan RESPONSE=$(curl -s -X POST http://localhost:5000/api/scans \ -H "Content-Type: application/json" \ -d '{"config_file":"/app/configs/production.yaml"}' \ -b cookies.txt) # Extract scan ID from response SCAN_ID=$(echo $RESPONSE | jq -r '.scan_id') echo "Scan ID: $SCAN_ID" # 3. Poll status every 5 seconds until complete while true; do STATUS=$(curl -s -X GET http://localhost:5000/api/scans/$SCAN_ID/status \ -b cookies.txt | jq -r '.status') echo "Status: $STATUS" if [ "$STATUS" == "completed" ] || [ "$STATUS" == "failed" ]; then break fi sleep 5 done # 4. Get full scan results curl -X GET http://localhost:5000/api/scans/$SCAN_ID \ -b cookies.txt | jq '.' # 5. Logout curl -X GET http://localhost:5000/auth/logout \ -b cookies.txt ``` ### Pagination Example ```bash #!/bin/bash # Get total number of scans TOTAL=$(curl -s -X GET "http://localhost:5000/api/scans?per_page=1" \ -b cookies.txt | jq -r '.total') echo "Total scans: $TOTAL" # Calculate number of pages (50 items per page) PER_PAGE=50 PAGES=$(( ($TOTAL + $PER_PAGE - 1) / $PER_PAGE )) echo "Total pages: $PAGES" # Fetch all pages for PAGE in $(seq 1 $PAGES); do echo "Fetching page $PAGE..." curl -s -X GET "http://localhost:5000/api/scans?page=$PAGE&per_page=$PER_PAGE" \ -b cookies.txt | jq '.scans[] | {id, timestamp, title, status}' done ``` ### Filter by Status ```bash # Get all running scans curl -X GET "http://localhost:5000/api/scans?status=running" \ -b cookies.txt | jq '.scans[] | {id, title, started_at}' # Get all failed scans curl -X GET "http://localhost:5000/api/scans?status=failed" \ -b cookies.txt | jq '.scans[] | {id, title, error_message}' # Get all completed scans from last 24 hours curl -X GET "http://localhost:5000/api/scans?status=completed" \ -b cookies.txt | jq '.scans[] | select(.completed_at > (now - 86400 | todate)) | {id, title, duration}' ``` --- ## Rate Limiting Currently no rate limiting is implemented. For production deployments, consider: - Adding nginx rate limiting - Implementing application-level rate limiting with Flask-Limiter - Setting connection limits in Gunicorn configuration --- ## Security Considerations ### Authentication - All API endpoints (except `/auth/login` and `/api/settings/health`) require authentication - Session cookies are httpOnly and secure (in production with HTTPS) - Passwords are hashed with bcrypt (cost factor 12) - Sensitive settings values are encrypted at rest ### CORS CORS is configured via environment variable `CORS_ORIGINS`. Default: `*` (allow all). For production, set to specific origins: ```bash CORS_ORIGINS=https://scanner.example.com,https://admin.example.com ``` ### HTTPS For production deployments: 1. Use HTTPS (TLS/SSL) for all requests 2. Set `SESSION_COOKIE_SECURE=True` in Flask config 3. Consider using a reverse proxy (nginx) with SSL termination ### Input Validation All inputs are validated: - Config file paths are checked for existence and valid YAML - Pagination parameters are sanitized (positive integers, max per_page: 100) - Scan IDs are validated as integers - Setting values are type-checked --- ## Versioning **Current Version:** 2.0 (Phase 2) API versioning will be implemented in Phase 5. For now, the API is considered unstable and may change between phases. **Breaking Changes:** - Phase 2 → Phase 3: Possible schema changes for scan comparison - Phase 3 → Phase 4: Possible authentication changes (token auth) --- ## Support For issues, questions, or feature requests: - GitHub Issues: https://github.com/anthropics/sneakyscanner/issues - Documentation: `/docs/ai/` directory in repository --- **Last Updated:** 2025-11-14 **Phase:** 2 - Flask Web App Core **Next Update:** Phase 3 - Dashboard & Scheduling