Spaces:
Sleeping
Sleeping
Nikhil Pravin Pise commited on
Commit Β·
aefac4f
1
Parent(s): 92981df
docs: update all documentation to reflect current codebase state
Browse files- Update README.md: correct test count (83 -> 30), fix test commands
- Rewrite START_HERE.md as proper onboarding guide
- Update QUICKSTART.md and CONTRIBUTING.md test commands
- Update api/ docs: Groq/Gemini as default providers (not Ollama)
- Update api/ARCHITECTURE.md: multi-provider LLM diagram
- Update api/GETTING_STARTED.md: Groq/Gemini prerequisites
- Update api/QUICK_REFERENCE.md: fix troubleshooting, commands
- Update docs/DEVELOPMENT.md: fix code examples, test commands
- Update docs/DEEP_REVIEW.md: add RESOLVED/OPEN status to all findings
- Update scripts/README.md: use .venv python paths
- Archive stale root-level docs to docs/archive/
- All 30 tests passing
This view is limited to 50 files because it contains too many changes. Β
See raw diff
- .agents/skills/api-docs-generator/SKILL.md +946 -0
- .agents/skills/api-rate-limiting/SKILL.md +371 -0
- .agents/skills/api-security-hardening/SKILL.md +659 -0
- .agents/skills/find-skills/SKILL.md +133 -0
- .agents/skills/github-actions-templates/SKILL.md +334 -0
- .agents/skills/github-pr-review-workflow/SKILL.md +485 -0
- .agents/skills/owasp-security-check/SKILL.md +451 -0
- .agents/skills/owasp-security-check/rules/api-security.md +148 -0
- .agents/skills/owasp-security-check/rules/authentication-failures.md +146 -0
- .agents/skills/owasp-security-check/rules/broken-access-control.md +111 -0
- .agents/skills/owasp-security-check/rules/cors-configuration.md +117 -0
- .agents/skills/owasp-security-check/rules/cryptographic-failures.md +125 -0
- .agents/skills/owasp-security-check/rules/csrf-protection.md +132 -0
- .agents/skills/owasp-security-check/rules/data-integrity-failures.md +137 -0
- .agents/skills/owasp-security-check/rules/file-upload-security.md +111 -0
- .agents/skills/owasp-security-check/rules/injection-attacks.md +103 -0
- .agents/skills/owasp-security-check/rules/insecure-design.md +142 -0
- .agents/skills/owasp-security-check/rules/logging-monitoring.md +151 -0
- .agents/skills/owasp-security-check/rules/rate-limiting.md +127 -0
- .agents/skills/owasp-security-check/rules/redirect-validation.md +110 -0
- .agents/skills/owasp-security-check/rules/secrets-management.md +111 -0
- .agents/skills/owasp-security-check/rules/security-headers.md +120 -0
- .agents/skills/owasp-security-check/rules/security-misconfiguration.md +110 -0
- .agents/skills/owasp-security-check/rules/sensitive-data-exposure.md +133 -0
- .agents/skills/owasp-security-check/rules/session-security.md +119 -0
- .agents/skills/owasp-security-check/rules/ssrf-attacks.md +100 -0
- .agents/skills/owasp-security-check/rules/vulnerable-dependencies.md +99 -0
- .agents/skills/python-testing-patterns/SKILL.md +1050 -0
- CONTRIBUTING.md +9 -4
- QUICKSTART.md +40 -34
- README.md +104 -99
- START_HERE.md +80 -0
- api/ARCHITECTURE.md +8 -8
- api/GETTING_STARTED.md +13 -21
- api/IMPLEMENTATION_COMPLETE.md +0 -452
- api/QUICK_REFERENCE.md +8 -9
- api/README.md +32 -31
- api/app/main.py +6 -6
- api/app/routes/analyze.py +4 -4
- api/app/routes/biomarkers.py +0 -3
- api/app/routes/health.py +1 -4
- api/app/services/extraction.py +58 -123
- api/app/services/ragbot.py +89 -39
- config/biomarker_references.json +10 -10
- data/chat_reports/report_Diabetes_20260223_124903.json +322 -0
- data/chat_reports/report_unknown_20260223_124439.json +27 -0
- docs/API.md +77 -101
- docs/ARCHITECTURE.md +18 -14
- docs/DEEP_REVIEW.md +119 -0
- docs/DEVELOPMENT.md +61 -68
.agents/skills/api-docs-generator/SKILL.md
ADDED
|
@@ -0,0 +1,946 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: api-docs-generator
|
| 3 |
+
description: Generates API documentation using OpenAPI/Swagger specifications with interactive documentation, code examples, and SDK generation. Use when users request "API documentation", "OpenAPI spec", "Swagger docs", "document API endpoints", or "generate API reference".
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# API Docs Generator
|
| 7 |
+
|
| 8 |
+
Create comprehensive API documentation with OpenAPI specifications and interactive documentation.
|
| 9 |
+
|
| 10 |
+
## Core Workflow
|
| 11 |
+
|
| 12 |
+
1. **Analyze API endpoints**: Review routes, methods, parameters
|
| 13 |
+
2. **Define OpenAPI spec**: Create specification in YAML/JSON
|
| 14 |
+
3. **Add schemas**: Define request/response models
|
| 15 |
+
4. **Include examples**: Add realistic example values
|
| 16 |
+
5. **Generate documentation**: Deploy interactive docs
|
| 17 |
+
6. **Create SDK**: Optional client library generation
|
| 18 |
+
|
| 19 |
+
## OpenAPI Specification Structure
|
| 20 |
+
|
| 21 |
+
```yaml
|
| 22 |
+
# openapi.yaml
|
| 23 |
+
openapi: 3.1.0
|
| 24 |
+
|
| 25 |
+
info:
|
| 26 |
+
title: My API
|
| 27 |
+
version: 1.0.0
|
| 28 |
+
description: |
|
| 29 |
+
API description with **Markdown** support.
|
| 30 |
+
|
| 31 |
+
## Authentication
|
| 32 |
+
All endpoints require Bearer token authentication.
|
| 33 |
+
contact:
|
| 34 |
+
name: API Support
|
| 35 |
+
email: api@example.com
|
| 36 |
+
url: https://docs.example.com
|
| 37 |
+
license:
|
| 38 |
+
name: MIT
|
| 39 |
+
url: https://opensource.org/licenses/MIT
|
| 40 |
+
|
| 41 |
+
servers:
|
| 42 |
+
- url: https://api.example.com/v1
|
| 43 |
+
description: Production
|
| 44 |
+
- url: https://staging-api.example.com/v1
|
| 45 |
+
description: Staging
|
| 46 |
+
- url: http://localhost:3000/v1
|
| 47 |
+
description: Development
|
| 48 |
+
|
| 49 |
+
tags:
|
| 50 |
+
- name: Users
|
| 51 |
+
description: User management endpoints
|
| 52 |
+
- name: Products
|
| 53 |
+
description: Product catalog endpoints
|
| 54 |
+
- name: Orders
|
| 55 |
+
description: Order processing endpoints
|
| 56 |
+
|
| 57 |
+
paths:
|
| 58 |
+
# Endpoints defined here
|
| 59 |
+
|
| 60 |
+
components:
|
| 61 |
+
# Reusable schemas, security, etc.
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
## Path Definitions
|
| 65 |
+
|
| 66 |
+
### Basic CRUD Endpoints
|
| 67 |
+
|
| 68 |
+
```yaml
|
| 69 |
+
paths:
|
| 70 |
+
/users:
|
| 71 |
+
get:
|
| 72 |
+
tags:
|
| 73 |
+
- Users
|
| 74 |
+
summary: List all users
|
| 75 |
+
description: Retrieve a paginated list of users
|
| 76 |
+
operationId: listUsers
|
| 77 |
+
parameters:
|
| 78 |
+
- $ref: '#/components/parameters/PageParam'
|
| 79 |
+
- $ref: '#/components/parameters/LimitParam'
|
| 80 |
+
- name: role
|
| 81 |
+
in: query
|
| 82 |
+
description: Filter by user role
|
| 83 |
+
schema:
|
| 84 |
+
type: string
|
| 85 |
+
enum: [admin, user, guest]
|
| 86 |
+
responses:
|
| 87 |
+
'200':
|
| 88 |
+
description: Successful response
|
| 89 |
+
content:
|
| 90 |
+
application/json:
|
| 91 |
+
schema:
|
| 92 |
+
$ref: '#/components/schemas/UserList'
|
| 93 |
+
example:
|
| 94 |
+
data:
|
| 95 |
+
- id: "usr_123"
|
| 96 |
+
email: "john@example.com"
|
| 97 |
+
name: "John Doe"
|
| 98 |
+
role: "admin"
|
| 99 |
+
createdAt: "2024-01-15T10:30:00Z"
|
| 100 |
+
pagination:
|
| 101 |
+
page: 1
|
| 102 |
+
limit: 20
|
| 103 |
+
total: 150
|
| 104 |
+
'401':
|
| 105 |
+
$ref: '#/components/responses/Unauthorized'
|
| 106 |
+
'500':
|
| 107 |
+
$ref: '#/components/responses/InternalError'
|
| 108 |
+
|
| 109 |
+
post:
|
| 110 |
+
tags:
|
| 111 |
+
- Users
|
| 112 |
+
summary: Create a new user
|
| 113 |
+
description: Create a new user account
|
| 114 |
+
operationId: createUser
|
| 115 |
+
requestBody:
|
| 116 |
+
required: true
|
| 117 |
+
content:
|
| 118 |
+
application/json:
|
| 119 |
+
schema:
|
| 120 |
+
$ref: '#/components/schemas/CreateUserRequest'
|
| 121 |
+
example:
|
| 122 |
+
email: "newuser@example.com"
|
| 123 |
+
name: "New User"
|
| 124 |
+
password: "securePassword123"
|
| 125 |
+
role: "user"
|
| 126 |
+
responses:
|
| 127 |
+
'201':
|
| 128 |
+
description: User created successfully
|
| 129 |
+
content:
|
| 130 |
+
application/json:
|
| 131 |
+
schema:
|
| 132 |
+
$ref: '#/components/schemas/User'
|
| 133 |
+
'400':
|
| 134 |
+
$ref: '#/components/responses/BadRequest'
|
| 135 |
+
'409':
|
| 136 |
+
description: User already exists
|
| 137 |
+
content:
|
| 138 |
+
application/json:
|
| 139 |
+
schema:
|
| 140 |
+
$ref: '#/components/schemas/Error'
|
| 141 |
+
example:
|
| 142 |
+
code: "USER_EXISTS"
|
| 143 |
+
message: "A user with this email already exists"
|
| 144 |
+
'422':
|
| 145 |
+
$ref: '#/components/responses/ValidationError'
|
| 146 |
+
|
| 147 |
+
/users/{userId}:
|
| 148 |
+
parameters:
|
| 149 |
+
- $ref: '#/components/parameters/UserId'
|
| 150 |
+
|
| 151 |
+
get:
|
| 152 |
+
tags:
|
| 153 |
+
- Users
|
| 154 |
+
summary: Get user by ID
|
| 155 |
+
description: Retrieve a specific user by their ID
|
| 156 |
+
operationId: getUserById
|
| 157 |
+
responses:
|
| 158 |
+
'200':
|
| 159 |
+
description: Successful response
|
| 160 |
+
content:
|
| 161 |
+
application/json:
|
| 162 |
+
schema:
|
| 163 |
+
$ref: '#/components/schemas/User'
|
| 164 |
+
'404':
|
| 165 |
+
$ref: '#/components/responses/NotFound'
|
| 166 |
+
|
| 167 |
+
patch:
|
| 168 |
+
tags:
|
| 169 |
+
- Users
|
| 170 |
+
summary: Update user
|
| 171 |
+
description: Update an existing user's information
|
| 172 |
+
operationId: updateUser
|
| 173 |
+
requestBody:
|
| 174 |
+
required: true
|
| 175 |
+
content:
|
| 176 |
+
application/json:
|
| 177 |
+
schema:
|
| 178 |
+
$ref: '#/components/schemas/UpdateUserRequest'
|
| 179 |
+
responses:
|
| 180 |
+
'200':
|
| 181 |
+
description: User updated successfully
|
| 182 |
+
content:
|
| 183 |
+
application/json:
|
| 184 |
+
schema:
|
| 185 |
+
$ref: '#/components/schemas/User'
|
| 186 |
+
'404':
|
| 187 |
+
$ref: '#/components/responses/NotFound'
|
| 188 |
+
'422':
|
| 189 |
+
$ref: '#/components/responses/ValidationError'
|
| 190 |
+
|
| 191 |
+
delete:
|
| 192 |
+
tags:
|
| 193 |
+
- Users
|
| 194 |
+
summary: Delete user
|
| 195 |
+
description: Permanently delete a user
|
| 196 |
+
operationId: deleteUser
|
| 197 |
+
responses:
|
| 198 |
+
'204':
|
| 199 |
+
description: User deleted successfully
|
| 200 |
+
'404':
|
| 201 |
+
$ref: '#/components/responses/NotFound'
|
| 202 |
+
```
|
| 203 |
+
|
| 204 |
+
## Component Schemas
|
| 205 |
+
|
| 206 |
+
### Data Models
|
| 207 |
+
|
| 208 |
+
```yaml
|
| 209 |
+
components:
|
| 210 |
+
schemas:
|
| 211 |
+
# Base User Schema
|
| 212 |
+
User:
|
| 213 |
+
type: object
|
| 214 |
+
properties:
|
| 215 |
+
id:
|
| 216 |
+
type: string
|
| 217 |
+
format: uuid
|
| 218 |
+
description: Unique user identifier
|
| 219 |
+
example: "usr_123abc"
|
| 220 |
+
readOnly: true
|
| 221 |
+
email:
|
| 222 |
+
type: string
|
| 223 |
+
format: email
|
| 224 |
+
description: User's email address
|
| 225 |
+
example: "john@example.com"
|
| 226 |
+
name:
|
| 227 |
+
type: string
|
| 228 |
+
minLength: 1
|
| 229 |
+
maxLength: 100
|
| 230 |
+
description: User's full name
|
| 231 |
+
example: "John Doe"
|
| 232 |
+
role:
|
| 233 |
+
$ref: '#/components/schemas/UserRole'
|
| 234 |
+
avatar:
|
| 235 |
+
type: string
|
| 236 |
+
format: uri
|
| 237 |
+
nullable: true
|
| 238 |
+
description: URL to user's avatar image
|
| 239 |
+
example: "https://cdn.example.com/avatars/123.jpg"
|
| 240 |
+
createdAt:
|
| 241 |
+
type: string
|
| 242 |
+
format: date-time
|
| 243 |
+
description: Account creation timestamp
|
| 244 |
+
readOnly: true
|
| 245 |
+
updatedAt:
|
| 246 |
+
type: string
|
| 247 |
+
format: date-time
|
| 248 |
+
description: Last update timestamp
|
| 249 |
+
readOnly: true
|
| 250 |
+
required:
|
| 251 |
+
- id
|
| 252 |
+
- email
|
| 253 |
+
- name
|
| 254 |
+
- role
|
| 255 |
+
- createdAt
|
| 256 |
+
|
| 257 |
+
UserRole:
|
| 258 |
+
type: string
|
| 259 |
+
enum:
|
| 260 |
+
- admin
|
| 261 |
+
- user
|
| 262 |
+
- guest
|
| 263 |
+
description: User's role in the system
|
| 264 |
+
example: "user"
|
| 265 |
+
|
| 266 |
+
# Request Schemas
|
| 267 |
+
CreateUserRequest:
|
| 268 |
+
type: object
|
| 269 |
+
properties:
|
| 270 |
+
email:
|
| 271 |
+
type: string
|
| 272 |
+
format: email
|
| 273 |
+
name:
|
| 274 |
+
type: string
|
| 275 |
+
minLength: 1
|
| 276 |
+
maxLength: 100
|
| 277 |
+
password:
|
| 278 |
+
type: string
|
| 279 |
+
format: password
|
| 280 |
+
minLength: 8
|
| 281 |
+
description: Must contain at least one uppercase, one lowercase, and one number
|
| 282 |
+
role:
|
| 283 |
+
$ref: '#/components/schemas/UserRole'
|
| 284 |
+
required:
|
| 285 |
+
- email
|
| 286 |
+
- name
|
| 287 |
+
- password
|
| 288 |
+
|
| 289 |
+
UpdateUserRequest:
|
| 290 |
+
type: object
|
| 291 |
+
properties:
|
| 292 |
+
name:
|
| 293 |
+
type: string
|
| 294 |
+
minLength: 1
|
| 295 |
+
maxLength: 100
|
| 296 |
+
role:
|
| 297 |
+
$ref: '#/components/schemas/UserRole'
|
| 298 |
+
avatar:
|
| 299 |
+
type: string
|
| 300 |
+
format: uri
|
| 301 |
+
nullable: true
|
| 302 |
+
minProperties: 1
|
| 303 |
+
|
| 304 |
+
# List Response
|
| 305 |
+
UserList:
|
| 306 |
+
type: object
|
| 307 |
+
properties:
|
| 308 |
+
data:
|
| 309 |
+
type: array
|
| 310 |
+
items:
|
| 311 |
+
$ref: '#/components/schemas/User'
|
| 312 |
+
pagination:
|
| 313 |
+
$ref: '#/components/schemas/Pagination'
|
| 314 |
+
|
| 315 |
+
Pagination:
|
| 316 |
+
type: object
|
| 317 |
+
properties:
|
| 318 |
+
page:
|
| 319 |
+
type: integer
|
| 320 |
+
minimum: 1
|
| 321 |
+
example: 1
|
| 322 |
+
limit:
|
| 323 |
+
type: integer
|
| 324 |
+
minimum: 1
|
| 325 |
+
maximum: 100
|
| 326 |
+
example: 20
|
| 327 |
+
total:
|
| 328 |
+
type: integer
|
| 329 |
+
minimum: 0
|
| 330 |
+
example: 150
|
| 331 |
+
hasMore:
|
| 332 |
+
type: boolean
|
| 333 |
+
example: true
|
| 334 |
+
|
| 335 |
+
# Error Schemas
|
| 336 |
+
Error:
|
| 337 |
+
type: object
|
| 338 |
+
properties:
|
| 339 |
+
code:
|
| 340 |
+
type: string
|
| 341 |
+
description: Machine-readable error code
|
| 342 |
+
example: "VALIDATION_ERROR"
|
| 343 |
+
message:
|
| 344 |
+
type: string
|
| 345 |
+
description: Human-readable error message
|
| 346 |
+
example: "The request body is invalid"
|
| 347 |
+
details:
|
| 348 |
+
type: array
|
| 349 |
+
items:
|
| 350 |
+
$ref: '#/components/schemas/ErrorDetail'
|
| 351 |
+
required:
|
| 352 |
+
- code
|
| 353 |
+
- message
|
| 354 |
+
|
| 355 |
+
ErrorDetail:
|
| 356 |
+
type: object
|
| 357 |
+
properties:
|
| 358 |
+
field:
|
| 359 |
+
type: string
|
| 360 |
+
description: The field that caused the error
|
| 361 |
+
example: "email"
|
| 362 |
+
message:
|
| 363 |
+
type: string
|
| 364 |
+
description: Description of the validation error
|
| 365 |
+
example: "Must be a valid email address"
|
| 366 |
+
```
|
| 367 |
+
|
| 368 |
+
## Parameters and Responses
|
| 369 |
+
|
| 370 |
+
```yaml
|
| 371 |
+
components:
|
| 372 |
+
parameters:
|
| 373 |
+
UserId:
|
| 374 |
+
name: userId
|
| 375 |
+
in: path
|
| 376 |
+
required: true
|
| 377 |
+
description: Unique user identifier
|
| 378 |
+
schema:
|
| 379 |
+
type: string
|
| 380 |
+
format: uuid
|
| 381 |
+
example: "usr_123abc"
|
| 382 |
+
|
| 383 |
+
PageParam:
|
| 384 |
+
name: page
|
| 385 |
+
in: query
|
| 386 |
+
description: Page number for pagination
|
| 387 |
+
schema:
|
| 388 |
+
type: integer
|
| 389 |
+
minimum: 1
|
| 390 |
+
default: 1
|
| 391 |
+
example: 1
|
| 392 |
+
|
| 393 |
+
LimitParam:
|
| 394 |
+
name: limit
|
| 395 |
+
in: query
|
| 396 |
+
description: Number of items per page
|
| 397 |
+
schema:
|
| 398 |
+
type: integer
|
| 399 |
+
minimum: 1
|
| 400 |
+
maximum: 100
|
| 401 |
+
default: 20
|
| 402 |
+
example: 20
|
| 403 |
+
|
| 404 |
+
SortParam:
|
| 405 |
+
name: sort
|
| 406 |
+
in: query
|
| 407 |
+
description: Sort field and direction
|
| 408 |
+
schema:
|
| 409 |
+
type: string
|
| 410 |
+
pattern: '^[a-zA-Z]+:(asc|desc)$'
|
| 411 |
+
example: "createdAt:desc"
|
| 412 |
+
|
| 413 |
+
responses:
|
| 414 |
+
BadRequest:
|
| 415 |
+
description: Bad request - invalid input
|
| 416 |
+
content:
|
| 417 |
+
application/json:
|
| 418 |
+
schema:
|
| 419 |
+
$ref: '#/components/schemas/Error'
|
| 420 |
+
example:
|
| 421 |
+
code: "BAD_REQUEST"
|
| 422 |
+
message: "Invalid request format"
|
| 423 |
+
|
| 424 |
+
Unauthorized:
|
| 425 |
+
description: Authentication required
|
| 426 |
+
content:
|
| 427 |
+
application/json:
|
| 428 |
+
schema:
|
| 429 |
+
$ref: '#/components/schemas/Error'
|
| 430 |
+
example:
|
| 431 |
+
code: "UNAUTHORIZED"
|
| 432 |
+
message: "Authentication token is missing or invalid"
|
| 433 |
+
|
| 434 |
+
Forbidden:
|
| 435 |
+
description: Permission denied
|
| 436 |
+
content:
|
| 437 |
+
application/json:
|
| 438 |
+
schema:
|
| 439 |
+
$ref: '#/components/schemas/Error'
|
| 440 |
+
example:
|
| 441 |
+
code: "FORBIDDEN"
|
| 442 |
+
message: "You don't have permission to access this resource"
|
| 443 |
+
|
| 444 |
+
NotFound:
|
| 445 |
+
description: Resource not found
|
| 446 |
+
content:
|
| 447 |
+
application/json:
|
| 448 |
+
schema:
|
| 449 |
+
$ref: '#/components/schemas/Error'
|
| 450 |
+
example:
|
| 451 |
+
code: "NOT_FOUND"
|
| 452 |
+
message: "The requested resource was not found"
|
| 453 |
+
|
| 454 |
+
ValidationError:
|
| 455 |
+
description: Validation error
|
| 456 |
+
content:
|
| 457 |
+
application/json:
|
| 458 |
+
schema:
|
| 459 |
+
$ref: '#/components/schemas/Error'
|
| 460 |
+
example:
|
| 461 |
+
code: "VALIDATION_ERROR"
|
| 462 |
+
message: "Request validation failed"
|
| 463 |
+
details:
|
| 464 |
+
- field: "email"
|
| 465 |
+
message: "Must be a valid email address"
|
| 466 |
+
- field: "password"
|
| 467 |
+
message: "Must be at least 8 characters"
|
| 468 |
+
|
| 469 |
+
InternalError:
|
| 470 |
+
description: Internal server error
|
| 471 |
+
content:
|
| 472 |
+
application/json:
|
| 473 |
+
schema:
|
| 474 |
+
$ref: '#/components/schemas/Error'
|
| 475 |
+
example:
|
| 476 |
+
code: "INTERNAL_ERROR"
|
| 477 |
+
message: "An unexpected error occurred"
|
| 478 |
+
```
|
| 479 |
+
|
| 480 |
+
## Security Definitions
|
| 481 |
+
|
| 482 |
+
```yaml
|
| 483 |
+
components:
|
| 484 |
+
securitySchemes:
|
| 485 |
+
BearerAuth:
|
| 486 |
+
type: http
|
| 487 |
+
scheme: bearer
|
| 488 |
+
bearerFormat: JWT
|
| 489 |
+
description: |
|
| 490 |
+
JWT token obtained from the /auth/login endpoint.
|
| 491 |
+
|
| 492 |
+
Example: `Authorization: Bearer eyJhbGciOiJIUzI1...`
|
| 493 |
+
|
| 494 |
+
ApiKeyAuth:
|
| 495 |
+
type: apiKey
|
| 496 |
+
in: header
|
| 497 |
+
name: X-API-Key
|
| 498 |
+
description: API key for server-to-server communication
|
| 499 |
+
|
| 500 |
+
OAuth2:
|
| 501 |
+
type: oauth2
|
| 502 |
+
description: OAuth 2.0 authentication
|
| 503 |
+
flows:
|
| 504 |
+
authorizationCode:
|
| 505 |
+
authorizationUrl: https://auth.example.com/oauth/authorize
|
| 506 |
+
tokenUrl: https://auth.example.com/oauth/token
|
| 507 |
+
scopes:
|
| 508 |
+
read:users: Read user information
|
| 509 |
+
write:users: Create and modify users
|
| 510 |
+
admin: Full administrative access
|
| 511 |
+
|
| 512 |
+
# Apply security globally
|
| 513 |
+
security:
|
| 514 |
+
- BearerAuth: []
|
| 515 |
+
|
| 516 |
+
# Or per-endpoint
|
| 517 |
+
paths:
|
| 518 |
+
/public/health:
|
| 519 |
+
get:
|
| 520 |
+
security: [] # No auth required
|
| 521 |
+
summary: Health check
|
| 522 |
+
responses:
|
| 523 |
+
'200':
|
| 524 |
+
description: Service is healthy
|
| 525 |
+
```
|
| 526 |
+
|
| 527 |
+
## Express/Node.js Integration
|
| 528 |
+
|
| 529 |
+
### Generate from Code with express-openapi
|
| 530 |
+
|
| 531 |
+
```typescript
|
| 532 |
+
// src/docs/openapi.ts
|
| 533 |
+
import { OpenAPIV3_1 } from 'openapi-types';
|
| 534 |
+
|
| 535 |
+
export const openApiDocument: OpenAPIV3_1.Document = {
|
| 536 |
+
openapi: '3.1.0',
|
| 537 |
+
info: {
|
| 538 |
+
title: 'My API',
|
| 539 |
+
version: '1.0.0',
|
| 540 |
+
description: 'API documentation',
|
| 541 |
+
},
|
| 542 |
+
servers: [
|
| 543 |
+
{ url: 'http://localhost:3000', description: 'Development' },
|
| 544 |
+
],
|
| 545 |
+
paths: {},
|
| 546 |
+
components: {
|
| 547 |
+
schemas: {},
|
| 548 |
+
securitySchemes: {
|
| 549 |
+
BearerAuth: {
|
| 550 |
+
type: 'http',
|
| 551 |
+
scheme: 'bearer',
|
| 552 |
+
bearerFormat: 'JWT',
|
| 553 |
+
},
|
| 554 |
+
},
|
| 555 |
+
},
|
| 556 |
+
};
|
| 557 |
+
```
|
| 558 |
+
|
| 559 |
+
### Swagger UI Express
|
| 560 |
+
|
| 561 |
+
```typescript
|
| 562 |
+
// src/docs/swagger.ts
|
| 563 |
+
import swaggerUi from 'swagger-ui-express';
|
| 564 |
+
import YAML from 'yamljs';
|
| 565 |
+
import path from 'path';
|
| 566 |
+
import { Express } from 'express';
|
| 567 |
+
|
| 568 |
+
export function setupSwagger(app: Express) {
|
| 569 |
+
const swaggerDocument = YAML.load(
|
| 570 |
+
path.join(__dirname, '../../openapi.yaml')
|
| 571 |
+
);
|
| 572 |
+
|
| 573 |
+
const options: swaggerUi.SwaggerUiOptions = {
|
| 574 |
+
explorer: true,
|
| 575 |
+
customSiteTitle: 'API Documentation',
|
| 576 |
+
customCss: '.swagger-ui .topbar { display: none }',
|
| 577 |
+
swaggerOptions: {
|
| 578 |
+
persistAuthorization: true,
|
| 579 |
+
displayRequestDuration: true,
|
| 580 |
+
filter: true,
|
| 581 |
+
showExtensions: true,
|
| 582 |
+
},
|
| 583 |
+
};
|
| 584 |
+
|
| 585 |
+
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, options));
|
| 586 |
+
app.get('/openapi.json', (req, res) => res.json(swaggerDocument));
|
| 587 |
+
}
|
| 588 |
+
```
|
| 589 |
+
|
| 590 |
+
### Zod to OpenAPI
|
| 591 |
+
|
| 592 |
+
```typescript
|
| 593 |
+
// src/schemas/user.ts
|
| 594 |
+
import { z } from 'zod';
|
| 595 |
+
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
|
| 596 |
+
|
| 597 |
+
extendZodWithOpenApi(z);
|
| 598 |
+
|
| 599 |
+
export const UserSchema = z.object({
|
| 600 |
+
id: z.string().uuid().openapi({ example: 'usr_123abc' }),
|
| 601 |
+
email: z.string().email().openapi({ example: 'john@example.com' }),
|
| 602 |
+
name: z.string().min(1).max(100).openapi({ example: 'John Doe' }),
|
| 603 |
+
role: z.enum(['admin', 'user', 'guest']).openapi({ example: 'user' }),
|
| 604 |
+
createdAt: z.string().datetime(),
|
| 605 |
+
}).openapi('User');
|
| 606 |
+
|
| 607 |
+
export const CreateUserSchema = z.object({
|
| 608 |
+
email: z.string().email(),
|
| 609 |
+
name: z.string().min(1).max(100),
|
| 610 |
+
password: z.string().min(8),
|
| 611 |
+
role: z.enum(['admin', 'user', 'guest']).optional().default('user'),
|
| 612 |
+
}).openapi('CreateUserRequest');
|
| 613 |
+
```
|
| 614 |
+
|
| 615 |
+
```typescript
|
| 616 |
+
// src/docs/generator.ts
|
| 617 |
+
import {
|
| 618 |
+
OpenAPIRegistry,
|
| 619 |
+
OpenApiGeneratorV31,
|
| 620 |
+
} from '@asteasolutions/zod-to-openapi';
|
| 621 |
+
import { UserSchema, CreateUserSchema } from '../schemas/user';
|
| 622 |
+
|
| 623 |
+
const registry = new OpenAPIRegistry();
|
| 624 |
+
|
| 625 |
+
// Register schemas
|
| 626 |
+
registry.register('User', UserSchema);
|
| 627 |
+
registry.register('CreateUserRequest', CreateUserSchema);
|
| 628 |
+
|
| 629 |
+
// Register endpoints
|
| 630 |
+
registry.registerPath({
|
| 631 |
+
method: 'get',
|
| 632 |
+
path: '/users',
|
| 633 |
+
tags: ['Users'],
|
| 634 |
+
summary: 'List all users',
|
| 635 |
+
responses: {
|
| 636 |
+
200: {
|
| 637 |
+
description: 'List of users',
|
| 638 |
+
content: {
|
| 639 |
+
'application/json': {
|
| 640 |
+
schema: z.array(UserSchema),
|
| 641 |
+
},
|
| 642 |
+
},
|
| 643 |
+
},
|
| 644 |
+
},
|
| 645 |
+
});
|
| 646 |
+
|
| 647 |
+
registry.registerPath({
|
| 648 |
+
method: 'post',
|
| 649 |
+
path: '/users',
|
| 650 |
+
tags: ['Users'],
|
| 651 |
+
summary: 'Create a user',
|
| 652 |
+
request: {
|
| 653 |
+
body: {
|
| 654 |
+
content: {
|
| 655 |
+
'application/json': {
|
| 656 |
+
schema: CreateUserSchema,
|
| 657 |
+
},
|
| 658 |
+
},
|
| 659 |
+
},
|
| 660 |
+
},
|
| 661 |
+
responses: {
|
| 662 |
+
201: {
|
| 663 |
+
description: 'User created',
|
| 664 |
+
content: {
|
| 665 |
+
'application/json': {
|
| 666 |
+
schema: UserSchema,
|
| 667 |
+
},
|
| 668 |
+
},
|
| 669 |
+
},
|
| 670 |
+
},
|
| 671 |
+
});
|
| 672 |
+
|
| 673 |
+
// Generate OpenAPI document
|
| 674 |
+
const generator = new OpenApiGeneratorV31(registry.definitions);
|
| 675 |
+
export const openApiDocument = generator.generateDocument({
|
| 676 |
+
openapi: '3.1.0',
|
| 677 |
+
info: {
|
| 678 |
+
title: 'My API',
|
| 679 |
+
version: '1.0.0',
|
| 680 |
+
},
|
| 681 |
+
});
|
| 682 |
+
```
|
| 683 |
+
|
| 684 |
+
## FastAPI Integration
|
| 685 |
+
|
| 686 |
+
```python
|
| 687 |
+
# main.py
|
| 688 |
+
from fastapi import FastAPI, HTTPException, Query
|
| 689 |
+
from fastapi.openapi.utils import get_openapi
|
| 690 |
+
from pydantic import BaseModel, EmailStr, Field
|
| 691 |
+
from typing import Optional
|
| 692 |
+
from datetime import datetime
|
| 693 |
+
from enum import Enum
|
| 694 |
+
|
| 695 |
+
app = FastAPI(
|
| 696 |
+
title="My API",
|
| 697 |
+
description="API documentation with FastAPI",
|
| 698 |
+
version="1.0.0",
|
| 699 |
+
docs_url="/docs",
|
| 700 |
+
redoc_url="/redoc",
|
| 701 |
+
)
|
| 702 |
+
|
| 703 |
+
|
| 704 |
+
class UserRole(str, Enum):
|
| 705 |
+
admin = "admin"
|
| 706 |
+
user = "user"
|
| 707 |
+
guest = "guest"
|
| 708 |
+
|
| 709 |
+
|
| 710 |
+
class UserBase(BaseModel):
|
| 711 |
+
email: EmailStr = Field(..., example="john@example.com")
|
| 712 |
+
name: str = Field(..., min_length=1, max_length=100, example="John Doe")
|
| 713 |
+
role: UserRole = Field(default=UserRole.user, example="user")
|
| 714 |
+
|
| 715 |
+
|
| 716 |
+
class UserCreate(UserBase):
|
| 717 |
+
password: str = Field(..., min_length=8, example="securePassword123")
|
| 718 |
+
|
| 719 |
+
|
| 720 |
+
class User(UserBase):
|
| 721 |
+
id: str = Field(..., example="usr_123abc")
|
| 722 |
+
created_at: datetime
|
| 723 |
+
updated_at: Optional[datetime] = None
|
| 724 |
+
|
| 725 |
+
class Config:
|
| 726 |
+
from_attributes = True
|
| 727 |
+
|
| 728 |
+
|
| 729 |
+
class UserList(BaseModel):
|
| 730 |
+
data: list[User]
|
| 731 |
+
total: int
|
| 732 |
+
page: int
|
| 733 |
+
limit: int
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
@app.get(
|
| 737 |
+
"/users",
|
| 738 |
+
response_model=UserList,
|
| 739 |
+
tags=["Users"],
|
| 740 |
+
summary="List all users",
|
| 741 |
+
description="Retrieve a paginated list of users",
|
| 742 |
+
)
|
| 743 |
+
async def list_users(
|
| 744 |
+
page: int = Query(1, ge=1, description="Page number"),
|
| 745 |
+
limit: int = Query(20, ge=1, le=100, description="Items per page"),
|
| 746 |
+
role: Optional[UserRole] = Query(None, description="Filter by role"),
|
| 747 |
+
):
|
| 748 |
+
# Implementation
|
| 749 |
+
pass
|
| 750 |
+
|
| 751 |
+
|
| 752 |
+
@app.post(
|
| 753 |
+
"/users",
|
| 754 |
+
response_model=User,
|
| 755 |
+
status_code=201,
|
| 756 |
+
tags=["Users"],
|
| 757 |
+
summary="Create a new user",
|
| 758 |
+
responses={
|
| 759 |
+
409: {"description": "User already exists"},
|
| 760 |
+
422: {"description": "Validation error"},
|
| 761 |
+
},
|
| 762 |
+
)
|
| 763 |
+
async def create_user(user: UserCreate):
|
| 764 |
+
# Implementation
|
| 765 |
+
pass
|
| 766 |
+
|
| 767 |
+
|
| 768 |
+
# Custom OpenAPI schema
|
| 769 |
+
def custom_openapi():
|
| 770 |
+
if app.openapi_schema:
|
| 771 |
+
return app.openapi_schema
|
| 772 |
+
|
| 773 |
+
openapi_schema = get_openapi(
|
| 774 |
+
title="My API",
|
| 775 |
+
version="1.0.0",
|
| 776 |
+
description="API documentation",
|
| 777 |
+
routes=app.routes,
|
| 778 |
+
)
|
| 779 |
+
|
| 780 |
+
# Add security scheme
|
| 781 |
+
openapi_schema["components"]["securitySchemes"] = {
|
| 782 |
+
"BearerAuth": {
|
| 783 |
+
"type": "http",
|
| 784 |
+
"scheme": "bearer",
|
| 785 |
+
"bearerFormat": "JWT",
|
| 786 |
+
}
|
| 787 |
+
}
|
| 788 |
+
openapi_schema["security"] = [{"BearerAuth": []}]
|
| 789 |
+
|
| 790 |
+
app.openapi_schema = openapi_schema
|
| 791 |
+
return app.openapi_schema
|
| 792 |
+
|
| 793 |
+
|
| 794 |
+
app.openapi = custom_openapi
|
| 795 |
+
```
|
| 796 |
+
|
| 797 |
+
## Documentation Generators
|
| 798 |
+
|
| 799 |
+
### Redoc
|
| 800 |
+
|
| 801 |
+
```html
|
| 802 |
+
<!-- docs/index.html -->
|
| 803 |
+
<!DOCTYPE html>
|
| 804 |
+
<html>
|
| 805 |
+
<head>
|
| 806 |
+
<title>API Documentation</title>
|
| 807 |
+
<meta charset="utf-8"/>
|
| 808 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 809 |
+
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
| 810 |
+
<style>
|
| 811 |
+
body { margin: 0; padding: 0; }
|
| 812 |
+
</style>
|
| 813 |
+
</head>
|
| 814 |
+
<body>
|
| 815 |
+
<redoc spec-url='/openapi.yaml' expand-responses="200,201"></redoc>
|
| 816 |
+
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
| 817 |
+
</body>
|
| 818 |
+
</html>
|
| 819 |
+
```
|
| 820 |
+
|
| 821 |
+
### Stoplight Elements
|
| 822 |
+
|
| 823 |
+
```html
|
| 824 |
+
<!DOCTYPE html>
|
| 825 |
+
<html>
|
| 826 |
+
<head>
|
| 827 |
+
<title>API Documentation</title>
|
| 828 |
+
<script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
|
| 829 |
+
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
|
| 830 |
+
</head>
|
| 831 |
+
<body>
|
| 832 |
+
<elements-api
|
| 833 |
+
apiDescriptionUrl="/openapi.yaml"
|
| 834 |
+
router="hash"
|
| 835 |
+
layout="sidebar"
|
| 836 |
+
/>
|
| 837 |
+
</body>
|
| 838 |
+
</html>
|
| 839 |
+
```
|
| 840 |
+
|
| 841 |
+
## SDK Generation
|
| 842 |
+
|
| 843 |
+
### OpenAPI Generator
|
| 844 |
+
|
| 845 |
+
```bash
|
| 846 |
+
# Install OpenAPI Generator
|
| 847 |
+
npm install -g @openapitools/openapi-generator-cli
|
| 848 |
+
|
| 849 |
+
# Generate TypeScript client
|
| 850 |
+
openapi-generator-cli generate \
|
| 851 |
+
-i openapi.yaml \
|
| 852 |
+
-g typescript-fetch \
|
| 853 |
+
-o ./sdk/typescript \
|
| 854 |
+
--additional-properties=supportsES6=true,npmName=@myorg/api-client
|
| 855 |
+
|
| 856 |
+
# Generate Python client
|
| 857 |
+
openapi-generator-cli generate \
|
| 858 |
+
-i openapi.yaml \
|
| 859 |
+
-g python \
|
| 860 |
+
-o ./sdk/python \
|
| 861 |
+
--additional-properties=packageName=myapi_client
|
| 862 |
+
```
|
| 863 |
+
|
| 864 |
+
### Configuration
|
| 865 |
+
|
| 866 |
+
```yaml
|
| 867 |
+
# openapitools.json
|
| 868 |
+
{
|
| 869 |
+
"$schema": "https://raw.githubusercontent.com/OpenAPITools/openapi-generator/master/modules/openapi-generator-gradle-plugin/src/main/resources/openapitools.json",
|
| 870 |
+
"spaces": 2,
|
| 871 |
+
"generator-cli": {
|
| 872 |
+
"version": "7.0.0",
|
| 873 |
+
"generators": {
|
| 874 |
+
"typescript-client": {
|
| 875 |
+
"generatorName": "typescript-fetch",
|
| 876 |
+
"inputSpec": "./openapi.yaml",
|
| 877 |
+
"output": "./sdk/typescript",
|
| 878 |
+
"additionalProperties": {
|
| 879 |
+
"supportsES6": true,
|
| 880 |
+
"npmName": "@myorg/api-client",
|
| 881 |
+
"npmVersion": "1.0.0"
|
| 882 |
+
}
|
| 883 |
+
}
|
| 884 |
+
}
|
| 885 |
+
}
|
| 886 |
+
}
|
| 887 |
+
```
|
| 888 |
+
|
| 889 |
+
## Validation
|
| 890 |
+
|
| 891 |
+
### Spectral Linting
|
| 892 |
+
|
| 893 |
+
```yaml
|
| 894 |
+
# .spectral.yaml
|
| 895 |
+
extends: ["spectral:oas", "spectral:asyncapi"]
|
| 896 |
+
|
| 897 |
+
rules:
|
| 898 |
+
operation-operationId: error
|
| 899 |
+
operation-description: warn
|
| 900 |
+
operation-tags: error
|
| 901 |
+
info-contact: warn
|
| 902 |
+
info-license: warn
|
| 903 |
+
oas3-schema: error
|
| 904 |
+
oas3-valid-media-example: warn
|
| 905 |
+
|
| 906 |
+
# Custom rules
|
| 907 |
+
path-must-have-tag:
|
| 908 |
+
given: "$.paths[*][*]"
|
| 909 |
+
severity: error
|
| 910 |
+
then:
|
| 911 |
+
field: tags
|
| 912 |
+
function: truthy
|
| 913 |
+
```
|
| 914 |
+
|
| 915 |
+
```bash
|
| 916 |
+
# Run linting
|
| 917 |
+
npx @stoplight/spectral-cli lint openapi.yaml
|
| 918 |
+
```
|
| 919 |
+
|
| 920 |
+
## Best Practices
|
| 921 |
+
|
| 922 |
+
1. **Use $ref for reusability**: Define schemas once, reference everywhere
|
| 923 |
+
2. **Include examples**: Add realistic examples for all schemas
|
| 924 |
+
3. **Document errors**: Describe all possible error responses
|
| 925 |
+
4. **Version your API**: Use URL or header versioning
|
| 926 |
+
5. **Group with tags**: Organize endpoints logically
|
| 927 |
+
6. **Add descriptions**: Explain every parameter and field
|
| 928 |
+
7. **Use security schemes**: Document authentication clearly
|
| 929 |
+
8. **Validate spec**: Use Spectral or similar tools
|
| 930 |
+
9. **Generate SDKs**: Automate client library creation
|
| 931 |
+
10. **Keep spec in sync**: Generate from code or validate against it
|
| 932 |
+
|
| 933 |
+
## Output Checklist
|
| 934 |
+
|
| 935 |
+
Every API documentation should include:
|
| 936 |
+
|
| 937 |
+
- [ ] Complete OpenAPI 3.x specification
|
| 938 |
+
- [ ] All endpoints documented with examples
|
| 939 |
+
- [ ] Request/response schemas with types
|
| 940 |
+
- [ ] Error responses documented
|
| 941 |
+
- [ ] Authentication scheme defined
|
| 942 |
+
- [ ] Parameters described with examples
|
| 943 |
+
- [ ] Interactive documentation deployed (Swagger UI/Redoc)
|
| 944 |
+
- [ ] Specification validated with linter
|
| 945 |
+
- [ ] SDK generation configured
|
| 946 |
+
- [ ] Versioning strategy documented
|
.agents/skills/api-rate-limiting/SKILL.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: api-rate-limiting
|
| 3 |
+
description: Implement API rate limiting strategies using token bucket, sliding window, and fixed window algorithms. Use when protecting APIs from abuse, managing traffic, or implementing tiered rate limits.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# API Rate Limiting
|
| 7 |
+
|
| 8 |
+
## Overview
|
| 9 |
+
|
| 10 |
+
Protect APIs from abuse and manage traffic using various rate limiting algorithms with per-user, per-IP, and per-endpoint strategies.
|
| 11 |
+
|
| 12 |
+
## When to Use
|
| 13 |
+
|
| 14 |
+
- Protecting APIs from brute force attacks
|
| 15 |
+
- Managing traffic spikes
|
| 16 |
+
- Implementing tiered service plans
|
| 17 |
+
- Preventing DoS attacks
|
| 18 |
+
- Fairness in resource allocation
|
| 19 |
+
- Enforcing quotas and usage limits
|
| 20 |
+
|
| 21 |
+
## Instructions
|
| 22 |
+
|
| 23 |
+
### 1. **Token Bucket Algorithm**
|
| 24 |
+
|
| 25 |
+
```javascript
|
| 26 |
+
// Token Bucket Rate Limiter
|
| 27 |
+
class TokenBucket {
|
| 28 |
+
constructor(capacity, refillRate) {
|
| 29 |
+
this.capacity = capacity;
|
| 30 |
+
this.tokens = capacity;
|
| 31 |
+
this.refillRate = refillRate; // tokens per second
|
| 32 |
+
this.lastRefillTime = Date.now();
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
refill() {
|
| 36 |
+
const now = Date.now();
|
| 37 |
+
const timePassed = (now - this.lastRefillTime) / 1000;
|
| 38 |
+
const tokensToAdd = timePassed * this.refillRate;
|
| 39 |
+
|
| 40 |
+
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
|
| 41 |
+
this.lastRefillTime = now;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
consume(tokens = 1) {
|
| 45 |
+
this.refill();
|
| 46 |
+
|
| 47 |
+
if (this.tokens >= tokens) {
|
| 48 |
+
this.tokens -= tokens;
|
| 49 |
+
return true;
|
| 50 |
+
}
|
| 51 |
+
return false;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
available() {
|
| 55 |
+
this.refill();
|
| 56 |
+
return Math.floor(this.tokens);
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Express middleware
|
| 61 |
+
const express = require('express');
|
| 62 |
+
const app = express();
|
| 63 |
+
|
| 64 |
+
const rateLimiters = new Map();
|
| 65 |
+
|
| 66 |
+
const tokenBucketRateLimit = (capacity, refillRate) => {
|
| 67 |
+
return (req, res, next) => {
|
| 68 |
+
const key = req.user?.id || req.ip;
|
| 69 |
+
|
| 70 |
+
if (!rateLimiters.has(key)) {
|
| 71 |
+
rateLimiters.set(key, new TokenBucket(capacity, refillRate));
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
const limiter = rateLimiters.get(key);
|
| 75 |
+
|
| 76 |
+
if (limiter.consume(1)) {
|
| 77 |
+
res.setHeader('X-RateLimit-Limit', capacity);
|
| 78 |
+
res.setHeader('X-RateLimit-Remaining', limiter.available());
|
| 79 |
+
next();
|
| 80 |
+
} else {
|
| 81 |
+
res.status(429).json({
|
| 82 |
+
error: 'Rate limit exceeded',
|
| 83 |
+
retryAfter: Math.ceil(1 / limiter.refillRate)
|
| 84 |
+
});
|
| 85 |
+
}
|
| 86 |
+
};
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
app.get('/api/data', tokenBucketRateLimit(100, 10), (req, res) => {
|
| 90 |
+
res.json({ data: 'api response' });
|
| 91 |
+
});
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
### 2. **Sliding Window Algorithm**
|
| 95 |
+
|
| 96 |
+
```javascript
|
| 97 |
+
class SlidingWindowLimiter {
|
| 98 |
+
constructor(maxRequests, windowSizeSeconds) {
|
| 99 |
+
this.maxRequests = maxRequests;
|
| 100 |
+
this.windowSize = windowSizeSeconds * 1000; // Convert to ms
|
| 101 |
+
this.requests = [];
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
isAllowed() {
|
| 105 |
+
const now = Date.now();
|
| 106 |
+
const windowStart = now - this.windowSize;
|
| 107 |
+
|
| 108 |
+
// Remove old requests outside window
|
| 109 |
+
this.requests = this.requests.filter(time => time > windowStart);
|
| 110 |
+
|
| 111 |
+
if (this.requests.length < this.maxRequests) {
|
| 112 |
+
this.requests.push(now);
|
| 113 |
+
return true;
|
| 114 |
+
}
|
| 115 |
+
return false;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
remaining() {
|
| 119 |
+
const now = Date.now();
|
| 120 |
+
const windowStart = now - this.windowSize;
|
| 121 |
+
this.requests = this.requests.filter(time => time > windowStart);
|
| 122 |
+
return Math.max(0, this.maxRequests - this.requests.length);
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
const slidingWindowRateLimit = (maxRequests, windowSeconds) => {
|
| 127 |
+
const limiters = new Map();
|
| 128 |
+
|
| 129 |
+
return (req, res, next) => {
|
| 130 |
+
const key = req.user?.id || req.ip;
|
| 131 |
+
|
| 132 |
+
if (!limiters.has(key)) {
|
| 133 |
+
limiters.set(key, new SlidingWindowLimiter(maxRequests, windowSeconds));
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
const limiter = limiters.get(key);
|
| 137 |
+
|
| 138 |
+
if (limiter.isAllowed()) {
|
| 139 |
+
res.setHeader('X-RateLimit-Limit', maxRequests);
|
| 140 |
+
res.setHeader('X-RateLimit-Remaining', limiter.remaining());
|
| 141 |
+
next();
|
| 142 |
+
} else {
|
| 143 |
+
res.status(429).json({ error: 'Rate limit exceeded' });
|
| 144 |
+
}
|
| 145 |
+
};
|
| 146 |
+
};
|
| 147 |
+
|
| 148 |
+
app.get('/api/search', slidingWindowRateLimit(30, 60), (req, res) => {
|
| 149 |
+
res.json({ results: [] });
|
| 150 |
+
});
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
### 3. **Redis-Based Rate Limiting**
|
| 154 |
+
|
| 155 |
+
```javascript
|
| 156 |
+
const redis = require('redis');
|
| 157 |
+
const client = redis.createClient();
|
| 158 |
+
|
| 159 |
+
// Sliding window with Redis
|
| 160 |
+
const redisRateLimit = (maxRequests, windowSeconds) => {
|
| 161 |
+
return async (req, res, next) => {
|
| 162 |
+
const key = `ratelimit:${req.user?.id || req.ip}`;
|
| 163 |
+
const now = Date.now();
|
| 164 |
+
const windowStart = now - (windowSeconds * 1000);
|
| 165 |
+
|
| 166 |
+
try {
|
| 167 |
+
// Remove old requests
|
| 168 |
+
await client.zremrangebyscore(key, 0, windowStart);
|
| 169 |
+
|
| 170 |
+
// Count requests in window
|
| 171 |
+
const count = await client.zcard(key);
|
| 172 |
+
|
| 173 |
+
if (count < maxRequests) {
|
| 174 |
+
// Add current request
|
| 175 |
+
await client.zadd(key, now, `${now}-${Math.random()}`);
|
| 176 |
+
// Set expiration
|
| 177 |
+
await client.expire(key, windowSeconds);
|
| 178 |
+
|
| 179 |
+
res.setHeader('X-RateLimit-Limit', maxRequests);
|
| 180 |
+
res.setHeader('X-RateLimit-Remaining', maxRequests - count - 1);
|
| 181 |
+
next();
|
| 182 |
+
} else {
|
| 183 |
+
const oldestRequest = await client.zrange(key, 0, 0);
|
| 184 |
+
const resetTime = parseInt(oldestRequest[0]) + (windowSeconds * 1000);
|
| 185 |
+
const retryAfter = Math.ceil((resetTime - now) / 1000);
|
| 186 |
+
|
| 187 |
+
res.set('Retry-After', retryAfter);
|
| 188 |
+
res.status(429).json({
|
| 189 |
+
error: 'Rate limit exceeded',
|
| 190 |
+
retryAfter
|
| 191 |
+
});
|
| 192 |
+
}
|
| 193 |
+
} catch (error) {
|
| 194 |
+
console.error('Rate limit error:', error);
|
| 195 |
+
next(); // Allow request if Redis fails
|
| 196 |
+
}
|
| 197 |
+
};
|
| 198 |
+
};
|
| 199 |
+
|
| 200 |
+
app.get('/api/expensive', redisRateLimit(10, 60), (req, res) => {
|
| 201 |
+
res.json({ result: 'expensive operation' });
|
| 202 |
+
});
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
### 4. **Tiered Rate Limiting**
|
| 206 |
+
|
| 207 |
+
```javascript
|
| 208 |
+
const RATE_LIMITS = {
|
| 209 |
+
free: { requests: 100, window: 3600 }, // 100 per hour
|
| 210 |
+
pro: { requests: 10000, window: 3600 }, // 10,000 per hour
|
| 211 |
+
enterprise: { requests: null, window: null } // Unlimited
|
| 212 |
+
};
|
| 213 |
+
|
| 214 |
+
const tieredRateLimit = async (req, res, next) => {
|
| 215 |
+
const user = req.user;
|
| 216 |
+
const plan = user?.plan || 'free';
|
| 217 |
+
const limits = RATE_LIMITS[plan];
|
| 218 |
+
|
| 219 |
+
if (!limits.requests) {
|
| 220 |
+
return next(); // Unlimited plan
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
const key = `ratelimit:${user.id}`;
|
| 224 |
+
const now = Date.now();
|
| 225 |
+
const windowStart = now - (limits.window * 1000);
|
| 226 |
+
|
| 227 |
+
try {
|
| 228 |
+
await client.zremrangebyscore(key, 0, windowStart);
|
| 229 |
+
const count = await client.zcard(key);
|
| 230 |
+
|
| 231 |
+
if (count < limits.requests) {
|
| 232 |
+
await client.zadd(key, now, `${now}-${Math.random()}`);
|
| 233 |
+
await client.expire(key, limits.window);
|
| 234 |
+
|
| 235 |
+
res.setHeader('X-RateLimit-Limit', limits.requests);
|
| 236 |
+
res.setHeader('X-RateLimit-Remaining', limits.requests - count - 1);
|
| 237 |
+
res.setHeader('X-Plan', plan);
|
| 238 |
+
next();
|
| 239 |
+
} else {
|
| 240 |
+
res.status(429).json({
|
| 241 |
+
error: 'Rate limit exceeded',
|
| 242 |
+
plan,
|
| 243 |
+
upgradeUrl: '/plans'
|
| 244 |
+
});
|
| 245 |
+
}
|
| 246 |
+
} catch (error) {
|
| 247 |
+
next();
|
| 248 |
+
}
|
| 249 |
+
};
|
| 250 |
+
|
| 251 |
+
app.use(tieredRateLimit);
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
### 5. **Python Rate Limiting (Flask)**
|
| 255 |
+
|
| 256 |
+
```python
|
| 257 |
+
from flask import Flask, request, jsonify
|
| 258 |
+
from flask_limiter import Limiter
|
| 259 |
+
from flask_limiter.util import get_remote_address
|
| 260 |
+
from datetime import datetime, timedelta
|
| 261 |
+
import redis
|
| 262 |
+
|
| 263 |
+
app = Flask(__name__)
|
| 264 |
+
limiter = Limiter(
|
| 265 |
+
app=app,
|
| 266 |
+
key_func=get_remote_address,
|
| 267 |
+
default_limits=["200 per day", "50 per hour"]
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
# Custom rate limit based on user plan
|
| 271 |
+
redis_client = redis.Redis(host='localhost', port=6379)
|
| 272 |
+
|
| 273 |
+
def get_rate_limit(user_id):
|
| 274 |
+
plan = redis_client.get(f'user:{user_id}:plan').decode()
|
| 275 |
+
limits = {
|
| 276 |
+
'free': (100, 3600),
|
| 277 |
+
'pro': (10000, 3600),
|
| 278 |
+
'enterprise': (None, None)
|
| 279 |
+
}
|
| 280 |
+
return limits.get(plan, (100, 3600))
|
| 281 |
+
|
| 282 |
+
@app.route('/api/data', methods=['GET'])
|
| 283 |
+
@limiter.limit("30 per minute")
|
| 284 |
+
def get_data():
|
| 285 |
+
return jsonify({'data': 'api response'}), 200
|
| 286 |
+
|
| 287 |
+
@app.route('/api/premium', methods=['GET'])
|
| 288 |
+
def get_premium_data():
|
| 289 |
+
user_id = request.user_id
|
| 290 |
+
max_requests, window = get_rate_limit(user_id)
|
| 291 |
+
|
| 292 |
+
if max_requests is None:
|
| 293 |
+
return jsonify({'data': 'unlimited data'}), 200
|
| 294 |
+
|
| 295 |
+
key = f'ratelimit:{user_id}'
|
| 296 |
+
current = redis_client.incr(key)
|
| 297 |
+
redis_client.expire(key, window)
|
| 298 |
+
|
| 299 |
+
if current <= max_requests:
|
| 300 |
+
return jsonify({'data': 'premium data'}), 200
|
| 301 |
+
else:
|
| 302 |
+
return jsonify({'error': 'Rate limit exceeded'}), 429
|
| 303 |
+
```
|
| 304 |
+
|
| 305 |
+
### 6. **Response Headers**
|
| 306 |
+
|
| 307 |
+
```javascript
|
| 308 |
+
// Standard rate limit headers
|
| 309 |
+
res.setHeader('X-RateLimit-Limit', maxRequests); // Total requests allowed
|
| 310 |
+
res.setHeader('X-RateLimit-Remaining', remaining); // Remaining requests
|
| 311 |
+
res.setHeader('X-RateLimit-Reset', resetTime); // Unix timestamp of reset
|
| 312 |
+
res.setHeader('Retry-After', secondsToWait); // How long to wait
|
| 313 |
+
|
| 314 |
+
// 429 Too Many Requests response
|
| 315 |
+
{
|
| 316 |
+
"error": "Rate limit exceeded",
|
| 317 |
+
"code": "RATE_LIMIT_EXCEEDED",
|
| 318 |
+
"retryAfter": 60,
|
| 319 |
+
"resetAt": "2025-01-15T15:00:00Z"
|
| 320 |
+
}
|
| 321 |
+
```
|
| 322 |
+
|
| 323 |
+
## Best Practices
|
| 324 |
+
|
| 325 |
+
### β
DO
|
| 326 |
+
- Include rate limit headers in responses
|
| 327 |
+
- Use Redis for distributed rate limiting
|
| 328 |
+
- Implement tiered limits for different user plans
|
| 329 |
+
- Set appropriate window sizes and limits
|
| 330 |
+
- Monitor rate limit metrics
|
| 331 |
+
- Provide clear retry guidance
|
| 332 |
+
- Document rate limits in API docs
|
| 333 |
+
- Test under high load
|
| 334 |
+
|
| 335 |
+
### β DON'T
|
| 336 |
+
- Use in-memory storage in production
|
| 337 |
+
- Set limits too restrictively
|
| 338 |
+
- Forget to include Retry-After header
|
| 339 |
+
- Ignore distributed scenarios
|
| 340 |
+
- Make rate limits public (security)
|
| 341 |
+
- Use simple counters for distributed systems
|
| 342 |
+
- Forget cleanup of old data
|
| 343 |
+
|
| 344 |
+
## Monitoring
|
| 345 |
+
|
| 346 |
+
```javascript
|
| 347 |
+
// Track rate limit metrics
|
| 348 |
+
const metrics = {
|
| 349 |
+
totalRequests: 0,
|
| 350 |
+
limitedRequests: 0,
|
| 351 |
+
byUser: new Map()
|
| 352 |
+
};
|
| 353 |
+
|
| 354 |
+
app.use((req, res, next) => {
|
| 355 |
+
metrics.totalRequests++;
|
| 356 |
+
res.on('finish', () => {
|
| 357 |
+
if (res.statusCode === 429) {
|
| 358 |
+
metrics.limitedRequests++;
|
| 359 |
+
}
|
| 360 |
+
});
|
| 361 |
+
next();
|
| 362 |
+
});
|
| 363 |
+
|
| 364 |
+
app.get('/metrics/rate-limit', (req, res) => {
|
| 365 |
+
res.json({
|
| 366 |
+
totalRequests: metrics.totalRequests,
|
| 367 |
+
limitedRequests: metrics.limitedRequests,
|
| 368 |
+
percentage: (metrics.limitedRequests / metrics.totalRequests * 100).toFixed(2)
|
| 369 |
+
});
|
| 370 |
+
});
|
| 371 |
+
```
|
.agents/skills/api-security-hardening/SKILL.md
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: api-security-hardening
|
| 3 |
+
description: Secure REST APIs with authentication, rate limiting, CORS, input validation, and security middleware. Use when building or hardening API endpoints against common attacks.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# API Security Hardening
|
| 7 |
+
|
| 8 |
+
## Overview
|
| 9 |
+
|
| 10 |
+
Implement comprehensive API security measures including authentication, authorization, rate limiting, input validation, and attack prevention to protect against common vulnerabilities.
|
| 11 |
+
|
| 12 |
+
## When to Use
|
| 13 |
+
|
| 14 |
+
- New API development
|
| 15 |
+
- Security audit remediation
|
| 16 |
+
- Production API hardening
|
| 17 |
+
- Compliance requirements
|
| 18 |
+
- High-traffic API protection
|
| 19 |
+
- Public API exposure
|
| 20 |
+
|
| 21 |
+
## Implementation Examples
|
| 22 |
+
|
| 23 |
+
### 1. **Node.js/Express API Security**
|
| 24 |
+
|
| 25 |
+
```javascript
|
| 26 |
+
// secure-api.js - Comprehensive API security
|
| 27 |
+
const express = require('express');
|
| 28 |
+
const helmet = require('helmet');
|
| 29 |
+
const rateLimit = require('express-rate-limit');
|
| 30 |
+
const mongoSanitize = require('express-mongo-sanitize');
|
| 31 |
+
const xss = require('xss-clean');
|
| 32 |
+
const hpp = require('hpp');
|
| 33 |
+
const cors = require('cors');
|
| 34 |
+
const jwt = require('jsonwebtoken');
|
| 35 |
+
const validator = require('validator');
|
| 36 |
+
|
| 37 |
+
class SecureAPIServer {
|
| 38 |
+
constructor() {
|
| 39 |
+
this.app = express();
|
| 40 |
+
this.setupSecurityMiddleware();
|
| 41 |
+
this.setupRoutes();
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
setupSecurityMiddleware() {
|
| 45 |
+
// 1. Helmet - Set security headers
|
| 46 |
+
this.app.use(helmet({
|
| 47 |
+
contentSecurityPolicy: {
|
| 48 |
+
directives: {
|
| 49 |
+
defaultSrc: ["'self'"],
|
| 50 |
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
| 51 |
+
scriptSrc: ["'self'"],
|
| 52 |
+
imgSrc: ["'self'", "data:", "https:"]
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
hsts: {
|
| 56 |
+
maxAge: 31536000,
|
| 57 |
+
includeSubDomains: true,
|
| 58 |
+
preload: true
|
| 59 |
+
}
|
| 60 |
+
}));
|
| 61 |
+
|
| 62 |
+
// 2. CORS configuration
|
| 63 |
+
const corsOptions = {
|
| 64 |
+
origin: (origin, callback) => {
|
| 65 |
+
const whitelist = [
|
| 66 |
+
'https://example.com',
|
| 67 |
+
'https://app.example.com'
|
| 68 |
+
];
|
| 69 |
+
|
| 70 |
+
if (!origin || whitelist.includes(origin)) {
|
| 71 |
+
callback(null, true);
|
| 72 |
+
} else {
|
| 73 |
+
callback(new Error('Not allowed by CORS'));
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
credentials: true,
|
| 77 |
+
optionsSuccessStatus: 200,
|
| 78 |
+
methods: ['GET', 'POST', 'PUT', 'DELETE'],
|
| 79 |
+
allowedHeaders: ['Content-Type', 'Authorization']
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
this.app.use(cors(corsOptions));
|
| 83 |
+
|
| 84 |
+
// 3. Rate limiting
|
| 85 |
+
const generalLimiter = rateLimit({
|
| 86 |
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
| 87 |
+
max: 100, // limit each IP to 100 requests per windowMs
|
| 88 |
+
message: 'Too many requests from this IP',
|
| 89 |
+
standardHeaders: true,
|
| 90 |
+
legacyHeaders: false,
|
| 91 |
+
handler: (req, res) => {
|
| 92 |
+
res.status(429).json({
|
| 93 |
+
error: 'rate_limit_exceeded',
|
| 94 |
+
message: 'Too many requests, please try again later',
|
| 95 |
+
retryAfter: req.rateLimit.resetTime
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
const authLimiter = rateLimit({
|
| 101 |
+
windowMs: 15 * 60 * 1000,
|
| 102 |
+
max: 5, // Stricter limit for auth endpoints
|
| 103 |
+
skipSuccessfulRequests: true
|
| 104 |
+
});
|
| 105 |
+
|
| 106 |
+
this.app.use('/api/', generalLimiter);
|
| 107 |
+
this.app.use('/api/auth/', authLimiter);
|
| 108 |
+
|
| 109 |
+
// 4. Body parsing with size limits
|
| 110 |
+
this.app.use(express.json({ limit: '10kb' }));
|
| 111 |
+
this.app.use(express.urlencoded({ extended: true, limit: '10kb' }));
|
| 112 |
+
|
| 113 |
+
// 5. NoSQL injection prevention
|
| 114 |
+
this.app.use(mongoSanitize());
|
| 115 |
+
|
| 116 |
+
// 6. XSS protection
|
| 117 |
+
this.app.use(xss());
|
| 118 |
+
|
| 119 |
+
// 7. HTTP Parameter Pollution prevention
|
| 120 |
+
this.app.use(hpp());
|
| 121 |
+
|
| 122 |
+
// 8. Request ID for tracking
|
| 123 |
+
this.app.use((req, res, next) => {
|
| 124 |
+
req.id = require('crypto').randomUUID();
|
| 125 |
+
res.setHeader('X-Request-ID', req.id);
|
| 126 |
+
next();
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
// 9. Security logging
|
| 130 |
+
this.app.use(this.securityLogger());
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
securityLogger() {
|
| 134 |
+
return (req, res, next) => {
|
| 135 |
+
const startTime = Date.now();
|
| 136 |
+
|
| 137 |
+
res.on('finish', () => {
|
| 138 |
+
const duration = Date.now() - startTime;
|
| 139 |
+
|
| 140 |
+
const logEntry = {
|
| 141 |
+
timestamp: new Date().toISOString(),
|
| 142 |
+
requestId: req.id,
|
| 143 |
+
method: req.method,
|
| 144 |
+
path: req.path,
|
| 145 |
+
statusCode: res.statusCode,
|
| 146 |
+
duration,
|
| 147 |
+
ip: req.ip,
|
| 148 |
+
userAgent: req.get('user-agent')
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
// Log suspicious activity
|
| 152 |
+
if (res.statusCode === 401 || res.statusCode === 403) {
|
| 153 |
+
console.warn('Security event:', logEntry);
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
if (res.statusCode >= 500) {
|
| 157 |
+
console.error('Server error:', logEntry);
|
| 158 |
+
}
|
| 159 |
+
});
|
| 160 |
+
|
| 161 |
+
next();
|
| 162 |
+
};
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// JWT authentication middleware
|
| 166 |
+
authenticateJWT() {
|
| 167 |
+
return (req, res, next) => {
|
| 168 |
+
const authHeader = req.headers.authorization;
|
| 169 |
+
|
| 170 |
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
| 171 |
+
return res.status(401).json({
|
| 172 |
+
error: 'unauthorized',
|
| 173 |
+
message: 'Missing or invalid authorization header'
|
| 174 |
+
});
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
const token = authHeader.substring(7);
|
| 178 |
+
|
| 179 |
+
try {
|
| 180 |
+
const decoded = jwt.verify(token, process.env.JWT_SECRET, {
|
| 181 |
+
algorithms: ['HS256'],
|
| 182 |
+
issuer: 'api.example.com',
|
| 183 |
+
audience: 'api.example.com'
|
| 184 |
+
});
|
| 185 |
+
|
| 186 |
+
req.user = decoded;
|
| 187 |
+
next();
|
| 188 |
+
} catch (error) {
|
| 189 |
+
if (error.name === 'TokenExpiredError') {
|
| 190 |
+
return res.status(401).json({
|
| 191 |
+
error: 'token_expired',
|
| 192 |
+
message: 'Token has expired'
|
| 193 |
+
});
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
return res.status(401).json({
|
| 197 |
+
error: 'invalid_token',
|
| 198 |
+
message: 'Invalid token'
|
| 199 |
+
});
|
| 200 |
+
}
|
| 201 |
+
};
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
// Input validation middleware
|
| 205 |
+
validateInput(schema) {
|
| 206 |
+
return (req, res, next) => {
|
| 207 |
+
const errors = [];
|
| 208 |
+
|
| 209 |
+
// Validate request body
|
| 210 |
+
if (schema.body) {
|
| 211 |
+
for (const [field, rules] of Object.entries(schema.body)) {
|
| 212 |
+
const value = req.body[field];
|
| 213 |
+
|
| 214 |
+
if (rules.required && !value) {
|
| 215 |
+
errors.push(`${field} is required`);
|
| 216 |
+
continue;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
if (value) {
|
| 220 |
+
// Type validation
|
| 221 |
+
if (rules.type === 'email' && !validator.isEmail(value)) {
|
| 222 |
+
errors.push(`${field} must be a valid email`);
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
if (rules.type === 'uuid' && !validator.isUUID(value)) {
|
| 226 |
+
errors.push(`${field} must be a valid UUID`);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
if (rules.type === 'url' && !validator.isURL(value)) {
|
| 230 |
+
errors.push(`${field} must be a valid URL`);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
// Length validation
|
| 234 |
+
if (rules.minLength && value.length < rules.minLength) {
|
| 235 |
+
errors.push(`${field} must be at least ${rules.minLength} characters`);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
if (rules.maxLength && value.length > rules.maxLength) {
|
| 239 |
+
errors.push(`${field} must be at most ${rules.maxLength} characters`);
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
// Pattern validation
|
| 243 |
+
if (rules.pattern && !rules.pattern.test(value)) {
|
| 244 |
+
errors.push(`${field} format is invalid`);
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
}
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
if (errors.length > 0) {
|
| 251 |
+
return res.status(400).json({
|
| 252 |
+
error: 'validation_error',
|
| 253 |
+
message: 'Input validation failed',
|
| 254 |
+
details: errors
|
| 255 |
+
});
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
next();
|
| 259 |
+
};
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// Authorization middleware
|
| 263 |
+
authorize(...roles) {
|
| 264 |
+
return (req, res, next) => {
|
| 265 |
+
if (!req.user) {
|
| 266 |
+
return res.status(401).json({
|
| 267 |
+
error: 'unauthorized',
|
| 268 |
+
message: 'Authentication required'
|
| 269 |
+
});
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
if (roles.length > 0 && !roles.includes(req.user.role)) {
|
| 273 |
+
return res.status(403).json({
|
| 274 |
+
error: 'forbidden',
|
| 275 |
+
message: 'Insufficient permissions'
|
| 276 |
+
});
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
next();
|
| 280 |
+
};
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
setupRoutes() {
|
| 284 |
+
// Public endpoint
|
| 285 |
+
this.app.get('/api/health', (req, res) => {
|
| 286 |
+
res.json({ status: 'healthy' });
|
| 287 |
+
});
|
| 288 |
+
|
| 289 |
+
// Protected endpoint with validation
|
| 290 |
+
this.app.post('/api/users',
|
| 291 |
+
this.authenticateJWT(),
|
| 292 |
+
this.authorize('admin'),
|
| 293 |
+
this.validateInput({
|
| 294 |
+
body: {
|
| 295 |
+
email: { required: true, type: 'email' },
|
| 296 |
+
name: { required: true, minLength: 2, maxLength: 100 },
|
| 297 |
+
password: { required: true, minLength: 8 }
|
| 298 |
+
}
|
| 299 |
+
}),
|
| 300 |
+
async (req, res) => {
|
| 301 |
+
try {
|
| 302 |
+
// Sanitized and validated input
|
| 303 |
+
const { email, name, password } = req.body;
|
| 304 |
+
|
| 305 |
+
// Process request
|
| 306 |
+
res.status(201).json({
|
| 307 |
+
message: 'User created successfully',
|
| 308 |
+
userId: '123'
|
| 309 |
+
});
|
| 310 |
+
} catch (error) {
|
| 311 |
+
res.status(500).json({
|
| 312 |
+
error: 'internal_error',
|
| 313 |
+
message: 'An error occurred'
|
| 314 |
+
});
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
);
|
| 318 |
+
|
| 319 |
+
// Error handling middleware
|
| 320 |
+
this.app.use((err, req, res, next) => {
|
| 321 |
+
console.error('Unhandled error:', err);
|
| 322 |
+
|
| 323 |
+
res.status(500).json({
|
| 324 |
+
error: 'internal_error',
|
| 325 |
+
message: 'An unexpected error occurred',
|
| 326 |
+
requestId: req.id
|
| 327 |
+
});
|
| 328 |
+
});
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
start(port = 3000) {
|
| 332 |
+
this.app.listen(port, () => {
|
| 333 |
+
console.log(`Secure API server running on port ${port}`);
|
| 334 |
+
});
|
| 335 |
+
}
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
// Usage
|
| 339 |
+
const server = new SecureAPIServer();
|
| 340 |
+
server.start(3000);
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
### 2. **Python FastAPI Security**
|
| 344 |
+
|
| 345 |
+
```python
|
| 346 |
+
# secure_api.py
|
| 347 |
+
from fastapi import FastAPI, HTTPException, Depends, Security, status
|
| 348 |
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
| 349 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 350 |
+
from fastapi.middleware.trustedhost import TrustedHostMiddleware
|
| 351 |
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
| 352 |
+
from slowapi.util import get_remote_address
|
| 353 |
+
from slowapi.errors import RateLimitExceeded
|
| 354 |
+
from pydantic import BaseModel, EmailStr, validator, Field
|
| 355 |
+
import jwt
|
| 356 |
+
from datetime import datetime, timedelta
|
| 357 |
+
import re
|
| 358 |
+
from typing import Optional, List
|
| 359 |
+
import secrets
|
| 360 |
+
|
| 361 |
+
app = FastAPI()
|
| 362 |
+
security = HTTPBearer()
|
| 363 |
+
limiter = Limiter(key_func=get_remote_address)
|
| 364 |
+
|
| 365 |
+
# Rate limiting
|
| 366 |
+
app.state.limiter = limiter
|
| 367 |
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 368 |
+
|
| 369 |
+
# CORS configuration
|
| 370 |
+
app.add_middleware(
|
| 371 |
+
CORSMiddleware,
|
| 372 |
+
allow_origins=[
|
| 373 |
+
"https://example.com",
|
| 374 |
+
"https://app.example.com"
|
| 375 |
+
],
|
| 376 |
+
allow_credentials=True,
|
| 377 |
+
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
| 378 |
+
allow_headers=["Content-Type", "Authorization"],
|
| 379 |
+
max_age=3600
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
# Trusted hosts
|
| 383 |
+
app.add_middleware(
|
| 384 |
+
TrustedHostMiddleware,
|
| 385 |
+
allowed_hosts=["example.com", "*.example.com"]
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
# Security headers middleware
|
| 389 |
+
@app.middleware("http")
|
| 390 |
+
async def add_security_headers(request, call_next):
|
| 391 |
+
response = await call_next(request)
|
| 392 |
+
|
| 393 |
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
| 394 |
+
response.headers["X-Frame-Options"] = "DENY"
|
| 395 |
+
response.headers["X-XSS-Protection"] = "1; mode=block"
|
| 396 |
+
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
|
| 397 |
+
response.headers["Content-Security-Policy"] = "default-src 'self'"
|
| 398 |
+
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
| 399 |
+
response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
|
| 400 |
+
|
| 401 |
+
return response
|
| 402 |
+
|
| 403 |
+
# Input validation models
|
| 404 |
+
class CreateUserRequest(BaseModel):
|
| 405 |
+
email: EmailStr
|
| 406 |
+
name: str = Field(..., min_length=2, max_length=100)
|
| 407 |
+
password: str = Field(..., min_length=8)
|
| 408 |
+
|
| 409 |
+
@validator('password')
|
| 410 |
+
def validate_password(cls, v):
|
| 411 |
+
if not re.search(r'[A-Z]', v):
|
| 412 |
+
raise ValueError('Password must contain uppercase letter')
|
| 413 |
+
if not re.search(r'[a-z]', v):
|
| 414 |
+
raise ValueError('Password must contain lowercase letter')
|
| 415 |
+
if not re.search(r'\d', v):
|
| 416 |
+
raise ValueError('Password must contain digit')
|
| 417 |
+
if not re.search(r'[!@#$%^&*]', v):
|
| 418 |
+
raise ValueError('Password must contain special character')
|
| 419 |
+
return v
|
| 420 |
+
|
| 421 |
+
@validator('name')
|
| 422 |
+
def validate_name(cls, v):
|
| 423 |
+
# Prevent XSS in name field
|
| 424 |
+
if re.search(r'[<>]', v):
|
| 425 |
+
raise ValueError('Name contains invalid characters')
|
| 426 |
+
return v
|
| 427 |
+
|
| 428 |
+
class APIKeyRequest(BaseModel):
|
| 429 |
+
name: str = Field(..., max_length=100)
|
| 430 |
+
expires_in_days: int = Field(30, ge=1, le=365)
|
| 431 |
+
|
| 432 |
+
# JWT token verification
|
| 433 |
+
def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
|
| 434 |
+
try:
|
| 435 |
+
token = credentials.credentials
|
| 436 |
+
|
| 437 |
+
payload = jwt.decode(
|
| 438 |
+
token,
|
| 439 |
+
"your-secret-key",
|
| 440 |
+
algorithms=["HS256"],
|
| 441 |
+
audience="api.example.com",
|
| 442 |
+
issuer="api.example.com"
|
| 443 |
+
)
|
| 444 |
+
|
| 445 |
+
return payload
|
| 446 |
+
|
| 447 |
+
except jwt.ExpiredSignatureError:
|
| 448 |
+
raise HTTPException(
|
| 449 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 450 |
+
detail="Token has expired"
|
| 451 |
+
)
|
| 452 |
+
except jwt.InvalidTokenError:
|
| 453 |
+
raise HTTPException(
|
| 454 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 455 |
+
detail="Invalid token"
|
| 456 |
+
)
|
| 457 |
+
|
| 458 |
+
# Role-based authorization
|
| 459 |
+
def require_role(required_roles: List[str]):
|
| 460 |
+
def role_checker(token_payload: dict = Depends(verify_token)):
|
| 461 |
+
user_role = token_payload.get('role')
|
| 462 |
+
|
| 463 |
+
if user_role not in required_roles:
|
| 464 |
+
raise HTTPException(
|
| 465 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 466 |
+
detail="Insufficient permissions"
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
return token_payload
|
| 470 |
+
|
| 471 |
+
return role_checker
|
| 472 |
+
|
| 473 |
+
# API key authentication
|
| 474 |
+
def verify_api_key(api_key: str):
|
| 475 |
+
# Constant-time comparison to prevent timing attacks
|
| 476 |
+
if not secrets.compare_digest(api_key, "expected-api-key"):
|
| 477 |
+
raise HTTPException(
|
| 478 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 479 |
+
detail="Invalid API key"
|
| 480 |
+
)
|
| 481 |
+
return True
|
| 482 |
+
|
| 483 |
+
# Endpoints
|
| 484 |
+
@app.get("/api/health")
|
| 485 |
+
@limiter.limit("100/minute")
|
| 486 |
+
async def health_check():
|
| 487 |
+
return {"status": "healthy"}
|
| 488 |
+
|
| 489 |
+
@app.post("/api/users")
|
| 490 |
+
@limiter.limit("10/minute")
|
| 491 |
+
async def create_user(
|
| 492 |
+
user: CreateUserRequest,
|
| 493 |
+
token_payload: dict = Depends(require_role(["admin"]))
|
| 494 |
+
):
|
| 495 |
+
"""Create new user (admin only)"""
|
| 496 |
+
|
| 497 |
+
# Hash password before storing
|
| 498 |
+
# hashed_password = bcrypt.hashpw(user.password.encode(), bcrypt.gensalt())
|
| 499 |
+
|
| 500 |
+
return {
|
| 501 |
+
"message": "User created successfully",
|
| 502 |
+
"user_id": "123"
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
@app.post("/api/keys")
|
| 506 |
+
@limiter.limit("5/hour")
|
| 507 |
+
async def create_api_key(
|
| 508 |
+
request: APIKeyRequest,
|
| 509 |
+
token_payload: dict = Depends(verify_token)
|
| 510 |
+
):
|
| 511 |
+
"""Generate API key"""
|
| 512 |
+
|
| 513 |
+
# Generate secure random API key
|
| 514 |
+
api_key = secrets.token_urlsafe(32)
|
| 515 |
+
|
| 516 |
+
expires_at = datetime.now() + timedelta(days=request.expires_in_days)
|
| 517 |
+
|
| 518 |
+
return {
|
| 519 |
+
"api_key": api_key,
|
| 520 |
+
"expires_at": expires_at.isoformat(),
|
| 521 |
+
"name": request.name
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
@app.get("/api/protected")
|
| 525 |
+
async def protected_endpoint(token_payload: dict = Depends(verify_token)):
|
| 526 |
+
return {
|
| 527 |
+
"message": "Access granted",
|
| 528 |
+
"user_id": token_payload.get("sub")
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
if __name__ == "__main__":
|
| 532 |
+
import uvicorn
|
| 533 |
+
uvicorn.run(app, host="0.0.0.0", port=8000, ssl_certfile="cert.pem", ssl_keyfile="key.pem")
|
| 534 |
+
```
|
| 535 |
+
|
| 536 |
+
### 3. **API Gateway Security Configuration**
|
| 537 |
+
|
| 538 |
+
```yaml
|
| 539 |
+
# nginx-api-gateway.conf
|
| 540 |
+
# Nginx API Gateway with security hardening
|
| 541 |
+
|
| 542 |
+
http {
|
| 543 |
+
# Security headers
|
| 544 |
+
add_header X-Frame-Options "DENY" always;
|
| 545 |
+
add_header X-Content-Type-Options "nosniff" always;
|
| 546 |
+
add_header X-XSS-Protection "1; mode=block" always;
|
| 547 |
+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
| 548 |
+
add_header Content-Security-Policy "default-src 'self'" always;
|
| 549 |
+
|
| 550 |
+
# Rate limiting zones
|
| 551 |
+
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
| 552 |
+
limit_req_zone $binary_remote_addr zone=auth_limit:10m rate=1r/s;
|
| 553 |
+
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
|
| 554 |
+
|
| 555 |
+
# Request body size limit
|
| 556 |
+
client_max_body_size 10M;
|
| 557 |
+
client_body_buffer_size 128k;
|
| 558 |
+
|
| 559 |
+
# Timeout settings
|
| 560 |
+
client_body_timeout 12;
|
| 561 |
+
client_header_timeout 12;
|
| 562 |
+
send_timeout 10;
|
| 563 |
+
|
| 564 |
+
server {
|
| 565 |
+
listen 443 ssl http2;
|
| 566 |
+
server_name api.example.com;
|
| 567 |
+
|
| 568 |
+
# SSL configuration
|
| 569 |
+
ssl_certificate /etc/ssl/certs/api.example.com.crt;
|
| 570 |
+
ssl_certificate_key /etc/ssl/private/api.example.com.key;
|
| 571 |
+
ssl_protocols TLSv1.2 TLSv1.3;
|
| 572 |
+
ssl_ciphers HIGH:!aNULL:!MD5;
|
| 573 |
+
ssl_prefer_server_ciphers on;
|
| 574 |
+
ssl_session_cache shared:SSL:10m;
|
| 575 |
+
ssl_session_timeout 10m;
|
| 576 |
+
|
| 577 |
+
# API endpoints
|
| 578 |
+
location /api/ {
|
| 579 |
+
# Rate limiting
|
| 580 |
+
limit_req zone=api_limit burst=20 nodelay;
|
| 581 |
+
limit_conn conn_limit 10;
|
| 582 |
+
|
| 583 |
+
# CORS headers
|
| 584 |
+
add_header Access-Control-Allow-Origin "https://app.example.com" always;
|
| 585 |
+
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE" always;
|
| 586 |
+
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
|
| 587 |
+
|
| 588 |
+
# Block common exploits
|
| 589 |
+
if ($request_method !~ ^(GET|POST|PUT|DELETE|HEAD)$ ) {
|
| 590 |
+
return 444;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
# Proxy to backend
|
| 594 |
+
proxy_pass http://backend:3000;
|
| 595 |
+
proxy_set_header Host $host;
|
| 596 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 597 |
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
| 598 |
+
proxy_set_header X-Forwarded-Proto $scheme;
|
| 599 |
+
|
| 600 |
+
# Timeouts
|
| 601 |
+
proxy_connect_timeout 60s;
|
| 602 |
+
proxy_send_timeout 60s;
|
| 603 |
+
proxy_read_timeout 60s;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
# Auth endpoints with stricter limits
|
| 607 |
+
location /api/auth/ {
|
| 608 |
+
limit_req zone=auth_limit burst=5 nodelay;
|
| 609 |
+
|
| 610 |
+
proxy_pass http://backend:3000;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
# Block access to sensitive files
|
| 614 |
+
location ~ /\. {
|
| 615 |
+
deny all;
|
| 616 |
+
return 404;
|
| 617 |
+
}
|
| 618 |
+
}
|
| 619 |
+
}
|
| 620 |
+
```
|
| 621 |
+
|
| 622 |
+
## Best Practices
|
| 623 |
+
|
| 624 |
+
### β
DO
|
| 625 |
+
- Use HTTPS everywhere
|
| 626 |
+
- Implement rate limiting
|
| 627 |
+
- Validate all inputs
|
| 628 |
+
- Use security headers
|
| 629 |
+
- Log security events
|
| 630 |
+
- Implement CORS properly
|
| 631 |
+
- Use strong authentication
|
| 632 |
+
- Version your APIs
|
| 633 |
+
|
| 634 |
+
### β DON'T
|
| 635 |
+
- Expose stack traces
|
| 636 |
+
- Return detailed errors
|
| 637 |
+
- Trust user input
|
| 638 |
+
- Use HTTP for APIs
|
| 639 |
+
- Skip input validation
|
| 640 |
+
- Ignore rate limiting
|
| 641 |
+
|
| 642 |
+
## Security Checklist
|
| 643 |
+
|
| 644 |
+
- [ ] HTTPS enforced
|
| 645 |
+
- [ ] Authentication required
|
| 646 |
+
- [ ] Authorization implemented
|
| 647 |
+
- [ ] Rate limiting active
|
| 648 |
+
- [ ] Input validation
|
| 649 |
+
- [ ] CORS configured
|
| 650 |
+
- [ ] Security headers set
|
| 651 |
+
- [ ] Error handling secure
|
| 652 |
+
- [ ] Logging enabled
|
| 653 |
+
- [ ] API versioning
|
| 654 |
+
|
| 655 |
+
## Resources
|
| 656 |
+
|
| 657 |
+
- [OWASP API Security Top 10](https://owasp.org/www-project-api-security/)
|
| 658 |
+
- [API Security Best Practices](https://github.com/shieldfy/API-Security-Checklist)
|
| 659 |
+
- [JWT Best Practices](https://tools.ietf.org/html/rfc8725)
|
.agents/skills/find-skills/SKILL.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: find-skills
|
| 3 |
+
description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Find Skills
|
| 7 |
+
|
| 8 |
+
This skill helps you discover and install skills from the open agent skills ecosystem.
|
| 9 |
+
|
| 10 |
+
## When to Use This Skill
|
| 11 |
+
|
| 12 |
+
Use this skill when the user:
|
| 13 |
+
|
| 14 |
+
- Asks "how do I do X" where X might be a common task with an existing skill
|
| 15 |
+
- Says "find a skill for X" or "is there a skill for X"
|
| 16 |
+
- Asks "can you do X" where X is a specialized capability
|
| 17 |
+
- Expresses interest in extending agent capabilities
|
| 18 |
+
- Wants to search for tools, templates, or workflows
|
| 19 |
+
- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.)
|
| 20 |
+
|
| 21 |
+
## What is the Skills CLI?
|
| 22 |
+
|
| 23 |
+
The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools.
|
| 24 |
+
|
| 25 |
+
**Key commands:**
|
| 26 |
+
|
| 27 |
+
- `npx skills find [query]` - Search for skills interactively or by keyword
|
| 28 |
+
- `npx skills add <package>` - Install a skill from GitHub or other sources
|
| 29 |
+
- `npx skills check` - Check for skill updates
|
| 30 |
+
- `npx skills update` - Update all installed skills
|
| 31 |
+
|
| 32 |
+
**Browse skills at:** https://skills.sh/
|
| 33 |
+
|
| 34 |
+
## How to Help Users Find Skills
|
| 35 |
+
|
| 36 |
+
### Step 1: Understand What They Need
|
| 37 |
+
|
| 38 |
+
When a user asks for help with something, identify:
|
| 39 |
+
|
| 40 |
+
1. The domain (e.g., React, testing, design, deployment)
|
| 41 |
+
2. The specific task (e.g., writing tests, creating animations, reviewing PRs)
|
| 42 |
+
3. Whether this is a common enough task that a skill likely exists
|
| 43 |
+
|
| 44 |
+
### Step 2: Search for Skills
|
| 45 |
+
|
| 46 |
+
Run the find command with a relevant query:
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
npx skills find [query]
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
For example:
|
| 53 |
+
|
| 54 |
+
- User asks "how do I make my React app faster?" β `npx skills find react performance`
|
| 55 |
+
- User asks "can you help me with PR reviews?" β `npx skills find pr review`
|
| 56 |
+
- User asks "I need to create a changelog" β `npx skills find changelog`
|
| 57 |
+
|
| 58 |
+
The command will return results like:
|
| 59 |
+
|
| 60 |
+
```
|
| 61 |
+
Install with npx skills add <owner/repo@skill>
|
| 62 |
+
|
| 63 |
+
vercel-labs/agent-skills@vercel-react-best-practices
|
| 64 |
+
β https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
### Step 3: Present Options to the User
|
| 68 |
+
|
| 69 |
+
When you find relevant skills, present them to the user with:
|
| 70 |
+
|
| 71 |
+
1. The skill name and what it does
|
| 72 |
+
2. The install command they can run
|
| 73 |
+
3. A link to learn more at skills.sh
|
| 74 |
+
|
| 75 |
+
Example response:
|
| 76 |
+
|
| 77 |
+
```
|
| 78 |
+
I found a skill that might help! The "vercel-react-best-practices" skill provides
|
| 79 |
+
React and Next.js performance optimization guidelines from Vercel Engineering.
|
| 80 |
+
|
| 81 |
+
To install it:
|
| 82 |
+
npx skills add vercel-labs/agent-skills@vercel-react-best-practices
|
| 83 |
+
|
| 84 |
+
Learn more: https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
### Step 4: Offer to Install
|
| 88 |
+
|
| 89 |
+
If the user wants to proceed, you can install the skill for them:
|
| 90 |
+
|
| 91 |
+
```bash
|
| 92 |
+
npx skills add <owner/repo@skill> -g -y
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts.
|
| 96 |
+
|
| 97 |
+
## Common Skill Categories
|
| 98 |
+
|
| 99 |
+
When searching, consider these common categories:
|
| 100 |
+
|
| 101 |
+
| Category | Example Queries |
|
| 102 |
+
| --------------- | ---------------------------------------- |
|
| 103 |
+
| Web Development | react, nextjs, typescript, css, tailwind |
|
| 104 |
+
| Testing | testing, jest, playwright, e2e |
|
| 105 |
+
| DevOps | deploy, docker, kubernetes, ci-cd |
|
| 106 |
+
| Documentation | docs, readme, changelog, api-docs |
|
| 107 |
+
| Code Quality | review, lint, refactor, best-practices |
|
| 108 |
+
| Design | ui, ux, design-system, accessibility |
|
| 109 |
+
| Productivity | workflow, automation, git |
|
| 110 |
+
|
| 111 |
+
## Tips for Effective Searches
|
| 112 |
+
|
| 113 |
+
1. **Use specific keywords**: "react testing" is better than just "testing"
|
| 114 |
+
2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd"
|
| 115 |
+
3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills`
|
| 116 |
+
|
| 117 |
+
## When No Skills Are Found
|
| 118 |
+
|
| 119 |
+
If no relevant skills exist:
|
| 120 |
+
|
| 121 |
+
1. Acknowledge that no existing skill was found
|
| 122 |
+
2. Offer to help with the task directly using your general capabilities
|
| 123 |
+
3. Suggest the user could create their own skill with `npx skills init`
|
| 124 |
+
|
| 125 |
+
Example:
|
| 126 |
+
|
| 127 |
+
```
|
| 128 |
+
I searched for skills related to "xyz" but didn't find any matches.
|
| 129 |
+
I can still help you with this task directly! Would you like me to proceed?
|
| 130 |
+
|
| 131 |
+
If this is something you do often, you could create your own skill:
|
| 132 |
+
npx skills init my-xyz-skill
|
| 133 |
+
```
|
.agents/skills/github-actions-templates/SKILL.md
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: github-actions-templates
|
| 3 |
+
description: Create production-ready GitHub Actions workflows for automated testing, building, and deploying applications. Use when setting up CI/CD with GitHub Actions, automating development workflows, or creating reusable workflow templates.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# GitHub Actions Templates
|
| 7 |
+
|
| 8 |
+
Production-ready GitHub Actions workflow patterns for testing, building, and deploying applications.
|
| 9 |
+
|
| 10 |
+
## Purpose
|
| 11 |
+
|
| 12 |
+
Create efficient, secure GitHub Actions workflows for continuous integration and deployment across various tech stacks.
|
| 13 |
+
|
| 14 |
+
## When to Use
|
| 15 |
+
|
| 16 |
+
- Automate testing and deployment
|
| 17 |
+
- Build Docker images and push to registries
|
| 18 |
+
- Deploy to Kubernetes clusters
|
| 19 |
+
- Run security scans
|
| 20 |
+
- Implement matrix builds for multiple environments
|
| 21 |
+
|
| 22 |
+
## Common Workflow Patterns
|
| 23 |
+
|
| 24 |
+
### Pattern 1: Test Workflow
|
| 25 |
+
|
| 26 |
+
```yaml
|
| 27 |
+
name: Test
|
| 28 |
+
|
| 29 |
+
on:
|
| 30 |
+
push:
|
| 31 |
+
branches: [main, develop]
|
| 32 |
+
pull_request:
|
| 33 |
+
branches: [main]
|
| 34 |
+
|
| 35 |
+
jobs:
|
| 36 |
+
test:
|
| 37 |
+
runs-on: ubuntu-latest
|
| 38 |
+
|
| 39 |
+
strategy:
|
| 40 |
+
matrix:
|
| 41 |
+
node-version: [18.x, 20.x]
|
| 42 |
+
|
| 43 |
+
steps:
|
| 44 |
+
- uses: actions/checkout@v4
|
| 45 |
+
|
| 46 |
+
- name: Use Node.js ${{ matrix.node-version }}
|
| 47 |
+
uses: actions/setup-node@v4
|
| 48 |
+
with:
|
| 49 |
+
node-version: ${{ matrix.node-version }}
|
| 50 |
+
cache: "npm"
|
| 51 |
+
|
| 52 |
+
- name: Install dependencies
|
| 53 |
+
run: npm ci
|
| 54 |
+
|
| 55 |
+
- name: Run linter
|
| 56 |
+
run: npm run lint
|
| 57 |
+
|
| 58 |
+
- name: Run tests
|
| 59 |
+
run: npm test
|
| 60 |
+
|
| 61 |
+
- name: Upload coverage
|
| 62 |
+
uses: codecov/codecov-action@v3
|
| 63 |
+
with:
|
| 64 |
+
files: ./coverage/lcov.info
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
**Reference:** See `assets/test-workflow.yml`
|
| 68 |
+
|
| 69 |
+
### Pattern 2: Build and Push Docker Image
|
| 70 |
+
|
| 71 |
+
```yaml
|
| 72 |
+
name: Build and Push
|
| 73 |
+
|
| 74 |
+
on:
|
| 75 |
+
push:
|
| 76 |
+
branches: [main]
|
| 77 |
+
tags: ["v*"]
|
| 78 |
+
|
| 79 |
+
env:
|
| 80 |
+
REGISTRY: ghcr.io
|
| 81 |
+
IMAGE_NAME: ${{ github.repository }}
|
| 82 |
+
|
| 83 |
+
jobs:
|
| 84 |
+
build:
|
| 85 |
+
runs-on: ubuntu-latest
|
| 86 |
+
permissions:
|
| 87 |
+
contents: read
|
| 88 |
+
packages: write
|
| 89 |
+
|
| 90 |
+
steps:
|
| 91 |
+
- uses: actions/checkout@v4
|
| 92 |
+
|
| 93 |
+
- name: Log in to Container Registry
|
| 94 |
+
uses: docker/login-action@v3
|
| 95 |
+
with:
|
| 96 |
+
registry: ${{ env.REGISTRY }}
|
| 97 |
+
username: ${{ github.actor }}
|
| 98 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 99 |
+
|
| 100 |
+
- name: Extract metadata
|
| 101 |
+
id: meta
|
| 102 |
+
uses: docker/metadata-action@v5
|
| 103 |
+
with:
|
| 104 |
+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
| 105 |
+
tags: |
|
| 106 |
+
type=ref,event=branch
|
| 107 |
+
type=ref,event=pr
|
| 108 |
+
type=semver,pattern={{version}}
|
| 109 |
+
type=semver,pattern={{major}}.{{minor}}
|
| 110 |
+
|
| 111 |
+
- name: Build and push
|
| 112 |
+
uses: docker/build-push-action@v5
|
| 113 |
+
with:
|
| 114 |
+
context: .
|
| 115 |
+
push: true
|
| 116 |
+
tags: ${{ steps.meta.outputs.tags }}
|
| 117 |
+
labels: ${{ steps.meta.outputs.labels }}
|
| 118 |
+
cache-from: type=gha
|
| 119 |
+
cache-to: type=gha,mode=max
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
**Reference:** See `assets/deploy-workflow.yml`
|
| 123 |
+
|
| 124 |
+
### Pattern 3: Deploy to Kubernetes
|
| 125 |
+
|
| 126 |
+
```yaml
|
| 127 |
+
name: Deploy to Kubernetes
|
| 128 |
+
|
| 129 |
+
on:
|
| 130 |
+
push:
|
| 131 |
+
branches: [main]
|
| 132 |
+
|
| 133 |
+
jobs:
|
| 134 |
+
deploy:
|
| 135 |
+
runs-on: ubuntu-latest
|
| 136 |
+
|
| 137 |
+
steps:
|
| 138 |
+
- uses: actions/checkout@v4
|
| 139 |
+
|
| 140 |
+
- name: Configure AWS credentials
|
| 141 |
+
uses: aws-actions/configure-aws-credentials@v4
|
| 142 |
+
with:
|
| 143 |
+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
| 144 |
+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
| 145 |
+
aws-region: us-west-2
|
| 146 |
+
|
| 147 |
+
- name: Update kubeconfig
|
| 148 |
+
run: |
|
| 149 |
+
aws eks update-kubeconfig --name production-cluster --region us-west-2
|
| 150 |
+
|
| 151 |
+
- name: Deploy to Kubernetes
|
| 152 |
+
run: |
|
| 153 |
+
kubectl apply -f k8s/
|
| 154 |
+
kubectl rollout status deployment/my-app -n production
|
| 155 |
+
kubectl get services -n production
|
| 156 |
+
|
| 157 |
+
- name: Verify deployment
|
| 158 |
+
run: |
|
| 159 |
+
kubectl get pods -n production
|
| 160 |
+
kubectl describe deployment my-app -n production
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
### Pattern 4: Matrix Build
|
| 164 |
+
|
| 165 |
+
```yaml
|
| 166 |
+
name: Matrix Build
|
| 167 |
+
|
| 168 |
+
on: [push, pull_request]
|
| 169 |
+
|
| 170 |
+
jobs:
|
| 171 |
+
build:
|
| 172 |
+
runs-on: ${{ matrix.os }}
|
| 173 |
+
|
| 174 |
+
strategy:
|
| 175 |
+
matrix:
|
| 176 |
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
| 177 |
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
| 178 |
+
|
| 179 |
+
steps:
|
| 180 |
+
- uses: actions/checkout@v4
|
| 181 |
+
|
| 182 |
+
- name: Set up Python
|
| 183 |
+
uses: actions/setup-python@v5
|
| 184 |
+
with:
|
| 185 |
+
python-version: ${{ matrix.python-version }}
|
| 186 |
+
|
| 187 |
+
- name: Install dependencies
|
| 188 |
+
run: |
|
| 189 |
+
python -m pip install --upgrade pip
|
| 190 |
+
pip install -r requirements.txt
|
| 191 |
+
|
| 192 |
+
- name: Run tests
|
| 193 |
+
run: pytest
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
**Reference:** See `assets/matrix-build.yml`
|
| 197 |
+
|
| 198 |
+
## Workflow Best Practices
|
| 199 |
+
|
| 200 |
+
1. **Use specific action versions** (@v4, not @latest)
|
| 201 |
+
2. **Cache dependencies** to speed up builds
|
| 202 |
+
3. **Use secrets** for sensitive data
|
| 203 |
+
4. **Implement status checks** on PRs
|
| 204 |
+
5. **Use matrix builds** for multi-version testing
|
| 205 |
+
6. **Set appropriate permissions**
|
| 206 |
+
7. **Use reusable workflows** for common patterns
|
| 207 |
+
8. **Implement approval gates** for production
|
| 208 |
+
9. **Add notification steps** for failures
|
| 209 |
+
10. **Use self-hosted runners** for sensitive workloads
|
| 210 |
+
|
| 211 |
+
## Reusable Workflows
|
| 212 |
+
|
| 213 |
+
```yaml
|
| 214 |
+
# .github/workflows/reusable-test.yml
|
| 215 |
+
name: Reusable Test Workflow
|
| 216 |
+
|
| 217 |
+
on:
|
| 218 |
+
workflow_call:
|
| 219 |
+
inputs:
|
| 220 |
+
node-version:
|
| 221 |
+
required: true
|
| 222 |
+
type: string
|
| 223 |
+
secrets:
|
| 224 |
+
NPM_TOKEN:
|
| 225 |
+
required: true
|
| 226 |
+
|
| 227 |
+
jobs:
|
| 228 |
+
test:
|
| 229 |
+
runs-on: ubuntu-latest
|
| 230 |
+
steps:
|
| 231 |
+
- uses: actions/checkout@v4
|
| 232 |
+
- uses: actions/setup-node@v4
|
| 233 |
+
with:
|
| 234 |
+
node-version: ${{ inputs.node-version }}
|
| 235 |
+
- run: npm ci
|
| 236 |
+
- run: npm test
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
**Use reusable workflow:**
|
| 240 |
+
|
| 241 |
+
```yaml
|
| 242 |
+
jobs:
|
| 243 |
+
call-test:
|
| 244 |
+
uses: ./.github/workflows/reusable-test.yml
|
| 245 |
+
with:
|
| 246 |
+
node-version: "20.x"
|
| 247 |
+
secrets:
|
| 248 |
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
## Security Scanning
|
| 252 |
+
|
| 253 |
+
```yaml
|
| 254 |
+
name: Security Scan
|
| 255 |
+
|
| 256 |
+
on:
|
| 257 |
+
push:
|
| 258 |
+
branches: [main]
|
| 259 |
+
pull_request:
|
| 260 |
+
branches: [main]
|
| 261 |
+
|
| 262 |
+
jobs:
|
| 263 |
+
security:
|
| 264 |
+
runs-on: ubuntu-latest
|
| 265 |
+
|
| 266 |
+
steps:
|
| 267 |
+
- uses: actions/checkout@v4
|
| 268 |
+
|
| 269 |
+
- name: Run Trivy vulnerability scanner
|
| 270 |
+
uses: aquasecurity/trivy-action@master
|
| 271 |
+
with:
|
| 272 |
+
scan-type: "fs"
|
| 273 |
+
scan-ref: "."
|
| 274 |
+
format: "sarif"
|
| 275 |
+
output: "trivy-results.sarif"
|
| 276 |
+
|
| 277 |
+
- name: Upload Trivy results to GitHub Security
|
| 278 |
+
uses: github/codeql-action/upload-sarif@v2
|
| 279 |
+
with:
|
| 280 |
+
sarif_file: "trivy-results.sarif"
|
| 281 |
+
|
| 282 |
+
- name: Run Snyk Security Scan
|
| 283 |
+
uses: snyk/actions/node@master
|
| 284 |
+
env:
|
| 285 |
+
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
| 286 |
+
```
|
| 287 |
+
|
| 288 |
+
## Deployment with Approvals
|
| 289 |
+
|
| 290 |
+
```yaml
|
| 291 |
+
name: Deploy to Production
|
| 292 |
+
|
| 293 |
+
on:
|
| 294 |
+
push:
|
| 295 |
+
tags: ["v*"]
|
| 296 |
+
|
| 297 |
+
jobs:
|
| 298 |
+
deploy:
|
| 299 |
+
runs-on: ubuntu-latest
|
| 300 |
+
environment:
|
| 301 |
+
name: production
|
| 302 |
+
url: https://app.example.com
|
| 303 |
+
|
| 304 |
+
steps:
|
| 305 |
+
- uses: actions/checkout@v4
|
| 306 |
+
|
| 307 |
+
- name: Deploy application
|
| 308 |
+
run: |
|
| 309 |
+
echo "Deploying to production..."
|
| 310 |
+
# Deployment commands here
|
| 311 |
+
|
| 312 |
+
- name: Notify Slack
|
| 313 |
+
if: success()
|
| 314 |
+
uses: slackapi/slack-github-action@v1
|
| 315 |
+
with:
|
| 316 |
+
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
|
| 317 |
+
payload: |
|
| 318 |
+
{
|
| 319 |
+
"text": "Deployment to production completed successfully!"
|
| 320 |
+
}
|
| 321 |
+
```
|
| 322 |
+
|
| 323 |
+
## Reference Files
|
| 324 |
+
|
| 325 |
+
- `assets/test-workflow.yml` - Testing workflow template
|
| 326 |
+
- `assets/deploy-workflow.yml` - Deployment workflow template
|
| 327 |
+
- `assets/matrix-build.yml` - Matrix build template
|
| 328 |
+
- `references/common-workflows.md` - Common workflow patterns
|
| 329 |
+
|
| 330 |
+
## Related Skills
|
| 331 |
+
|
| 332 |
+
- `gitlab-ci-patterns` - For GitLab CI workflows
|
| 333 |
+
- `deployment-pipeline-design` - For pipeline architecture
|
| 334 |
+
- `secrets-management` - For secrets handling
|
.agents/skills/github-pr-review-workflow/SKILL.md
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: github-pr-review-workflow
|
| 3 |
+
description: Complete workflow for handling GitHub PR reviews using gh pr-review extension
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# GitHub PR Review Workflow
|
| 7 |
+
|
| 8 |
+
Complete workflow for reviewing, addressing feedback, and resolving threads in GitHub pull requests using the `gh-pr-review` extension from agynio/gh-pr-review.
|
| 9 |
+
|
| 10 |
+
**For gh-pr-review documentation, see:** https://github.com/agynio/gh-pr-review
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## Installation
|
| 15 |
+
|
| 16 |
+
**Install gh-pr-review extension (if not already installed):**
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
gh extension install agynio/gh-pr-review
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
**Verify installation:**
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
gh pr-review --help
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
---
|
| 29 |
+
|
| 30 |
+
## Workflow Overview
|
| 31 |
+
|
| 32 |
+
```
|
| 33 |
+
PR Review Request
|
| 34 |
+
ββ Get PR number/repo context
|
| 35 |
+
ββ List all review threads
|
| 36 |
+
ββ Analyze feedback and comments
|
| 37 |
+
ββ Validate whether each comment applies and explain decisions
|
| 38 |
+
ββ Implement fixes in code
|
| 39 |
+
ββ Run tests (unit + lint + typecheck)
|
| 40 |
+
ββ Reply to all open review threads with explanations
|
| 41 |
+
ββ Wait up to 5 minutes for follow-up
|
| 42 |
+
ββ Resolve review threads (or address follow-ups)
|
| 43 |
+
ββ Commit and push changes
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## Step-by-Step Process
|
| 49 |
+
|
| 50 |
+
### 1. Get PR Context
|
| 51 |
+
|
| 52 |
+
**Get current PR details:**
|
| 53 |
+
|
| 54 |
+
```bash
|
| 55 |
+
# Get PR number
|
| 56 |
+
gh pr view --json number
|
| 57 |
+
|
| 58 |
+
# Get PR title and status
|
| 59 |
+
gh pr view --json title,author,state,reviews
|
| 60 |
+
|
| 61 |
+
# Get repository info (for gh pr-review)
|
| 62 |
+
git remote get-url origin
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
**Output:** PR number (e.g., `<PR_NUMBER>`) and repo (e.g., `<OWNER/REPO>`)
|
| 66 |
+
|
| 67 |
+
---
|
| 68 |
+
|
| 69 |
+
### 2. List Review Threads
|
| 70 |
+
|
| 71 |
+
**List all review threads (active and outdated):**
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
# From PR root directory
|
| 75 |
+
gh pr-review threads list --pr <PR_NUMBER> --repo <OWNER/REPO>
|
| 76 |
+
|
| 77 |
+
# Example:
|
| 78 |
+
gh pr-review threads list --pr <PR_NUMBER> --repo <OWNER/REPO>
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
**Response format:**
|
| 82 |
+
|
| 83 |
+
```json
|
| 84 |
+
[
|
| 85 |
+
{
|
| 86 |
+
"threadId": "<THREAD_ID>",
|
| 87 |
+
"isResolved": false,
|
| 88 |
+
"updatedAt": "2026-01-17T22:48:36Z",
|
| 89 |
+
"path": "path/to/file.ts",
|
| 90 |
+
"line": 42,
|
| 91 |
+
"isOutdated": false
|
| 92 |
+
}
|
| 93 |
+
]
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
**Key fields:**
|
| 97 |
+
|
| 98 |
+
- `threadId`: GraphQL node ID for resolving/replying
|
| 99 |
+
- `isResolved`: Current status
|
| 100 |
+
- `isOutdated`: Whether code has changed since comment
|
| 101 |
+
- `path` + `line`: File location
|
| 102 |
+
|
| 103 |
+
If all review threads are resolved or none are present, search for normal comments and analyse them:
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
gh pr view <PR_NUMBER> --comments --json author,comments,reviews
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
---
|
| 111 |
+
|
| 112 |
+
### 3. Read and Analyze Feedback
|
| 113 |
+
|
| 114 |
+
**Get review comments via GitHub API:**
|
| 115 |
+
|
| 116 |
+
```bash
|
| 117 |
+
# Get all review comments for PR
|
| 118 |
+
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/comments
|
| 119 |
+
|
| 120 |
+
# With jq for cleaner output
|
| 121 |
+
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/comments \
|
| 122 |
+
--jq '.[] | {id,body,author,created_at,line,path}'
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
**Read the specific files mentioned:**
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
# Read the file context to understand feedback
|
| 129 |
+
cat <path>
|
| 130 |
+
# or use Read tool
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
**Categorize feedback:**
|
| 134 |
+
|
| 135 |
+
- **High priority**: Security issues, bugs, breaking changes
|
| 136 |
+
- **Medium priority**: Code quality, maintainability, test coverage
|
| 137 |
+
- **Low priority**: Style, documentation, nice-to-haves
|
| 138 |
+
|
| 139 |
+
**Validate applicability before changing code (required):**
|
| 140 |
+
|
| 141 |
+
- Confirm each comment is accurate and relevant to the current code.
|
| 142 |
+
- If a suggestion is incorrect, outdated, or doesnβt make sense in this codebase, **reply with a detailed explanation** of why it was not implemented.
|
| 143 |
+
- Do not skip a change simply because it is time-consumingβeither implement it or explain clearly why it should not be done.
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
### 4. Implement Fixes
|
| 148 |
+
|
| 149 |
+
**Edit the files mentioned in review:**
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
# Use Edit tool or bash
|
| 153 |
+
edit <file_path> <oldString> <newString>
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
**Follow repository conventions:**
|
| 157 |
+
|
| 158 |
+
- Check existing patterns in similar files
|
| 159 |
+
- Follow AGENTS.md guidelines
|
| 160 |
+
- Maintain code style consistency
|
| 161 |
+
- Add/update tests for new logic
|
| 162 |
+
|
| 163 |
+
---
|
| 164 |
+
|
| 165 |
+
### 5. Verify Changes (CRITICAL)
|
| 166 |
+
|
| 167 |
+
**Always run tests before replying:**
|
| 168 |
+
|
| 169 |
+
```bash
|
| 170 |
+
# Run project tests
|
| 171 |
+
bun run test
|
| 172 |
+
|
| 173 |
+
# Or specific test suites
|
| 174 |
+
bun run test:unit
|
| 175 |
+
bun run test:unit:watch
|
| 176 |
+
bun run test:e2e
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
**Run type checking:**
|
| 180 |
+
|
| 181 |
+
```bash
|
| 182 |
+
bun run typecheck
|
| 183 |
+
# or
|
| 184 |
+
tsc --noEmit
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
**Run linting:**
|
| 188 |
+
|
| 189 |
+
```bash
|
| 190 |
+
bun run lint
|
| 191 |
+
# or
|
| 192 |
+
eslint
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
**Verify all pass:**
|
| 196 |
+
|
| 197 |
+
- β No TypeScript errors
|
| 198 |
+
- β No ESLint warnings/errors
|
| 199 |
+
- β All unit tests pass
|
| 200 |
+
- β E2E tests pass (if relevant)
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
### 6. Commit and Push Changes
|
| 205 |
+
|
| 206 |
+
**Stage and commit changes:**
|
| 207 |
+
|
| 208 |
+
```bash
|
| 209 |
+
# Check status
|
| 210 |
+
git status
|
| 211 |
+
|
| 212 |
+
# Stage modified files
|
| 213 |
+
git add <files>
|
| 214 |
+
|
| 215 |
+
# Commit with clear message
|
| 216 |
+
git commit -m "<type>(<scope>): <description>
|
| 217 |
+
|
| 218 |
+
# Example:
|
| 219 |
+
git commit -m "refactor(emails): centralize from name logic and improve sanitization
|
| 220 |
+
|
| 221 |
+
- Extract RESEND_FROM_NAME constant to lib/emails/from-address.ts
|
| 222 |
+
- Replace duplicated logic in lib/auth.ts and app/actions/contact.ts
|
| 223 |
+
- Improve formatFromAddress sanitization (RFC 5322 chars)
|
| 224 |
+
- Add test cases for additional sanitization patterns"
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
**Push to remote:**
|
| 228 |
+
|
| 229 |
+
```bash
|
| 230 |
+
git push
|
| 231 |
+
```
|
| 232 |
+
|
| 233 |
+
**Verify working tree:**
|
| 234 |
+
|
| 235 |
+
```bash
|
| 236 |
+
git status
|
| 237 |
+
# Should show: "nothing to commit, working tree clean"
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
---
|
| 241 |
+
|
| 242 |
+
### 7. Reply to Review Threads
|
| 243 |
+
|
| 244 |
+
**Reply with explanation of fixes:**
|
| 245 |
+
|
| 246 |
+
```bash
|
| 247 |
+
gh pr-review comments reply \
|
| 248 |
+
--pr <PR_NUMBER> \
|
| 249 |
+
--repo <OWNER/REPO> \
|
| 250 |
+
--thread-id <THREAD_ID> \
|
| 251 |
+
--body "<your explanation>"
|
| 252 |
+
```
|
| 253 |
+
|
| 254 |
+
**Best practices for replies:**
|
| 255 |
+
|
| 256 |
+
- Acknowledge the feedback
|
| 257 |
+
- Explain what was changed
|
| 258 |
+
- Reference specific commit(s) if relevant
|
| 259 |
+
- Be concise but clear
|
| 260 |
+
- Use code fences for code snippets
|
| 261 |
+
|
| 262 |
+
**Example reply:**
|
| 263 |
+
|
| 264 |
+
```bash
|
| 265 |
+
gh pr-review comments reply \
|
| 266 |
+
--pr <PR_NUMBER> \
|
| 267 |
+
--repo <OWNER/REPO> \
|
| 268 |
+
--thread-id <THREAD_ID> \
|
| 269 |
+
--body "$(cat <<'EOF'
|
| 270 |
+
@reviewer Thanks for the feedback! I've addressed your suggestions:
|
| 271 |
+
|
| 272 |
+
1. Applied the requested refactor in the relevant module
|
| 273 |
+
2. Removed duplicated logic in the affected call sites
|
| 274 |
+
3. Improved sanitization to match the projectβs expectations
|
| 275 |
+
4. Added/updated tests for the new behavior
|
| 276 |
+
|
| 277 |
+
Changes committed in abc1234, all tests pass.
|
| 278 |
+
EOF
|
| 279 |
+
)"
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
**Note:** Use heredoc for multi-line bodies to avoid shell escaping issues.
|
| 283 |
+
**Note:** Always start replies with `@reviewer` (e.g., `@gemini-code-assist ...` or `@greptile β¦`) after you push changes. There can be multiple reviewers, so always look for the exact comment from which reviewer the comment is.
|
| 284 |
+
|
| 285 |
+
If this was a normal comment and not a review (see step 2), you can use this to answer:
|
| 286 |
+
|
| 287 |
+
```bash
|
| 288 |
+
gh pr comment <PR_NUMBER> --body "$(cat <<'EOF'
|
| 289 |
+
@reviewer β¦ <same as above>
|
| 290 |
+
EOF
|
| 291 |
+
)"
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
You can also just react to the comment if appropriate.
|
| 295 |
+
|
| 296 |
+
**Reply to all open threads first:**
|
| 297 |
+
|
| 298 |
+
1. Respond to every open comment with what you did **or** why it was not done.
|
| 299 |
+
2. Only after all replies are posted, proceed to the wait/resolve phase.
|
| 300 |
+
|
| 301 |
+
---
|
| 302 |
+
|
| 303 |
+
### 8. Wait for Follow-ups and Resolve Threads
|
| 304 |
+
|
| 305 |
+
**After implementing fixes, pushing the commit, and replying to all open comments, wait up to 5 minutes for follow-ups:**
|
| 306 |
+
|
| 307 |
+
```bash
|
| 308 |
+
# Wait for a minute for reviewer response
|
| 309 |
+
sleep 60
|
| 310 |
+
|
| 311 |
+
# Re-check for new replies or new threads
|
| 312 |
+
gh pr-review threads list --pr <PR_NUMBER> --repo <OWNER/REPO>
|
| 313 |
+
```
|
| 314 |
+
|
| 315 |
+
Do this step up to 5 times to wait for up to 5 minutes.
|
| 316 |
+
|
| 317 |
+
**If there is a follow-up hint, address it (steps 3-7) and then resolve.**
|
| 318 |
+
|
| 319 |
+
**If there is a confirmation, resolve the thread:**
|
| 320 |
+
|
| 321 |
+
```bash
|
| 322 |
+
gh pr-review threads resolve \
|
| 323 |
+
--pr <PR_NUMBER> \
|
| 324 |
+
--repo <OWNER/REPO> \
|
| 325 |
+
--thread-id <THREAD_ID>
|
| 326 |
+
```
|
| 327 |
+
|
| 328 |
+
**Response:**
|
| 329 |
+
|
| 330 |
+
```json
|
| 331 |
+
{
|
| 332 |
+
"thread_node_id": "PRRT_kwDOQkQlKs5p24lu",
|
| 333 |
+
"is_resolved": true
|
| 334 |
+
}
|
| 335 |
+
```
|
| 336 |
+
|
| 337 |
+
**Batch resolve multiple threads:**
|
| 338 |
+
|
| 339 |
+
```bash
|
| 340 |
+
# Resolve outdated threads first
|
| 341 |
+
gh pr-review threads resolve --pr <PR_NUMBER> --repo <OWNER/REPO> --thread-id <THREAD_ID_OUTDATED_1>
|
| 342 |
+
gh pr-review threads resolve --pr <PR_NUMBER> --repo <OWNER/REPO> --thread-id <THREAD_ID_OUTDATED_2>
|
| 343 |
+
|
| 344 |
+
# Then resolve active threads after replying
|
| 345 |
+
gh pr-review threads resolve --pr <PR_NUMBER> --repo <OWNER/REPO> --thread-id <THREAD_ID_ACTIVE>
|
| 346 |
+
```
|
| 347 |
+
|
| 348 |
+
**Strategy:**
|
| 349 |
+
|
| 350 |
+
1. Resolve outdated threads (isOutdated: true) - no reply needed
|
| 351 |
+
2. Reply to active threads explaining fixes (or non-changes)
|
| 352 |
+
3. Wait up to 5 minutes for a response
|
| 353 |
+
4. Resolve active threads after confirmation or no response
|
| 354 |
+
|
| 355 |
+
---
|
| 356 |
+
|
| 357 |
+
### 9. Verify All Threads Resolved
|
| 358 |
+
|
| 359 |
+
**Final check:**
|
| 360 |
+
|
| 361 |
+
```bash
|
| 362 |
+
gh pr-review threads list --pr <PR_NUMBER> --repo <OWNER/REPO>
|
| 363 |
+
```
|
| 364 |
+
|
| 365 |
+
**Expected output:** All threads show `isResolved: true`
|
| 366 |
+
|
| 367 |
+
---
|
| 368 |
+
|
| 369 |
+
## Complete Example Workflow
|
| 370 |
+
|
| 371 |
+
```bash
|
| 372 |
+
# 1. Get PR context
|
| 373 |
+
gh pr view --json number
|
| 374 |
+
git remote get-url origin
|
| 375 |
+
|
| 376 |
+
# 2. List review threads
|
| 377 |
+
gh pr-review threads list --pr <PR_NUMBER> --repo <OWNER/REPO>
|
| 378 |
+
|
| 379 |
+
# 3. Read comments and files
|
| 380 |
+
gh api repos/<OWNER>/<REPO>/pulls/<PR_NUMBER>/comments --jq '.[] | {id,body,path,line}'
|
| 381 |
+
cat path/to/file.ts
|
| 382 |
+
|
| 383 |
+
# 4. Implement fixes
|
| 384 |
+
edit path/to/file.ts <oldString> <newString>
|
| 385 |
+
|
| 386 |
+
# 5. Run tests
|
| 387 |
+
bun run test:unit -- tests/path/to/file.test.ts
|
| 388 |
+
bun run typecheck
|
| 389 |
+
bun run lint
|
| 390 |
+
|
| 391 |
+
# 6. Commit and push
|
| 392 |
+
git add lib/emails/from-address.ts
|
| 393 |
+
git commit -m "fix: address PR review feedback"
|
| 394 |
+
git push
|
| 395 |
+
|
| 396 |
+
# 7. Reply to threads
|
| 397 |
+
gh pr-review comments reply --pr <PR_NUMBER> --repo <OWNER/REPO> \
|
| 398 |
+
--thread-id <THREAD_ID> --body "$(cat <<'EOF'
|
| 399 |
+
@reviewer Thanks for review! I've addressed all feedback:
|
| 400 |
+
1. Centralized logic
|
| 401 |
+
2. Improved sanitization
|
| 402 |
+
3. Added tests
|
| 403 |
+
|
| 404 |
+
Changes in abc1234.
|
| 405 |
+
EOF
|
| 406 |
+
)"
|
| 407 |
+
|
| 408 |
+
# 8. Wait then resolve threads
|
| 409 |
+
sleep 300
|
| 410 |
+
gh pr-review threads list --pr <PR_NUMBER> --repo <OWNER/REPO>
|
| 411 |
+
gh pr-review threads resolve --pr <PR_NUMBER> --repo <OWNER/REPO> \
|
| 412 |
+
--thread-id <THREAD_ID>
|
| 413 |
+
|
| 414 |
+
# 9. Verify
|
| 415 |
+
gh pr-review threads list --pr <PR_NUMBER> --repo <OWNER/REPO>
|
| 416 |
+
git status
|
| 417 |
+
```
|
| 418 |
+
|
| 419 |
+
---
|
| 420 |
+
|
| 421 |
+
## gh-pr-review Commands Reference
|
| 422 |
+
|
| 423 |
+
| Command | Purpose |
|
| 424 |
+
| -------------------------------- | ------------------------- |
|
| 425 |
+
| `gh pr-review threads list` | List all review threads |
|
| 426 |
+
| `gh pr-review threads resolve` | Resolve a specific thread |
|
| 427 |
+
| `gh pr-review threads unresolve` | Reopen a resolved thread |
|
| 428 |
+
| `gh pr-review comments reply` | Reply to a review thread |
|
| 429 |
+
| `gh pr-review review` | Manage pending reviews |
|
| 430 |
+
|
| 431 |
+
**Common flags:**
|
| 432 |
+
|
| 433 |
+
- `--pr <number>`: Pull request number
|
| 434 |
+
- `-R, --repo <owner/repo>`: Repository identifier
|
| 435 |
+
- `--thread-id <id>`: GraphQL thread node ID
|
| 436 |
+
|
| 437 |
+
---
|
| 438 |
+
|
| 439 |
+
## Troubleshooting
|
| 440 |
+
|
| 441 |
+
| Issue | Solution |
|
| 442 |
+
| -------------------------------------------------------- | ------------------------------------------------------------- |
|
| 443 |
+
| `command not found: gh-pr-review` | Install extension: `gh extension install agynio/gh-pr-review` |
|
| 444 |
+
| `must specify a pull request via --pr` | Run from PR directory or add `--pr <number>` |
|
| 445 |
+
| `--repo must be owner/repo when using numeric selectors` | Add `-R <owner/repo>` or run from authenticated repo |
|
| 446 |
+
| Shell escaping issues with `--body` | Use heredoc: `--body "$(cat <<'EOF'\n...\nEOF)"` |
|
| 447 |
+
| Thread not found | Check threadId is exact GraphQL ID, not PR number |
|
| 448 |
+
|
| 449 |
+
---
|
| 450 |
+
|
| 451 |
+
## Best Practices
|
| 452 |
+
|
| 453 |
+
**Before replying:**
|
| 454 |
+
|
| 455 |
+
- β Read all review comments carefully
|
| 456 |
+
- β Understand the intent (suggestion vs. blocker)
|
| 457 |
+
- β Check if similar issues exist elsewhere
|
| 458 |
+
|
| 459 |
+
**When implementing fixes:**
|
| 460 |
+
|
| 461 |
+
- β Follow existing code patterns
|
| 462 |
+
- β Update/add tests for changes
|
| 463 |
+
- β Run full test suite
|
| 464 |
+
- β Check lint and type errors
|
| 465 |
+
|
| 466 |
+
**When replying:**
|
| 467 |
+
|
| 468 |
+
- β Be polite and appreciative
|
| 469 |
+
- β Explain what was changed
|
| 470 |
+
- β Reference specific files/lines
|
| 471 |
+
- β Keep it concise
|
| 472 |
+
|
| 473 |
+
**Before resolving:**
|
| 474 |
+
|
| 475 |
+
- β Ensure all issues are addressed
|
| 476 |
+
- β Verify tests pass
|
| 477 |
+
- β Commit changes to branch
|
| 478 |
+
|
| 479 |
+
---
|
| 480 |
+
|
| 481 |
+
## Resources
|
| 482 |
+
|
| 483 |
+
- [gh-pr-review GitHub](https://github.com/agynio/gh-pr-review)
|
| 484 |
+
- [GitHub GraphQL API: Pull Requests](https://docs.github.com/en/graphql/guides/using-the-graphql-api-for-pull-requests)
|
| 485 |
+
- [gh CLI Documentation](https://cli.github.com/manual/)
|
.agents/skills/owasp-security-check/SKILL.md
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: owasp-security-check
|
| 3 |
+
description: Security audit guidelines for web applications and REST APIs based on OWASP Top 10 and web security best practices. Use when checking code for vulnerabilities, reviewing auth/authz, auditing APIs, or before production deployment.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# OWASP Security Check
|
| 7 |
+
|
| 8 |
+
Comprehensive security audit patterns for web applications and REST APIs. Contains 20 rules across 5 categories covering OWASP Top 10 and common web vulnerabilities.
|
| 9 |
+
|
| 10 |
+
## When to Apply
|
| 11 |
+
|
| 12 |
+
Use this skill when:
|
| 13 |
+
|
| 14 |
+
- Auditing a codebase for security vulnerabilities
|
| 15 |
+
- Reviewing user-provided file or folder for security issues
|
| 16 |
+
- Checking authentication/authorization implementations
|
| 17 |
+
- Evaluating REST API security
|
| 18 |
+
- Assessing data protection measures
|
| 19 |
+
- Reviewing configuration and deployment settings
|
| 20 |
+
- Before production deployment
|
| 21 |
+
- After adding new features that handle sensitive data
|
| 22 |
+
|
| 23 |
+
## How to Use This Skill
|
| 24 |
+
|
| 25 |
+
1. **Identify application type** - Web app, REST API, SPA, SSR, or mixed
|
| 26 |
+
2. **Scan by priority** - Start with CRITICAL rules, then HIGH, then MEDIUM
|
| 27 |
+
3. **Review relevant rule files** - Load specific rules from @rules/ directory
|
| 28 |
+
4. **Report findings** - Note severity, file location, and impact
|
| 29 |
+
5. **Provide remediation** - Give concrete code examples for fixes
|
| 30 |
+
|
| 31 |
+
## Audit Workflow
|
| 32 |
+
|
| 33 |
+
### Step 1: Systematic Review by Priority
|
| 34 |
+
|
| 35 |
+
Work through categories by priority:
|
| 36 |
+
|
| 37 |
+
1. **CRITICAL**: Authentication & Authorization, Data Protection, Input/Output Security
|
| 38 |
+
2. **HIGH**: Configuration & Headers
|
| 39 |
+
3. **MEDIUM**: API & Monitoring
|
| 40 |
+
|
| 41 |
+
### Step 2: Generate Report
|
| 42 |
+
|
| 43 |
+
Format findings as:
|
| 44 |
+
|
| 45 |
+
- **Severity**: CRITICAL | HIGH | MEDIUM | LOW
|
| 46 |
+
- **Category**: Rule name
|
| 47 |
+
- **File**: Path and line number
|
| 48 |
+
- **Issue**: What's wrong
|
| 49 |
+
- **Impact**: Security consequence
|
| 50 |
+
- **Fix**: Code example of remediation
|
| 51 |
+
|
| 52 |
+
## Rules Summary
|
| 53 |
+
|
| 54 |
+
### Authentication & Authorization (CRITICAL)
|
| 55 |
+
|
| 56 |
+
#### broken-access-control - @rules/broken-access-control.md
|
| 57 |
+
|
| 58 |
+
Check for missing authorization, IDOR, privilege escalation.
|
| 59 |
+
|
| 60 |
+
```typescript
|
| 61 |
+
// Bad: No authorization check
|
| 62 |
+
async function getUser(req: Request): Promise<Response> {
|
| 63 |
+
let url = new URL(req.url);
|
| 64 |
+
let userId = url.searchParams.get("id");
|
| 65 |
+
let user = await db.user.findUnique({ where: { id: userId } });
|
| 66 |
+
return new Response(JSON.stringify(user));
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Good: Verify ownership
|
| 70 |
+
async function getUser(req: Request): Promise<Response> {
|
| 71 |
+
let session = await getSession(req);
|
| 72 |
+
let url = new URL(req.url);
|
| 73 |
+
let userId = url.searchParams.get("id");
|
| 74 |
+
|
| 75 |
+
if (session.userId !== userId && !session.isAdmin) {
|
| 76 |
+
return new Response("Forbidden", { status: 403 });
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
let user = await db.user.findUnique({ where: { id: userId } });
|
| 80 |
+
return new Response(JSON.stringify(user));
|
| 81 |
+
}
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
#### authentication-failures - @rules/authentication-failures.md
|
| 85 |
+
|
| 86 |
+
Check for weak authentication, missing MFA, session issues.
|
| 87 |
+
|
| 88 |
+
```typescript
|
| 89 |
+
// Bad: Weak password check
|
| 90 |
+
if (password.length >= 6) {
|
| 91 |
+
/* allow */
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Good: Strong password requirements
|
| 95 |
+
function validatePassword(password: string) {
|
| 96 |
+
if (password.length < 12) return false;
|
| 97 |
+
if (!/[A-Z]/.test(password)) return false;
|
| 98 |
+
if (!/[a-z]/.test(password)) return false;
|
| 99 |
+
if (!/[0-9]/.test(password)) return false;
|
| 100 |
+
if (!/[^A-Za-z0-9]/.test(password)) return false;
|
| 101 |
+
return true;
|
| 102 |
+
}
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
### Data Protection (CRITICAL)
|
| 106 |
+
|
| 107 |
+
#### cryptographic-failures - @rules/cryptographic-failures.md
|
| 108 |
+
|
| 109 |
+
Check for weak encryption, plaintext storage, bad hashing.
|
| 110 |
+
|
| 111 |
+
```typescript
|
| 112 |
+
// Bad: MD5 for passwords
|
| 113 |
+
let hash = crypto.createHash("md5").update(password).digest("hex");
|
| 114 |
+
|
| 115 |
+
// Good: bcrypt with salt
|
| 116 |
+
let hash = await bcrypt(password, 12);
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
#### sensitive-data-exposure - @rules/sensitive-data-exposure.md
|
| 120 |
+
|
| 121 |
+
Check for PII in logs/responses, error messages leaking info.
|
| 122 |
+
|
| 123 |
+
```typescript
|
| 124 |
+
// Bad: Exposing sensitive data
|
| 125 |
+
return new Response(JSON.stringify(user)); // Contains password hash, email, etc.
|
| 126 |
+
|
| 127 |
+
// Good: Return only needed fields
|
| 128 |
+
return new Response(
|
| 129 |
+
JSON.stringify({
|
| 130 |
+
id: user.id,
|
| 131 |
+
username: user.username,
|
| 132 |
+
displayName: user.displayName,
|
| 133 |
+
}),
|
| 134 |
+
);
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
#### data-integrity-failures - @rules/data-integrity-failures.md
|
| 138 |
+
|
| 139 |
+
Check for unsigned data, insecure deserialization.
|
| 140 |
+
|
| 141 |
+
```typescript
|
| 142 |
+
// Bad: Trusting unsigned JWT
|
| 143 |
+
let decoded = JSON.parse(atob(token.split(".")[1]));
|
| 144 |
+
if (decoded.isAdmin) {
|
| 145 |
+
/* grant access */
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// Good: Verify signature
|
| 149 |
+
let payload = await verifyJWT(token, secret);
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
#### secrets-management - @rules/secrets-management.md
|
| 153 |
+
|
| 154 |
+
Check for hardcoded secrets, exposed env vars.
|
| 155 |
+
|
| 156 |
+
```typescript
|
| 157 |
+
// Bad: Hardcoded secret
|
| 158 |
+
const API_KEY = "sk_live_a1b2c3d4e5f6";
|
| 159 |
+
|
| 160 |
+
// Good: Environment variables
|
| 161 |
+
let API_KEY = process.env.API_KEY;
|
| 162 |
+
if (!API_KEY) throw new Error("API_KEY not configured");
|
| 163 |
+
```
|
| 164 |
+
|
| 165 |
+
### Input/Output Security (CRITICAL)
|
| 166 |
+
|
| 167 |
+
#### injection-attacks - @rules/injection-attacks.md
|
| 168 |
+
|
| 169 |
+
Check for SQL, XSS, NoSQL, Command, Path Traversal injection.
|
| 170 |
+
|
| 171 |
+
```typescript
|
| 172 |
+
// Bad: SQL injection
|
| 173 |
+
let query = `SELECT * FROM users WHERE email = '${email}'`;
|
| 174 |
+
|
| 175 |
+
// Good: Parameterized query
|
| 176 |
+
let user = await db.user.findUnique({ where: { email } });
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
#### ssrf-attacks - @rules/ssrf-attacks.md
|
| 180 |
+
|
| 181 |
+
Check for unvalidated URLs, internal network access.
|
| 182 |
+
|
| 183 |
+
```typescript
|
| 184 |
+
// Bad: Fetching user-provided URL
|
| 185 |
+
let url = await req.json().then((d) => d.url);
|
| 186 |
+
let response = await fetch(url);
|
| 187 |
+
|
| 188 |
+
// Good: Validate against allowlist
|
| 189 |
+
const ALLOWED_DOMAINS = ["api.example.com", "cdn.example.com"];
|
| 190 |
+
let url = new URL(await req.json().then((d) => d.url));
|
| 191 |
+
if (!ALLOWED_DOMAINS.includes(url.hostname)) {
|
| 192 |
+
return new Response("Invalid URL", { status: 400 });
|
| 193 |
+
}
|
| 194 |
+
```
|
| 195 |
+
|
| 196 |
+
#### file-upload-security - @rules/file-upload-security.md
|
| 197 |
+
|
| 198 |
+
Check for unrestricted uploads, MIME validation.
|
| 199 |
+
|
| 200 |
+
```typescript
|
| 201 |
+
// Bad: No file type validation
|
| 202 |
+
let file = await req.formData().then((fd) => fd.get("file"));
|
| 203 |
+
await writeFile(`./uploads/${file.name}`, file);
|
| 204 |
+
|
| 205 |
+
// Good: Validate type and extension
|
| 206 |
+
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"];
|
| 207 |
+
const ALLOWED_EXTS = [".jpg", ".jpeg", ".png", ".webp"];
|
| 208 |
+
let file = await req.formData().then((fd) => fd.get("file") as File);
|
| 209 |
+
|
| 210 |
+
if (!ALLOWED_TYPES.includes(file.type)) {
|
| 211 |
+
return new Response("Invalid file type", { status: 400 });
|
| 212 |
+
}
|
| 213 |
+
```
|
| 214 |
+
|
| 215 |
+
#### redirect-validation - @rules/redirect-validation.md
|
| 216 |
+
|
| 217 |
+
Check for open redirects, unvalidated redirect URLs.
|
| 218 |
+
|
| 219 |
+
```typescript
|
| 220 |
+
// Bad: Unvalidated redirect
|
| 221 |
+
let returnUrl = new URL(req.url).searchParams.get("return");
|
| 222 |
+
return Response.redirect(returnUrl);
|
| 223 |
+
|
| 224 |
+
// Good: Validate redirect URL
|
| 225 |
+
let returnUrl = new URL(req.url).searchParams.get("return");
|
| 226 |
+
let allowed = ["/dashboard", "/profile", "/settings"];
|
| 227 |
+
if (!allowed.includes(returnUrl)) {
|
| 228 |
+
return Response.redirect("/");
|
| 229 |
+
}
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
### Configuration & Headers (HIGH)
|
| 233 |
+
|
| 234 |
+
#### insecure-design - @rules/insecure-design.md
|
| 235 |
+
|
| 236 |
+
Check for security anti-patterns in architecture.
|
| 237 |
+
|
| 238 |
+
```typescript
|
| 239 |
+
// Bad: Security by obscurity
|
| 240 |
+
let isAdmin = req.headers.get("x-admin-secret") === "admin123";
|
| 241 |
+
|
| 242 |
+
// Good: Proper role-based access control
|
| 243 |
+
let session = await getSession(req);
|
| 244 |
+
let isAdmin = await db.user
|
| 245 |
+
.findUnique({
|
| 246 |
+
where: { id: session.userId },
|
| 247 |
+
})
|
| 248 |
+
.then((u) => u.role === "ADMIN");
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
#### security-misconfiguration - @rules/security-misconfiguration.md
|
| 252 |
+
|
| 253 |
+
Check for default configs, debug mode, error handling.
|
| 254 |
+
|
| 255 |
+
```typescript
|
| 256 |
+
// Bad: Exposing stack traces
|
| 257 |
+
catch (error) {
|
| 258 |
+
return new Response(error.stack, { status: 500 });
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
// Good: Generic error message
|
| 262 |
+
catch (error) {
|
| 263 |
+
console.error(error); // Log server-side only
|
| 264 |
+
return new Response("Internal server error", { status: 500 });
|
| 265 |
+
}
|
| 266 |
+
```
|
| 267 |
+
|
| 268 |
+
#### security-headers - @rules/security-headers.md
|
| 269 |
+
|
| 270 |
+
Check for CSP, HSTS, X-Frame-Options, etc.
|
| 271 |
+
|
| 272 |
+
```typescript
|
| 273 |
+
// Bad: No security headers
|
| 274 |
+
return new Response(html);
|
| 275 |
+
|
| 276 |
+
// Good: Security headers set
|
| 277 |
+
return new Response(html, {
|
| 278 |
+
headers: {
|
| 279 |
+
"Content-Security-Policy": "default-src 'self'",
|
| 280 |
+
"X-Frame-Options": "DENY",
|
| 281 |
+
"X-Content-Type-Options": "nosniff",
|
| 282 |
+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
| 283 |
+
},
|
| 284 |
+
});
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
#### cors-configuration - @rules/cors-configuration.md
|
| 288 |
+
|
| 289 |
+
Check for overly permissive CORS.
|
| 290 |
+
|
| 291 |
+
```typescript
|
| 292 |
+
// Bad: Wildcard with credentials
|
| 293 |
+
headers.set("Access-Control-Allow-Origin", "*");
|
| 294 |
+
headers.set("Access-Control-Allow-Credentials", "true");
|
| 295 |
+
|
| 296 |
+
// Good: Specific origin
|
| 297 |
+
let allowedOrigins = ["https://app.example.com"];
|
| 298 |
+
let origin = req.headers.get("origin");
|
| 299 |
+
if (origin && allowedOrigins.includes(origin)) {
|
| 300 |
+
headers.set("Access-Control-Allow-Origin", origin);
|
| 301 |
+
}
|
| 302 |
+
```
|
| 303 |
+
|
| 304 |
+
#### csrf-protection - @rules/csrf-protection.md
|
| 305 |
+
|
| 306 |
+
Check for CSRF tokens, SameSite cookies.
|
| 307 |
+
|
| 308 |
+
```typescript
|
| 309 |
+
// Bad: No CSRF protection
|
| 310 |
+
let cookies = parseCookies(req.headers.get("cookie"));
|
| 311 |
+
let session = await getSession(cookies.sessionId);
|
| 312 |
+
|
| 313 |
+
// Good: SameSite cookie + token validation
|
| 314 |
+
return new Response("OK", {
|
| 315 |
+
headers: {
|
| 316 |
+
"Set-Cookie": "session=abc; SameSite=Strict; Secure; HttpOnly",
|
| 317 |
+
},
|
| 318 |
+
});
|
| 319 |
+
```
|
| 320 |
+
|
| 321 |
+
#### session-security - @rules/session-security.md
|
| 322 |
+
|
| 323 |
+
Check for cookie flags, JWT issues, token storage.
|
| 324 |
+
|
| 325 |
+
```typescript
|
| 326 |
+
// Bad: Insecure cookie
|
| 327 |
+
return new Response("OK", {
|
| 328 |
+
headers: { "Set-Cookie": "session=abc123" },
|
| 329 |
+
});
|
| 330 |
+
|
| 331 |
+
// Good: Secure cookie with all flags
|
| 332 |
+
return new Response("OK", {
|
| 333 |
+
headers: {
|
| 334 |
+
"Set-Cookie":
|
| 335 |
+
"session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/; Max-Age=3600",
|
| 336 |
+
},
|
| 337 |
+
});
|
| 338 |
+
```
|
| 339 |
+
|
| 340 |
+
### API & Monitoring (MEDIUM-HIGH)
|
| 341 |
+
|
| 342 |
+
#### api-security - @rules/api-security.md
|
| 343 |
+
|
| 344 |
+
Check for REST API vulnerabilities, mass assignment.
|
| 345 |
+
|
| 346 |
+
```typescript
|
| 347 |
+
// Bad: Mass assignment vulnerability
|
| 348 |
+
let userData = await req.json();
|
| 349 |
+
await db.user.update({ where: { id }, data: userData });
|
| 350 |
+
|
| 351 |
+
// Good: Explicitly allow fields
|
| 352 |
+
let { displayName, bio } = await req.json();
|
| 353 |
+
await db.user.update({
|
| 354 |
+
where: { id },
|
| 355 |
+
data: { displayName, bio }, // Only allowed fields
|
| 356 |
+
});
|
| 357 |
+
```
|
| 358 |
+
|
| 359 |
+
#### rate-limiting - @rules/rate-limiting.md
|
| 360 |
+
|
| 361 |
+
Check for missing rate limits, brute force prevention.
|
| 362 |
+
|
| 363 |
+
```typescript
|
| 364 |
+
// Bad: No rate limiting
|
| 365 |
+
async function login(req: Request): Promise<Response> {
|
| 366 |
+
let { email, password } = await req.json();
|
| 367 |
+
// Allows unlimited login attempts
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
// Good: Rate limiting
|
| 371 |
+
let ip = req.headers.get("x-forwarded-for");
|
| 372 |
+
let { success } = await ratelimit.limit(ip);
|
| 373 |
+
if (!success) {
|
| 374 |
+
return new Response("Too many requests", { status: 429 });
|
| 375 |
+
}
|
| 376 |
+
```
|
| 377 |
+
|
| 378 |
+
#### logging-monitoring - @rules/logging-monitoring.md
|
| 379 |
+
|
| 380 |
+
Check for insufficient logging, sensitive data in logs.
|
| 381 |
+
|
| 382 |
+
```typescript
|
| 383 |
+
// Bad: Logging sensitive data
|
| 384 |
+
console.log("User login:", { email, password, ssn });
|
| 385 |
+
|
| 386 |
+
// Good: Log events without sensitive data
|
| 387 |
+
console.log("User login attempt", {
|
| 388 |
+
email,
|
| 389 |
+
ip: req.headers.get("x-forwarded-for"),
|
| 390 |
+
timestamp: new Date().toISOString(),
|
| 391 |
+
});
|
| 392 |
+
```
|
| 393 |
+
|
| 394 |
+
#### vulnerable-dependencies - @rules/vulnerable-dependencies.md
|
| 395 |
+
|
| 396 |
+
Check for outdated packages, known CVEs.
|
| 397 |
+
|
| 398 |
+
```bash
|
| 399 |
+
# Bad: No dependency checking
|
| 400 |
+
npm install
|
| 401 |
+
|
| 402 |
+
# Good: Regular audits
|
| 403 |
+
npm audit
|
| 404 |
+
npm audit fix
|
| 405 |
+
```
|
| 406 |
+
|
| 407 |
+
## Common Vulnerability Patterns
|
| 408 |
+
|
| 409 |
+
Quick reference of patterns to look for:
|
| 410 |
+
|
| 411 |
+
- **User input without validation**: `req.json()` β immediate use
|
| 412 |
+
- **Missing auth checks**: Routes without authorization middleware
|
| 413 |
+
- **Hardcoded secrets**: Strings containing "password", "secret", "key"
|
| 414 |
+
- **SQL injection**: String concatenation in queries
|
| 415 |
+
- **XSS**: `dangerouslySetInnerHTML`, `.innerHTML`
|
| 416 |
+
- **Weak crypto**: `md5`, `sha1` for passwords
|
| 417 |
+
- **Missing headers**: No CSP, HSTS, or security headers
|
| 418 |
+
- **CORS wildcards**: `Access-Control-Allow-Origin: *` with credentials
|
| 419 |
+
- **Insecure cookies**: Missing Secure, HttpOnly, SameSite flags
|
| 420 |
+
- **Path traversal**: User input in file paths without validation
|
| 421 |
+
|
| 422 |
+
## Severity Quick Reference
|
| 423 |
+
|
| 424 |
+
**Fix Immediately (CRITICAL):**
|
| 425 |
+
|
| 426 |
+
- SQL/XSS/Command Injection
|
| 427 |
+
- Missing authentication on sensitive endpoints
|
| 428 |
+
- Hardcoded secrets in code
|
| 429 |
+
- Plaintext password storage
|
| 430 |
+
- IDOR vulnerabilities
|
| 431 |
+
|
| 432 |
+
**Fix Soon (HIGH):**
|
| 433 |
+
|
| 434 |
+
- Missing CSRF protection
|
| 435 |
+
- Weak password requirements
|
| 436 |
+
- Missing security headers
|
| 437 |
+
- Overly permissive CORS
|
| 438 |
+
- Insecure session management
|
| 439 |
+
|
| 440 |
+
**Fix When Possible (MEDIUM):**
|
| 441 |
+
|
| 442 |
+
- Missing rate limiting
|
| 443 |
+
- Incomplete logging
|
| 444 |
+
- Outdated dependencies (no known exploits)
|
| 445 |
+
- Missing input validation on non-critical fields
|
| 446 |
+
|
| 447 |
+
**Improve (LOW):**
|
| 448 |
+
|
| 449 |
+
- Missing optional security headers
|
| 450 |
+
- Verbose error messages (non-production)
|
| 451 |
+
- Suboptimal crypto parameters
|
.agents/skills/owasp-security-check/rules/api-security.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: REST API Security
|
| 3 |
+
impact: MEDIUM
|
| 4 |
+
tags: [api, rest, mass-assignment, versioning]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# REST API Security
|
| 8 |
+
|
| 9 |
+
Check for REST API vulnerabilities including mass assignment, lack of validation, and missing resource limits.
|
| 10 |
+
|
| 11 |
+
> **Related:** Input validation in [injection-attacks.md](injection-attacks.md). Authentication in [authentication-failures.md](authentication-failures.md). Rate limiting in [rate-limiting.md](rate-limiting.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Mass assignment**: Users modify protected fields
|
| 16 |
+
- **Over-fetching**: Expose unnecessary data
|
| 17 |
+
- **Resource exhaustion**: Unlimited result sets
|
| 18 |
+
- **API abuse**: Missing versioning and documentation
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
- [ ] Mass assignment in update operations
|
| 23 |
+
- [ ] No pagination on list endpoints
|
| 24 |
+
- [ ] Missing Content-Type validation
|
| 25 |
+
- [ ] No API versioning
|
| 26 |
+
- [ ] Excessive data in responses
|
| 27 |
+
- [ ] Missing rate limits
|
| 28 |
+
|
| 29 |
+
## Bad Patterns
|
| 30 |
+
|
| 31 |
+
```typescript
|
| 32 |
+
// Bad: Mass assignment
|
| 33 |
+
async function updateUser(req: Request): Promise<Response> {
|
| 34 |
+
let session = await getSession(req);
|
| 35 |
+
let data = await req.json();
|
| 36 |
+
|
| 37 |
+
// VULNERABLE: User can set isAdmin, role, etc.!
|
| 38 |
+
await db.users.update({
|
| 39 |
+
where: { id: session.userId },
|
| 40 |
+
data, // Dangerous - accepts all fields!
|
| 41 |
+
});
|
| 42 |
+
|
| 43 |
+
return new Response("Updated");
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Bad: No pagination
|
| 47 |
+
async function getUsers(req: Request): Promise<Response> {
|
| 48 |
+
// VULNERABLE: Could return millions of records
|
| 49 |
+
let users = await db.users.findMany();
|
| 50 |
+
|
| 51 |
+
return Response.json(users);
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Bad: No input validation
|
| 55 |
+
async function createPost(req: Request): Promise<Response> {
|
| 56 |
+
let data = await req.json();
|
| 57 |
+
|
| 58 |
+
// VULNERABLE: No validation of data types or values
|
| 59 |
+
await db.posts.create({ data });
|
| 60 |
+
|
| 61 |
+
return new Response("Created", { status: 201 });
|
| 62 |
+
}
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
## Good Patterns
|
| 66 |
+
|
| 67 |
+
```typescript
|
| 68 |
+
// Good: Explicit field allowlist
|
| 69 |
+
async function updateUser(req: Request): Promise<Response> {
|
| 70 |
+
let session = await getSession(req);
|
| 71 |
+
let body = await req.json();
|
| 72 |
+
|
| 73 |
+
let allowedFields = {
|
| 74 |
+
displayName: body.displayName,
|
| 75 |
+
bio: body.bio,
|
| 76 |
+
avatar: body.avatar,
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
if (
|
| 80 |
+
allowedFields.displayName &&
|
| 81 |
+
typeof allowedFields.displayName !== "string"
|
| 82 |
+
) {
|
| 83 |
+
return new Response("Invalid displayName", { status: 400 });
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
await db.users.update({
|
| 87 |
+
where: { id: session.userId },
|
| 88 |
+
data: allowedFields,
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
return new Response("Updated");
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Good: Pagination with limits
|
| 95 |
+
async function getUsers(req: Request): Promise<Response> {
|
| 96 |
+
let url = new URL(req.url);
|
| 97 |
+
let page = parseInt(url.searchParams.get("page") || "1");
|
| 98 |
+
let limit = Math.min(parseInt(url.searchParams.get("limit") || "20"), 100);
|
| 99 |
+
|
| 100 |
+
let users = await db.users.findMany({
|
| 101 |
+
take: limit,
|
| 102 |
+
skip: (page - 1) * limit,
|
| 103 |
+
});
|
| 104 |
+
|
| 105 |
+
return Response.json({ data: users, page, limit });
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Good: Input validation
|
| 109 |
+
async function createPost(req: Request): Promise<Response> {
|
| 110 |
+
let session = await getSession(req);
|
| 111 |
+
let body = await req.json();
|
| 112 |
+
|
| 113 |
+
if (
|
| 114 |
+
!body.title ||
|
| 115 |
+
typeof body.title !== "string" ||
|
| 116 |
+
body.title.length > 200
|
| 117 |
+
) {
|
| 118 |
+
return new Response("Invalid title", { status: 400 });
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
if (
|
| 122 |
+
!body.content ||
|
| 123 |
+
typeof body.content !== "string" ||
|
| 124 |
+
body.content.length > 50000
|
| 125 |
+
) {
|
| 126 |
+
return new Response("Invalid content", { status: 400 });
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
await db.posts.create({
|
| 130 |
+
data: {
|
| 131 |
+
title: body.title,
|
| 132 |
+
content: body.content,
|
| 133 |
+
authorId: session.userId,
|
| 134 |
+
},
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
return new Response("Created", { status: 201 });
|
| 138 |
+
}
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
## Rules
|
| 142 |
+
|
| 143 |
+
1. **Prevent mass assignment** - Explicitly define allowed fields
|
| 144 |
+
2. **Always paginate lists** - Enforce maximum page size
|
| 145 |
+
3. **Validate input types** - Check types and constraints
|
| 146 |
+
4. **Version your API** - Use `/api/v1/` prefix for versioning
|
| 147 |
+
5. **Limit response data** - Return only necessary fields
|
| 148 |
+
6. **Validate Content-Type** - Ensure correct headers
|
.agents/skills/owasp-security-check/rules/authentication-failures.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Authentication Failures
|
| 3 |
+
impact: CRITICAL
|
| 4 |
+
tags: [authentication, passwords, mfa, sessions, owasp-a07]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Authentication Failures
|
| 8 |
+
|
| 9 |
+
Check for weak authentication mechanisms, missing MFA, session management issues, and credential handling vulnerabilities.
|
| 10 |
+
|
| 11 |
+
> **Related:** Session security in [session-security.md](session-security.md). Rate limiting in [rate-limiting.md](rate-limiting.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Account takeover**: Attackers gain unauthorized access to user accounts
|
| 16 |
+
- **Credential stuffing**: Weak auth enables automated attacks
|
| 17 |
+
- **Session hijacking**: Improper session management allows theft
|
| 18 |
+
- **Brute force attacks**: Weak passwords and no rate limiting enable guessing
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
- [ ] Weak password requirements (length < 12, no complexity)
|
| 23 |
+
- [ ] No multi-factor authentication option
|
| 24 |
+
- [ ] Passwords stored in plaintext or with weak hashing (MD5, SHA1)
|
| 25 |
+
- [ ] Missing account lockout after failed attempts
|
| 26 |
+
- [ ] Session tokens predictable or not securely generated
|
| 27 |
+
- [ ] No session expiration or timeout
|
| 28 |
+
- [ ] Session not regenerated after login
|
| 29 |
+
- [ ] Credentials exposed in URLs or logs
|
| 30 |
+
|
| 31 |
+
## Bad Patterns
|
| 32 |
+
|
| 33 |
+
```typescript
|
| 34 |
+
// Bad: Weak password hashing (SHA-256 too fast)
|
| 35 |
+
const hash = crypto.createHash("sha256").update(password).digest("hex");
|
| 36 |
+
|
| 37 |
+
// Bad: No password requirements
|
| 38 |
+
async function signup(req: Request): Promise<Response> {
|
| 39 |
+
let { email, password } = await req.json();
|
| 40 |
+
// Accepts "123" as valid password!
|
| 41 |
+
await db.users.create({
|
| 42 |
+
data: { email, password: await bcrypt(password, 10) },
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
// Bad: Timing attack reveals if email exists
|
| 47 |
+
const user = await db.users.findUnique({ where: { email } });
|
| 48 |
+
if (!user) return new Response("Invalid", { status: 401 }); // Early return!
|
| 49 |
+
if (!(await bcrypt.compare(password, user.password))) {
|
| 50 |
+
return new Response("Invalid", { status: 401 });
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// Bad: No rate limiting or account lockout
|
| 54 |
+
async function login(req: Request): Promise<Response> {
|
| 55 |
+
// Unlimited attempts allowed!
|
| 56 |
+
let user = await authenticate(email, password);
|
| 57 |
+
}
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
## Good Patterns
|
| 61 |
+
|
| 62 |
+
```typescript
|
| 63 |
+
// Good: bcrypt with proper cost factor
|
| 64 |
+
const hash = await bcrypt(password, 12); // Cost factor 12+
|
| 65 |
+
|
| 66 |
+
// Good: Strong password validation
|
| 67 |
+
function validatePassword(password: string): string | null {
|
| 68 |
+
if (password.length < 12) return "Password must be β₯12 characters";
|
| 69 |
+
if (!/[A-Z]/.test(password)) return "Must include uppercase";
|
| 70 |
+
if (!/[a-z]/.test(password)) return "Must include lowercase";
|
| 71 |
+
if (!/[0-9]/.test(password)) return "Must include number";
|
| 72 |
+
return null;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
async function signup(req: Request): Promise<Response> {
|
| 76 |
+
let { email, password } = await req.json();
|
| 77 |
+
|
| 78 |
+
let error = validatePassword(password);
|
| 79 |
+
if (error) return new Response(error, { status: 400 });
|
| 80 |
+
|
| 81 |
+
await db.users.create({
|
| 82 |
+
data: { email, password: await bcrypt(password, 12) },
|
| 83 |
+
});
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Good: Constant-time comparison
|
| 87 |
+
async function login(req: Request): Promise<Response> {
|
| 88 |
+
let { email, password } = await req.json();
|
| 89 |
+
let user = await db.users.findUnique({ where: { email } });
|
| 90 |
+
|
| 91 |
+
// Always compare (constant time)
|
| 92 |
+
let hash = user?.password || "$2b$12$fakehash...";
|
| 93 |
+
let valid = await bcrypt.compare(password, hash);
|
| 94 |
+
|
| 95 |
+
if (!user || !valid) {
|
| 96 |
+
return new Response("Invalid credentials", { status: 401 });
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
return createSession(user);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// Good: Account lockout after failed attempts
|
| 103 |
+
async function loginWithLockout(req: Request): Promise<Response> {
|
| 104 |
+
let { email, password } = await req.json();
|
| 105 |
+
let user = await db.users.findUnique({ where: { email } });
|
| 106 |
+
|
| 107 |
+
if (user?.lockedUntil && user.lockedUntil > new Date()) {
|
| 108 |
+
return new Response("Account locked", { status: 423 });
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
let valid = user && (await bcrypt.compare(password, user.password));
|
| 112 |
+
|
| 113 |
+
if (!user || !valid) {
|
| 114 |
+
let attempts = (user?.failedAttempts || 0) + 1;
|
| 115 |
+
await db.users.update({
|
| 116 |
+
where: { email },
|
| 117 |
+
data: {
|
| 118 |
+
failedAttempts: attempts,
|
| 119 |
+
lockedUntil:
|
| 120 |
+
attempts >= 5 ? new Date(Date.now() + 30 * 60 * 1000) : null,
|
| 121 |
+
},
|
| 122 |
+
});
|
| 123 |
+
return new Response("Invalid credentials", { status: 401 });
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
// Reset on success
|
| 127 |
+
await db.users.update({
|
| 128 |
+
where: { id: user.id },
|
| 129 |
+
data: { failedAttempts: 0, lockedUntil: null },
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
return createSession(user);
|
| 133 |
+
}
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
## Rules
|
| 137 |
+
|
| 138 |
+
1. **Require strong passwords** - Minimum 12 characters with complexity
|
| 139 |
+
2. **Hash passwords properly** - Use bcrypt, argon2, or scrypt (never MD5/SHA1)
|
| 140 |
+
3. **Implement rate limiting** - Limit authentication attempts per IP/account
|
| 141 |
+
4. **Use secure session tokens** - Cryptographically random tokens
|
| 142 |
+
5. **Set session expiration** - Both absolute and idle timeout
|
| 143 |
+
6. **Regenerate session on login** - Prevent session fixation attacks
|
| 144 |
+
7. **Implement account lockout** - Temporarily lock after multiple failures
|
| 145 |
+
8. **Support MFA** - Especially for privileged accounts
|
| 146 |
+
9. **Never log credentials** - Don't log passwords, tokens, or reset links
|
.agents/skills/owasp-security-check/rules/broken-access-control.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Broken Access Control
|
| 3 |
+
impact: CRITICAL
|
| 4 |
+
tags: [access-control, authorization, idor, owasp-a01]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Broken Access Control
|
| 8 |
+
|
| 9 |
+
Check for missing authorization checks, insecure direct object references (IDOR), privilege escalation, and path traversal.
|
| 10 |
+
|
| 11 |
+
> **Related:** Path traversal in [injection-attacks.md](injection-attacks.md) and [file-upload-security.md](file-upload-security.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Data breach**: Users access others' sensitive data
|
| 16 |
+
- **Privilege escalation**: Regular users gain admin access
|
| 17 |
+
- **Data manipulation**: Unauthorized modification or deletion
|
| 18 |
+
- **Compliance violation**: GDPR, HIPAA, PCI-DSS penalties
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
- [ ] Routes accessing resources without verifying ownership
|
| 23 |
+
- [ ] User IDs taken from request params without validation
|
| 24 |
+
- [ ] Admin endpoints without role checks
|
| 25 |
+
- [ ] File paths constructed from user input
|
| 26 |
+
- [ ] Authorization checks that can be bypassed
|
| 27 |
+
- [ ] Horizontal privilege escalation (user Aβuser B's data)
|
| 28 |
+
- [ ] Vertical privilege escalation (userβadmin functions)
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: No authorization check
|
| 34 |
+
const userId = url.searchParams.get("id");
|
| 35 |
+
const user = await db.users.findUnique({ where: { id: userId } });
|
| 36 |
+
return Response.json(user); // Anyone can access!
|
| 37 |
+
|
| 38 |
+
// Bad: No role check
|
| 39 |
+
await db.users.delete({ where: { id: userId } }); // No admin verification!
|
| 40 |
+
|
| 41 |
+
// Bad: Path traversal
|
| 42 |
+
const filename = url.searchParams.get("file");
|
| 43 |
+
const content = await fs.readFile(`./uploads/${filename}`, "utf-8");
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
## Good Patterns
|
| 47 |
+
|
| 48 |
+
```typescript
|
| 49 |
+
// Good: Verify ownership before access
|
| 50 |
+
async function getUserProfile(req: Request): Promise<Response> {
|
| 51 |
+
let session = await getSession(req);
|
| 52 |
+
let url = new URL(req.url);
|
| 53 |
+
let userId = url.searchParams.get("id");
|
| 54 |
+
|
| 55 |
+
if (session.userId !== userId && !session.isAdmin) {
|
| 56 |
+
return new Response("Forbidden", { status: 403 });
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
let user = await db.users.findUnique({ where: { id: userId } });
|
| 60 |
+
return Response.json(user);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Good: Role-based access control
|
| 64 |
+
async function deleteUser(req: Request): Promise<Response> {
|
| 65 |
+
let session = await getSession(req);
|
| 66 |
+
|
| 67 |
+
let user = await db.users.findUnique({
|
| 68 |
+
where: { id: session.userId },
|
| 69 |
+
select: { role: true },
|
| 70 |
+
});
|
| 71 |
+
|
| 72 |
+
if (user.role !== "ADMIN") {
|
| 73 |
+
return new Response("Forbidden", { status: 403 });
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
let url = new URL(req.url);
|
| 77 |
+
let userId = url.searchParams.get("id");
|
| 78 |
+
|
| 79 |
+
await db.users.delete({ where: { id: userId } });
|
| 80 |
+
return new Response("Deleted");
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Good: Prevent path traversal
|
| 84 |
+
async function downloadFile(req: Request): Promise<Response> {
|
| 85 |
+
let url = new URL(req.url);
|
| 86 |
+
let filename = url.searchParams.get("file");
|
| 87 |
+
let ALLOWED = ["terms.pdf", "privacy.pdf", "guide.pdf"];
|
| 88 |
+
|
| 89 |
+
if (
|
| 90 |
+
!filename ||
|
| 91 |
+
!ALLOWED.includes(filename) ||
|
| 92 |
+
filename.includes("..") ||
|
| 93 |
+
filename.includes("/")
|
| 94 |
+
) {
|
| 95 |
+
return new Response("Invalid file", { status: 400 });
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
let content = await fs.readFile(`./documents/${filename}`, "utf-8");
|
| 99 |
+
return new Response(content);
|
| 100 |
+
}
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
## Rules
|
| 104 |
+
|
| 105 |
+
1. **Never trust user input for authorization** - Verify against server-side session
|
| 106 |
+
2. **Check ownership on every resource access** - Don't assume URL ID is valid
|
| 107 |
+
3. **Implement deny-by-default** - Require explicit permission grants
|
| 108 |
+
4. **Use role-based access control** - Define clear roles and check them
|
| 109 |
+
5. **Validate file paths** - Never construct paths directly from user input
|
| 110 |
+
6. **Log authorization failures** - Track denied access for monitoring
|
| 111 |
+
7. **Test with different roles** - Verify unprivileged users can't access privileged resources
|
.agents/skills/owasp-security-check/rules/cors-configuration.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: CORS Configuration
|
| 3 |
+
impact: HIGH
|
| 4 |
+
tags: [cors, cross-origin, same-origin-policy, owasp]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# CORS Configuration
|
| 8 |
+
|
| 9 |
+
Check for overly permissive Cross-Origin Resource Sharing (CORS) policies that allow unauthorized cross-origin requests.
|
| 10 |
+
|
| 11 |
+
> **Related:** CSRF protection in [csrf-protection.md](csrf-protection.md). Security headers in [security-headers.md](security-headers.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Unauthorized access**: Malicious sites can access your API
|
| 16 |
+
- **Credential theft**: CORS with credentials exposes sensitive data
|
| 17 |
+
- **CSRF attacks**: Improper CORS enables cross-site attacks
|
| 18 |
+
- **Data leakage**: Private APIs exposed to untrusted origins
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
- [ ] `Access-Control-Allow-Origin: *` with credentials
|
| 23 |
+
- [ ] Reflecting request origin without validation
|
| 24 |
+
- [ ] Missing origin validation
|
| 25 |
+
- [ ] Overly permissive allowed methods/headers
|
| 26 |
+
- [ ] No CORS policy on sensitive endpoints
|
| 27 |
+
|
| 28 |
+
## Bad Patterns
|
| 29 |
+
|
| 30 |
+
```typescript
|
| 31 |
+
// Bad: Wildcard with credentials
|
| 32 |
+
return Response.json(data, {
|
| 33 |
+
headers: {
|
| 34 |
+
"Access-Control-Allow-Origin": "*",
|
| 35 |
+
"Access-Control-Allow-Credentials": "true",
|
| 36 |
+
},
|
| 37 |
+
});
|
| 38 |
+
|
| 39 |
+
// Bad: Reflecting any origin
|
| 40 |
+
const origin = req.headers.get("origin");
|
| 41 |
+
return Response.json(data, {
|
| 42 |
+
headers: {
|
| 43 |
+
"Access-Control-Allow-Origin": origin || "*",
|
| 44 |
+
"Access-Control-Allow-Credentials": "true",
|
| 45 |
+
},
|
| 46 |
+
});
|
| 47 |
+
|
| 48 |
+
// Bad: Weak regex
|
| 49 |
+
return /.*\.yourdomain\.com/.test(origin); // evil-yourdomain.com matches!
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Good Patterns
|
| 53 |
+
|
| 54 |
+
```typescript
|
| 55 |
+
// Good: Strict origin allowlist
|
| 56 |
+
const ALLOWED_ORIGINS = [
|
| 57 |
+
"https://yourdomain.com",
|
| 58 |
+
"https://app.yourdomain.com",
|
| 59 |
+
"https://admin.yourdomain.com",
|
| 60 |
+
];
|
| 61 |
+
|
| 62 |
+
async function handler(req: Request): Promise<Response> {
|
| 63 |
+
let origin = req.headers.get("origin");
|
| 64 |
+
let corsHeaders: Record<string, string> = {};
|
| 65 |
+
|
| 66 |
+
if (origin && ALLOWED_ORIGINS.includes(origin)) {
|
| 67 |
+
corsHeaders["Access-Control-Allow-Origin"] = origin;
|
| 68 |
+
corsHeaders["Access-Control-Allow-Credentials"] = "true";
|
| 69 |
+
corsHeaders["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE";
|
| 70 |
+
corsHeaders["Access-Control-Allow-Headers"] = "Content-Type, Authorization";
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return Response.json(data, { headers: corsHeaders });
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Good: Environment-based CORS
|
| 77 |
+
function getAllowedOrigins(): string[] {
|
| 78 |
+
if (process.env.NODE_ENV === "production") {
|
| 79 |
+
return ["https://yourdomain.com", "https://app.yourdomain.com"];
|
| 80 |
+
}
|
| 81 |
+
return ["http://localhost:3000", "http://localhost:5173"];
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Good: Preflight request handling
|
| 85 |
+
async function corsHandler(req: Request): Response | null {
|
| 86 |
+
let origin = req.headers.get("origin");
|
| 87 |
+
let allowed = getAllowedOrigins();
|
| 88 |
+
|
| 89 |
+
if (!origin || !allowed.includes(origin)) {
|
| 90 |
+
return new Response("Origin not allowed", { status: 403 });
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
let corsHeaders = {
|
| 94 |
+
"Access-Control-Allow-Origin": origin,
|
| 95 |
+
"Access-Control-Allow-Credentials": "true",
|
| 96 |
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH",
|
| 97 |
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
| 98 |
+
"Access-Control-Max-Age": "86400",
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
if (req.method === "OPTIONS") {
|
| 102 |
+
return new Response(null, { status: 204, headers: corsHeaders });
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
return null;
|
| 106 |
+
}
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
## Rules
|
| 110 |
+
|
| 111 |
+
1. **Never use `Access-Control-Allow-Origin: *` with credentials** - Pick one or the other
|
| 112 |
+
2. **Use strict origin allowlist** - Explicitly list allowed origins
|
| 113 |
+
3. **Validate origin before reflecting** - Don't blindly reflect request origin
|
| 114 |
+
4. **Separate dev and prod origins** - Don't allow localhost in production
|
| 115 |
+
5. **Limit allowed methods** - Only necessary HTTP methods
|
| 116 |
+
6. **Limit allowed headers** - Only required headers
|
| 117 |
+
7. **Handle preflight requests** - Respond to OPTIONS correctly
|
.agents/skills/owasp-security-check/rules/cryptographic-failures.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Cryptographic Failures
|
| 3 |
+
impact: CRITICAL
|
| 4 |
+
tags: [cryptography, encryption, hashing, tls, owasp-a02]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Cryptographic Failures
|
| 8 |
+
|
| 9 |
+
Check for weak encryption, improper key management, plaintext storage of sensitive data, and missing encryption in transit.
|
| 10 |
+
|
| 11 |
+
> **Related:** Password hashing in [authentication-failures.md](authentication-failures.md). Secrets in [secrets-management.md](secrets-management.md). Data signing in [data-integrity-failures.md](data-integrity-failures.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Data breach**: Sensitive data exposed if stolen
|
| 16 |
+
- **Compliance violation**: GDPR, PCI-DSS require encryption
|
| 17 |
+
- **Man-in-the-middle**: Unencrypted connections intercepted
|
| 18 |
+
- **Password compromise**: Weak hashing enables rainbow table attacks
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
- [ ] Sensitive data stored in plaintext (passwords, tokens, PII)
|
| 23 |
+
- [ ] Weak hashing algorithms (MD5, SHA1) for passwords
|
| 24 |
+
- [ ] Weak encryption algorithms (DES, RC4, ECB mode)
|
| 25 |
+
- [ ] Hardcoded encryption keys or predictable keys
|
| 26 |
+
- [ ] Missing HTTPS/TLS for data transmission
|
| 27 |
+
- [ ] Insufficient key length (< 2048 bits for RSA, < 256 bits symmetric)
|
| 28 |
+
- [ ] No encryption for sensitive data at rest
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: MD5 for password hashing
|
| 34 |
+
async function hashPassword(password: string): Promise<string> {
|
| 35 |
+
// VULNERABLE: MD5 is too fast, easily cracked
|
| 36 |
+
return crypto.createHash("md5").update(password).digest("hex");
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Bad: Storing passwords in plaintext
|
| 40 |
+
await db.users.create({
|
| 41 |
+
data: {
|
| 42 |
+
email,
|
| 43 |
+
password, // VULNERABLE: Plaintext!
|
| 44 |
+
},
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
// Bad: Weak encryption algorithm
|
| 48 |
+
const cipher = crypto.createCipher("des", "weak-key"); // VULNERABLE: DES is weak
|
| 49 |
+
|
| 50 |
+
// Bad: Hardcoded encryption key
|
| 51 |
+
const ENCRYPTION_KEY = "my-secret-key-12345"; // VULNERABLE: Hardcoded
|
| 52 |
+
|
| 53 |
+
function encryptData(data: string): string {
|
| 54 |
+
const cipher = crypto.createCipheriv("aes-256-cbc", ENCRYPTION_KEY, iv);
|
| 55 |
+
return cipher.update(data, "utf8", "hex");
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Bad: No encryption for sensitive data
|
| 59 |
+
await db.creditCards.create({
|
| 60 |
+
data: {
|
| 61 |
+
number: "4111111111111111", // VULNERABLE: Plaintext
|
| 62 |
+
cvv: "123",
|
| 63 |
+
expiresAt: "12/25",
|
| 64 |
+
},
|
| 65 |
+
});
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## Good Patterns
|
| 69 |
+
|
| 70 |
+
```typescript
|
| 71 |
+
// Good: bcrypt for password hashing
|
| 72 |
+
async function hashPassword(password: string): Promise<string> {
|
| 73 |
+
return await bcrypt(password, 12);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Good: AES-256-GCM encryption
|
| 77 |
+
function encryptData(plaintext: string): { encrypted: string; iv: string } {
|
| 78 |
+
let key = Buffer.from(process.env.ENCRYPTION_KEY!, "hex");
|
| 79 |
+
let iv = crypto.randomBytes(16);
|
| 80 |
+
|
| 81 |
+
let cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
| 82 |
+
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
| 83 |
+
encrypted += cipher.final("hex");
|
| 84 |
+
encrypted += cipher.getAuthTag().toString("hex");
|
| 85 |
+
|
| 86 |
+
return { encrypted, iv: iv.toString("hex") };
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
function decryptData(encrypted: string, ivHex: string): string {
|
| 90 |
+
let key = Buffer.from(process.env.ENCRYPTION_KEY!, "hex");
|
| 91 |
+
let iv = Buffer.from(ivHex, "hex");
|
| 92 |
+
let authTag = Buffer.from(encrypted.slice(-32), "hex");
|
| 93 |
+
let ciphertext = encrypted.slice(0, -32);
|
| 94 |
+
|
| 95 |
+
let decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
| 96 |
+
decipher.setAuthTag(authTag);
|
| 97 |
+
|
| 98 |
+
return decipher.update(ciphertext, "hex", "utf8") + decipher.final("utf8");
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// Good: Encrypt sensitive fields
|
| 102 |
+
async function saveCreditCard(req: Request): Promise<Response> {
|
| 103 |
+
let { number, cvv } = await req.json();
|
| 104 |
+
|
| 105 |
+
let { encrypted: encryptedNumber, iv: numberIv } = encryptData(number);
|
| 106 |
+
let { encrypted: encryptedCvv, iv: cvvIv } = encryptData(cvv);
|
| 107 |
+
|
| 108 |
+
await db.creditCards.create({
|
| 109 |
+
data: { encryptedNumber, numberIv, encryptedCvv, cvvIv },
|
| 110 |
+
});
|
| 111 |
+
|
| 112 |
+
return new Response("Saved", { status: 201 });
|
| 113 |
+
}
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
## Rules
|
| 117 |
+
|
| 118 |
+
1. **Use strong password hashing** - bcrypt, argon2, or scrypt (never MD5/SHA1)
|
| 119 |
+
2. **Use modern encryption** - AES-256-GCM or ChaCha20-Poly1305
|
| 120 |
+
3. **Never hardcode keys** - Use environment variables or key management systems
|
| 121 |
+
4. **Encrypt sensitive data at rest** - PII, credentials, financial data
|
| 122 |
+
5. **Enforce HTTPS/TLS** - All data in transit must be encrypted
|
| 123 |
+
6. **Use sufficient key lengths** - RSA β₯ 2048 bits, symmetric β₯ 256 bits
|
| 124 |
+
7. **Generate random IVs** - New random IV for each encryption operation
|
| 125 |
+
8. **Rotate keys regularly** - Implement key rotation policies
|
.agents/skills/owasp-security-check/rules/csrf-protection.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: CSRF Protection
|
| 3 |
+
impact: HIGH
|
| 4 |
+
tags: [csrf, tokens, cookies, same-site]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# CSRF Protection
|
| 8 |
+
|
| 9 |
+
Check for Cross-Site Request Forgery protection on state-changing operations.
|
| 10 |
+
|
| 11 |
+
> **Related:** Session cookie configuration is covered in [session-security.md](session-security.md). CORS configuration is covered in [cors-configuration.md](cors-configuration.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Unauthorized actions**: Attackers perform actions as victim
|
| 16 |
+
- **Account takeover**: Change email/password without consent
|
| 17 |
+
- **Financial fraud**: Unauthorized transfers
|
| 18 |
+
- **Data manipulation**: Modify user data
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
**Vulnerability Indicators:**
|
| 23 |
+
|
| 24 |
+
- [ ] State-changing endpoints accept GET requests
|
| 25 |
+
- [ ] No CSRF tokens on forms
|
| 26 |
+
- [ ] Cookies without SameSite attribute
|
| 27 |
+
- [ ] Missing Origin/Referer validation
|
| 28 |
+
- [ ] No double-submit cookie pattern
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: No SameSite on cookie
|
| 34 |
+
return new Response("OK", {
|
| 35 |
+
headers: { "Set-Cookie": "session=abc123; HttpOnly; Secure" },
|
| 36 |
+
});
|
| 37 |
+
|
| 38 |
+
// Bad: State change via GET
|
| 39 |
+
async function deleteAccount(req: Request): Promise<Response> {
|
| 40 |
+
let userId = new URL(req.url).searchParams.get("id");
|
| 41 |
+
await db.users.delete({ where: { id: userId } });
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Bad: No CSRF token
|
| 45 |
+
const { to, amount } = await req.json();
|
| 46 |
+
await transfer(to, amount); // Attacker can trigger!
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Good Patterns
|
| 50 |
+
|
| 51 |
+
```typescript
|
| 52 |
+
// Good: SameSite cookie
|
| 53 |
+
async function login(req: Request): Promise<Response> {
|
| 54 |
+
return new Response("OK", {
|
| 55 |
+
headers: {
|
| 56 |
+
"Set-Cookie": "session=abc123; HttpOnly; Secure; SameSite=Strict; Path=/",
|
| 57 |
+
},
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
// Good: CSRF token validation
|
| 62 |
+
async function generateCSRFToken(sessionId: string): Promise<string> {
|
| 63 |
+
let token = crypto.randomBytes(32).toString("hex");
|
| 64 |
+
|
| 65 |
+
await db.csrfToken.create({
|
| 66 |
+
data: {
|
| 67 |
+
token,
|
| 68 |
+
sessionId,
|
| 69 |
+
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
| 70 |
+
},
|
| 71 |
+
});
|
| 72 |
+
|
| 73 |
+
return token;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
async function validateCSRFToken(
|
| 77 |
+
sessionId: string,
|
| 78 |
+
token: string,
|
| 79 |
+
): Promise<boolean> {
|
| 80 |
+
let stored = await db.csrfToken.findFirst({
|
| 81 |
+
where: { token, sessionId, expiresAt: { gt: new Date() } },
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
if (stored) {
|
| 85 |
+
await db.csrfToken.delete({ where: { id: stored.id } });
|
| 86 |
+
return true;
|
| 87 |
+
}
|
| 88 |
+
return false;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
async function transferMoney(req: Request): Promise<Response> {
|
| 92 |
+
let session = await getSession(req);
|
| 93 |
+
let { to, amount, csrfToken } = await req.json();
|
| 94 |
+
|
| 95 |
+
if (!(await validateCSRFToken(session.id, csrfToken))) {
|
| 96 |
+
return new Response("Invalid CSRF token", { status: 403 });
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
await transfer(to, amount);
|
| 100 |
+
return new Response("OK");
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Good: Double-submit cookie pattern
|
| 104 |
+
async function setupCSRF(req: Request): Promise<Response> {
|
| 105 |
+
let token = crypto.randomBytes(32).toString("hex");
|
| 106 |
+
|
| 107 |
+
return Response.json(
|
| 108 |
+
{ csrfToken: token },
|
| 109 |
+
{
|
| 110 |
+
headers: {
|
| 111 |
+
"Set-Cookie": `csrf=${token}; SameSite=Strict; Secure`,
|
| 112 |
+
"Content-Type": "application/json",
|
| 113 |
+
},
|
| 114 |
+
},
|
| 115 |
+
);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
async function validateDoubleSubmit(req: Request): Promise<boolean> {
|
| 119 |
+
let cookies = parseCookies(req.headers.get("cookie"));
|
| 120 |
+
let { csrfToken } = await req.json();
|
| 121 |
+
|
| 122 |
+
return cookies.csrf === csrfToken;
|
| 123 |
+
}
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
## Rules
|
| 127 |
+
|
| 128 |
+
1. **Use SameSite=Strict or Lax** - On all session cookies
|
| 129 |
+
2. **No state changes via GET** - Use POST/PUT/DELETE
|
| 130 |
+
3. **Implement CSRF tokens** - For session-based auth
|
| 131 |
+
4. **Double-submit cookie** - Alternative to tokens
|
| 132 |
+
5. **Validate Origin header** - Additional protection layer
|
.agents/skills/owasp-security-check/rules/data-integrity-failures.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Software and Data Integrity Failures
|
| 3 |
+
impact: CRITICAL
|
| 4 |
+
tags: [integrity, jwt, serialization, ci-cd, owasp-a08]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Software and Data Integrity Failures
|
| 8 |
+
|
| 9 |
+
Check for unsigned data, insecure deserialization, and lack of integrity verification in code and data.
|
| 10 |
+
|
| 11 |
+
> **Related:** JWT signing in [cryptographic-failures.md](cryptographic-failures.md) and [session-security.md](session-security.md). Dependency integrity in [vulnerable-dependencies.md](vulnerable-dependencies.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Data tampering**: Attackers modify unsigned data
|
| 16 |
+
- **Remote code execution**: Insecure deserialization exploits
|
| 17 |
+
- **Supply chain attacks**: Unsigned packages or builds
|
| 18 |
+
- **Trust violations**: Cannot verify data authenticity
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
- [ ] JWT tokens decoded without signature verification
|
| 23 |
+
- [ ] Accepting unsigned or unverified data
|
| 24 |
+
- [ ] Insecure deserialization of user input
|
| 25 |
+
- [ ] No integrity checks on file downloads
|
| 26 |
+
- [ ] Missing code signing in CI/CD
|
| 27 |
+
- [ ] Auto-update without verification
|
| 28 |
+
- [ ] Using eval() or Function() with external data
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: No signature verification
|
| 34 |
+
async function handleWebhook(req: Request): Promise<Response> {
|
| 35 |
+
const payload = await req.json();
|
| 36 |
+
// Trusting payload without verification!
|
| 37 |
+
await processOrder(payload);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Bad: JWT without verification
|
| 41 |
+
async function getUser(req: Request): Promise<Response> {
|
| 42 |
+
let token = req.headers.get("authorization")?.split(" ")[1];
|
| 43 |
+
let payload = JSON.parse(atob(token!.split(".")[1])); // Just decode!
|
| 44 |
+
// Attacker can modify payload
|
| 45 |
+
return Response.json({ userId: payload.sub });
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Bad: No integrity check on downloads
|
| 49 |
+
async function downloadUpdate(req: Request): Promise<Response> {
|
| 50 |
+
let file = await fetch("https://cdn.example.com/update.zip");
|
| 51 |
+
// No checksum verification
|
| 52 |
+
return new Response(file.body);
|
| 53 |
+
}
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Good Patterns
|
| 57 |
+
|
| 58 |
+
```typescript
|
| 59 |
+
// Good: Verify webhook signature
|
| 60 |
+
async function handleWebhook(req: Request): Promise<Response> {
|
| 61 |
+
let signature = req.headers.get("x-webhook-signature");
|
| 62 |
+
let payload = await req.text();
|
| 63 |
+
|
| 64 |
+
let expected = crypto
|
| 65 |
+
.createHmac("sha256", process.env.WEBHOOK_SECRET!)
|
| 66 |
+
.update(payload)
|
| 67 |
+
.digest("hex");
|
| 68 |
+
|
| 69 |
+
if (signature !== expected) {
|
| 70 |
+
return new Response("Invalid signature", { status: 401 });
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
await processOrder(JSON.parse(payload));
|
| 74 |
+
return new Response("OK");
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Good: Verify JWT signature
|
| 78 |
+
async function getUser(req: Request): Promise<Response> {
|
| 79 |
+
let token = req.headers.get("authorization")?.split(" ")[1];
|
| 80 |
+
|
| 81 |
+
if (!token) {
|
| 82 |
+
return new Response("Unauthorized", { status: 401 });
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
let payload = await verifyJWT(token, process.env.JWT_SECRET!);
|
| 86 |
+
|
| 87 |
+
let user = await db.users.findUnique({
|
| 88 |
+
where: { id: payload.sub },
|
| 89 |
+
});
|
| 90 |
+
|
| 91 |
+
return Response.json(user);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
// Good: Verify file integrity with checksum
|
| 95 |
+
async function downloadUpdate(req: Request): Promise<Response> {
|
| 96 |
+
let file = await fetch("https://cdn.example.com/update.zip");
|
| 97 |
+
let buffer = await file.arrayBuffer();
|
| 98 |
+
|
| 99 |
+
let hash = crypto
|
| 100 |
+
.createHash("sha256")
|
| 101 |
+
.update(Buffer.from(buffer))
|
| 102 |
+
.digest("hex");
|
| 103 |
+
let expected = "a1b2c3d4..."; // From trusted source
|
| 104 |
+
|
| 105 |
+
if (hash !== expected) {
|
| 106 |
+
return new Response("Integrity check failed", { status: 400 });
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
return new Response(buffer);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Good: Signed cookies
|
| 113 |
+
function signCookie(value: string, secret: string): string {
|
| 114 |
+
let sig = crypto.createHmac("sha256", secret).update(value).digest("hex");
|
| 115 |
+
return `${value}.${sig}`;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
function verifyCookie(signedValue: string, secret: string): string | null {
|
| 119 |
+
let [value, signature] = signedValue.split(".");
|
| 120 |
+
let expected = crypto
|
| 121 |
+
.createHmac("sha256", secret)
|
| 122 |
+
.update(value)
|
| 123 |
+
.digest("hex");
|
| 124 |
+
return signature === expected ? value : null;
|
| 125 |
+
}
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
## Rules
|
| 129 |
+
|
| 130 |
+
1. **Always verify JWT signatures** - Never decode without verification
|
| 131 |
+
2. **Never trust client data** - Look up prices, roles, permissions server-side
|
| 132 |
+
3. **Use JSON.parse, never eval** - Safe deserialization only
|
| 133 |
+
4. **Use Subresource Integrity** - For all CDN-loaded scripts/styles
|
| 134 |
+
5. **Sign cookies** - Use HMAC for tamper detection
|
| 135 |
+
6. **Verify checksums** - For downloaded code and updates
|
| 136 |
+
7. **Lock dependency versions** - Use lockfiles to ensure integrity
|
| 137 |
+
8. **Sign code in CI/CD** - Verify builds haven't been tampered with
|
.agents/skills/owasp-security-check/rules/file-upload-security.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: File Upload Security
|
| 3 |
+
impact: HIGH
|
| 4 |
+
tags: [file-upload, mime-types, path-traversal]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# File Upload Security
|
| 8 |
+
|
| 9 |
+
Check for secure file upload handling including type validation, size limits, and safe storage.
|
| 10 |
+
|
| 11 |
+
> **Related:** Path traversal is also covered in [injection-attacks.md](injection-attacks.md) and [broken-access-control.md](broken-access-control.md). XSS prevention is covered in [injection-attacks.md](injection-attacks.md) and [security-headers.md](security-headers.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Malware upload**: Attackers upload malicious files
|
| 16 |
+
- **Path traversal**: Overwrite system files
|
| 17 |
+
- **XSS via files**: SVG/HTML files execute scripts
|
| 18 |
+
- **Resource exhaustion**: Huge file uploads
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
**Vulnerability Indicators:**
|
| 23 |
+
|
| 24 |
+
- [ ] No file type validation
|
| 25 |
+
- [ ] No file size limits
|
| 26 |
+
- [ ] Original filename used for storage
|
| 27 |
+
- [ ] Files stored in web-accessible directory
|
| 28 |
+
- [ ] No MIME type validation
|
| 29 |
+
- [ ] Both extension and MIME type not checked
|
| 30 |
+
|
| 31 |
+
## Bad Patterns
|
| 32 |
+
|
| 33 |
+
```typescript
|
| 34 |
+
// Bad: No validation
|
| 35 |
+
async function uploadFile(req: Request): Promise<Response> {
|
| 36 |
+
let formData = await req.formData();
|
| 37 |
+
let file = formData.get("file") as File;
|
| 38 |
+
|
| 39 |
+
// No type or size checking!
|
| 40 |
+
await writeFile(`./uploads/${file.name}`, file);
|
| 41 |
+
|
| 42 |
+
return new Response("Uploaded");
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Bad: Using original filename
|
| 46 |
+
await writeFile(`./public/uploads/${file.name}`, buffer);
|
| 47 |
+
// User could upload "../../etc/passwd"
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
## Good Patterns
|
| 51 |
+
|
| 52 |
+
```typescript
|
| 53 |
+
// Good: Comprehensive file validation
|
| 54 |
+
const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"];
|
| 55 |
+
const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp"];
|
| 56 |
+
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
| 57 |
+
|
| 58 |
+
async function uploadFile(req: Request): Promise<Response> {
|
| 59 |
+
let formData = await req.formData();
|
| 60 |
+
let file = formData.get("file") as File;
|
| 61 |
+
|
| 62 |
+
if (!file) {
|
| 63 |
+
return new Response("No file provided", { status: 400 });
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
if (!ALLOWED_MIME_TYPES.includes(file.type)) {
|
| 67 |
+
return new Response("Invalid file type", { status: 400 });
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if (file.size > MAX_FILE_SIZE) {
|
| 71 |
+
return new Response("File too large", { status: 400 });
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
let ext = path.extname(file.name).toLowerCase();
|
| 75 |
+
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
| 76 |
+
return new Response("Invalid file extension", { status: 400 });
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Generate safe random filename
|
| 80 |
+
let randomName = crypto.randomBytes(16).toString("hex");
|
| 81 |
+
let safeFilename = `${randomName}${ext}`;
|
| 82 |
+
|
| 83 |
+
// Store outside web root
|
| 84 |
+
let uploadPath = path.join(process.cwd(), "private", "uploads", safeFilename);
|
| 85 |
+
|
| 86 |
+
let buffer = await file.arrayBuffer();
|
| 87 |
+
await writeFile(uploadPath, Buffer.from(buffer));
|
| 88 |
+
|
| 89 |
+
// Store metadata
|
| 90 |
+
let uploadedFile = await db.file.create({
|
| 91 |
+
data: {
|
| 92 |
+
filename: safeFilename,
|
| 93 |
+
originalName: file.name.slice(0, 255),
|
| 94 |
+
mimeType: file.type,
|
| 95 |
+
size: file.size,
|
| 96 |
+
uploadedAt: new Date(),
|
| 97 |
+
},
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
return Response.json(uploadedFile, { status: 201 });
|
| 101 |
+
}
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## Rules
|
| 105 |
+
|
| 106 |
+
1. **Validate MIME type** - Check file.type
|
| 107 |
+
2. **Validate extension** - Check file extension
|
| 108 |
+
3. **Enforce size limits** - Prevent huge uploads
|
| 109 |
+
4. **Generate random filenames** - Don't use user input
|
| 110 |
+
5. **Store outside web root** - Not in public/
|
| 111 |
+
6. **Validate both MIME and extension** - Double check
|
.agents/skills/owasp-security-check/rules/injection-attacks.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Injection Attack Prevention
|
| 3 |
+
impact: CRITICAL
|
| 4 |
+
tags: [injection, sql, xss, nosql, command-injection, path-traversal, owasp-a03]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Injection Attack Prevention
|
| 8 |
+
|
| 9 |
+
Check for SQL injection, XSS, NoSQL injection, Command injection, and Path Traversal through proper input validation and output encoding.
|
| 10 |
+
|
| 11 |
+
> **Related:** XSS headers in [security-headers.md](security-headers.md). File upload path traversal in [file-upload-security.md](file-upload-security.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Data breach**: SQL/NoSQL injection exposes entire databases
|
| 16 |
+
- **Account takeover**: XSS steals session cookies and credentials
|
| 17 |
+
- **Remote code execution**: Command injection compromises servers
|
| 18 |
+
- **Data manipulation**: Unauthorized modification or deletion
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
- [ ] String concatenation or template literals in database queries
|
| 23 |
+
- [ ] User input rendered in HTML without escaping
|
| 24 |
+
- [ ] User input passed to shell commands (`exec`, `spawn` with `shell: true`)
|
| 25 |
+
- [ ] User input used in file paths without validation
|
| 26 |
+
- [ ] Dynamic code execution (`eval`, `Function` constructor, `setTimeout` with strings)
|
| 27 |
+
- [ ] `dangerouslySetInnerHTML` or `.innerHTML` with user content
|
| 28 |
+
- [ ] NoSQL queries accepting raw objects with `$where`, `$regex`, `$ne` operators
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: SQL injection
|
| 34 |
+
const query = `SELECT * FROM users WHERE email = '${email}'`;
|
| 35 |
+
|
| 36 |
+
// Bad: XSS via dangerouslySetInnerHTML
|
| 37 |
+
<div dangerouslySetInnerHTML={{ __html: comment }} />
|
| 38 |
+
|
| 39 |
+
// Bad: Command injection
|
| 40 |
+
execSync(`convert ${filename} output.jpg`);
|
| 41 |
+
|
| 42 |
+
// Bad: Path traversal
|
| 43 |
+
const content = await fs.readFile(`./uploads/${filename}`, "utf-8");
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
## Good Patterns
|
| 47 |
+
|
| 48 |
+
```typescript
|
| 49 |
+
// Good: Parameterized query
|
| 50 |
+
async function getUser(req: Request): Promise<Response> {
|
| 51 |
+
let url = new URL(req.url);
|
| 52 |
+
let email = url.searchParams.get("email");
|
| 53 |
+
|
| 54 |
+
let user = await db.users.findUnique({ where: { email } });
|
| 55 |
+
return Response.json(user);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
// Good: React auto-escapes by default
|
| 59 |
+
function UserComment({ comment }: { comment: string }) {
|
| 60 |
+
return <div>{comment}</div>;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
// Good: Avoid shell commands, validate strictly
|
| 64 |
+
async function convertImage(req: Request): Promise<Response> {
|
| 65 |
+
let formData = await req.formData();
|
| 66 |
+
let file = formData.get("file") as File;
|
| 67 |
+
|
| 68 |
+
let ALLOWED = ["image/jpeg", "image/png", "image/webp"];
|
| 69 |
+
|
| 70 |
+
if (!ALLOWED.includes(file.type)) {
|
| 71 |
+
return new Response("Invalid type", { status: 400 });
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
let buffer = await file.arrayBuffer();
|
| 75 |
+
// Use image library, not shell
|
| 76 |
+
return new Response("Uploaded", { status: 200 });
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Good: Allowlist for file paths
|
| 80 |
+
async function readFile(req: Request): Promise<Response> {
|
| 81 |
+
let url = new URL(req.url);
|
| 82 |
+
let filename = url.searchParams.get("file");
|
| 83 |
+
|
| 84 |
+
let ALLOWED = ["terms.pdf", "privacy.pdf", "guide.pdf"];
|
| 85 |
+
|
| 86 |
+
if (!filename || !ALLOWED.includes(filename) || filename.includes("..")) {
|
| 87 |
+
return new Response("Invalid file", { status: 400 });
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
let content = await fs.readFile(`./documents/${filename}`, "utf-8");
|
| 91 |
+
return new Response(content);
|
| 92 |
+
}
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
## Rules
|
| 96 |
+
|
| 97 |
+
1. **Always use parameterized queries** - Never concatenate user input into SQL
|
| 98 |
+
2. **Validate all input** - Use type checks and format validation
|
| 99 |
+
3. **Escape output by context** - HTML, JavaScript, SQL require different escaping
|
| 100 |
+
4. **Use allowlists over denylists** - Explicitly allow known-good values
|
| 101 |
+
5. **Never use eval()** - Find safe alternatives for dynamic execution
|
| 102 |
+
6. **Avoid shell commands** - Use libraries or built-in APIs instead
|
| 103 |
+
7. **Validate file paths** - Prevent directory traversal with strict validation
|
.agents/skills/owasp-security-check/rules/insecure-design.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Insecure Design
|
| 3 |
+
impact: HIGH
|
| 4 |
+
tags: [design, architecture, threat-modeling, owasp-a04]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Insecure Design
|
| 8 |
+
|
| 9 |
+
Check for security anti-patterns and flaws in application architecture that can't be fixed by implementation alone.
|
| 10 |
+
|
| 11 |
+
## Why
|
| 12 |
+
|
| 13 |
+
- **Fundamental flaws**: Can't be patched, require redesign
|
| 14 |
+
- **Business logic bypass**: Attackers exploit workflow flaws
|
| 15 |
+
- **Privilege escalation**: Design allows unauthorized access
|
| 16 |
+
- **Data corruption**: Race conditions and logic errors
|
| 17 |
+
|
| 18 |
+
## What to Check
|
| 19 |
+
|
| 20 |
+
**Vulnerability Indicators:**
|
| 21 |
+
|
| 22 |
+
- [ ] Security by obscurity instead of proper access control
|
| 23 |
+
- [ ] Missing rate limiting on expensive operations
|
| 24 |
+
- [ ] No input validation on business logic
|
| 25 |
+
- [ ] Race conditions in multi-step workflows
|
| 26 |
+
- [ ] Trust boundaries not defined
|
| 27 |
+
- [ ] Missing defense in depth
|
| 28 |
+
- [ ] No threat modeling performed
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: Security by obscurity
|
| 34 |
+
if (req.headers.get("x-admin-secret") === "admin123") {
|
| 35 |
+
// Admin operations
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
// Bad: Race condition in balance check
|
| 39 |
+
const balance = await getBalance(from);
|
| 40 |
+
if (balance >= amount) {
|
| 41 |
+
// Race: balance could change here!
|
| 42 |
+
await updateBalance(from, balance - amount);
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Bad: No rate limiting
|
| 46 |
+
async function generateReport(req: Request): Promise<Response> {
|
| 47 |
+
const report = await runExpensiveQuery(); // Can DoS
|
| 48 |
+
return new Response(report);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Bad: Trust user role from client
|
| 52 |
+
const { isAdmin } = await req.json();
|
| 53 |
+
if (isAdmin) {
|
| 54 |
+
await db.users.delete({ where: { id } }); // User can claim admin!
|
| 55 |
+
}
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## Good Patterns
|
| 59 |
+
|
| 60 |
+
```typescript
|
| 61 |
+
// Good: Proper RBAC
|
| 62 |
+
async function adminEndpoint(req: Request): Promise<Response> {
|
| 63 |
+
let session = await getSession(req);
|
| 64 |
+
let user = await db.users.findUnique({
|
| 65 |
+
where: { id: session.userId },
|
| 66 |
+
select: { role: true },
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
if (user.role !== "ADMIN") {
|
| 70 |
+
return new Response("Forbidden", { status: 403 });
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
// Admin operations
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// Good: Transaction for atomic operations
|
| 77 |
+
async function transferMoney(from: string, to: string, amount: number) {
|
| 78 |
+
await db.$transaction(async (tx) => {
|
| 79 |
+
let fromAccount = await tx.account.findUnique({
|
| 80 |
+
where: { id: from },
|
| 81 |
+
select: { balance: true },
|
| 82 |
+
});
|
| 83 |
+
|
| 84 |
+
if (!fromAccount || fromAccount.balance < amount) {
|
| 85 |
+
throw new Error("Insufficient funds");
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
await tx.account.update({
|
| 89 |
+
where: { id: from },
|
| 90 |
+
data: { balance: { decrement: amount } },
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
await tx.account.update({
|
| 94 |
+
where: { id: to },
|
| 95 |
+
data: { balance: { increment: amount } },
|
| 96 |
+
});
|
| 97 |
+
});
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Good: Rate limiting on expensive operations
|
| 101 |
+
async function generateReport(req: Request): Promise<Response> {
|
| 102 |
+
let session = await getSession(req);
|
| 103 |
+
|
| 104 |
+
let { success } = await reportLimit.limit(session.userId);
|
| 105 |
+
if (!success) {
|
| 106 |
+
return new Response("Rate limit exceeded", { status: 429 });
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
let report = await runExpensiveQuery();
|
| 110 |
+
return new Response(report);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
// Good: Server-side role verification
|
| 114 |
+
async function deleteUser(req: Request): Promise<Response> {
|
| 115 |
+
let session = await getSession(req);
|
| 116 |
+
|
| 117 |
+
let user = await db.users.findUnique({
|
| 118 |
+
where: { id: session.userId },
|
| 119 |
+
select: { role: true },
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
if (user.role !== "ADMIN") {
|
| 123 |
+
return new Response("Forbidden", { status: 403 });
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
let { targetUserId } = await req.json();
|
| 127 |
+
await db.users.delete({ where: { id: targetUserId } });
|
| 128 |
+
|
| 129 |
+
return new Response("Deleted");
|
| 130 |
+
}
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
## Rules
|
| 134 |
+
|
| 135 |
+
1. **Don't rely on security by obscurity** - Use proper authentication
|
| 136 |
+
2. **Use transactions for atomic operations** - Prevent race conditions
|
| 137 |
+
3. **Rate limit expensive operations** - Prevent resource exhaustion
|
| 138 |
+
4. **Verify privileges server-side** - Never trust client data
|
| 139 |
+
5. **Implement defense in depth** - Multiple layers of security
|
| 140 |
+
6. **Perform threat modeling** - Identify risks in design phase
|
| 141 |
+
7. **Define trust boundaries** - Know what to validate
|
| 142 |
+
8. **Fail securely** - Default deny, not default allow
|
.agents/skills/owasp-security-check/rules/logging-monitoring.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Security Logging and Monitoring Failures
|
| 3 |
+
impact: MEDIUM
|
| 4 |
+
tags: [logging, monitoring, incident-response, owasp-a09]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Security Logging and Monitoring Failures
|
| 8 |
+
|
| 9 |
+
Check for insufficient logging of security events, missing monitoring, and lack of incident response capabilities.
|
| 10 |
+
|
| 11 |
+
> **Related:** Preventing sensitive data in logs is covered in [sensitive-data-exposure.md](sensitive-data-exposure.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Delayed breach detection**: Attacks go unnoticed for months
|
| 16 |
+
- **No audit trail**: Can't investigate incidents
|
| 17 |
+
- **Compliance violations**: Regulations require logging
|
| 18 |
+
- **Unable to respond**: No visibility into attacks
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
**Vulnerability Indicators:**
|
| 23 |
+
|
| 24 |
+
- [ ] No logging of authentication attempts
|
| 25 |
+
- [ ] Sensitive data in logs (passwords, tokens)
|
| 26 |
+
- [ ] No monitoring or alerting on suspicious activity
|
| 27 |
+
- [ ] Logs not retained long enough
|
| 28 |
+
- [ ] No log integrity protection
|
| 29 |
+
- [ ] Missing request IDs for tracing
|
| 30 |
+
|
| 31 |
+
## Bad Patterns
|
| 32 |
+
|
| 33 |
+
```typescript
|
| 34 |
+
// Bad: No logging of security events
|
| 35 |
+
async function login(req: Request): Promise<Response> {
|
| 36 |
+
let { email, password } = await req.json();
|
| 37 |
+
|
| 38 |
+
let user = await authenticate(email, password);
|
| 39 |
+
|
| 40 |
+
if (!user) {
|
| 41 |
+
// No logging of failed attempt
|
| 42 |
+
return new Response("Invalid credentials", { status: 401 });
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return createSession(user);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
// Bad: Logging sensitive data
|
| 49 |
+
console.log("User data:", {
|
| 50 |
+
email,
|
| 51 |
+
password, // Don't log passwords!
|
| 52 |
+
creditCard,
|
| 53 |
+
});
|
| 54 |
+
|
| 55 |
+
// Bad: No structured logging
|
| 56 |
+
console.log("User logged in");
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
## Good Patterns
|
| 60 |
+
|
| 61 |
+
```typescript
|
| 62 |
+
// Good: Log security events with context
|
| 63 |
+
async function login(req: Request): Promise<Response> {
|
| 64 |
+
let { email, password } = await req.json();
|
| 65 |
+
let ip = req.headers.get("x-forwarded-for");
|
| 66 |
+
|
| 67 |
+
let user = await authenticate(email, password);
|
| 68 |
+
|
| 69 |
+
if (!user) {
|
| 70 |
+
logger.warn("Failed login", {
|
| 71 |
+
email,
|
| 72 |
+
ip,
|
| 73 |
+
timestamp: new Date().toISOString(),
|
| 74 |
+
});
|
| 75 |
+
return new Response("Invalid credentials", { status: 401 });
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
logger.info("Successful login", { userId: user.id, email, ip });
|
| 79 |
+
return createSession(user);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// Good: Structured logging with sanitization
|
| 83 |
+
function createLogger() {
|
| 84 |
+
let sensitiveKeys = ["password", "token", "secret", "apiKey"];
|
| 85 |
+
|
| 86 |
+
function sanitize(obj: any): any {
|
| 87 |
+
if (typeof obj !== "object" || obj === null) return obj;
|
| 88 |
+
let sanitized: any = {};
|
| 89 |
+
for (const [key, value] of Object.entries(obj)) {
|
| 90 |
+
sanitized[key] = sensitiveKeys.some((sk) =>
|
| 91 |
+
key.toLowerCase().includes(sk),
|
| 92 |
+
)
|
| 93 |
+
? "[REDACTED]"
|
| 94 |
+
: typeof value === "object"
|
| 95 |
+
? sanitize(value)
|
| 96 |
+
: value;
|
| 97 |
+
}
|
| 98 |
+
return sanitized;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
return {
|
| 102 |
+
info(message: string, context?: Record<string, unknown>) {
|
| 103 |
+
console.log(
|
| 104 |
+
JSON.stringify({
|
| 105 |
+
level: "info",
|
| 106 |
+
message,
|
| 107 |
+
context: context ? sanitize(context) : undefined,
|
| 108 |
+
timestamp: new Date().toISOString(),
|
| 109 |
+
}),
|
| 110 |
+
);
|
| 111 |
+
},
|
| 112 |
+
warn(message: string, context?: Record<string, unknown>) {
|
| 113 |
+
console.warn(
|
| 114 |
+
JSON.stringify({
|
| 115 |
+
level: "warn",
|
| 116 |
+
message,
|
| 117 |
+
context: context ? sanitize(context) : undefined,
|
| 118 |
+
timestamp: new Date().toISOString(),
|
| 119 |
+
}),
|
| 120 |
+
);
|
| 121 |
+
},
|
| 122 |
+
error(message: string, error: Error, context?: Record<string, unknown>) {
|
| 123 |
+
console.error(
|
| 124 |
+
JSON.stringify({
|
| 125 |
+
level: "error",
|
| 126 |
+
message,
|
| 127 |
+
context: {
|
| 128 |
+
error: error.message,
|
| 129 |
+
stack: error.stack,
|
| 130 |
+
...sanitize(context || {}),
|
| 131 |
+
},
|
| 132 |
+
timestamp: new Date().toISOString(),
|
| 133 |
+
}),
|
| 134 |
+
);
|
| 135 |
+
},
|
| 136 |
+
};
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
const logger = createLogger();
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
## Rules
|
| 143 |
+
|
| 144 |
+
1. **Log all authentication events** - Successes and failures
|
| 145 |
+
2. **Log authorization failures** - When access is denied
|
| 146 |
+
3. **Don't log sensitive data** - Sanitize passwords, tokens, PII
|
| 147 |
+
4. **Use structured logging** - JSON format for parsing
|
| 148 |
+
5. **Include context** - User ID, IP, timestamp, request ID
|
| 149 |
+
6. **Monitor and alert** - Set up alerts for suspicious patterns
|
| 150 |
+
7. **Retain logs appropriately** - Balance storage and compliance
|
| 151 |
+
8. **Protect log integrity** - Prevent tampering
|
.agents/skills/owasp-security-check/rules/rate-limiting.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Rate Limiting and DoS Prevention
|
| 3 |
+
impact: MEDIUM
|
| 4 |
+
tags: [rate-limiting, dos, brute-force]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Rate Limiting and DoS Prevention
|
| 8 |
+
|
| 9 |
+
Check for rate limiting on authentication endpoints, APIs, and resource-intensive operations to prevent abuse and denial of service.
|
| 10 |
+
|
| 11 |
+
> **Related:** Authentication rate limiting is covered in [authentication-failures.md](authentication-failures.md). API rate limiting is covered in [api-security.md](api-security.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Brute force prevention**: Stop password guessing attacks
|
| 16 |
+
- **Resource exhaustion**: Prevent server overload
|
| 17 |
+
- **Cost control**: Limit API abuse and costs
|
| 18 |
+
- **Fair usage**: Ensure availability for all users
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
**Vulnerability Indicators:**
|
| 23 |
+
|
| 24 |
+
- [ ] No rate limiting on login/signup endpoints
|
| 25 |
+
- [ ] No rate limiting on password reset
|
| 26 |
+
- [ ] Unlimited API requests
|
| 27 |
+
- [ ] No throttling on expensive operations
|
| 28 |
+
- [ ] Missing 429 (Too Many Requests) responses
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: No rate limiting on login
|
| 34 |
+
async function login(req: Request): Promise<Response> {
|
| 35 |
+
let { email, password } = await req.json();
|
| 36 |
+
|
| 37 |
+
// Allows unlimited login attempts
|
| 38 |
+
let user = await authenticate(email, password);
|
| 39 |
+
|
| 40 |
+
if (!user) {
|
| 41 |
+
return new Response("Invalid credentials", { status: 401 });
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return createSession(user);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
// Bad: No API rate limiting
|
| 48 |
+
async function apiEndpoint(req: Request): Promise<Response> {
|
| 49 |
+
// Can be called unlimited times
|
| 50 |
+
let data = await expensiveQuery();
|
| 51 |
+
return Response.json(data);
|
| 52 |
+
}
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
## Good Patterns
|
| 56 |
+
|
| 57 |
+
```typescript
|
| 58 |
+
// Good: Rate limiting with Redis
|
| 59 |
+
const loginRateLimit = new Ratelimit({
|
| 60 |
+
redis,
|
| 61 |
+
limiter: Ratelimit.slidingWindow(5, "15m"), // 5 attempts per 15 min
|
| 62 |
+
analytics: true,
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
async function login(req: Request): Promise<Response> {
|
| 66 |
+
let ip = req.headers.get("x-forwarded-for") || "unknown";
|
| 67 |
+
|
| 68 |
+
let { success, limit, remaining, reset } = await loginRateLimit.limit(ip);
|
| 69 |
+
|
| 70 |
+
if (!success) {
|
| 71 |
+
return new Response("Too many login attempts", {
|
| 72 |
+
status: 429,
|
| 73 |
+
headers: {
|
| 74 |
+
"Retry-After": String(Math.ceil((reset - Date.now()) / 1000)),
|
| 75 |
+
"X-RateLimit-Limit": String(limit),
|
| 76 |
+
"X-RateLimit-Remaining": String(remaining),
|
| 77 |
+
"X-RateLimit-Reset": String(reset),
|
| 78 |
+
},
|
| 79 |
+
});
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
let { email, password } = await req.json();
|
| 83 |
+
let user = await authenticate(email, password);
|
| 84 |
+
|
| 85 |
+
if (!user) {
|
| 86 |
+
return new Response("Invalid credentials", { status: 401 });
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
return createSession(user);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Good: Per-user API rate limiting
|
| 93 |
+
const apiRateLimit = new Ratelimit({
|
| 94 |
+
redis,
|
| 95 |
+
limiter: Ratelimit.slidingWindow(100, "1h"),
|
| 96 |
+
});
|
| 97 |
+
|
| 98 |
+
async function apiEndpoint(req: Request): Promise<Response> {
|
| 99 |
+
let session = await getSession(req);
|
| 100 |
+
if (!session) return new Response("Unauthorized", { status: 401 });
|
| 101 |
+
|
| 102 |
+
let { success } = await apiRateLimit.limit(session.userId);
|
| 103 |
+
if (!success) return new Response("Rate limit exceeded", { status: 429 });
|
| 104 |
+
|
| 105 |
+
let data = await performOperation();
|
| 106 |
+
return Response.json(data);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
// Good: Tiered rate limiting
|
| 110 |
+
function getRateLimit(tier: string): Ratelimit {
|
| 111 |
+
let limits = {
|
| 112 |
+
free: Ratelimit.slidingWindow(10, "1h"),
|
| 113 |
+
pro: Ratelimit.slidingWindow(100, "1h"),
|
| 114 |
+
enterprise: Ratelimit.slidingWindow(1000, "1h"),
|
| 115 |
+
};
|
| 116 |
+
return new Ratelimit({ redis, limiter: limits[tier] || limits.free });
|
| 117 |
+
}
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
## Rules
|
| 121 |
+
|
| 122 |
+
1. **Rate limit auth endpoints** - Prevent brute force
|
| 123 |
+
2. **Per-IP and per-user limits** - Multiple layers
|
| 124 |
+
3. **Return 429 status** - Standard rate limit response
|
| 125 |
+
4. **Include retry headers** - Retry-After, X-RateLimit-\*
|
| 126 |
+
5. **Different limits for tiers** - Free vs paid users
|
| 127 |
+
6. **Rate limit expensive operations** - Reports, exports, search
|
.agents/skills/owasp-security-check/rules/redirect-validation.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Open Redirect Prevention
|
| 3 |
+
impact: MEDIUM
|
| 4 |
+
tags: [redirects, phishing, open-redirect]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Open Redirect Prevention
|
| 8 |
+
|
| 9 |
+
Check for unvalidated redirect and forward URLs that could be used for phishing attacks.
|
| 10 |
+
|
| 11 |
+
> **Related:** SSRF prevention (server-side URL validation) is covered in [ssrf-attacks.md](ssrf-attacks.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Phishing attacks**: Legitimate domain redirects to malicious site
|
| 16 |
+
- **Credential theft**: Users trust your domain and enter credentials
|
| 17 |
+
- **OAuth attacks**: Redirect after auth to steal tokens
|
| 18 |
+
- **Trust abuse**: Your domain's reputation exploited
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
**Vulnerability Indicators:**
|
| 23 |
+
|
| 24 |
+
- [ ] Redirect URLs from query parameters
|
| 25 |
+
- [ ] No validation of redirect target
|
| 26 |
+
- [ ] External redirects allowed without warning
|
| 27 |
+
- [ ] OAuth return_uri not validated
|
| 28 |
+
|
| 29 |
+
## Bad Patterns
|
| 30 |
+
|
| 31 |
+
```typescript
|
| 32 |
+
// Bad: Unvalidated redirect
|
| 33 |
+
async function callback(req: Request): Promise<Response> {
|
| 34 |
+
let url = new URL(req.url);
|
| 35 |
+
let returnUrl = url.searchParams.get("return");
|
| 36 |
+
|
| 37 |
+
// Attacker can set return=https://evil.com
|
| 38 |
+
return Response.redirect(returnUrl!);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Bad: No validation on OAuth callback
|
| 42 |
+
async function oauthCallback(req: Request): Promise<Response> {
|
| 43 |
+
let url = new URL(req.url);
|
| 44 |
+
let redirectUri = url.searchParams.get("redirect_uri");
|
| 45 |
+
|
| 46 |
+
// Complete OAuth flow...
|
| 47 |
+
|
| 48 |
+
return Response.redirect(redirectUri!);
|
| 49 |
+
}
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
## Good Patterns
|
| 53 |
+
|
| 54 |
+
```typescript
|
| 55 |
+
// Good: Validate against allowlist
|
| 56 |
+
const ALLOWED_REDIRECTS = ["/dashboard", "/profile", "/settings"];
|
| 57 |
+
|
| 58 |
+
async function callback(req: Request): Promise<Response> {
|
| 59 |
+
let url = new URL(req.url);
|
| 60 |
+
let returnUrl = url.searchParams.get("return") || "/";
|
| 61 |
+
|
| 62 |
+
if (!ALLOWED_REDIRECTS.includes(returnUrl)) {
|
| 63 |
+
return Response.redirect("/");
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return Response.redirect(returnUrl);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
// Good: Validate URL is relative
|
| 70 |
+
function isValidRedirect(url: string): boolean {
|
| 71 |
+
return url.startsWith("/") && !url.startsWith("//");
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
async function callback(req: Request): Promise<Response> {
|
| 75 |
+
let url = new URL(req.url);
|
| 76 |
+
let returnUrl = url.searchParams.get("return") || "/";
|
| 77 |
+
|
| 78 |
+
if (!isValidRedirect(returnUrl)) {
|
| 79 |
+
return Response.redirect("/");
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
return Response.redirect(returnUrl);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Good: Validate OAuth redirect_uri
|
| 86 |
+
const ALLOWED_OAUTH_REDIRECTS = [
|
| 87 |
+
"https://app.example.com/callback",
|
| 88 |
+
"https://admin.example.com/callback",
|
| 89 |
+
];
|
| 90 |
+
|
| 91 |
+
async function oauthCallback(req: Request): Promise<Response> {
|
| 92 |
+
let url = new URL(req.url);
|
| 93 |
+
let redirectUri = url.searchParams.get("redirect_uri");
|
| 94 |
+
|
| 95 |
+
if (!redirectUri || !ALLOWED_OAUTH_REDIRECTS.includes(redirectUri)) {
|
| 96 |
+
return new Response("Invalid redirect_uri", { status: 400 });
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Complete OAuth flow...
|
| 100 |
+
return Response.redirect(redirectUri);
|
| 101 |
+
}
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## Rules
|
| 105 |
+
|
| 106 |
+
1. **Validate redirect URLs** - Use allowlist
|
| 107 |
+
2. **Only allow relative URLs** - Starts with / not //
|
| 108 |
+
3. **Never trust user input** - For redirect targets
|
| 109 |
+
4. **Validate OAuth redirects** - Pre-registered URIs only
|
| 110 |
+
5. **Default to safe redirect** - Home page if invalid
|
.agents/skills/owasp-security-check/rules/secrets-management.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Secrets Management
|
| 3 |
+
impact: CRITICAL
|
| 4 |
+
tags: [secrets, api-keys, environment-variables, credentials]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Secrets Management
|
| 8 |
+
|
| 9 |
+
Check for hardcoded secrets, exposed API keys, and improper credential management.
|
| 10 |
+
|
| 11 |
+
> **Related:** Encryption key management in [cryptographic-failures.md](cryptographic-failures.md). Sensitive data exposure in [sensitive-data-exposure.md](sensitive-data-exposure.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Credential exposure**: API keys in code can be stolen
|
| 16 |
+
- **Repository leaks**: Committed secrets in Git history
|
| 17 |
+
- **Unauthorized access**: Exposed keys grant system access
|
| 18 |
+
- **Compliance violations**: Regulations require secret protection
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
- [ ] Hardcoded API keys, passwords, tokens in code
|
| 23 |
+
- [ ] Secrets committed to version control
|
| 24 |
+
- [ ] .env files committed to repository
|
| 25 |
+
- [ ] API keys in client-side code
|
| 26 |
+
- [ ] Secrets in logs or error messages
|
| 27 |
+
- [ ] No secret rotation policy
|
| 28 |
+
|
| 29 |
+
## Bad Patterns
|
| 30 |
+
|
| 31 |
+
```typescript
|
| 32 |
+
// Bad: Hardcoded API key
|
| 33 |
+
const STRIPE_SECRET_KEY = "sk_live_51H..."; // VULNERABLE!
|
| 34 |
+
|
| 35 |
+
// Bad: Hardcoded database password
|
| 36 |
+
const db = createConnection({
|
| 37 |
+
host: "localhost",
|
| 38 |
+
user: "admin",
|
| 39 |
+
password: "SuperSecret123!" // VULNERABLE!
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
// Bad: Secret in client-side code
|
| 43 |
+
const config = {
|
| 44 |
+
apiKey: "AIzaSyB..." // VULNERABLE: Exposed in browser
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
// Bad: .env file committed to Git
|
| 48 |
+
// .env (in repository) - VULNERABLE!
|
| 49 |
+
DATABASE_URL=postgresql://user:password@localhost/db
|
| 50 |
+
API_SECRET=my-secret-key
|
| 51 |
+
|
| 52 |
+
// Bad: Logging secrets
|
| 53 |
+
console.log("Connecting with API key:", process.env.API_KEY);
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Good Patterns
|
| 57 |
+
|
| 58 |
+
```typescript
|
| 59 |
+
// Good: Use environment variables
|
| 60 |
+
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
|
| 61 |
+
|
| 62 |
+
if (!STRIPE_SECRET_KEY) {
|
| 63 |
+
throw new Error("STRIPE_SECRET_KEY not set");
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
// Good: Validate env vars at startup
|
| 67 |
+
function validateEnv() {
|
| 68 |
+
let required = ["DATABASE_URL", "JWT_SECRET", "STRIPE_SECRET_KEY"];
|
| 69 |
+
let missing = required.filter((key) => !process.env[key]);
|
| 70 |
+
if (missing.length > 0) {
|
| 71 |
+
throw new Error(`Missing env vars: ${missing.join(", ")}`);
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
// Good: Add .env to .gitignore (never commit secrets)
|
| 76 |
+
// Good: Provide .env.example for documentation (safe to commit)
|
| 77 |
+
|
| 78 |
+
// Good: Secret rotation
|
| 79 |
+
async function rotateApiKey(userId: string) {
|
| 80 |
+
let newKey = crypto.randomBytes(32).toString("hex");
|
| 81 |
+
await db.apiKeys.create({
|
| 82 |
+
data: {
|
| 83 |
+
userId,
|
| 84 |
+
key: newKey,
|
| 85 |
+
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
|
| 86 |
+
},
|
| 87 |
+
});
|
| 88 |
+
return newKey;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Good: Use secret management service
|
| 92 |
+
async function getSecret(name: string): Promise<string> {
|
| 93 |
+
if (process.env.NODE_ENV === "production") {
|
| 94 |
+
return await secretsManager.getSecretValue(name);
|
| 95 |
+
}
|
| 96 |
+
let value = process.env[name];
|
| 97 |
+
if (!value) throw new Error(`Secret ${name} not found`);
|
| 98 |
+
return value;
|
| 99 |
+
}
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
## Rules
|
| 103 |
+
|
| 104 |
+
1. **Never hardcode secrets** - Use environment variables or secret managers
|
| 105 |
+
2. **Add .env to .gitignore** - Never commit secret files
|
| 106 |
+
3. **Rotate secrets regularly** - Implement expiration and rotation
|
| 107 |
+
4. **Validate env vars at startup** - Fail fast if secrets missing
|
| 108 |
+
5. **Don't log secrets** - Sanitize logs to remove sensitive values
|
| 109 |
+
6. **No secrets in client code** - Keep API keys server-side only
|
| 110 |
+
7. **Use secret management services** - For production (AWS Secrets Manager, Vault, etc.)
|
| 111 |
+
8. **Scan Git history** - Use tools to find accidentally committed secrets
|
.agents/skills/owasp-security-check/rules/security-headers.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Security Headers
|
| 3 |
+
impact: HIGH
|
| 4 |
+
tags: [headers, csp, hsts, xss, clickjacking, owasp]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Security Headers
|
| 8 |
+
|
| 9 |
+
Check for proper HTTP security headers that protect against XSS, clickjacking, MIME sniffing, and downgrade attacks.
|
| 10 |
+
|
| 11 |
+
> **Related:** XSS input validation in [injection-attacks.md](injection-attacks.md). CORS in [cors-configuration.md](cors-configuration.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **XSS protection**: CSP prevents script injection
|
| 16 |
+
- **Clickjacking prevention**: X-Frame-Options stops iframe embedding
|
| 17 |
+
- **HTTPS enforcement**: HSTS ensures encrypted connections
|
| 18 |
+
- **MIME sniffing attacks**: X-Content-Type-Options prevents content confusion
|
| 19 |
+
- **Information leakage**: Referrer-Policy controls referrer data
|
| 20 |
+
|
| 21 |
+
## What to Check
|
| 22 |
+
|
| 23 |
+
- [ ] Missing Content-Security-Policy header
|
| 24 |
+
- [ ] Missing Strict-Transport-Security (HSTS)
|
| 25 |
+
- [ ] Missing X-Frame-Options
|
| 26 |
+
- [ ] Missing X-Content-Type-Options
|
| 27 |
+
- [ ] Overly permissive CSP (`unsafe-inline`, `unsafe-eval`)
|
| 28 |
+
- [ ] No Permissions-Policy
|
| 29 |
+
- [ ] Missing Referrer-Policy
|
| 30 |
+
|
| 31 |
+
## Bad Patterns
|
| 32 |
+
|
| 33 |
+
```typescript
|
| 34 |
+
// Bad: No security headers
|
| 35 |
+
async function handler(req: Request): Promise<Response> {
|
| 36 |
+
let html = "<html><body>Hello</body></html>";
|
| 37 |
+
|
| 38 |
+
// VULNERABLE: Missing all security headers
|
| 39 |
+
return new Response(html, {
|
| 40 |
+
headers: { "Content-Type": "text/html" },
|
| 41 |
+
});
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Bad: Permissive CSP
|
| 45 |
+
const headers = {
|
| 46 |
+
// VULNERABLE: unsafe-inline allows XSS
|
| 47 |
+
"Content-Security-Policy": "default-src * 'unsafe-inline' 'unsafe-eval'",
|
| 48 |
+
};
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
## Good Patterns
|
| 52 |
+
|
| 53 |
+
```typescript
|
| 54 |
+
// Good: Comprehensive security headers
|
| 55 |
+
function getSecurityHeaders(): Record<string, string> {
|
| 56 |
+
return {
|
| 57 |
+
"Content-Security-Policy": [
|
| 58 |
+
"default-src 'self'",
|
| 59 |
+
"script-src 'self'",
|
| 60 |
+
"style-src 'self' 'unsafe-inline'",
|
| 61 |
+
"img-src 'self' data: https:",
|
| 62 |
+
"font-src 'self'",
|
| 63 |
+
"connect-src 'self'",
|
| 64 |
+
"frame-ancestors 'none'",
|
| 65 |
+
"base-uri 'self'",
|
| 66 |
+
"form-action 'self'",
|
| 67 |
+
].join("; "),
|
| 68 |
+
"X-Frame-Options": "DENY",
|
| 69 |
+
"Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
| 70 |
+
"X-Content-Type-Options": "nosniff",
|
| 71 |
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
| 72 |
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
| 73 |
+
};
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
async function handler(req: Request): Promise<Response> {
|
| 77 |
+
let html = "<html><body>Hello</body></html>";
|
| 78 |
+
|
| 79 |
+
return new Response(html, {
|
| 80 |
+
headers: {
|
| 81 |
+
"Content-Type": "text/html",
|
| 82 |
+
...getSecurityHeaders(),
|
| 83 |
+
},
|
| 84 |
+
});
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Good: CSP with nonces for inline scripts
|
| 88 |
+
async function renderPage(req: Request): Promise<Response> {
|
| 89 |
+
let nonce = crypto.randomBytes(16).toString("base64");
|
| 90 |
+
|
| 91 |
+
let html = `
|
| 92 |
+
<!DOCTYPE html>
|
| 93 |
+
<html>
|
| 94 |
+
<head>
|
| 95 |
+
<script nonce="${nonce}">
|
| 96 |
+
console.log('This script is allowed');
|
| 97 |
+
</script>
|
| 98 |
+
</head>
|
| 99 |
+
<body>Content</body>
|
| 100 |
+
</html>
|
| 101 |
+
`;
|
| 102 |
+
|
| 103 |
+
return new Response(html, {
|
| 104 |
+
headers: {
|
| 105 |
+
"Content-Type": "text/html",
|
| 106 |
+
"Content-Security-Policy": `default-src 'self'; script-src 'self' 'nonce-${nonce}'`,
|
| 107 |
+
},
|
| 108 |
+
});
|
| 109 |
+
}
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
## Rules
|
| 113 |
+
|
| 114 |
+
1. **Always set CSP** - Strict policy without `unsafe-inline`/`unsafe-eval`
|
| 115 |
+
2. **Enable HSTS** - Minimum 1 year, include subdomains
|
| 116 |
+
3. **Set X-Frame-Options** - Use `DENY` or `SAMEORIGIN`
|
| 117 |
+
4. **Set X-Content-Type-Options** - Always `nosniff`
|
| 118 |
+
5. **Configure Referrer-Policy** - `strict-origin-when-cross-origin`
|
| 119 |
+
6. **Use nonces for inline scripts** - When inline scripts are needed
|
| 120 |
+
7. **Set Permissions-Policy** - Restrict unnecessary browser features
|
.agents/skills/owasp-security-check/rules/security-misconfiguration.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Security Misconfiguration
|
| 3 |
+
impact: HIGH
|
| 4 |
+
tags: [configuration, defaults, error-handling, owasp-a05]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Security Misconfiguration
|
| 8 |
+
|
| 9 |
+
Check for insecure default configurations, unnecessary features enabled, verbose error messages, and missing security patches.
|
| 10 |
+
|
| 11 |
+
## Why
|
| 12 |
+
|
| 13 |
+
- **Information disclosure**: Verbose errors reveal system details
|
| 14 |
+
- **Unauthorized access**: Default credentials still active
|
| 15 |
+
- **Attack surface**: Unnecessary features expose vulnerabilities
|
| 16 |
+
- **Known vulnerabilities**: Outdated software with public exploits
|
| 17 |
+
|
| 18 |
+
## What to Check
|
| 19 |
+
|
| 20 |
+
**Vulnerability Indicators:**
|
| 21 |
+
|
| 22 |
+
- [ ] Debug mode enabled in production
|
| 23 |
+
- [ ] Default credentials not changed
|
| 24 |
+
- [ ] Unnecessary features/endpoints enabled
|
| 25 |
+
- [ ] Detailed error messages in production
|
| 26 |
+
- [ ] Directory listing enabled
|
| 27 |
+
- [ ] Outdated dependencies
|
| 28 |
+
- [ ] Missing security patches
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: Debug mode in production
|
| 34 |
+
const DEBUG = true; // Should be from env
|
| 35 |
+
if (DEBUG) {
|
| 36 |
+
console.log("Detailed system info:", process.env);
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
// Bad: Verbose error messages
|
| 40 |
+
catch (error) {
|
| 41 |
+
return Response.json({
|
| 42 |
+
error: error.message,
|
| 43 |
+
stack: error.stack,
|
| 44 |
+
query: sqlQuery,
|
| 45 |
+
env: process.env
|
| 46 |
+
}, { status: 500 });
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Bad: Default credentials
|
| 50 |
+
const ADMIN_PASSWORD = "admin123";
|
| 51 |
+
|
| 52 |
+
// Bad: Unnecessary admin endpoints exposed
|
| 53 |
+
async function debugInfo(req: Request): Promise<Response> {
|
| 54 |
+
return Response.json({
|
| 55 |
+
env: process.env,
|
| 56 |
+
config: appConfig,
|
| 57 |
+
routes: allRoutes
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
## Good Patterns
|
| 63 |
+
|
| 64 |
+
```typescript
|
| 65 |
+
// Good: Environment-aware configuration
|
| 66 |
+
const isProduction = process.env.NODE_ENV === "production";
|
| 67 |
+
|
| 68 |
+
const config = {
|
| 69 |
+
debug: !isProduction,
|
| 70 |
+
logLevel: isProduction ? "error" : "debug",
|
| 71 |
+
errorDetails: !isProduction
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
// Good: Generic error messages in production
|
| 75 |
+
catch (error) {
|
| 76 |
+
console.error("Error:", error);
|
| 77 |
+
|
| 78 |
+
let message = isProduction
|
| 79 |
+
? "An error occurred"
|
| 80 |
+
: error.message;
|
| 81 |
+
|
| 82 |
+
return Response.json({ error: message }, { status: 500 });
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Good: Strong credentials from environment
|
| 86 |
+
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
| 87 |
+
if (!ADMIN_PASSWORD || ADMIN_PASSWORD.length < 20) {
|
| 88 |
+
throw new Error("ADMIN_PASSWORD must be set and strong");
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Good: Disable debug endpoints in production
|
| 92 |
+
async function debugInfo(req: Request): Promise<Response> {
|
| 93 |
+
if (process.env.NODE_ENV === "production") {
|
| 94 |
+
return new Response("Not found", { status: 404 });
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
return Response.json({ routes: publicRoutes });
|
| 98 |
+
}
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Rules
|
| 102 |
+
|
| 103 |
+
1. **Disable debug mode in production** - No verbose logging or errors
|
| 104 |
+
2. **Change default credentials** - Require strong passwords
|
| 105 |
+
3. **Disable unnecessary features** - Minimize attack surface
|
| 106 |
+
4. **Generic error messages** - Don't reveal system details
|
| 107 |
+
5. **Keep dependencies updated** - Regularly patch vulnerabilities
|
| 108 |
+
6. **Remove development endpoints** - No debug/admin routes in production
|
| 109 |
+
7. **Secure default configurations** - Fail securely by default
|
| 110 |
+
8. **Regular security audits** - npm audit, dependency checks
|
.agents/skills/owasp-security-check/rules/sensitive-data-exposure.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Sensitive Data Exposure
|
| 3 |
+
impact: CRITICAL
|
| 4 |
+
tags: [data-exposure, pii, privacy, information-disclosure, owasp]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Sensitive Data Exposure
|
| 8 |
+
|
| 9 |
+
Check for PII, credentials, and sensitive data exposed in API responses, error messages, logs, or client-side code.
|
| 10 |
+
|
| 11 |
+
> **Related:** Encryption in [cryptographic-failures.md](cryptographic-failures.md). Secrets in [secrets-management.md](secrets-management.md). Logging in [logging-monitoring.md](logging-monitoring.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Privacy violation**: Exposes users' personal information
|
| 16 |
+
- **Compliance risk**: GDPR, CCPA, HIPAA violations
|
| 17 |
+
- **Identity theft**: PII enables fraud and impersonation
|
| 18 |
+
- **Credential theft**: Exposed secrets enable account takeover
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
- [ ] Password hashes returned in API responses
|
| 23 |
+
- [ ] Email, phone, SSN in public endpoints
|
| 24 |
+
- [ ] Error messages revealing stack traces or database info
|
| 25 |
+
- [ ] Debug information in production
|
| 26 |
+
- [ ] API keys, tokens in client-side code
|
| 27 |
+
- [ ] Excessive data in responses (return only what's needed)
|
| 28 |
+
- [ ] Sensitive data logged to console or files
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: Returning all user fields including sensitive data
|
| 34 |
+
async function getUser(req: Request): Promise<Response> {
|
| 35 |
+
let user = await db.users.findUnique({ where: { id } });
|
| 36 |
+
// Returns password hash, email, tokens, etc.
|
| 37 |
+
return Response.json(user);
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Bad: Logging sensitive data
|
| 41 |
+
console.log("User login:", { email, password, creditCard });
|
| 42 |
+
|
| 43 |
+
// Bad: Exposing internal IDs
|
| 44 |
+
return Response.json({
|
| 45 |
+
internalUserId: user.id,
|
| 46 |
+
databaseId: user.dbId,
|
| 47 |
+
});
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
## Good Patterns
|
| 51 |
+
|
| 52 |
+
```typescript
|
| 53 |
+
// Good: Explicit field selection
|
| 54 |
+
async function getUser(req: Request): Promise<Response> {
|
| 55 |
+
let session = await getSession(req);
|
| 56 |
+
|
| 57 |
+
let user = await db.users.findUnique({
|
| 58 |
+
where: { id: session.userId },
|
| 59 |
+
select: {
|
| 60 |
+
id: true,
|
| 61 |
+
name: true,
|
| 62 |
+
avatar: true,
|
| 63 |
+
createdAt: true,
|
| 64 |
+
// Excludes: password, email, tokens, etc.
|
| 65 |
+
},
|
| 66 |
+
});
|
| 67 |
+
|
| 68 |
+
return Response.json(user);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// Good: DTO for public profiles
|
| 72 |
+
async function getUserProfile(req: Request): Promise<Response> {
|
| 73 |
+
let url = new URL(req.url);
|
| 74 |
+
let userId = url.searchParams.get("id");
|
| 75 |
+
|
| 76 |
+
let user = await db.users.findUnique({
|
| 77 |
+
where: { id: userId },
|
| 78 |
+
select: { id: true, name: true, avatar: true, bio: true },
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
return Response.json(user);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// Good: Conditional field exposure
|
| 85 |
+
async function getUserProfile(req: Request): Promise<Response> {
|
| 86 |
+
let session = await getSession(req);
|
| 87 |
+
let url = new URL(req.url);
|
| 88 |
+
let userId = url.searchParams.get("id");
|
| 89 |
+
let isOwn = session?.userId === userId;
|
| 90 |
+
|
| 91 |
+
let user = await db.users.findUnique({
|
| 92 |
+
where: { id: userId },
|
| 93 |
+
select: {
|
| 94 |
+
id: true,
|
| 95 |
+
name: true,
|
| 96 |
+
avatar: true,
|
| 97 |
+
bio: true,
|
| 98 |
+
email: isOwn,
|
| 99 |
+
emailVerified: isOwn,
|
| 100 |
+
},
|
| 101 |
+
});
|
| 102 |
+
|
| 103 |
+
return Response.json(user);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// Good: Sanitize logs
|
| 107 |
+
function sanitizeForLogging(obj: any): any {
|
| 108 |
+
let sensitive = ["password", "token", "secret", "apiKey", "creditCard"];
|
| 109 |
+
let sanitized = { ...obj };
|
| 110 |
+
|
| 111 |
+
for (const key of Object.keys(sanitized)) {
|
| 112 |
+
if (sensitive.some((s) => key.toLowerCase().includes(s))) {
|
| 113 |
+
sanitized[key] = "[REDACTED]";
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
return sanitized;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
console.log("Login attempt:", sanitizeForLogging({ email, password }));
|
| 121 |
+
// Output: { email: "user@example.com", password: "[REDACTED]" }
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
## Rules
|
| 125 |
+
|
| 126 |
+
1. **Never return password hashes** - Even hashed, they can be cracked
|
| 127 |
+
2. **Use explicit field selection** - Don't return entire database records
|
| 128 |
+
3. **Create DTOs for responses** - Define exactly what fields are public
|
| 129 |
+
4. **Generic error messages** - Don't expose system details to users
|
| 130 |
+
5. **Log full errors server-side** - Return generic messages to clients
|
| 131 |
+
6. **Sanitize logs** - Redact passwords, tokens, PII before logging
|
| 132 |
+
7. **Different views for different users** - Own profile vs others' profiles
|
| 133 |
+
8. **Disable debug in production** - No verbose errors or stack traces
|
.agents/skills/owasp-security-check/rules/session-security.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Session Security
|
| 3 |
+
impact: HIGH
|
| 4 |
+
tags: [sessions, cookies, jwt, tokens]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Session Security
|
| 8 |
+
|
| 9 |
+
Check for secure session management including cookie flags, token storage, and session lifecycle.
|
| 10 |
+
|
| 11 |
+
> **Related:** Authentication is covered in [authentication-failures.md](authentication-failures.md). CSRF protection is covered in [csrf-protection.md](csrf-protection.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Session hijacking**: Attackers steal session tokens
|
| 16 |
+
- **Session fixation**: Attackers set known session ID
|
| 17 |
+
- **XSS token theft**: JavaScript access to tokens
|
| 18 |
+
- **CSRF attacks**: Missing cookie protection
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
**Vulnerability Indicators:**
|
| 23 |
+
|
| 24 |
+
- [ ] Cookies missing HttpOnly flag
|
| 25 |
+
- [ ] Cookies missing Secure flag
|
| 26 |
+
- [ ] Cookies missing SameSite attribute
|
| 27 |
+
- [ ] JWT stored in localStorage
|
| 28 |
+
- [ ] Sessions never expire
|
| 29 |
+
- [ ] Session not regenerated after login
|
| 30 |
+
- [ ] Predictable session IDs
|
| 31 |
+
|
| 32 |
+
## Bad Patterns
|
| 33 |
+
|
| 34 |
+
```typescript
|
| 35 |
+
// Bad: No security flags on cookie
|
| 36 |
+
return new Response("OK", {
|
| 37 |
+
headers: { "Set-Cookie": `session=${sessionId}` },
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
// Bad: Session never expires
|
| 41 |
+
await db.session.create({
|
| 42 |
+
data: { id: sessionId, userId }, // No expiresAt!
|
| 43 |
+
});
|
| 44 |
+
|
| 45 |
+
// Bad: Predictable session ID
|
| 46 |
+
const sessionId = `${Date.now()}-${Math.random()}`;
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Good Patterns
|
| 50 |
+
|
| 51 |
+
```typescript
|
| 52 |
+
// Good: Secure cookie with all flags
|
| 53 |
+
async function createSession(userId: string): Promise<Response> {
|
| 54 |
+
let sessionId = crypto.randomBytes(32).toString("hex");
|
| 55 |
+
|
| 56 |
+
await db.session.create({
|
| 57 |
+
data: {
|
| 58 |
+
id: sessionId,
|
| 59 |
+
userId,
|
| 60 |
+
expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour
|
| 61 |
+
createdAt: new Date(),
|
| 62 |
+
},
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
return new Response("OK", {
|
| 66 |
+
headers: {
|
| 67 |
+
"Set-Cookie": [
|
| 68 |
+
`session=${sessionId}`,
|
| 69 |
+
"HttpOnly",
|
| 70 |
+
"Secure",
|
| 71 |
+
"SameSite=Strict",
|
| 72 |
+
"Path=/",
|
| 73 |
+
"Max-Age=3600",
|
| 74 |
+
].join("; "),
|
| 75 |
+
},
|
| 76 |
+
});
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// Good: Session validation with expiry
|
| 80 |
+
async function validateSession(req: Request): Promise<string | null> {
|
| 81 |
+
let sessionId = getCookie(req, "session");
|
| 82 |
+
if (!sessionId) return null;
|
| 83 |
+
|
| 84 |
+
let session = await db.session.findUnique({ where: { id: sessionId } });
|
| 85 |
+
if (!session || session.expiresAt < new Date()) {
|
| 86 |
+
if (session) await db.session.delete({ where: { id: sessionId } });
|
| 87 |
+
return null;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
// Extend session (sliding expiration)
|
| 91 |
+
await db.session.update({
|
| 92 |
+
where: { id: sessionId },
|
| 93 |
+
data: { expiresAt: new Date(Date.now() + 60 * 60 * 1000) },
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
return session.userId;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Good: Logout invalidates session
|
| 100 |
+
async function logout(req: Request): Promise<Response> {
|
| 101 |
+
let sessionId = getCookie(req, "session");
|
| 102 |
+
if (sessionId) await db.session.delete({ where: { id: sessionId } });
|
| 103 |
+
|
| 104 |
+
return new Response("OK", {
|
| 105 |
+
headers: { "Set-Cookie": "session=; Max-Age=0; Path=/" },
|
| 106 |
+
});
|
| 107 |
+
}
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
## Rules
|
| 111 |
+
|
| 112 |
+
1. **Set HttpOnly flag** - Prevent XSS token theft
|
| 113 |
+
2. **Set Secure flag** - HTTPS only
|
| 114 |
+
3. **Set SameSite=Strict** - CSRF protection
|
| 115 |
+
4. **Use cryptographically random IDs** - crypto.randomBytes
|
| 116 |
+
5. **Set expiration** - Both absolute and idle timeout
|
| 117 |
+
6. **Regenerate on login** - Prevent session fixation
|
| 118 |
+
7. **Don't store in localStorage** - Use HttpOnly cookies
|
| 119 |
+
8. **Validate on every request** - Check expiry and validity
|
.agents/skills/owasp-security-check/rules/ssrf-attacks.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Server-Side Request Forgery (SSRF)
|
| 3 |
+
impact: CRITICAL
|
| 4 |
+
tags: [ssrf, url-validation, owasp-a10]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Server-Side Request Forgery (SSRF)
|
| 8 |
+
|
| 9 |
+
Check for unvalidated URLs that allow attackers to make requests to internal services or arbitrary external URLs.
|
| 10 |
+
|
| 11 |
+
> **Related:** URL validation in redirects is covered in [redirect-validation.md](redirect-validation.md).
|
| 12 |
+
|
| 13 |
+
## Why
|
| 14 |
+
|
| 15 |
+
- **Internal network access**: Attackers reach internal services
|
| 16 |
+
- **Cloud metadata exposure**: Access to AWS/GCP metadata endpoints
|
| 17 |
+
- **Port scanning**: Map internal network
|
| 18 |
+
- **Bypass firewall**: Access protected resources
|
| 19 |
+
|
| 20 |
+
## What to Check
|
| 21 |
+
|
| 22 |
+
**Vulnerability Indicators:**
|
| 23 |
+
|
| 24 |
+
- [ ] User-provided URLs passed to fetch/axios without validation
|
| 25 |
+
- [ ] No allowlist for allowed domains
|
| 26 |
+
- [ ] Missing checks for internal IP ranges
|
| 27 |
+
- [ ] Webhook URLs not validated
|
| 28 |
+
- [ ] URL redirects followed automatically
|
| 29 |
+
|
| 30 |
+
## Bad Patterns
|
| 31 |
+
|
| 32 |
+
```typescript
|
| 33 |
+
// Bad: Fetching user-provided URL
|
| 34 |
+
async function fetchUrl(req: Request): Promise<Response> {
|
| 35 |
+
let { url } = await req.json();
|
| 36 |
+
|
| 37 |
+
// SSRF: Can access internal services!
|
| 38 |
+
let response = await fetch(url);
|
| 39 |
+
let data = await response.text();
|
| 40 |
+
|
| 41 |
+
return new Response(data);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// Bad: No validation on webhook URL
|
| 45 |
+
async function registerWebhook(req: Request): Promise<Response> {
|
| 46 |
+
let { webhookUrl } = await req.json();
|
| 47 |
+
|
| 48 |
+
await db.webhook.create({
|
| 49 |
+
data: { url: webhookUrl },
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
// Later: fetch(webhookUrl) - could be internal
|
| 53 |
+
}
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Good Patterns
|
| 57 |
+
|
| 58 |
+
```typescript
|
| 59 |
+
// Good: Validate against allowlist
|
| 60 |
+
const ALLOWED_DOMAINS = ["api.example.com", "cdn.example.com"];
|
| 61 |
+
|
| 62 |
+
async function fetchUrl(req: Request): Promise<Response> {
|
| 63 |
+
let { url } = await req.json();
|
| 64 |
+
let parsedUrl = new URL(url);
|
| 65 |
+
|
| 66 |
+
if (parsedUrl.protocol !== "https:") {
|
| 67 |
+
return new Response("Only HTTPS allowed", { status: 400 });
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
if (!ALLOWED_DOMAINS.includes(parsedUrl.hostname)) {
|
| 71 |
+
return new Response("Domain not allowed", { status: 400 });
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
if (isInternalIP(parsedUrl.hostname)) {
|
| 75 |
+
return new Response("Internal IPs not allowed", { status: 400 });
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
let response = await fetch(url, { redirect: "manual" });
|
| 79 |
+
return new Response(await response.text());
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
function isInternalIP(hostname: string): boolean {
|
| 83 |
+
return [
|
| 84 |
+
/^127\./,
|
| 85 |
+
/^10\./,
|
| 86 |
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
| 87 |
+
/^192\.168\./,
|
| 88 |
+
/^169\.254\./,
|
| 89 |
+
/^localhost$/i,
|
| 90 |
+
].some((range) => range.test(hostname));
|
| 91 |
+
}
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
## Rules
|
| 95 |
+
|
| 96 |
+
1. **Validate URLs against allowlist** - Never trust user URLs
|
| 97 |
+
2. **Block internal IP ranges** - 127.0.0.1, 10.x, 192.168.x, etc.
|
| 98 |
+
3. **Enforce HTTPS** - No HTTP or other protocols
|
| 99 |
+
4. **Disable redirects** - Or validate redirect targets
|
| 100 |
+
5. **Block cloud metadata** - 169.254.169.254 (AWS/GCP/Azure)
|
.agents/skills/owasp-security-check/rules/vulnerable-dependencies.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Vulnerable and Outdated Dependencies
|
| 3 |
+
impact: MEDIUM
|
| 4 |
+
tags: [dependencies, supply-chain, owasp-a06]
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# Vulnerable and Outdated Dependencies
|
| 8 |
+
|
| 9 |
+
Check for outdated packages with known security vulnerabilities and supply chain risks.
|
| 10 |
+
|
| 11 |
+
## Why
|
| 12 |
+
|
| 13 |
+
- **Known exploits**: Public CVEs make attacks easy
|
| 14 |
+
- **Supply chain attacks**: Compromised packages
|
| 15 |
+
- **Transitive dependencies**: Vulnerabilities deep in dependency tree
|
| 16 |
+
- **Maintenance risk**: Unmaintained packages won't get patches
|
| 17 |
+
|
| 18 |
+
## What to Check
|
| 19 |
+
|
| 20 |
+
- [ ] Dependencies with known CVEs or security advisories
|
| 21 |
+
- [ ] Severely outdated packages (major versions behind current)
|
| 22 |
+
- [ ] Packages without recent updates (abandoned/unmaintained)
|
| 23 |
+
- [ ] Missing dependency lockfiles
|
| 24 |
+
- [ ] Wildcard or loose version constraints in production
|
| 25 |
+
- [ ] Unused dependencies bloating the project
|
| 26 |
+
- [ ] Development dependencies bundled in production builds
|
| 27 |
+
- [ ] Transitive vulnerabilities in indirect dependencies
|
| 28 |
+
|
| 29 |
+
## Bad Patterns
|
| 30 |
+
|
| 31 |
+
```typescript
|
| 32 |
+
// Bad: Wildcard versions allow unexpected updates
|
| 33 |
+
// package.json
|
| 34 |
+
{
|
| 35 |
+
"dependencies": {
|
| 36 |
+
"express": "*", // Any version can be installed
|
| 37 |
+
"react": "^18.0.0" // Minor/patch versions can change
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Bad: No lockfile means versions drift between installs
|
| 42 |
+
// Missing: package-lock.json, yarn.lock, pnpm-lock.yaml, etc.
|
| 43 |
+
|
| 44 |
+
// Bad: Dev dependencies mixed with production
|
| 45 |
+
{
|
| 46 |
+
"dependencies": {
|
| 47 |
+
"express": "4.18.2",
|
| 48 |
+
"jest": "29.5.0", // Should be devDependency
|
| 49 |
+
"eslint": "8.40.0" // Should be devDependency
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
## Good Patterns
|
| 55 |
+
|
| 56 |
+
````typescript
|
| 57 |
+
// Good: Pinned versions with lockfile
|
| 58 |
+
{
|
| 59 |
+
"dependencies": {
|
| 60 |
+
"express": "4.18.2", // Exact version pinned
|
| 61 |
+
"react": "18.2.0"
|
| 62 |
+
},
|
| 63 |
+
"devDependencies": {
|
| 64 |
+
"jest": "29.5.0",
|
| 65 |
+
"eslint": "8.40.0"
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
// Plus: Lockfile committed (package-lock.json, yarn.lock, etc.)
|
| 69 |
+
|
| 70 |
+
// Good: Regular dependency audits in CI/CD
|
| 71 |
+
// .github/workflows/security.yml
|
| 72 |
+
```yaml
|
| 73 |
+
name: Security Audit
|
| 74 |
+
on: [push, pull_request]
|
| 75 |
+
jobs:
|
| 76 |
+
audit:
|
| 77 |
+
runs-on: ubuntu-latest
|
| 78 |
+
steps:
|
| 79 |
+
- uses: actions/checkout@v3
|
| 80 |
+
- run: npm audit --production # Or: pip-audit, bundle audit, etc.
|
| 81 |
+
````
|
| 82 |
+
|
| 83 |
+
**Before installing new packages:**
|
| 84 |
+
|
| 85 |
+
- Check package age and download stats
|
| 86 |
+
- Review maintainer history
|
| 87 |
+
- Scan for known vulnerabilities
|
| 88 |
+
- Verify package scope matches intent (avoid typosquatting)
|
| 89 |
+
|
| 90 |
+
## Rules
|
| 91 |
+
|
| 92 |
+
1. **Always use lockfiles** - Commit dependency lockfiles for reproducible builds
|
| 93 |
+
2. **Pin production versions** - Use exact versions for production dependencies
|
| 94 |
+
3. **Audit regularly** - Run security audits in CI/CD and before deployments
|
| 95 |
+
4. **Keep dependencies updated** - Use automated update tools
|
| 96 |
+
5. **Separate dev dependencies** - Keep development tools separate from production
|
| 97 |
+
6. **Remove unused packages** - Regularly clean up unused dependencies
|
| 98 |
+
7. **Review before adding** - Check package age, maintainers, and reputation
|
| 99 |
+
8. **Monitor advisories** - Subscribe to security advisories for critical dependencies
|
.agents/skills/python-testing-patterns/SKILL.md
ADDED
|
@@ -0,0 +1,1050 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: python-testing-patterns
|
| 3 |
+
description: Implement comprehensive testing strategies with pytest, fixtures, mocking, and test-driven development. Use when writing Python tests, setting up test suites, or implementing testing best practices.
|
| 4 |
+
---
|
| 5 |
+
|
| 6 |
+
# Python Testing Patterns
|
| 7 |
+
|
| 8 |
+
Comprehensive guide to implementing robust testing strategies in Python using pytest, fixtures, mocking, parameterization, and test-driven development practices.
|
| 9 |
+
|
| 10 |
+
## When to Use This Skill
|
| 11 |
+
|
| 12 |
+
- Writing unit tests for Python code
|
| 13 |
+
- Setting up test suites and test infrastructure
|
| 14 |
+
- Implementing test-driven development (TDD)
|
| 15 |
+
- Creating integration tests for APIs and services
|
| 16 |
+
- Mocking external dependencies and services
|
| 17 |
+
- Testing async code and concurrent operations
|
| 18 |
+
- Setting up continuous testing in CI/CD
|
| 19 |
+
- Implementing property-based testing
|
| 20 |
+
- Testing database operations
|
| 21 |
+
- Debugging failing tests
|
| 22 |
+
|
| 23 |
+
## Core Concepts
|
| 24 |
+
|
| 25 |
+
### 1. Test Types
|
| 26 |
+
|
| 27 |
+
- **Unit Tests**: Test individual functions/classes in isolation
|
| 28 |
+
- **Integration Tests**: Test interaction between components
|
| 29 |
+
- **Functional Tests**: Test complete features end-to-end
|
| 30 |
+
- **Performance Tests**: Measure speed and resource usage
|
| 31 |
+
|
| 32 |
+
### 2. Test Structure (AAA Pattern)
|
| 33 |
+
|
| 34 |
+
- **Arrange**: Set up test data and preconditions
|
| 35 |
+
- **Act**: Execute the code under test
|
| 36 |
+
- **Assert**: Verify the results
|
| 37 |
+
|
| 38 |
+
### 3. Test Coverage
|
| 39 |
+
|
| 40 |
+
- Measure what code is exercised by tests
|
| 41 |
+
- Identify untested code paths
|
| 42 |
+
- Aim for meaningful coverage, not just high percentages
|
| 43 |
+
|
| 44 |
+
### 4. Test Isolation
|
| 45 |
+
|
| 46 |
+
- Tests should be independent
|
| 47 |
+
- No shared state between tests
|
| 48 |
+
- Each test should clean up after itself
|
| 49 |
+
|
| 50 |
+
## Quick Start
|
| 51 |
+
|
| 52 |
+
```python
|
| 53 |
+
# test_example.py
|
| 54 |
+
def add(a, b):
|
| 55 |
+
return a + b
|
| 56 |
+
|
| 57 |
+
def test_add():
|
| 58 |
+
"""Basic test example."""
|
| 59 |
+
result = add(2, 3)
|
| 60 |
+
assert result == 5
|
| 61 |
+
|
| 62 |
+
def test_add_negative():
|
| 63 |
+
"""Test with negative numbers."""
|
| 64 |
+
assert add(-1, 1) == 0
|
| 65 |
+
|
| 66 |
+
# Run with: pytest test_example.py
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
## Fundamental Patterns
|
| 70 |
+
|
| 71 |
+
### Pattern 1: Basic pytest Tests
|
| 72 |
+
|
| 73 |
+
```python
|
| 74 |
+
# test_calculator.py
|
| 75 |
+
import pytest
|
| 76 |
+
|
| 77 |
+
class Calculator:
|
| 78 |
+
"""Simple calculator for testing."""
|
| 79 |
+
|
| 80 |
+
def add(self, a: float, b: float) -> float:
|
| 81 |
+
return a + b
|
| 82 |
+
|
| 83 |
+
def subtract(self, a: float, b: float) -> float:
|
| 84 |
+
return a - b
|
| 85 |
+
|
| 86 |
+
def multiply(self, a: float, b: float) -> float:
|
| 87 |
+
return a * b
|
| 88 |
+
|
| 89 |
+
def divide(self, a: float, b: float) -> float:
|
| 90 |
+
if b == 0:
|
| 91 |
+
raise ValueError("Cannot divide by zero")
|
| 92 |
+
return a / b
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def test_addition():
|
| 96 |
+
"""Test addition."""
|
| 97 |
+
calc = Calculator()
|
| 98 |
+
assert calc.add(2, 3) == 5
|
| 99 |
+
assert calc.add(-1, 1) == 0
|
| 100 |
+
assert calc.add(0, 0) == 0
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def test_subtraction():
|
| 104 |
+
"""Test subtraction."""
|
| 105 |
+
calc = Calculator()
|
| 106 |
+
assert calc.subtract(5, 3) == 2
|
| 107 |
+
assert calc.subtract(0, 5) == -5
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def test_multiplication():
|
| 111 |
+
"""Test multiplication."""
|
| 112 |
+
calc = Calculator()
|
| 113 |
+
assert calc.multiply(3, 4) == 12
|
| 114 |
+
assert calc.multiply(0, 5) == 0
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def test_division():
|
| 118 |
+
"""Test division."""
|
| 119 |
+
calc = Calculator()
|
| 120 |
+
assert calc.divide(6, 3) == 2
|
| 121 |
+
assert calc.divide(5, 2) == 2.5
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def test_division_by_zero():
|
| 125 |
+
"""Test division by zero raises error."""
|
| 126 |
+
calc = Calculator()
|
| 127 |
+
with pytest.raises(ValueError, match="Cannot divide by zero"):
|
| 128 |
+
calc.divide(5, 0)
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
### Pattern 2: Fixtures for Setup and Teardown
|
| 132 |
+
|
| 133 |
+
```python
|
| 134 |
+
# test_database.py
|
| 135 |
+
import pytest
|
| 136 |
+
from typing import Generator
|
| 137 |
+
|
| 138 |
+
class Database:
|
| 139 |
+
"""Simple database class."""
|
| 140 |
+
|
| 141 |
+
def __init__(self, connection_string: str):
|
| 142 |
+
self.connection_string = connection_string
|
| 143 |
+
self.connected = False
|
| 144 |
+
|
| 145 |
+
def connect(self):
|
| 146 |
+
"""Connect to database."""
|
| 147 |
+
self.connected = True
|
| 148 |
+
|
| 149 |
+
def disconnect(self):
|
| 150 |
+
"""Disconnect from database."""
|
| 151 |
+
self.connected = False
|
| 152 |
+
|
| 153 |
+
def query(self, sql: str) -> list:
|
| 154 |
+
"""Execute query."""
|
| 155 |
+
if not self.connected:
|
| 156 |
+
raise RuntimeError("Not connected")
|
| 157 |
+
return [{"id": 1, "name": "Test"}]
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
@pytest.fixture
|
| 161 |
+
def db() -> Generator[Database, None, None]:
|
| 162 |
+
"""Fixture that provides connected database."""
|
| 163 |
+
# Setup
|
| 164 |
+
database = Database("sqlite:///:memory:")
|
| 165 |
+
database.connect()
|
| 166 |
+
|
| 167 |
+
# Provide to test
|
| 168 |
+
yield database
|
| 169 |
+
|
| 170 |
+
# Teardown
|
| 171 |
+
database.disconnect()
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def test_database_query(db):
|
| 175 |
+
"""Test database query with fixture."""
|
| 176 |
+
results = db.query("SELECT * FROM users")
|
| 177 |
+
assert len(results) == 1
|
| 178 |
+
assert results[0]["name"] == "Test"
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
@pytest.fixture(scope="session")
|
| 182 |
+
def app_config():
|
| 183 |
+
"""Session-scoped fixture - created once per test session."""
|
| 184 |
+
return {
|
| 185 |
+
"database_url": "postgresql://localhost/test",
|
| 186 |
+
"api_key": "test-key",
|
| 187 |
+
"debug": True
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
@pytest.fixture(scope="module")
|
| 192 |
+
def api_client(app_config):
|
| 193 |
+
"""Module-scoped fixture - created once per test module."""
|
| 194 |
+
# Setup expensive resource
|
| 195 |
+
client = {"config": app_config, "session": "active"}
|
| 196 |
+
yield client
|
| 197 |
+
# Cleanup
|
| 198 |
+
client["session"] = "closed"
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def test_api_client(api_client):
|
| 202 |
+
"""Test using api client fixture."""
|
| 203 |
+
assert api_client["session"] == "active"
|
| 204 |
+
assert api_client["config"]["debug"] is True
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
### Pattern 3: Parameterized Tests
|
| 208 |
+
|
| 209 |
+
```python
|
| 210 |
+
# test_validation.py
|
| 211 |
+
import pytest
|
| 212 |
+
|
| 213 |
+
def is_valid_email(email: str) -> bool:
|
| 214 |
+
"""Check if email is valid."""
|
| 215 |
+
return "@" in email and "." in email.split("@")[1]
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
@pytest.mark.parametrize("email,expected", [
|
| 219 |
+
("user@example.com", True),
|
| 220 |
+
("test.user@domain.co.uk", True),
|
| 221 |
+
("invalid.email", False),
|
| 222 |
+
("@example.com", False),
|
| 223 |
+
("user@domain", False),
|
| 224 |
+
("", False),
|
| 225 |
+
])
|
| 226 |
+
def test_email_validation(email, expected):
|
| 227 |
+
"""Test email validation with various inputs."""
|
| 228 |
+
assert is_valid_email(email) == expected
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
@pytest.mark.parametrize("a,b,expected", [
|
| 232 |
+
(2, 3, 5),
|
| 233 |
+
(0, 0, 0),
|
| 234 |
+
(-1, 1, 0),
|
| 235 |
+
(100, 200, 300),
|
| 236 |
+
(-5, -5, -10),
|
| 237 |
+
])
|
| 238 |
+
def test_addition_parameterized(a, b, expected):
|
| 239 |
+
"""Test addition with multiple parameter sets."""
|
| 240 |
+
from test_calculator import Calculator
|
| 241 |
+
calc = Calculator()
|
| 242 |
+
assert calc.add(a, b) == expected
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# Using pytest.param for special cases
|
| 246 |
+
@pytest.mark.parametrize("value,expected", [
|
| 247 |
+
pytest.param(1, True, id="positive"),
|
| 248 |
+
pytest.param(0, False, id="zero"),
|
| 249 |
+
pytest.param(-1, False, id="negative"),
|
| 250 |
+
])
|
| 251 |
+
def test_is_positive(value, expected):
|
| 252 |
+
"""Test with custom test IDs."""
|
| 253 |
+
assert (value > 0) == expected
|
| 254 |
+
```
|
| 255 |
+
|
| 256 |
+
### Pattern 4: Mocking with unittest.mock
|
| 257 |
+
|
| 258 |
+
```python
|
| 259 |
+
# test_api_client.py
|
| 260 |
+
import pytest
|
| 261 |
+
from unittest.mock import Mock, patch, MagicMock
|
| 262 |
+
import requests
|
| 263 |
+
|
| 264 |
+
class APIClient:
|
| 265 |
+
"""Simple API client."""
|
| 266 |
+
|
| 267 |
+
def __init__(self, base_url: str):
|
| 268 |
+
self.base_url = base_url
|
| 269 |
+
|
| 270 |
+
def get_user(self, user_id: int) -> dict:
|
| 271 |
+
"""Fetch user from API."""
|
| 272 |
+
response = requests.get(f"{self.base_url}/users/{user_id}")
|
| 273 |
+
response.raise_for_status()
|
| 274 |
+
return response.json()
|
| 275 |
+
|
| 276 |
+
def create_user(self, data: dict) -> dict:
|
| 277 |
+
"""Create new user."""
|
| 278 |
+
response = requests.post(f"{self.base_url}/users", json=data)
|
| 279 |
+
response.raise_for_status()
|
| 280 |
+
return response.json()
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def test_get_user_success():
|
| 284 |
+
"""Test successful API call with mock."""
|
| 285 |
+
client = APIClient("https://api.example.com")
|
| 286 |
+
|
| 287 |
+
mock_response = Mock()
|
| 288 |
+
mock_response.json.return_value = {"id": 1, "name": "John Doe"}
|
| 289 |
+
mock_response.raise_for_status.return_value = None
|
| 290 |
+
|
| 291 |
+
with patch("requests.get", return_value=mock_response) as mock_get:
|
| 292 |
+
user = client.get_user(1)
|
| 293 |
+
|
| 294 |
+
assert user["id"] == 1
|
| 295 |
+
assert user["name"] == "John Doe"
|
| 296 |
+
mock_get.assert_called_once_with("https://api.example.com/users/1")
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def test_get_user_not_found():
|
| 300 |
+
"""Test API call with 404 error."""
|
| 301 |
+
client = APIClient("https://api.example.com")
|
| 302 |
+
|
| 303 |
+
mock_response = Mock()
|
| 304 |
+
mock_response.raise_for_status.side_effect = requests.HTTPError("404 Not Found")
|
| 305 |
+
|
| 306 |
+
with patch("requests.get", return_value=mock_response):
|
| 307 |
+
with pytest.raises(requests.HTTPError):
|
| 308 |
+
client.get_user(999)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
@patch("requests.post")
|
| 312 |
+
def test_create_user(mock_post):
|
| 313 |
+
"""Test user creation with decorator syntax."""
|
| 314 |
+
client = APIClient("https://api.example.com")
|
| 315 |
+
|
| 316 |
+
mock_post.return_value.json.return_value = {"id": 2, "name": "Jane Doe"}
|
| 317 |
+
mock_post.return_value.raise_for_status.return_value = None
|
| 318 |
+
|
| 319 |
+
user_data = {"name": "Jane Doe", "email": "jane@example.com"}
|
| 320 |
+
result = client.create_user(user_data)
|
| 321 |
+
|
| 322 |
+
assert result["id"] == 2
|
| 323 |
+
mock_post.assert_called_once()
|
| 324 |
+
call_args = mock_post.call_args
|
| 325 |
+
assert call_args.kwargs["json"] == user_data
|
| 326 |
+
```
|
| 327 |
+
|
| 328 |
+
### Pattern 5: Testing Exceptions
|
| 329 |
+
|
| 330 |
+
```python
|
| 331 |
+
# test_exceptions.py
|
| 332 |
+
import pytest
|
| 333 |
+
|
| 334 |
+
def divide(a: float, b: float) -> float:
|
| 335 |
+
"""Divide a by b."""
|
| 336 |
+
if b == 0:
|
| 337 |
+
raise ZeroDivisionError("Division by zero")
|
| 338 |
+
if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
|
| 339 |
+
raise TypeError("Arguments must be numbers")
|
| 340 |
+
return a / b
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def test_zero_division():
|
| 344 |
+
"""Test exception is raised for division by zero."""
|
| 345 |
+
with pytest.raises(ZeroDivisionError):
|
| 346 |
+
divide(10, 0)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def test_zero_division_with_message():
|
| 350 |
+
"""Test exception message."""
|
| 351 |
+
with pytest.raises(ZeroDivisionError, match="Division by zero"):
|
| 352 |
+
divide(5, 0)
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def test_type_error():
|
| 356 |
+
"""Test type error exception."""
|
| 357 |
+
with pytest.raises(TypeError, match="must be numbers"):
|
| 358 |
+
divide("10", 5)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def test_exception_info():
|
| 362 |
+
"""Test accessing exception info."""
|
| 363 |
+
with pytest.raises(ValueError) as exc_info:
|
| 364 |
+
int("not a number")
|
| 365 |
+
|
| 366 |
+
assert "invalid literal" in str(exc_info.value)
|
| 367 |
+
```
|
| 368 |
+
|
| 369 |
+
## Advanced Patterns
|
| 370 |
+
|
| 371 |
+
### Pattern 6: Testing Async Code
|
| 372 |
+
|
| 373 |
+
```python
|
| 374 |
+
# test_async.py
|
| 375 |
+
import pytest
|
| 376 |
+
import asyncio
|
| 377 |
+
|
| 378 |
+
async def fetch_data(url: str) -> dict:
|
| 379 |
+
"""Fetch data asynchronously."""
|
| 380 |
+
await asyncio.sleep(0.1)
|
| 381 |
+
return {"url": url, "data": "result"}
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
@pytest.mark.asyncio
|
| 385 |
+
async def test_fetch_data():
|
| 386 |
+
"""Test async function."""
|
| 387 |
+
result = await fetch_data("https://api.example.com")
|
| 388 |
+
assert result["url"] == "https://api.example.com"
|
| 389 |
+
assert "data" in result
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
@pytest.mark.asyncio
|
| 393 |
+
async def test_concurrent_fetches():
|
| 394 |
+
"""Test concurrent async operations."""
|
| 395 |
+
urls = ["url1", "url2", "url3"]
|
| 396 |
+
tasks = [fetch_data(url) for url in urls]
|
| 397 |
+
results = await asyncio.gather(*tasks)
|
| 398 |
+
|
| 399 |
+
assert len(results) == 3
|
| 400 |
+
assert all("data" in r for r in results)
|
| 401 |
+
|
| 402 |
+
|
| 403 |
+
@pytest.fixture
|
| 404 |
+
async def async_client():
|
| 405 |
+
"""Async fixture."""
|
| 406 |
+
client = {"connected": True}
|
| 407 |
+
yield client
|
| 408 |
+
client["connected"] = False
|
| 409 |
+
|
| 410 |
+
|
| 411 |
+
@pytest.mark.asyncio
|
| 412 |
+
async def test_with_async_fixture(async_client):
|
| 413 |
+
"""Test using async fixture."""
|
| 414 |
+
assert async_client["connected"] is True
|
| 415 |
+
```
|
| 416 |
+
|
| 417 |
+
### Pattern 7: Monkeypatch for Testing
|
| 418 |
+
|
| 419 |
+
```python
|
| 420 |
+
# test_environment.py
|
| 421 |
+
import os
|
| 422 |
+
import pytest
|
| 423 |
+
|
| 424 |
+
def get_database_url() -> str:
|
| 425 |
+
"""Get database URL from environment."""
|
| 426 |
+
return os.environ.get("DATABASE_URL", "sqlite:///:memory:")
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
def test_database_url_default():
|
| 430 |
+
"""Test default database URL."""
|
| 431 |
+
# Will use actual environment variable if set
|
| 432 |
+
url = get_database_url()
|
| 433 |
+
assert url
|
| 434 |
+
|
| 435 |
+
|
| 436 |
+
def test_database_url_custom(monkeypatch):
|
| 437 |
+
"""Test custom database URL with monkeypatch."""
|
| 438 |
+
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
|
| 439 |
+
assert get_database_url() == "postgresql://localhost/test"
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
def test_database_url_not_set(monkeypatch):
|
| 443 |
+
"""Test when env var is not set."""
|
| 444 |
+
monkeypatch.delenv("DATABASE_URL", raising=False)
|
| 445 |
+
assert get_database_url() == "sqlite:///:memory:"
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
class Config:
|
| 449 |
+
"""Configuration class."""
|
| 450 |
+
|
| 451 |
+
def __init__(self):
|
| 452 |
+
self.api_key = "production-key"
|
| 453 |
+
|
| 454 |
+
def get_api_key(self):
|
| 455 |
+
return self.api_key
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def test_monkeypatch_attribute(monkeypatch):
|
| 459 |
+
"""Test monkeypatching object attributes."""
|
| 460 |
+
config = Config()
|
| 461 |
+
monkeypatch.setattr(config, "api_key", "test-key")
|
| 462 |
+
assert config.get_api_key() == "test-key"
|
| 463 |
+
```
|
| 464 |
+
|
| 465 |
+
### Pattern 8: Temporary Files and Directories
|
| 466 |
+
|
| 467 |
+
```python
|
| 468 |
+
# test_file_operations.py
|
| 469 |
+
import pytest
|
| 470 |
+
from pathlib import Path
|
| 471 |
+
|
| 472 |
+
def save_data(filepath: Path, data: str):
|
| 473 |
+
"""Save data to file."""
|
| 474 |
+
filepath.write_text(data)
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def load_data(filepath: Path) -> str:
|
| 478 |
+
"""Load data from file."""
|
| 479 |
+
return filepath.read_text()
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
def test_file_operations(tmp_path):
|
| 483 |
+
"""Test file operations with temporary directory."""
|
| 484 |
+
# tmp_path is a pathlib.Path object
|
| 485 |
+
test_file = tmp_path / "test_data.txt"
|
| 486 |
+
|
| 487 |
+
# Save data
|
| 488 |
+
save_data(test_file, "Hello, World!")
|
| 489 |
+
|
| 490 |
+
# Verify file exists
|
| 491 |
+
assert test_file.exists()
|
| 492 |
+
|
| 493 |
+
# Load and verify data
|
| 494 |
+
data = load_data(test_file)
|
| 495 |
+
assert data == "Hello, World!"
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def test_multiple_files(tmp_path):
|
| 499 |
+
"""Test with multiple temporary files."""
|
| 500 |
+
files = {
|
| 501 |
+
"file1.txt": "Content 1",
|
| 502 |
+
"file2.txt": "Content 2",
|
| 503 |
+
"file3.txt": "Content 3"
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
for filename, content in files.items():
|
| 507 |
+
filepath = tmp_path / filename
|
| 508 |
+
save_data(filepath, content)
|
| 509 |
+
|
| 510 |
+
# Verify all files created
|
| 511 |
+
assert len(list(tmp_path.iterdir())) == 3
|
| 512 |
+
|
| 513 |
+
# Verify contents
|
| 514 |
+
for filename, expected_content in files.items():
|
| 515 |
+
filepath = tmp_path / filename
|
| 516 |
+
assert load_data(filepath) == expected_content
|
| 517 |
+
```
|
| 518 |
+
|
| 519 |
+
### Pattern 9: Custom Fixtures and Conftest
|
| 520 |
+
|
| 521 |
+
```python
|
| 522 |
+
# conftest.py
|
| 523 |
+
"""Shared fixtures for all tests."""
|
| 524 |
+
import pytest
|
| 525 |
+
|
| 526 |
+
@pytest.fixture(scope="session")
|
| 527 |
+
def database_url():
|
| 528 |
+
"""Provide database URL for all tests."""
|
| 529 |
+
return "postgresql://localhost/test_db"
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
@pytest.fixture(autouse=True)
|
| 533 |
+
def reset_database(database_url):
|
| 534 |
+
"""Auto-use fixture that runs before each test."""
|
| 535 |
+
# Setup: Clear database
|
| 536 |
+
print(f"Clearing database: {database_url}")
|
| 537 |
+
yield
|
| 538 |
+
# Teardown: Clean up
|
| 539 |
+
print("Test completed")
|
| 540 |
+
|
| 541 |
+
|
| 542 |
+
@pytest.fixture
|
| 543 |
+
def sample_user():
|
| 544 |
+
"""Provide sample user data."""
|
| 545 |
+
return {
|
| 546 |
+
"id": 1,
|
| 547 |
+
"name": "Test User",
|
| 548 |
+
"email": "test@example.com"
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
@pytest.fixture
|
| 553 |
+
def sample_users():
|
| 554 |
+
"""Provide list of sample users."""
|
| 555 |
+
return [
|
| 556 |
+
{"id": 1, "name": "User 1"},
|
| 557 |
+
{"id": 2, "name": "User 2"},
|
| 558 |
+
{"id": 3, "name": "User 3"},
|
| 559 |
+
]
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
# Parametrized fixture
|
| 563 |
+
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
|
| 564 |
+
def db_backend(request):
|
| 565 |
+
"""Fixture that runs tests with different database backends."""
|
| 566 |
+
return request.param
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
def test_with_db_backend(db_backend):
|
| 570 |
+
"""This test will run 3 times with different backends."""
|
| 571 |
+
print(f"Testing with {db_backend}")
|
| 572 |
+
assert db_backend in ["sqlite", "postgresql", "mysql"]
|
| 573 |
+
```
|
| 574 |
+
|
| 575 |
+
### Pattern 10: Property-Based Testing
|
| 576 |
+
|
| 577 |
+
```python
|
| 578 |
+
# test_properties.py
|
| 579 |
+
from hypothesis import given, strategies as st
|
| 580 |
+
import pytest
|
| 581 |
+
|
| 582 |
+
def reverse_string(s: str) -> str:
|
| 583 |
+
"""Reverse a string."""
|
| 584 |
+
return s[::-1]
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
@given(st.text())
|
| 588 |
+
def test_reverse_twice_is_original(s):
|
| 589 |
+
"""Property: reversing twice returns original."""
|
| 590 |
+
assert reverse_string(reverse_string(s)) == s
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
@given(st.text())
|
| 594 |
+
def test_reverse_length(s):
|
| 595 |
+
"""Property: reversed string has same length."""
|
| 596 |
+
assert len(reverse_string(s)) == len(s)
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
@given(st.integers(), st.integers())
|
| 600 |
+
def test_addition_commutative(a, b):
|
| 601 |
+
"""Property: addition is commutative."""
|
| 602 |
+
assert a + b == b + a
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
@given(st.lists(st.integers()))
|
| 606 |
+
def test_sorted_list_properties(lst):
|
| 607 |
+
"""Property: sorted list is ordered."""
|
| 608 |
+
sorted_lst = sorted(lst)
|
| 609 |
+
|
| 610 |
+
# Same length
|
| 611 |
+
assert len(sorted_lst) == len(lst)
|
| 612 |
+
|
| 613 |
+
# All elements present
|
| 614 |
+
assert set(sorted_lst) == set(lst)
|
| 615 |
+
|
| 616 |
+
# Is ordered
|
| 617 |
+
for i in range(len(sorted_lst) - 1):
|
| 618 |
+
assert sorted_lst[i] <= sorted_lst[i + 1]
|
| 619 |
+
```
|
| 620 |
+
|
| 621 |
+
## Test Design Principles
|
| 622 |
+
|
| 623 |
+
### One Behavior Per Test
|
| 624 |
+
|
| 625 |
+
Each test should verify exactly one behavior. This makes failures easy to diagnose and tests easy to maintain.
|
| 626 |
+
|
| 627 |
+
```python
|
| 628 |
+
# BAD - testing multiple behaviors
|
| 629 |
+
def test_user_service():
|
| 630 |
+
user = service.create_user(data)
|
| 631 |
+
assert user.id is not None
|
| 632 |
+
assert user.email == data["email"]
|
| 633 |
+
updated = service.update_user(user.id, {"name": "New"})
|
| 634 |
+
assert updated.name == "New"
|
| 635 |
+
|
| 636 |
+
# GOOD - focused tests
|
| 637 |
+
def test_create_user_assigns_id():
|
| 638 |
+
user = service.create_user(data)
|
| 639 |
+
assert user.id is not None
|
| 640 |
+
|
| 641 |
+
def test_create_user_stores_email():
|
| 642 |
+
user = service.create_user(data)
|
| 643 |
+
assert user.email == data["email"]
|
| 644 |
+
|
| 645 |
+
def test_update_user_changes_name():
|
| 646 |
+
user = service.create_user(data)
|
| 647 |
+
updated = service.update_user(user.id, {"name": "New"})
|
| 648 |
+
assert updated.name == "New"
|
| 649 |
+
```
|
| 650 |
+
|
| 651 |
+
### Test Error Paths
|
| 652 |
+
|
| 653 |
+
Always test failure cases, not just happy paths.
|
| 654 |
+
|
| 655 |
+
```python
|
| 656 |
+
def test_get_user_raises_not_found():
|
| 657 |
+
with pytest.raises(UserNotFoundError) as exc_info:
|
| 658 |
+
service.get_user("nonexistent-id")
|
| 659 |
+
|
| 660 |
+
assert "nonexistent-id" in str(exc_info.value)
|
| 661 |
+
|
| 662 |
+
def test_create_user_rejects_invalid_email():
|
| 663 |
+
with pytest.raises(ValueError, match="Invalid email format"):
|
| 664 |
+
service.create_user({"email": "not-an-email"})
|
| 665 |
+
```
|
| 666 |
+
|
| 667 |
+
## Testing Best Practices
|
| 668 |
+
|
| 669 |
+
### Test Organization
|
| 670 |
+
|
| 671 |
+
```python
|
| 672 |
+
# tests/
|
| 673 |
+
# __init__.py
|
| 674 |
+
# conftest.py # Shared fixtures
|
| 675 |
+
# test_unit/ # Unit tests
|
| 676 |
+
# test_models.py
|
| 677 |
+
# test_utils.py
|
| 678 |
+
# test_integration/ # Integration tests
|
| 679 |
+
# test_api.py
|
| 680 |
+
# test_database.py
|
| 681 |
+
# test_e2e/ # End-to-end tests
|
| 682 |
+
# test_workflows.py
|
| 683 |
+
```
|
| 684 |
+
|
| 685 |
+
### Test Naming Convention
|
| 686 |
+
|
| 687 |
+
A common pattern: `test_<unit>_<scenario>_<expected_outcome>`. Adapt to your team's preferences.
|
| 688 |
+
|
| 689 |
+
```python
|
| 690 |
+
# Pattern: test_<unit>_<scenario>_<expected>
|
| 691 |
+
def test_create_user_with_valid_data_returns_user():
|
| 692 |
+
...
|
| 693 |
+
|
| 694 |
+
def test_create_user_with_duplicate_email_raises_conflict():
|
| 695 |
+
...
|
| 696 |
+
|
| 697 |
+
def test_get_user_with_unknown_id_returns_none():
|
| 698 |
+
...
|
| 699 |
+
|
| 700 |
+
# Good test names - clear and descriptive
|
| 701 |
+
def test_user_creation_with_valid_data():
|
| 702 |
+
"""Clear name describes what is being tested."""
|
| 703 |
+
pass
|
| 704 |
+
|
| 705 |
+
def test_login_fails_with_invalid_password():
|
| 706 |
+
"""Name describes expected behavior."""
|
| 707 |
+
pass
|
| 708 |
+
|
| 709 |
+
def test_api_returns_404_for_missing_resource():
|
| 710 |
+
"""Specific about inputs and expected outcomes."""
|
| 711 |
+
pass
|
| 712 |
+
|
| 713 |
+
# Bad test names - avoid these
|
| 714 |
+
def test_1(): # Not descriptive
|
| 715 |
+
pass
|
| 716 |
+
|
| 717 |
+
def test_user(): # Too vague
|
| 718 |
+
pass
|
| 719 |
+
|
| 720 |
+
def test_function(): # Doesn't explain what's tested
|
| 721 |
+
pass
|
| 722 |
+
```
|
| 723 |
+
|
| 724 |
+
### Testing Retry Behavior
|
| 725 |
+
|
| 726 |
+
Verify that retry logic works correctly using mock side effects.
|
| 727 |
+
|
| 728 |
+
```python
|
| 729 |
+
from unittest.mock import Mock
|
| 730 |
+
|
| 731 |
+
def test_retries_on_transient_error():
|
| 732 |
+
"""Test that service retries on transient failures."""
|
| 733 |
+
client = Mock()
|
| 734 |
+
# Fail twice, then succeed
|
| 735 |
+
client.request.side_effect = [
|
| 736 |
+
ConnectionError("Failed"),
|
| 737 |
+
ConnectionError("Failed"),
|
| 738 |
+
{"status": "ok"},
|
| 739 |
+
]
|
| 740 |
+
|
| 741 |
+
service = ServiceWithRetry(client, max_retries=3)
|
| 742 |
+
result = service.fetch()
|
| 743 |
+
|
| 744 |
+
assert result == {"status": "ok"}
|
| 745 |
+
assert client.request.call_count == 3
|
| 746 |
+
|
| 747 |
+
def test_gives_up_after_max_retries():
|
| 748 |
+
"""Test that service stops retrying after max attempts."""
|
| 749 |
+
client = Mock()
|
| 750 |
+
client.request.side_effect = ConnectionError("Failed")
|
| 751 |
+
|
| 752 |
+
service = ServiceWithRetry(client, max_retries=3)
|
| 753 |
+
|
| 754 |
+
with pytest.raises(ConnectionError):
|
| 755 |
+
service.fetch()
|
| 756 |
+
|
| 757 |
+
assert client.request.call_count == 3
|
| 758 |
+
|
| 759 |
+
def test_does_not_retry_on_permanent_error():
|
| 760 |
+
"""Test that permanent errors are not retried."""
|
| 761 |
+
client = Mock()
|
| 762 |
+
client.request.side_effect = ValueError("Invalid input")
|
| 763 |
+
|
| 764 |
+
service = ServiceWithRetry(client, max_retries=3)
|
| 765 |
+
|
| 766 |
+
with pytest.raises(ValueError):
|
| 767 |
+
service.fetch()
|
| 768 |
+
|
| 769 |
+
# Only called once - no retry for ValueError
|
| 770 |
+
assert client.request.call_count == 1
|
| 771 |
+
```
|
| 772 |
+
|
| 773 |
+
### Mocking Time with Freezegun
|
| 774 |
+
|
| 775 |
+
Use freezegun to control time in tests for predictable time-dependent behavior.
|
| 776 |
+
|
| 777 |
+
```python
|
| 778 |
+
from freezegun import freeze_time
|
| 779 |
+
from datetime import datetime, timedelta
|
| 780 |
+
|
| 781 |
+
@freeze_time("2026-01-15 10:00:00")
|
| 782 |
+
def test_token_expiry():
|
| 783 |
+
"""Test token expires at correct time."""
|
| 784 |
+
token = create_token(expires_in_seconds=3600)
|
| 785 |
+
assert token.expires_at == datetime(2026, 1, 15, 11, 0, 0)
|
| 786 |
+
|
| 787 |
+
@freeze_time("2026-01-15 10:00:00")
|
| 788 |
+
def test_is_expired_returns_false_before_expiry():
|
| 789 |
+
"""Test token is not expired when within validity period."""
|
| 790 |
+
token = create_token(expires_in_seconds=3600)
|
| 791 |
+
assert not token.is_expired()
|
| 792 |
+
|
| 793 |
+
@freeze_time("2026-01-15 12:00:00")
|
| 794 |
+
def test_is_expired_returns_true_after_expiry():
|
| 795 |
+
"""Test token is expired after validity period."""
|
| 796 |
+
token = Token(expires_at=datetime(2026, 1, 15, 11, 30, 0))
|
| 797 |
+
assert token.is_expired()
|
| 798 |
+
|
| 799 |
+
def test_with_time_travel():
|
| 800 |
+
"""Test behavior across time using freeze_time context."""
|
| 801 |
+
with freeze_time("2026-01-01") as frozen_time:
|
| 802 |
+
item = create_item()
|
| 803 |
+
assert item.created_at == datetime(2026, 1, 1)
|
| 804 |
+
|
| 805 |
+
# Move forward in time
|
| 806 |
+
frozen_time.move_to("2026-01-15")
|
| 807 |
+
assert item.age_days == 14
|
| 808 |
+
```
|
| 809 |
+
|
| 810 |
+
### Test Markers
|
| 811 |
+
|
| 812 |
+
```python
|
| 813 |
+
# test_markers.py
|
| 814 |
+
import pytest
|
| 815 |
+
|
| 816 |
+
@pytest.mark.slow
|
| 817 |
+
def test_slow_operation():
|
| 818 |
+
"""Mark slow tests."""
|
| 819 |
+
import time
|
| 820 |
+
time.sleep(2)
|
| 821 |
+
|
| 822 |
+
|
| 823 |
+
@pytest.mark.integration
|
| 824 |
+
def test_database_integration():
|
| 825 |
+
"""Mark integration tests."""
|
| 826 |
+
pass
|
| 827 |
+
|
| 828 |
+
|
| 829 |
+
@pytest.mark.skip(reason="Feature not implemented yet")
|
| 830 |
+
def test_future_feature():
|
| 831 |
+
"""Skip tests temporarily."""
|
| 832 |
+
pass
|
| 833 |
+
|
| 834 |
+
|
| 835 |
+
@pytest.mark.skipif(os.name == "nt", reason="Unix only test")
|
| 836 |
+
def test_unix_specific():
|
| 837 |
+
"""Conditional skip."""
|
| 838 |
+
pass
|
| 839 |
+
|
| 840 |
+
|
| 841 |
+
@pytest.mark.xfail(reason="Known bug #123")
|
| 842 |
+
def test_known_bug():
|
| 843 |
+
"""Mark expected failures."""
|
| 844 |
+
assert False
|
| 845 |
+
|
| 846 |
+
|
| 847 |
+
# Run with:
|
| 848 |
+
# pytest -m slow # Run only slow tests
|
| 849 |
+
# pytest -m "not slow" # Skip slow tests
|
| 850 |
+
# pytest -m integration # Run integration tests
|
| 851 |
+
```
|
| 852 |
+
|
| 853 |
+
### Coverage Reporting
|
| 854 |
+
|
| 855 |
+
```bash
|
| 856 |
+
# Install coverage
|
| 857 |
+
pip install pytest-cov
|
| 858 |
+
|
| 859 |
+
# Run tests with coverage
|
| 860 |
+
pytest --cov=myapp tests/
|
| 861 |
+
|
| 862 |
+
# Generate HTML report
|
| 863 |
+
pytest --cov=myapp --cov-report=html tests/
|
| 864 |
+
|
| 865 |
+
# Fail if coverage below threshold
|
| 866 |
+
pytest --cov=myapp --cov-fail-under=80 tests/
|
| 867 |
+
|
| 868 |
+
# Show missing lines
|
| 869 |
+
pytest --cov=myapp --cov-report=term-missing tests/
|
| 870 |
+
```
|
| 871 |
+
|
| 872 |
+
## Testing Database Code
|
| 873 |
+
|
| 874 |
+
```python
|
| 875 |
+
# test_database_models.py
|
| 876 |
+
import pytest
|
| 877 |
+
from sqlalchemy import create_engine, Column, Integer, String
|
| 878 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 879 |
+
from sqlalchemy.orm import sessionmaker, Session
|
| 880 |
+
|
| 881 |
+
Base = declarative_base()
|
| 882 |
+
|
| 883 |
+
|
| 884 |
+
class User(Base):
|
| 885 |
+
"""User model."""
|
| 886 |
+
__tablename__ = "users"
|
| 887 |
+
|
| 888 |
+
id = Column(Integer, primary_key=True)
|
| 889 |
+
name = Column(String(50))
|
| 890 |
+
email = Column(String(100), unique=True)
|
| 891 |
+
|
| 892 |
+
|
| 893 |
+
@pytest.fixture(scope="function")
|
| 894 |
+
def db_session() -> Session:
|
| 895 |
+
"""Create in-memory database for testing."""
|
| 896 |
+
engine = create_engine("sqlite:///:memory:")
|
| 897 |
+
Base.metadata.create_all(engine)
|
| 898 |
+
|
| 899 |
+
SessionLocal = sessionmaker(bind=engine)
|
| 900 |
+
session = SessionLocal()
|
| 901 |
+
|
| 902 |
+
yield session
|
| 903 |
+
|
| 904 |
+
session.close()
|
| 905 |
+
|
| 906 |
+
|
| 907 |
+
def test_create_user(db_session):
|
| 908 |
+
"""Test creating a user."""
|
| 909 |
+
user = User(name="Test User", email="test@example.com")
|
| 910 |
+
db_session.add(user)
|
| 911 |
+
db_session.commit()
|
| 912 |
+
|
| 913 |
+
assert user.id is not None
|
| 914 |
+
assert user.name == "Test User"
|
| 915 |
+
|
| 916 |
+
|
| 917 |
+
def test_query_user(db_session):
|
| 918 |
+
"""Test querying users."""
|
| 919 |
+
user1 = User(name="User 1", email="user1@example.com")
|
| 920 |
+
user2 = User(name="User 2", email="user2@example.com")
|
| 921 |
+
|
| 922 |
+
db_session.add_all([user1, user2])
|
| 923 |
+
db_session.commit()
|
| 924 |
+
|
| 925 |
+
users = db_session.query(User).all()
|
| 926 |
+
assert len(users) == 2
|
| 927 |
+
|
| 928 |
+
|
| 929 |
+
def test_unique_email_constraint(db_session):
|
| 930 |
+
"""Test unique email constraint."""
|
| 931 |
+
from sqlalchemy.exc import IntegrityError
|
| 932 |
+
|
| 933 |
+
user1 = User(name="User 1", email="same@example.com")
|
| 934 |
+
user2 = User(name="User 2", email="same@example.com")
|
| 935 |
+
|
| 936 |
+
db_session.add(user1)
|
| 937 |
+
db_session.commit()
|
| 938 |
+
|
| 939 |
+
db_session.add(user2)
|
| 940 |
+
|
| 941 |
+
with pytest.raises(IntegrityError):
|
| 942 |
+
db_session.commit()
|
| 943 |
+
```
|
| 944 |
+
|
| 945 |
+
## CI/CD Integration
|
| 946 |
+
|
| 947 |
+
```yaml
|
| 948 |
+
# .github/workflows/test.yml
|
| 949 |
+
name: Tests
|
| 950 |
+
|
| 951 |
+
on: [push, pull_request]
|
| 952 |
+
|
| 953 |
+
jobs:
|
| 954 |
+
test:
|
| 955 |
+
runs-on: ubuntu-latest
|
| 956 |
+
|
| 957 |
+
strategy:
|
| 958 |
+
matrix:
|
| 959 |
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
| 960 |
+
|
| 961 |
+
steps:
|
| 962 |
+
- uses: actions/checkout@v3
|
| 963 |
+
|
| 964 |
+
- name: Set up Python
|
| 965 |
+
uses: actions/setup-python@v4
|
| 966 |
+
with:
|
| 967 |
+
python-version: ${{ matrix.python-version }}
|
| 968 |
+
|
| 969 |
+
- name: Install dependencies
|
| 970 |
+
run: |
|
| 971 |
+
pip install -e ".[dev]"
|
| 972 |
+
pip install pytest pytest-cov
|
| 973 |
+
|
| 974 |
+
- name: Run tests
|
| 975 |
+
run: |
|
| 976 |
+
pytest --cov=myapp --cov-report=xml
|
| 977 |
+
|
| 978 |
+
- name: Upload coverage
|
| 979 |
+
uses: codecov/codecov-action@v3
|
| 980 |
+
with:
|
| 981 |
+
file: ./coverage.xml
|
| 982 |
+
```
|
| 983 |
+
|
| 984 |
+
## Configuration Files
|
| 985 |
+
|
| 986 |
+
```ini
|
| 987 |
+
# pytest.ini
|
| 988 |
+
[pytest]
|
| 989 |
+
testpaths = tests
|
| 990 |
+
python_files = test_*.py
|
| 991 |
+
python_classes = Test*
|
| 992 |
+
python_functions = test_*
|
| 993 |
+
addopts =
|
| 994 |
+
-v
|
| 995 |
+
--strict-markers
|
| 996 |
+
--tb=short
|
| 997 |
+
--cov=myapp
|
| 998 |
+
--cov-report=term-missing
|
| 999 |
+
markers =
|
| 1000 |
+
slow: marks tests as slow
|
| 1001 |
+
integration: marks integration tests
|
| 1002 |
+
unit: marks unit tests
|
| 1003 |
+
e2e: marks end-to-end tests
|
| 1004 |
+
```
|
| 1005 |
+
|
| 1006 |
+
```toml
|
| 1007 |
+
# pyproject.toml
|
| 1008 |
+
[tool.pytest.ini_options]
|
| 1009 |
+
testpaths = ["tests"]
|
| 1010 |
+
python_files = ["test_*.py"]
|
| 1011 |
+
addopts = [
|
| 1012 |
+
"-v",
|
| 1013 |
+
"--cov=myapp",
|
| 1014 |
+
"--cov-report=term-missing",
|
| 1015 |
+
]
|
| 1016 |
+
|
| 1017 |
+
[tool.coverage.run]
|
| 1018 |
+
source = ["myapp"]
|
| 1019 |
+
omit = ["*/tests/*", "*/migrations/*"]
|
| 1020 |
+
|
| 1021 |
+
[tool.coverage.report]
|
| 1022 |
+
exclude_lines = [
|
| 1023 |
+
"pragma: no cover",
|
| 1024 |
+
"def __repr__",
|
| 1025 |
+
"raise AssertionError",
|
| 1026 |
+
"raise NotImplementedError",
|
| 1027 |
+
]
|
| 1028 |
+
```
|
| 1029 |
+
|
| 1030 |
+
## Resources
|
| 1031 |
+
|
| 1032 |
+
- **pytest documentation**: https://docs.pytest.org/
|
| 1033 |
+
- **unittest.mock**: https://docs.python.org/3/library/unittest.mock.html
|
| 1034 |
+
- **hypothesis**: Property-based testing
|
| 1035 |
+
- **pytest-asyncio**: Testing async code
|
| 1036 |
+
- **pytest-cov**: Coverage reporting
|
| 1037 |
+
- **pytest-mock**: pytest wrapper for mock
|
| 1038 |
+
|
| 1039 |
+
## Best Practices Summary
|
| 1040 |
+
|
| 1041 |
+
1. **Write tests first** (TDD) or alongside code
|
| 1042 |
+
2. **One assertion per test** when possible
|
| 1043 |
+
3. **Use descriptive test names** that explain behavior
|
| 1044 |
+
4. **Keep tests independent** and isolated
|
| 1045 |
+
5. **Use fixtures** for setup and teardown
|
| 1046 |
+
6. **Mock external dependencies** appropriately
|
| 1047 |
+
7. **Parametrize tests** to reduce duplication
|
| 1048 |
+
8. **Test edge cases** and error conditions
|
| 1049 |
+
9. **Measure coverage** but focus on quality
|
| 1050 |
+
10. **Run tests in CI/CD** on every commit
|
CONTRIBUTING.md
CHANGED
|
@@ -155,14 +155,19 @@ cp .env.template .env
|
|
| 155 |
### 5. Run Tests
|
| 156 |
|
| 157 |
```bash
|
| 158 |
-
# Run
|
| 159 |
-
pytest
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
|
| 161 |
# Run with coverage
|
| 162 |
-
pytest --cov=src
|
| 163 |
|
| 164 |
# Run specific test file
|
| 165 |
-
pytest tests/
|
| 166 |
```
|
| 167 |
|
| 168 |
## Style Guidelines
|
|
|
|
| 155 |
### 5. Run Tests
|
| 156 |
|
| 157 |
```bash
|
| 158 |
+
# Run unit tests (30 tests)
|
| 159 |
+
.venv\Scripts\python.exe -m pytest tests/ -q \
|
| 160 |
+
--ignore=tests/test_basic.py \
|
| 161 |
+
--ignore=tests/test_diabetes_patient.py \
|
| 162 |
+
--ignore=tests/test_evolution_loop.py \
|
| 163 |
+
--ignore=tests/test_evolution_quick.py \
|
| 164 |
+
--ignore=tests/test_evaluation_system.py
|
| 165 |
|
| 166 |
# Run with coverage
|
| 167 |
+
.venv\Scripts\python.exe -m pytest --cov=src tests/
|
| 168 |
|
| 169 |
# Run specific test file
|
| 170 |
+
.venv\Scripts\python.exe -m pytest tests/test_codebase_fixes.py -v
|
| 171 |
```
|
| 172 |
|
| 173 |
## Style Guidelines
|
QUICKSTART.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
Get up and running in **5 minutes**!
|
| 4 |
|
| 5 |
-
## Step 1: Prerequisites
|
| 6 |
|
| 7 |
Before you begin, ensure you have:
|
| 8 |
|
| 9 |
-
-
|
| 10 |
-
-
|
| 11 |
-
-
|
| 12 |
- [Groq](https://console.groq.com/keys) - Recommended (Fast & Free)
|
| 13 |
- [Google Gemini](https://aistudio.google.com/app/apikey) - Alternative
|
| 14 |
|
| 15 |
**System Requirements:**
|
| 16 |
- 4GB+ RAM
|
| 17 |
- 2GB free disk space
|
| 18 |
-
- No GPU required
|
| 19 |
|
| 20 |
---
|
| 21 |
|
| 22 |
-
## Step 2: Installation
|
| 23 |
|
| 24 |
### Clone the Repository
|
| 25 |
|
|
@@ -52,7 +52,7 @@ pip install -r requirements.txt
|
|
| 52 |
|
| 53 |
---
|
| 54 |
|
| 55 |
-
## Step 3: Configuration
|
| 56 |
|
| 57 |
### Copy Environment Template
|
| 58 |
|
|
@@ -95,7 +95,7 @@ EMBEDDING_PROVIDER="google"
|
|
| 95 |
|
| 96 |
---
|
| 97 |
|
| 98 |
-
## Step 4: Verify Installation
|
| 99 |
|
| 100 |
Quick system check:
|
| 101 |
|
|
@@ -112,7 +112,7 @@ If you see "β
Success!" you're good to go!
|
|
| 112 |
|
| 113 |
---
|
| 114 |
|
| 115 |
-
## Step 5: Run Your First Analysis
|
| 116 |
|
| 117 |
### Interactive Chat Mode
|
| 118 |
|
|
@@ -134,7 +134,7 @@ You: My glucose is 185, HbA1c is 8.2, and cholesterol is 210
|
|
| 134 |
|
| 135 |
---
|
| 136 |
|
| 137 |
-
## Common Commands
|
| 138 |
|
| 139 |
### Chat Interface
|
| 140 |
```bash
|
|
@@ -150,17 +150,16 @@ quit # Exit
|
|
| 150 |
### Python API
|
| 151 |
```python
|
| 152 |
from src.workflow import create_guild
|
| 153 |
-
from src.state import PatientInput
|
| 154 |
|
| 155 |
# Create the guild
|
| 156 |
guild = create_guild()
|
| 157 |
|
| 158 |
# Analyze biomarkers
|
| 159 |
-
result = guild.run(
|
| 160 |
-
biomarkers
|
| 161 |
-
model_prediction
|
| 162 |
-
patient_context
|
| 163 |
-
)
|
| 164 |
|
| 165 |
print(result)
|
| 166 |
```
|
|
@@ -177,7 +176,7 @@ python -m uvicorn app.main:app --reload
|
|
| 177 |
|
| 178 |
---
|
| 179 |
|
| 180 |
-
## Troubleshooting
|
| 181 |
|
| 182 |
### Import Error: "No module named 'langchain'"
|
| 183 |
|
|
@@ -224,14 +223,14 @@ python src/pdf_processor.py
|
|
| 224 |
|
| 225 |
---
|
| 226 |
|
| 227 |
-
## Next Steps
|
| 228 |
|
| 229 |
### Learn More
|
| 230 |
|
| 231 |
- **[Full Documentation](README.md)** - Complete system overview
|
| 232 |
-
- **[API Guide](
|
| 233 |
- **[Contributing](CONTRIBUTING.md)** - How to contribute
|
| 234 |
-
- **[Architecture](docs/)** - Deep dive into system design
|
| 235 |
|
| 236 |
### Customize
|
| 237 |
|
|
@@ -242,22 +241,27 @@ python src/pdf_processor.py
|
|
| 242 |
### Run Tests
|
| 243 |
|
| 244 |
```bash
|
| 245 |
-
#
|
| 246 |
-
python tests/
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
```
|
| 251 |
|
| 252 |
---
|
| 253 |
|
| 254 |
-
## Example Session
|
| 255 |
|
| 256 |
```
|
| 257 |
$ python scripts/chat.py
|
| 258 |
|
| 259 |
======================================================================
|
| 260 |
-
|
| 261 |
======================================================================
|
| 262 |
|
| 263 |
You can:
|
|
@@ -295,7 +299,7 @@ Your elevated glucose and HbA1c indicate Type 2 Diabetes...
|
|
| 295 |
|
| 296 |
---
|
| 297 |
|
| 298 |
-
## Getting Help
|
| 299 |
|
| 300 |
- **Issues**: [GitHub Issues](https://github.com/yourusername/RagBot/issues)
|
| 301 |
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/RagBot/discussions)
|
|
@@ -303,11 +307,11 @@ Your elevated glucose and HbA1c indicate Type 2 Diabetes...
|
|
| 303 |
|
| 304 |
---
|
| 305 |
|
| 306 |
-
## Quick Reference Card
|
| 307 |
|
| 308 |
```
|
| 309 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 310 |
-
β
|
| 311 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
| 312 |
β START CHAT: python scripts/chat.py β
|
| 313 |
β START API: cd api && uvicorn app.main:app --reload β
|
|
@@ -320,8 +324,10 @@ Your elevated glucose and HbA1c indicate Type 2 Diabetes...
|
|
| 320 |
β quit - Exit β
|
| 321 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
| 322 |
β SUPPORTED BIOMARKERS: 24 total β
|
| 323 |
-
β Glucose, HbA1c, Cholesterol, LDL
|
| 324 |
-
β
|
|
|
|
|
|
|
| 325 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
| 326 |
β DETECTED DISEASES: 5 types β
|
| 327 |
β Diabetes, Anemia, Heart Disease, β
|
|
@@ -331,4 +337,4 @@ Your elevated glucose and HbA1c indicate Type 2 Diabetes...
|
|
| 331 |
|
| 332 |
---
|
| 333 |
|
| 334 |
-
**Ready to
|
|
|
|
| 1 |
+
# Quick Start Guide - RagBot
|
| 2 |
|
| 3 |
Get up and running in **5 minutes**!
|
| 4 |
|
| 5 |
+
## Step 1: Prerequisites
|
| 6 |
|
| 7 |
Before you begin, ensure you have:
|
| 8 |
|
| 9 |
+
- **Python 3.11+** installed ([Download](https://www.python.org/downloads/))
|
| 10 |
+
- **Git** installed ([Download](https://git-scm.com/downloads))
|
| 11 |
+
- **FREE API Key** from one of:
|
| 12 |
- [Groq](https://console.groq.com/keys) - Recommended (Fast & Free)
|
| 13 |
- [Google Gemini](https://aistudio.google.com/app/apikey) - Alternative
|
| 14 |
|
| 15 |
**System Requirements:**
|
| 16 |
- 4GB+ RAM
|
| 17 |
- 2GB free disk space
|
| 18 |
+
- No GPU required
|
| 19 |
|
| 20 |
---
|
| 21 |
|
| 22 |
+
## Step 2: Installation
|
| 23 |
|
| 24 |
### Clone the Repository
|
| 25 |
|
|
|
|
| 52 |
|
| 53 |
---
|
| 54 |
|
| 55 |
+
## Step 3: Configuration
|
| 56 |
|
| 57 |
### Copy Environment Template
|
| 58 |
|
|
|
|
| 95 |
|
| 96 |
---
|
| 97 |
|
| 98 |
+
## Step 4: Verify Installation
|
| 99 |
|
| 100 |
Quick system check:
|
| 101 |
|
|
|
|
| 112 |
|
| 113 |
---
|
| 114 |
|
| 115 |
+
## Step 5: Run Your First Analysis
|
| 116 |
|
| 117 |
### Interactive Chat Mode
|
| 118 |
|
|
|
|
| 134 |
|
| 135 |
---
|
| 136 |
|
| 137 |
+
## Common Commands
|
| 138 |
|
| 139 |
### Chat Interface
|
| 140 |
```bash
|
|
|
|
| 150 |
### Python API
|
| 151 |
```python
|
| 152 |
from src.workflow import create_guild
|
|
|
|
| 153 |
|
| 154 |
# Create the guild
|
| 155 |
guild = create_guild()
|
| 156 |
|
| 157 |
# Analyze biomarkers
|
| 158 |
+
result = guild.run({
|
| 159 |
+
"biomarkers": {"Glucose": 185, "HbA1c": 8.2},
|
| 160 |
+
"model_prediction": {"disease": "Diabetes", "confidence": 0.87},
|
| 161 |
+
"patient_context": {"age": 52, "gender": "male"}
|
| 162 |
+
})
|
| 163 |
|
| 164 |
print(result)
|
| 165 |
```
|
|
|
|
| 176 |
|
| 177 |
---
|
| 178 |
|
| 179 |
+
## Troubleshooting
|
| 180 |
|
| 181 |
### Import Error: "No module named 'langchain'"
|
| 182 |
|
|
|
|
| 223 |
|
| 224 |
---
|
| 225 |
|
| 226 |
+
## Next Steps
|
| 227 |
|
| 228 |
### Learn More
|
| 229 |
|
| 230 |
- **[Full Documentation](README.md)** - Complete system overview
|
| 231 |
+
- **[API Guide](docs/API.md)** - REST API documentation
|
| 232 |
- **[Contributing](CONTRIBUTING.md)** - How to contribute
|
| 233 |
+
- **[Architecture](docs/ARCHITECTURE.md)** - Deep dive into system design
|
| 234 |
|
| 235 |
### Customize
|
| 236 |
|
|
|
|
| 241 |
### Run Tests
|
| 242 |
|
| 243 |
```bash
|
| 244 |
+
# Run unit tests (30 tests, no API keys needed)
|
| 245 |
+
.venv\Scripts\python.exe -m pytest tests/ -q \
|
| 246 |
+
--ignore=tests/test_basic.py \
|
| 247 |
+
--ignore=tests/test_diabetes_patient.py \
|
| 248 |
+
--ignore=tests/test_evolution_loop.py \
|
| 249 |
+
--ignore=tests/test_evolution_quick.py \
|
| 250 |
+
--ignore=tests/test_evaluation_system.py
|
| 251 |
+
|
| 252 |
+
# Run integration tests (requires Groq/Gemini API key)
|
| 253 |
+
.venv\Scripts\python.exe -m pytest tests/test_diabetes_patient.py -v
|
| 254 |
```
|
| 255 |
|
| 256 |
---
|
| 257 |
|
| 258 |
+
## Example Session
|
| 259 |
|
| 260 |
```
|
| 261 |
$ python scripts/chat.py
|
| 262 |
|
| 263 |
======================================================================
|
| 264 |
+
RagBot - Interactive Chat
|
| 265 |
======================================================================
|
| 266 |
|
| 267 |
You can:
|
|
|
|
| 299 |
|
| 300 |
---
|
| 301 |
|
| 302 |
+
## Getting Help
|
| 303 |
|
| 304 |
- **Issues**: [GitHub Issues](https://github.com/yourusername/RagBot/issues)
|
| 305 |
- **Discussions**: [GitHub Discussions](https://github.com/yourusername/RagBot/discussions)
|
|
|
|
| 307 |
|
| 308 |
---
|
| 309 |
|
| 310 |
+
## Quick Reference Card
|
| 311 |
|
| 312 |
```
|
| 313 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 314 |
+
β RagBot Cheat Sheet β
|
| 315 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
| 316 |
β START CHAT: python scripts/chat.py β
|
| 317 |
β START API: cd api && uvicorn app.main:app --reload β
|
|
|
|
| 324 |
β quit - Exit β
|
| 325 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
| 326 |
β SUPPORTED BIOMARKERS: 24 total β
|
| 327 |
+
β Glucose, HbA1c, Cholesterol, LDL Cholesterol, β
|
| 328 |
+
β HDL Cholesterol, Triglycerides, Hemoglobin, β
|
| 329 |
+
β Platelets, White Blood Cells, Red Blood Cells, β
|
| 330 |
+
β BMI, Systolic Blood Pressure, and more... β
|
| 331 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
| 332 |
β DETECTED DISEASES: 5 types β
|
| 333 |
β Diabetes, Anemia, Heart Disease, β
|
|
|
|
| 337 |
|
| 338 |
---
|
| 339 |
|
| 340 |
+
**Ready to analyze biomarkers? Let's go!**
|
README.md
CHANGED
|
@@ -2,16 +2,17 @@
|
|
| 2 |
|
| 3 |
A production-ready biomarker analysis system combining 6 specialized AI agents with medical knowledge retrieval to provide evidence-based insights on blood test results in **15-25 seconds**.
|
| 4 |
|
| 5 |
-
##
|
| 6 |
|
| 7 |
- **6 Specialist Agents** - Biomarker validation, disease prediction, RAG-powered analysis, confidence assessment
|
| 8 |
-
- **Medical Knowledge Base** - 750+ pages of clinical guidelines (FAISS vector store
|
| 9 |
- **Multiple Interfaces** - Interactive CLI chat, REST API, ready for web/mobile integration
|
| 10 |
- **Evidence-Based** - All recommendations backed by retrieved medical literature
|
| 11 |
-
- **Free
|
| 12 |
-
- **
|
|
|
|
| 13 |
|
| 14 |
-
##
|
| 15 |
|
| 16 |
**Installation (5 minutes):**
|
| 17 |
|
|
@@ -36,7 +37,7 @@ python scripts/chat.py
|
|
| 36 |
|
| 37 |
See **[QUICKSTART.md](QUICKSTART.md)** for detailed setup instructions.
|
| 38 |
|
| 39 |
-
##
|
| 40 |
|
| 41 |
| Document | Purpose |
|
| 42 |
|----------|---------|
|
|
@@ -48,7 +49,7 @@ See **[QUICKSTART.md](QUICKSTART.md)** for detailed setup instructions.
|
|
| 48 |
| [**scripts/README.md**](scripts/README.md) | Utility scripts reference |
|
| 49 |
| [**examples/README.md**](examples/) | Web/mobile integration examples |
|
| 50 |
|
| 51 |
-
##
|
| 52 |
|
| 53 |
### Interactive CLI
|
| 54 |
|
|
@@ -57,116 +58,134 @@ python scripts/chat.py
|
|
| 57 |
|
| 58 |
You: My glucose is 140 and HbA1c is 10
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
```
|
| 65 |
|
| 66 |
### REST API
|
| 67 |
|
| 68 |
```bash
|
| 69 |
# Start server
|
| 70 |
-
|
|
|
|
| 71 |
|
| 72 |
-
#
|
| 73 |
-
curl -X POST http://localhost:8000/api/v1/analyze \
|
| 74 |
-H "Content-Type: application/json" \
|
| 75 |
-d '{
|
| 76 |
"biomarkers": {"Glucose": 140, "HbA1c": 10.0}
|
| 77 |
}'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
```
|
| 79 |
|
| 80 |
See **[docs/API.md](docs/API.md)** for full API reference.
|
| 81 |
|
| 82 |
-
##
|
| 83 |
|
| 84 |
```
|
| 85 |
RagBot/
|
| 86 |
βββ src/ # Core application
|
|
|
|
| 87 |
β βββ workflow.py # Multi-agent orchestration (LangGraph)
|
|
|
|
| 88 |
β βββ biomarker_validator.py # Validation logic
|
|
|
|
|
|
|
| 89 |
β βββ pdf_processor.py # Vector store management
|
|
|
|
| 90 |
β βββ agents/ # 6 specialist agents
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
β
|
| 92 |
-
βββ api/ # REST API (
|
| 93 |
β βββ app/main.py # FastAPI server
|
| 94 |
-
β
|
|
|
|
|
|
|
| 95 |
β
|
| 96 |
βββ scripts/ # Utilities
|
| 97 |
-
β βββ chat.py # Interactive CLI
|
| 98 |
β βββ setup_embeddings.py # Vector store builder
|
| 99 |
β
|
| 100 |
βββ config/ # Configuration
|
| 101 |
-
β βββ biomarker_references.json #
|
| 102 |
β
|
| 103 |
βββ data/ # Data storage
|
| 104 |
β βββ medical_pdfs/ # Source documents
|
| 105 |
β βββ vector_stores/ # FAISS database
|
| 106 |
β
|
| 107 |
-
βββ tests/ # Test suite
|
| 108 |
βββ examples/ # Integration examples
|
| 109 |
βββ docs/ # Documentation
|
| 110 |
-
β βββ ARCHITECTURE.md # System design
|
| 111 |
-
β βββ API.md # API reference
|
| 112 |
-
β βββ DEVELOPMENT.md # Development guide
|
| 113 |
-
β βββ archive/ # Old docs
|
| 114 |
-
β βββ plans/ # Planning docs
|
| 115 |
β
|
| 116 |
βββ QUICKSTART.md # Setup guide
|
| 117 |
βββ CONTRIBUTING.md # Contribution guidelines
|
| 118 |
βββ requirements.txt # Python dependencies
|
| 119 |
-
βββ .env.template # Configuration template
|
| 120 |
βββ LICENSE
|
| 121 |
```
|
| 122 |
|
| 123 |
-
##
|
| 124 |
|
| 125 |
| Component | Technology | Purpose |
|
| 126 |
|-----------|-----------|---------|
|
| 127 |
| Orchestration | **LangGraph** | Multi-agent workflow control |
|
| 128 |
| LLM | **Groq (LLaMA 3.3-70B)** | Fast, free inference |
|
| 129 |
-
|
|
|
|
|
| 130 |
| Vector DB | **FAISS** | Efficient similarity search |
|
| 131 |
| API | **FastAPI** | REST endpoints |
|
| 132 |
-
|
|
| 133 |
|
| 134 |
-
##
|
| 135 |
|
| 136 |
```
|
| 137 |
User Input ("My glucose is 140...")
|
| 138 |
-
|
| 139 |
-
[Biomarker Extraction]
|
| 140 |
-
|
| 141 |
-
[
|
| 142 |
-
|
| 143 |
-
[RAG Retrieval]
|
| 144 |
-
|
| 145 |
-
[6
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
[Output]
|
| 154 |
```
|
| 155 |
|
| 156 |
-
##
|
| 157 |
|
| 158 |
-
|
| 159 |
-
- **
|
| 160 |
-
- **
|
| 161 |
-
- **
|
| 162 |
-
- **
|
| 163 |
-
- **
|
| 164 |
-
- **
|
| 165 |
-
-
|
|
|
|
| 166 |
|
| 167 |
-
See
|
| 168 |
|
| 169 |
-
##
|
| 170 |
|
| 171 |
- Diabetes
|
| 172 |
- Anemia
|
|
@@ -175,48 +194,40 @@ See `config/biomarker_references.json` for complete list.
|
|
| 175 |
- Thalassemia
|
| 176 |
- (Extensible - add custom domains)
|
| 177 |
|
| 178 |
-
##
|
| 179 |
|
| 180 |
- All processing runs **locally** after setup
|
| 181 |
-
- No personal health data
|
| 182 |
- Embeddings computed locally or cached
|
| 183 |
-
- Fully **HIPAA-compliant** architecture ready
|
| 184 |
- Vector store derived from public medical literature
|
| 185 |
-
- Can operate completely offline
|
| 186 |
|
| 187 |
-
##
|
| 188 |
|
| 189 |
-
- **Response Time**: 15-25 seconds (
|
| 190 |
-
- **Knowledge Base**: 750 pages
|
| 191 |
-
- **
|
| 192 |
-
- **Cost**: Free (Groq API + local embeddings)
|
| 193 |
- **Hardware**: CPU-only (no GPU needed)
|
| 194 |
|
| 195 |
-
##
|
| 196 |
-
|
| 197 |
-
1. **CLI** - Interactive chatbot (development/testing)
|
| 198 |
-
2. **REST API** - FastAPI server (production)
|
| 199 |
-
3. **Docker** - Containerized deployment
|
| 200 |
-
4. **Embedded** - Direct Python library import
|
| 201 |
-
5. **Web** - JavaScript/React integration
|
| 202 |
-
6. **Mobile** - React Native / Flutter
|
| 203 |
-
|
| 204 |
-
See **[examples/README.md](examples/)** for integration patterns.
|
| 205 |
-
|
| 206 |
-
## π§ͺ Testing
|
| 207 |
|
| 208 |
```bash
|
| 209 |
-
# Run
|
| 210 |
-
pytest tests/ -
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
```
|
| 218 |
|
| 219 |
-
##
|
| 220 |
|
| 221 |
Contributions welcome! See **[CONTRIBUTING.md](CONTRIBUTING.md)** for:
|
| 222 |
- Code style guidelines
|
|
@@ -224,7 +235,7 @@ Contributions welcome! See **[CONTRIBUTING.md](CONTRIBUTING.md)** for:
|
|
| 224 |
- Testing requirements
|
| 225 |
- Development setup
|
| 226 |
|
| 227 |
-
##
|
| 228 |
|
| 229 |
Want to extend RagBot?
|
| 230 |
|
|
@@ -233,17 +244,11 @@ Want to extend RagBot?
|
|
| 233 |
- **Create custom agents**: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#creating-a-custom-analysis-agent)
|
| 234 |
- **Switch LLM providers**: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#switching-llm-providers)
|
| 235 |
|
| 236 |
-
##
|
| 237 |
|
| 238 |
MIT License - See [LICENSE](LICENSE)
|
| 239 |
|
| 240 |
-
##
|
| 241 |
-
|
| 242 |
-
- **Issues**: GitHub Issues for bugs and feature requests
|
| 243 |
-
- **Discussion**: GitHub Discussions for questions
|
| 244 |
-
- **Docs**: Full documentation in `/docs` folder
|
| 245 |
-
|
| 246 |
-
## π Resources
|
| 247 |
|
| 248 |
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
|
| 249 |
- [Groq API Docs](https://console.groq.com)
|
|
@@ -252,8 +257,8 @@ MIT License - See [LICENSE](LICENSE)
|
|
| 252 |
|
| 253 |
---
|
| 254 |
|
| 255 |
-
**Ready to get started?**
|
| 256 |
|
| 257 |
-
**Want to understand the architecture?**
|
| 258 |
|
| 259 |
-
**Looking to integrate with your app?**
|
|
|
|
| 2 |
|
| 3 |
A production-ready biomarker analysis system combining 6 specialized AI agents with medical knowledge retrieval to provide evidence-based insights on blood test results in **15-25 seconds**.
|
| 4 |
|
| 5 |
+
## Key Features
|
| 6 |
|
| 7 |
- **6 Specialist Agents** - Biomarker validation, disease prediction, RAG-powered analysis, confidence assessment
|
| 8 |
+
- **Medical Knowledge Base** - 750+ pages of clinical guidelines (FAISS vector store)
|
| 9 |
- **Multiple Interfaces** - Interactive CLI chat, REST API, ready for web/mobile integration
|
| 10 |
- **Evidence-Based** - All recommendations backed by retrieved medical literature
|
| 11 |
+
- **Free Cloud LLMs** - Uses Groq (LLaMA 3.3-70B) or Google Gemini - no cost
|
| 12 |
+
- **Biomarker Normalization** - 80+ aliases mapped to 24 canonical biomarker names
|
| 13 |
+
- **Production-Ready** - Full error handling, safety alerts, confidence scoring, 30 unit tests
|
| 14 |
|
| 15 |
+
## Quick Start
|
| 16 |
|
| 17 |
**Installation (5 minutes):**
|
| 18 |
|
|
|
|
| 37 |
|
| 38 |
See **[QUICKSTART.md](QUICKSTART.md)** for detailed setup instructions.
|
| 39 |
|
| 40 |
+
## Documentation
|
| 41 |
|
| 42 |
| Document | Purpose |
|
| 43 |
|----------|---------|
|
|
|
|
| 49 |
| [**scripts/README.md**](scripts/README.md) | Utility scripts reference |
|
| 50 |
| [**examples/README.md**](examples/) | Web/mobile integration examples |
|
| 51 |
|
| 52 |
+
## Usage
|
| 53 |
|
| 54 |
### Interactive CLI
|
| 55 |
|
|
|
|
| 58 |
|
| 59 |
You: My glucose is 140 and HbA1c is 10
|
| 60 |
|
| 61 |
+
Primary Finding: Diabetes (100% confidence)
|
| 62 |
+
Critical Alerts: Hyperglycemia, elevated HbA1c
|
| 63 |
+
Recommendations: Seek medical attention, lifestyle changes
|
| 64 |
+
Actions: Physical activity, reduce carbs, weight loss
|
| 65 |
```
|
| 66 |
|
| 67 |
### REST API
|
| 68 |
|
| 69 |
```bash
|
| 70 |
# Start server
|
| 71 |
+
cd api
|
| 72 |
+
python -m uvicorn app.main:app
|
| 73 |
|
| 74 |
+
# Analyze biomarkers (structured input)
|
| 75 |
+
curl -X POST http://localhost:8000/api/v1/analyze/structured \
|
| 76 |
-H "Content-Type: application/json" \
|
| 77 |
-d '{
|
| 78 |
"biomarkers": {"Glucose": 140, "HbA1c": 10.0}
|
| 79 |
}'
|
| 80 |
+
|
| 81 |
+
# Analyze biomarkers (natural language)
|
| 82 |
+
curl -X POST http://localhost:8000/api/v1/analyze/natural \
|
| 83 |
+
-H "Content-Type: application/json" \
|
| 84 |
+
-d '{
|
| 85 |
+
"message": "My glucose is 140 and HbA1c is 10"
|
| 86 |
+
}'
|
| 87 |
```
|
| 88 |
|
| 89 |
See **[docs/API.md](docs/API.md)** for full API reference.
|
| 90 |
|
| 91 |
+
## Project Structure
|
| 92 |
|
| 93 |
```
|
| 94 |
RagBot/
|
| 95 |
βββ src/ # Core application
|
| 96 |
+
β βββ __init__.py
|
| 97 |
β βββ workflow.py # Multi-agent orchestration (LangGraph)
|
| 98 |
+
β βββ state.py # Pydantic state models
|
| 99 |
β βββ biomarker_validator.py # Validation logic
|
| 100 |
+
β βββ biomarker_normalization.py # Name normalization (80+ aliases)
|
| 101 |
+
β βββ llm_config.py # LLM/embedding provider config
|
| 102 |
β βββ pdf_processor.py # Vector store management
|
| 103 |
+
β βββ config.py # Global configuration
|
| 104 |
β βββ agents/ # 6 specialist agents
|
| 105 |
+
β βββ __init__.py
|
| 106 |
+
β βββ biomarker_analyzer.py
|
| 107 |
+
β βββ disease_explainer.py
|
| 108 |
+
β βββ biomarker_linker.py
|
| 109 |
+
β βββ clinical_guidelines.py
|
| 110 |
+
β βββ confidence_assessor.py
|
| 111 |
+
β βββ response_synthesizer.py
|
| 112 |
β
|
| 113 |
+
βββ api/ # REST API (FastAPI)
|
| 114 |
β βββ app/main.py # FastAPI server
|
| 115 |
+
β βββ app/routes/ # API endpoints
|
| 116 |
+
β βββ app/models/schemas.py # Pydantic request/response schemas
|
| 117 |
+
β βββ app/services/ # Business logic
|
| 118 |
β
|
| 119 |
βββ scripts/ # Utilities
|
| 120 |
+
β βββ chat.py # Interactive CLI chatbot
|
| 121 |
β βββ setup_embeddings.py # Vector store builder
|
| 122 |
β
|
| 123 |
βββ config/ # Configuration
|
| 124 |
+
β βββ biomarker_references.json # 24 biomarker reference ranges
|
| 125 |
β
|
| 126 |
βββ data/ # Data storage
|
| 127 |
β βββ medical_pdfs/ # Source documents
|
| 128 |
β βββ vector_stores/ # FAISS database
|
| 129 |
β
|
| 130 |
+
βββ tests/ # Test suite (30 tests)
|
| 131 |
βββ examples/ # Integration examples
|
| 132 |
βββ docs/ # Documentation
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
β
|
| 134 |
βββ QUICKSTART.md # Setup guide
|
| 135 |
βββ CONTRIBUTING.md # Contribution guidelines
|
| 136 |
βββ requirements.txt # Python dependencies
|
|
|
|
| 137 |
βββ LICENSE
|
| 138 |
```
|
| 139 |
|
| 140 |
+
## Technology Stack
|
| 141 |
|
| 142 |
| Component | Technology | Purpose |
|
| 143 |
|-----------|-----------|---------|
|
| 144 |
| Orchestration | **LangGraph** | Multi-agent workflow control |
|
| 145 |
| LLM | **Groq (LLaMA 3.3-70B)** | Fast, free inference |
|
| 146 |
+
| LLM (Alt) | **Google Gemini 2.0 Flash** | Free alternative |
|
| 147 |
+
| Embeddings | **Google Gemini / HuggingFace** | Vector representations |
|
| 148 |
| Vector DB | **FAISS** | Efficient similarity search |
|
| 149 |
| API | **FastAPI** | REST endpoints |
|
| 150 |
+
| Validation | **Pydantic V2** | Type safety & schemas |
|
| 151 |
|
| 152 |
+
## How It Works
|
| 153 |
|
| 154 |
```
|
| 155 |
User Input ("My glucose is 140...")
|
| 156 |
+
|
|
| 157 |
+
[Biomarker Extraction] -> Parse & normalize (80+ aliases)
|
| 158 |
+
|
|
| 159 |
+
[Disease Prediction] -> Rule-based + LLM hypothesis
|
| 160 |
+
|
|
| 161 |
+
[RAG Retrieval] -> Get medical docs from FAISS vector store
|
| 162 |
+
|
|
| 163 |
+
[6 Agent Pipeline via LangGraph]
|
| 164 |
+
|-- Biomarker Analyzer (validation + safety alerts)
|
| 165 |
+
|-- Disease Explainer (RAG pathophysiology)
|
| 166 |
+
|-- Biomarker-Disease Linker (RAG key drivers)
|
| 167 |
+
|-- Clinical Guidelines (RAG recommendations)
|
| 168 |
+
|-- Confidence Assessor (reliability scoring)
|
| 169 |
+
+-- Response Synthesizer (final structured report)
|
| 170 |
+
|
|
| 171 |
+
[Output] -> Comprehensive report with safety alerts
|
| 172 |
```
|
| 173 |
|
| 174 |
+
## Supported Biomarkers (24)
|
| 175 |
|
| 176 |
+
- **Glucose Control**: Glucose, HbA1c, Insulin
|
| 177 |
+
- **Lipids**: Cholesterol, LDL Cholesterol, HDL Cholesterol, Triglycerides
|
| 178 |
+
- **Body Metrics**: BMI
|
| 179 |
+
- **Blood Cells**: Hemoglobin, Platelets, White Blood Cells, Red Blood Cells, Hematocrit
|
| 180 |
+
- **RBC Indices**: Mean Corpuscular Volume, Mean Corpuscular Hemoglobin, MCHC
|
| 181 |
+
- **Cardiovascular**: Heart Rate, Systolic Blood Pressure, Diastolic Blood Pressure, Troponin
|
| 182 |
+
- **Inflammation**: C-reactive Protein
|
| 183 |
+
- **Liver**: ALT, AST
|
| 184 |
+
- **Kidney**: Creatinine
|
| 185 |
|
| 186 |
+
See [config/biomarker_references.json](config/biomarker_references.json) for full reference ranges.
|
| 187 |
|
| 188 |
+
## Disease Coverage
|
| 189 |
|
| 190 |
- Diabetes
|
| 191 |
- Anemia
|
|
|
|
| 194 |
- Thalassemia
|
| 195 |
- (Extensible - add custom domains)
|
| 196 |
|
| 197 |
+
## Privacy & Security
|
| 198 |
|
| 199 |
- All processing runs **locally** after setup
|
| 200 |
+
- No personal health data stored
|
| 201 |
- Embeddings computed locally or cached
|
|
|
|
| 202 |
- Vector store derived from public medical literature
|
| 203 |
+
- Can operate completely offline with Ollama provider
|
| 204 |
|
| 205 |
+
## Performance
|
| 206 |
|
| 207 |
+
- **Response Time**: 15-25 seconds (6 agents + RAG retrieval)
|
| 208 |
+
- **Knowledge Base**: 750 pages, 2,609 document chunks
|
| 209 |
+
- **Cost**: Free (Groq/Gemini API + local/cloud embeddings)
|
|
|
|
| 210 |
- **Hardware**: CPU-only (no GPU needed)
|
| 211 |
|
| 212 |
+
## Testing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
```bash
|
| 215 |
+
# Run unit tests (30 tests)
|
| 216 |
+
.venv\Scripts\python.exe -m pytest tests/ -q \
|
| 217 |
+
--ignore=tests/test_basic.py \
|
| 218 |
+
--ignore=tests/test_diabetes_patient.py \
|
| 219 |
+
--ignore=tests/test_evolution_loop.py \
|
| 220 |
+
--ignore=tests/test_evolution_quick.py \
|
| 221 |
+
--ignore=tests/test_evaluation_system.py
|
| 222 |
+
|
| 223 |
+
# Run specific test file
|
| 224 |
+
.venv\Scripts\python.exe -m pytest tests/test_codebase_fixes.py -v
|
| 225 |
+
|
| 226 |
+
# Run all tests (includes integration tests requiring LLM API keys)
|
| 227 |
+
.venv\Scripts\python.exe -m pytest tests/ -v
|
| 228 |
```
|
| 229 |
|
| 230 |
+
## Contributing
|
| 231 |
|
| 232 |
Contributions welcome! See **[CONTRIBUTING.md](CONTRIBUTING.md)** for:
|
| 233 |
- Code style guidelines
|
|
|
|
| 235 |
- Testing requirements
|
| 236 |
- Development setup
|
| 237 |
|
| 238 |
+
## Development
|
| 239 |
|
| 240 |
Want to extend RagBot?
|
| 241 |
|
|
|
|
| 244 |
- **Create custom agents**: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#creating-a-custom-analysis-agent)
|
| 245 |
- **Switch LLM providers**: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md#switching-llm-providers)
|
| 246 |
|
| 247 |
+
## License
|
| 248 |
|
| 249 |
MIT License - See [LICENSE](LICENSE)
|
| 250 |
|
| 251 |
+
## Resources
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
|
| 254 |
- [Groq API Docs](https://console.groq.com)
|
|
|
|
| 257 |
|
| 258 |
---
|
| 259 |
|
| 260 |
+
**Ready to get started?** -> [QUICKSTART.md](QUICKSTART.md)
|
| 261 |
|
| 262 |
+
**Want to understand the architecture?** -> [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
| 263 |
|
| 264 |
+
**Looking to integrate with your app?** -> [examples/README.md](examples/)
|
START_HERE.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Start Here β RagBot
|
| 2 |
+
|
| 3 |
+
Welcome to **RagBot**, a multi-agent RAG system for medical biomarker analysis.
|
| 4 |
+
|
| 5 |
+
## 5-Minute Setup
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
# 1. Clone and install
|
| 9 |
+
git clone https://github.com/yourusername/ragbot.git
|
| 10 |
+
cd ragbot
|
| 11 |
+
python -m venv .venv
|
| 12 |
+
.venv\Scripts\activate # Windows
|
| 13 |
+
pip install -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# 2. Add your free API key to .env
|
| 16 |
+
# Get one at https://console.groq.com/keys (Groq, recommended)
|
| 17 |
+
# or https://aistudio.google.com/app/apikey (Google Gemini)
|
| 18 |
+
cp .env.template .env
|
| 19 |
+
# Edit .env with your key
|
| 20 |
+
|
| 21 |
+
# 3. Start chatting
|
| 22 |
+
python scripts/chat.py
|
| 23 |
+
```
|
| 24 |
+
|
| 25 |
+
For the full walkthrough, see [QUICKSTART.md](QUICKSTART.md).
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## Key Documentation
|
| 30 |
+
|
| 31 |
+
| Document | What it covers |
|
| 32 |
+
|----------|----------------|
|
| 33 |
+
| [QUICKSTART.md](QUICKSTART.md) | Detailed setup, configuration, troubleshooting |
|
| 34 |
+
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | System design, agent pipeline, data flow |
|
| 35 |
+
| [docs/API.md](docs/API.md) | REST API endpoints and usage examples |
|
| 36 |
+
| [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | Extending the system β new biomarkers, agents, domains |
|
| 37 |
+
| [CONTRIBUTING.md](CONTRIBUTING.md) | Code style, PR process, testing guidelines |
|
| 38 |
+
| [scripts/README.md](scripts/README.md) | CLI scripts and utilities |
|
| 39 |
+
| [examples/README.md](examples/) | Web/mobile integration examples |
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## Project at a Glance
|
| 44 |
+
|
| 45 |
+
- **6 specialist AI agents** orchestrated via LangGraph
|
| 46 |
+
- **24 supported biomarkers** with 80+ name aliases
|
| 47 |
+
- **FAISS vector store** over 750 pages of medical literature
|
| 48 |
+
- **Free LLM inference** via Groq (LLaMA 3.3-70B) or Google Gemini
|
| 49 |
+
- **Two interfaces**: interactive CLI chat + REST API (FastAPI)
|
| 50 |
+
- **30 unit tests** passing, Pydantic V2 throughout
|
| 51 |
+
|
| 52 |
+
---
|
| 53 |
+
|
| 54 |
+
## Quick Commands
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
# Interactive chat
|
| 58 |
+
python scripts/chat.py
|
| 59 |
+
|
| 60 |
+
# Run unit tests
|
| 61 |
+
.venv\Scripts\python.exe -m pytest tests/ -q ^
|
| 62 |
+
--ignore=tests/test_basic.py ^
|
| 63 |
+
--ignore=tests/test_diabetes_patient.py ^
|
| 64 |
+
--ignore=tests/test_evolution_loop.py ^
|
| 65 |
+
--ignore=tests/test_evolution_quick.py ^
|
| 66 |
+
--ignore=tests/test_evaluation_system.py
|
| 67 |
+
|
| 68 |
+
# Start REST API
|
| 69 |
+
cd api && python -m uvicorn app.main:app --reload
|
| 70 |
+
|
| 71 |
+
# Rebuild vector store (after adding new PDFs)
|
| 72 |
+
python scripts/setup_embeddings.py
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
## Need Help?
|
| 78 |
+
|
| 79 |
+
- Check [QUICKSTART.md β Troubleshooting](QUICKSTART.md#troubleshooting)
|
| 80 |
+
- Open a [GitHub Issue](https://github.com/yourusername/RagBot/issues)
|
api/ARCHITECTURE.md
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
# RagBot API - Architecture Diagrams
|
| 2 |
|
| 3 |
-
##
|
| 4 |
|
| 5 |
```
|
| 6 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
-
β
|
| 8 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
| 9 |
β β
|
| 10 |
β βββββββββββββββββββ ββββββββββββββββββββββββββββ β
|
| 11 |
-
β β
|
| 12 |
-
β β
|
| 13 |
β β β β β β
|
| 14 |
β β Models: β β Endpoints: β β
|
| 15 |
-
β β -
|
| 16 |
-
β β -
|
| 17 |
-
β β
|
| 18 |
β βββββββββββββββββββ βββββββββββββ¬βββββββββββββββ β
|
| 19 |
β β β
|
| 20 |
β βββββββββββββΌβββββββββββββββ β
|
|
@@ -24,7 +24,7 @@
|
|
| 24 |
β β - 6 Specialist Agents β β
|
| 25 |
β β - LangGraph Workflow β β
|
| 26 |
β β - FAISS Vector Store β β
|
| 27 |
-
β β - 2,
|
| 28 |
β ββββββββββββββββββββββββββββ β
|
| 29 |
β β
|
| 30 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 1 |
# RagBot API - Architecture Diagrams
|
| 2 |
|
| 3 |
+
## System Architecture
|
| 4 |
|
| 5 |
```
|
| 6 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
+
β RagBot API Server β
|
| 8 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
|
| 9 |
β β
|
| 10 |
β βββββββββββββββββββ ββββββββββββββββββββββββββββ β
|
| 11 |
+
β β Cloud LLM API ββββββββββββββββ€ FastAPI Server β β
|
| 12 |
+
β β (Groq/Gemini) β LLM Calls β Port: 8000 β β
|
| 13 |
β β β β β β
|
| 14 |
β β Models: β β Endpoints: β β
|
| 15 |
+
β β - LLaMA 3.3-70Bβ β - /api/v1/health β β
|
| 16 |
+
β β - Gemini Flash β β - /api/v1/biomarkers β β
|
| 17 |
+
β β (or Ollama) β β - /api/v1/analyze/* β β
|
| 18 |
β βββββββββββββββββββ βββββββββββββ¬βββββββββββββββ β
|
| 19 |
β β β
|
| 20 |
β βββββββββββββΌβββββββββββββββ β
|
|
|
|
| 24 |
β β - 6 Specialist Agents β β
|
| 25 |
β β - LangGraph Workflow β β
|
| 26 |
β β - FAISS Vector Store β β
|
| 27 |
+
β β - 2,609 medical chunks β β
|
| 28 |
β ββββββββββββββββββββββββββββ β
|
| 29 |
β β
|
| 30 |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
api/GETTING_STARTED.md
CHANGED
|
@@ -4,39 +4,31 @@ Follow these steps to get your API running in 5 minutes:
|
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
-
##
|
| 8 |
|
| 9 |
Before starting, ensure you have:
|
| 10 |
|
| 11 |
-
1. **
|
| 12 |
-
```powershell
|
| 13 |
-
# Check if Ollama is running
|
| 14 |
-
curl http://localhost:11434/api/version
|
| 15 |
-
|
| 16 |
-
# If not, start it
|
| 17 |
-
ollama serve
|
| 18 |
-
```
|
| 19 |
-
|
| 20 |
-
2. **Required models pulled**
|
| 21 |
-
```powershell
|
| 22 |
-
ollama list
|
| 23 |
-
|
| 24 |
-
# If missing, pull them
|
| 25 |
-
ollama pull llama3.1:8b-instruct
|
| 26 |
-
ollama pull qwen2:7b
|
| 27 |
-
```
|
| 28 |
-
|
| 29 |
-
3. **Python 3.11+**
|
| 30 |
```powershell
|
| 31 |
python --version
|
| 32 |
```
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
```powershell
|
| 36 |
# From RagBot root directory
|
| 37 |
pip install -r requirements.txt
|
| 38 |
```
|
| 39 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
---
|
| 41 |
|
| 42 |
## π Step 1: Install API Dependencies (30 seconds)
|
|
|
|
| 4 |
|
| 5 |
---
|
| 6 |
|
| 7 |
+
## Prerequisites
|
| 8 |
|
| 9 |
Before starting, ensure you have:
|
| 10 |
|
| 11 |
+
1. **Python 3.11+** installed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
```powershell
|
| 13 |
python --version
|
| 14 |
```
|
| 15 |
|
| 16 |
+
2. **A free API key** from one of:
|
| 17 |
+
- [Groq](https://console.groq.com/keys) β Recommended (fast, free LLaMA 3.3-70B)
|
| 18 |
+
- [Google Gemini](https://aistudio.google.com/app/apikey) β Alternative
|
| 19 |
+
|
| 20 |
+
3. **RagBot dependencies installed**
|
| 21 |
```powershell
|
| 22 |
# From RagBot root directory
|
| 23 |
pip install -r requirements.txt
|
| 24 |
```
|
| 25 |
|
| 26 |
+
4. **`.env` configured** in project root with your API key:
|
| 27 |
+
```
|
| 28 |
+
GROQ_API_KEY=gsk_...
|
| 29 |
+
LLM_PROVIDER=groq
|
| 30 |
+
```
|
| 31 |
+
|
| 32 |
---
|
| 33 |
|
| 34 |
## π Step 1: Install API Dependencies (30 seconds)
|
api/IMPLEMENTATION_COMPLETE.md
DELETED
|
@@ -1,452 +0,0 @@
|
|
| 1 |
-
# RagBot API - Implementation Complete β
|
| 2 |
-
|
| 3 |
-
**Date:** November 23, 2025
|
| 4 |
-
**Status:** β
COMPLETE - Ready to Run
|
| 5 |
-
|
| 6 |
-
---
|
| 7 |
-
|
| 8 |
-
## π¦ What Was Built
|
| 9 |
-
|
| 10 |
-
A complete FastAPI REST API that exposes your RagBot system for web integration.
|
| 11 |
-
|
| 12 |
-
### β
All 15 Tasks Completed
|
| 13 |
-
|
| 14 |
-
1. β
API folder structure created
|
| 15 |
-
2. β
Pydantic request/response models (comprehensive schemas)
|
| 16 |
-
3. β
Biomarker extraction service (natural language β JSON)
|
| 17 |
-
4. β
RagBot workflow wrapper (analysis orchestration)
|
| 18 |
-
5. β
Health check endpoint
|
| 19 |
-
6. β
Biomarkers list endpoint
|
| 20 |
-
7. β
Natural language analysis endpoint
|
| 21 |
-
8. β
Structured analysis endpoint
|
| 22 |
-
9. β
Example endpoint (pre-run diabetes case)
|
| 23 |
-
10. β
FastAPI main application (with CORS, error handling, logging)
|
| 24 |
-
11. β
requirements.txt
|
| 25 |
-
12. β
Dockerfile (multi-stage)
|
| 26 |
-
13. β
docker-compose.yml
|
| 27 |
-
14. β
Comprehensive README
|
| 28 |
-
15. β
.env configuration
|
| 29 |
-
|
| 30 |
-
**Bonus Files:**
|
| 31 |
-
- β
.gitignore
|
| 32 |
-
- β
test_api.ps1 (PowerShell test suite)
|
| 33 |
-
- β
QUICK_REFERENCE.md (cheat sheet)
|
| 34 |
-
|
| 35 |
-
---
|
| 36 |
-
|
| 37 |
-
## π Complete Structure
|
| 38 |
-
|
| 39 |
-
```
|
| 40 |
-
RagBot/
|
| 41 |
-
βββ api/ β NEW - Your API!
|
| 42 |
-
β βββ app/
|
| 43 |
-
β β βββ __init__.py
|
| 44 |
-
β β βββ main.py # FastAPI application
|
| 45 |
-
β β βββ models/
|
| 46 |
-
β β β βββ __init__.py
|
| 47 |
-
β β β βββ schemas.py # 15+ Pydantic models
|
| 48 |
-
β β βββ routes/
|
| 49 |
-
β β β βββ __init__.py
|
| 50 |
-
β β β βββ analyze.py # 3 analysis endpoints
|
| 51 |
-
β β β βββ biomarkers.py # List endpoint
|
| 52 |
-
β β β βββ health.py # Health check
|
| 53 |
-
β β βββ services/
|
| 54 |
-
β β βββ __init__.py
|
| 55 |
-
β β βββ extraction.py # Natural language extraction
|
| 56 |
-
β β βββ ragbot.py # Workflow wrapper (370 lines)
|
| 57 |
-
β βββ .env # Configuration (ready to use)
|
| 58 |
-
β βββ .env.example # Template
|
| 59 |
-
β βββ .gitignore
|
| 60 |
-
β βββ requirements.txt # FastAPI dependencies
|
| 61 |
-
β βββ Dockerfile # Multi-stage build
|
| 62 |
-
β βββ docker-compose.yml # One-command deployment
|
| 63 |
-
β βββ README.md # 500+ lines documentation
|
| 64 |
-
β βββ QUICK_REFERENCE.md # Cheat sheet
|
| 65 |
-
β βββ test_api.ps1 # Test suite
|
| 66 |
-
β
|
| 67 |
-
βββ [Original RagBot files unchanged]
|
| 68 |
-
```
|
| 69 |
-
|
| 70 |
-
---
|
| 71 |
-
|
| 72 |
-
## π― API Endpoints
|
| 73 |
-
|
| 74 |
-
### 5 Endpoints Ready to Use:
|
| 75 |
-
|
| 76 |
-
1. **GET /api/v1/health**
|
| 77 |
-
- Check API status
|
| 78 |
-
- Verify Ollama connection
|
| 79 |
-
- Vector store status
|
| 80 |
-
|
| 81 |
-
2. **GET /api/v1/biomarkers**
|
| 82 |
-
- List all 24 supported biomarkers
|
| 83 |
-
- Reference ranges
|
| 84 |
-
- Clinical significance
|
| 85 |
-
|
| 86 |
-
3. **POST /api/v1/analyze/natural**
|
| 87 |
-
- Natural language input
|
| 88 |
-
- LLM extraction
|
| 89 |
-
- Full detailed analysis
|
| 90 |
-
|
| 91 |
-
4. **POST /api/v1/analyze/structured**
|
| 92 |
-
- Direct JSON biomarkers
|
| 93 |
-
- Skip extraction
|
| 94 |
-
- Full detailed analysis
|
| 95 |
-
|
| 96 |
-
5. **GET /api/v1/example**
|
| 97 |
-
- Pre-run diabetes case
|
| 98 |
-
- Testing/demo
|
| 99 |
-
- Same as CLI `example` command
|
| 100 |
-
|
| 101 |
-
---
|
| 102 |
-
|
| 103 |
-
## π How to Run
|
| 104 |
-
|
| 105 |
-
### Option 1: Local Development
|
| 106 |
-
|
| 107 |
-
```powershell
|
| 108 |
-
# From api/ directory
|
| 109 |
-
cd C:\Users\admin\OneDrive\Documents\GitHub\RagBot\api
|
| 110 |
-
|
| 111 |
-
# Install dependencies (first time only)
|
| 112 |
-
pip install -r ../requirements.txt
|
| 113 |
-
pip install -r requirements.txt
|
| 114 |
-
|
| 115 |
-
# Start Ollama (in separate terminal)
|
| 116 |
-
ollama serve
|
| 117 |
-
|
| 118 |
-
# Start API
|
| 119 |
-
python -m uvicorn app.main:app --reload --port 8000
|
| 120 |
-
```
|
| 121 |
-
|
| 122 |
-
**API will be at:** http://localhost:8000
|
| 123 |
-
|
| 124 |
-
### Option 2: Docker (One Command)
|
| 125 |
-
|
| 126 |
-
```powershell
|
| 127 |
-
cd C:\Users\admin\OneDrive\Documents\GitHub\RagBot\api
|
| 128 |
-
docker-compose up --build
|
| 129 |
-
```
|
| 130 |
-
|
| 131 |
-
**API will be at:** http://localhost:8000
|
| 132 |
-
|
| 133 |
-
---
|
| 134 |
-
|
| 135 |
-
## β
Test Your API
|
| 136 |
-
|
| 137 |
-
### Quick Test (PowerShell)
|
| 138 |
-
```powershell
|
| 139 |
-
.\test_api.ps1
|
| 140 |
-
```
|
| 141 |
-
|
| 142 |
-
This runs 6 tests:
|
| 143 |
-
1. β
API online check
|
| 144 |
-
2. β
Health check
|
| 145 |
-
3. β
Biomarkers list
|
| 146 |
-
4. β
Example endpoint
|
| 147 |
-
5. β
Structured analysis
|
| 148 |
-
6. β
Natural language analysis
|
| 149 |
-
|
| 150 |
-
### Manual Test (cURL)
|
| 151 |
-
```bash
|
| 152 |
-
# Health check
|
| 153 |
-
curl http://localhost:8000/api/v1/health
|
| 154 |
-
|
| 155 |
-
# Get example
|
| 156 |
-
curl http://localhost:8000/api/v1/example
|
| 157 |
-
|
| 158 |
-
# Natural language analysis
|
| 159 |
-
curl -X POST http://localhost:8000/api/v1/analyze/natural \
|
| 160 |
-
-H "Content-Type: application/json" \
|
| 161 |
-
-d "{\"message\": \"My glucose is 185 and HbA1c is 8.2\"}"
|
| 162 |
-
```
|
| 163 |
-
|
| 164 |
-
---
|
| 165 |
-
|
| 166 |
-
## π Documentation
|
| 167 |
-
|
| 168 |
-
Once running, visit:
|
| 169 |
-
- **Swagger UI:** http://localhost:8000/docs
|
| 170 |
-
- **ReDoc:** http://localhost:8000/redoc
|
| 171 |
-
- **API Info:** http://localhost:8000/
|
| 172 |
-
|
| 173 |
-
---
|
| 174 |
-
|
| 175 |
-
## π¨ Response Format
|
| 176 |
-
|
| 177 |
-
**Full Detailed Response Includes:**
|
| 178 |
-
- β
Extracted biomarkers (if natural language)
|
| 179 |
-
- β
Disease prediction with confidence
|
| 180 |
-
- β
All biomarker flags (status, ranges, warnings)
|
| 181 |
-
- β
Safety alerts (critical values)
|
| 182 |
-
- β
Key drivers (why this prediction)
|
| 183 |
-
- β
Disease explanation (pathophysiology, citations)
|
| 184 |
-
- β
Recommendations (immediate actions, lifestyle, monitoring)
|
| 185 |
-
- β
Confidence assessment (reliability, limitations)
|
| 186 |
-
- β
All agent outputs (complete workflow detail)
|
| 187 |
-
- β
Workflow metadata (SOP version, timestamps)
|
| 188 |
-
- β
Conversational summary (human-friendly text)
|
| 189 |
-
- β
Processing time
|
| 190 |
-
|
| 191 |
-
**Nothing is hidden - full transparency!**
|
| 192 |
-
|
| 193 |
-
---
|
| 194 |
-
|
| 195 |
-
## π Integration Examples
|
| 196 |
-
|
| 197 |
-
### From Your Backend (Node.js)
|
| 198 |
-
```javascript
|
| 199 |
-
const axios = require('axios');
|
| 200 |
-
|
| 201 |
-
async function analyzeBiomarkers(userInput) {
|
| 202 |
-
const response = await axios.post('http://localhost:8000/api/v1/analyze/natural', {
|
| 203 |
-
message: userInput,
|
| 204 |
-
patient_context: {
|
| 205 |
-
age: 52,
|
| 206 |
-
gender: 'male'
|
| 207 |
-
}
|
| 208 |
-
});
|
| 209 |
-
|
| 210 |
-
return response.data;
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
// Use it
|
| 214 |
-
const result = await analyzeBiomarkers("My glucose is 185 and HbA1c is 8.2");
|
| 215 |
-
console.log(result.prediction.disease); // "Diabetes"
|
| 216 |
-
console.log(result.conversational_summary); // Full friendly text
|
| 217 |
-
```
|
| 218 |
-
|
| 219 |
-
### From Your Backend (Python)
|
| 220 |
-
```python
|
| 221 |
-
import requests
|
| 222 |
-
|
| 223 |
-
def analyze_biomarkers(user_input):
|
| 224 |
-
response = requests.post(
|
| 225 |
-
'http://localhost:8000/api/v1/analyze/natural',
|
| 226 |
-
json={
|
| 227 |
-
'message': user_input,
|
| 228 |
-
'patient_context': {'age': 52, 'gender': 'male'}
|
| 229 |
-
}
|
| 230 |
-
)
|
| 231 |
-
return response.json()
|
| 232 |
-
|
| 233 |
-
# Use it
|
| 234 |
-
result = analyze_biomarkers("My glucose is 185 and HbA1c is 8.2")
|
| 235 |
-
print(result['prediction']['disease']) # Diabetes
|
| 236 |
-
```
|
| 237 |
-
|
| 238 |
-
---
|
| 239 |
-
|
| 240 |
-
## ποΈ Architecture
|
| 241 |
-
|
| 242 |
-
```
|
| 243 |
-
βββββββββββββββββββββββββββββββββββββββββββ
|
| 244 |
-
β YOUR LAPTOP (MVP) β
|
| 245 |
-
βββββββββββββββββββββββββββββββββββββββββββ€
|
| 246 |
-
β β
|
| 247 |
-
β ββββββββββββ ββββββββββββββββββ β
|
| 248 |
-
β β Ollama ββββββββ€ FastAPI:8000 β β
|
| 249 |
-
β β :11434 β β β β
|
| 250 |
-
β ββββββββββββ ββββββββββ¬ββββββββ β
|
| 251 |
-
β β β
|
| 252 |
-
β βββββββββββΌβββββββββ β
|
| 253 |
-
β β RagBot Core β β
|
| 254 |
-
β β (imported pkg) β β
|
| 255 |
-
β ββββββββββββββββββββ β
|
| 256 |
-
β β
|
| 257 |
-
βββββββββββββββββββββββββββββββββββββββββββ
|
| 258 |
-
β²
|
| 259 |
-
β HTTP Requests (JSON)
|
| 260 |
-
β
|
| 261 |
-
βββββββββββ΄ββββββββββ
|
| 262 |
-
β Your Backend β
|
| 263 |
-
β Server :3000 β
|
| 264 |
-
βββββββββββ¬ββββββββββ
|
| 265 |
-
β
|
| 266 |
-
βββββββββββΌββββββββββ
|
| 267 |
-
β Your Frontend β
|
| 268 |
-
β (Website) β
|
| 269 |
-
βββββββββββββββββββββ
|
| 270 |
-
```
|
| 271 |
-
|
| 272 |
-
---
|
| 273 |
-
|
| 274 |
-
## βοΈ Key Features Implemented
|
| 275 |
-
|
| 276 |
-
### 1. Natural Language Extraction β
|
| 277 |
-
- Uses llama3.1:8b-instruct
|
| 278 |
-
- Handles 30+ biomarker name variations
|
| 279 |
-
- Extracts patient context (age, gender, BMI)
|
| 280 |
-
|
| 281 |
-
### 2. Complete Workflow Integration β
|
| 282 |
-
- Imports from existing RagBot
|
| 283 |
-
- Zero changes to source code
|
| 284 |
-
- All 6 agents execute
|
| 285 |
-
- Full RAG retrieval
|
| 286 |
-
|
| 287 |
-
### 3. Comprehensive Responses β
|
| 288 |
-
- Every field from workflow preserved
|
| 289 |
-
- Agent outputs included
|
| 290 |
-
- Citations and evidence
|
| 291 |
-
- Conversational summary generated
|
| 292 |
-
|
| 293 |
-
### 4. Error Handling β
|
| 294 |
-
- Validation errors (422)
|
| 295 |
-
- Extraction failures (400)
|
| 296 |
-
- Service unavailable (503)
|
| 297 |
-
- Internal errors (500)
|
| 298 |
-
- Detailed error messages
|
| 299 |
-
|
| 300 |
-
### 5. CORS Support β
|
| 301 |
-
- Allows all origins (MVP)
|
| 302 |
-
- Configurable in .env
|
| 303 |
-
- Ready for production lockdown
|
| 304 |
-
|
| 305 |
-
### 6. Docker Ready β
|
| 306 |
-
- Multi-stage build
|
| 307 |
-
- Health checks
|
| 308 |
-
- Volume mounts
|
| 309 |
-
- Resource limits
|
| 310 |
-
|
| 311 |
-
---
|
| 312 |
-
|
| 313 |
-
## π Performance
|
| 314 |
-
|
| 315 |
-
- **Startup:** 10-30 seconds (loads vector store)
|
| 316 |
-
- **Analysis:** 3-10 seconds per request
|
| 317 |
-
- **Concurrent:** Supported (FastAPI async)
|
| 318 |
-
- **Memory:** ~2-4GB
|
| 319 |
-
|
| 320 |
-
---
|
| 321 |
-
|
| 322 |
-
## π Security Notes
|
| 323 |
-
|
| 324 |
-
**Current Setup (MVP):**
|
| 325 |
-
- β
CORS: All origins allowed
|
| 326 |
-
- β
Authentication: None
|
| 327 |
-
- β
HTTPS: Not configured
|
| 328 |
-
- β
Rate Limiting: Not implemented
|
| 329 |
-
|
| 330 |
-
**For Production (TODO):**
|
| 331 |
-
- π Restrict CORS to your domain
|
| 332 |
-
- π Add API key authentication
|
| 333 |
-
- π Enable HTTPS
|
| 334 |
-
- π Implement rate limiting
|
| 335 |
-
- π Add request logging
|
| 336 |
-
|
| 337 |
-
---
|
| 338 |
-
|
| 339 |
-
## π Next Steps
|
| 340 |
-
|
| 341 |
-
### 1. Start the API
|
| 342 |
-
```powershell
|
| 343 |
-
cd api
|
| 344 |
-
python -m uvicorn app.main:app --reload --port 8000
|
| 345 |
-
```
|
| 346 |
-
|
| 347 |
-
### 2. Test It
|
| 348 |
-
```powershell
|
| 349 |
-
.\test_api.ps1
|
| 350 |
-
```
|
| 351 |
-
|
| 352 |
-
### 3. Integrate with Your Backend
|
| 353 |
-
```javascript
|
| 354 |
-
// Your backend makes requests to localhost:8000
|
| 355 |
-
const result = await fetch('http://localhost:8000/api/v1/analyze/natural', {
|
| 356 |
-
method: 'POST',
|
| 357 |
-
headers: {'Content-Type': 'application/json'},
|
| 358 |
-
body: JSON.stringify({message: userInput})
|
| 359 |
-
});
|
| 360 |
-
```
|
| 361 |
-
|
| 362 |
-
### 4. Display Results on Frontend
|
| 363 |
-
```javascript
|
| 364 |
-
// Your frontend gets data from your backend
|
| 365 |
-
// Display conversational_summary or build custom UI from analysis object
|
| 366 |
-
```
|
| 367 |
-
|
| 368 |
-
---
|
| 369 |
-
|
| 370 |
-
## π Documentation Files
|
| 371 |
-
|
| 372 |
-
1. **README.md** - Complete guide (500+ lines)
|
| 373 |
-
- Quick start
|
| 374 |
-
- All endpoints
|
| 375 |
-
- Request/response examples
|
| 376 |
-
- Deployment instructions
|
| 377 |
-
- Troubleshooting
|
| 378 |
-
- Integration examples
|
| 379 |
-
|
| 380 |
-
2. **QUICK_REFERENCE.md** - Cheat sheet
|
| 381 |
-
- Common commands
|
| 382 |
-
- Code snippets
|
| 383 |
-
- Quick fixes
|
| 384 |
-
|
| 385 |
-
3. **Swagger UI** - Interactive docs
|
| 386 |
-
- http://localhost:8000/docs
|
| 387 |
-
- Try endpoints live
|
| 388 |
-
- See all schemas
|
| 389 |
-
|
| 390 |
-
---
|
| 391 |
-
|
| 392 |
-
## β¨ What Makes This Special
|
| 393 |
-
|
| 394 |
-
1. **No Source Code Changes** β
|
| 395 |
-
- RagBot repo untouched
|
| 396 |
-
- Imports as package
|
| 397 |
-
- Completely separate
|
| 398 |
-
|
| 399 |
-
2. **Full Detail Preserved** β
|
| 400 |
-
- Every agent output
|
| 401 |
-
- All citations
|
| 402 |
-
- Complete metadata
|
| 403 |
-
- Nothing hidden
|
| 404 |
-
|
| 405 |
-
3. **Natural Language + Structured** β
|
| 406 |
-
- Both input methods
|
| 407 |
-
- Automatic extraction
|
| 408 |
-
- Or direct biomarkers
|
| 409 |
-
|
| 410 |
-
4. **Production Ready** β
|
| 411 |
-
- Error handling
|
| 412 |
-
- Logging
|
| 413 |
-
- Health checks
|
| 414 |
-
- Docker support
|
| 415 |
-
|
| 416 |
-
5. **Developer Friendly** β
|
| 417 |
-
- Auto-generated docs
|
| 418 |
-
- Type safety (Pydantic)
|
| 419 |
-
- Hot reload
|
| 420 |
-
- Test suite
|
| 421 |
-
|
| 422 |
-
---
|
| 423 |
-
|
| 424 |
-
## π You're Ready!
|
| 425 |
-
|
| 426 |
-
Everything is implemented and ready to use. Just:
|
| 427 |
-
|
| 428 |
-
1. **Start Ollama:** `ollama serve`
|
| 429 |
-
2. **Start API:** `python -m uvicorn app.main:app --reload --port 8000`
|
| 430 |
-
3. **Test:** `.\test_api.ps1`
|
| 431 |
-
4. **Integrate:** Make HTTP requests from your backend
|
| 432 |
-
|
| 433 |
-
Your RagBot is now API-ready! π
|
| 434 |
-
|
| 435 |
-
---
|
| 436 |
-
|
| 437 |
-
## π€ Support
|
| 438 |
-
|
| 439 |
-
- Check [README.md](README.md) for detailed docs
|
| 440 |
-
- Check [QUICK_REFERENCE.md](QUICK_REFERENCE.md) for snippets
|
| 441 |
-
- Visit http://localhost:8000/docs for interactive API docs
|
| 442 |
-
- All code is well-commented
|
| 443 |
-
|
| 444 |
-
---
|
| 445 |
-
|
| 446 |
-
**Built:** November 23, 2025
|
| 447 |
-
**Status:** β
Production-Ready MVP
|
| 448 |
-
**Lines of Code:** ~1,800 (API only)
|
| 449 |
-
**Files Created:** 20
|
| 450 |
-
**Time to Deploy:** 2 minutes with Docker
|
| 451 |
-
|
| 452 |
-
π **Congratulations! Your RAG-BOT is now web-ready!** π
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
api/QUICK_REFERENCE.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
```powershell
|
| 7 |
# From api/ directory
|
| 8 |
cd C:\Users\admin\OneDrive\Documents\GitHub\RagBot\api
|
| 9 |
-
python -m uvicorn app.main:app --reload --port 8000
|
| 10 |
```
|
| 11 |
|
| 12 |
### Start API (Docker)
|
|
@@ -93,19 +93,18 @@ netstat -ano | findstr :8000
|
|
| 93 |
taskkill /PID <PID> /F
|
| 94 |
```
|
| 95 |
|
| 96 |
-
###
|
| 97 |
```powershell
|
| 98 |
-
# Check
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
#
|
| 102 |
-
ollama serve
|
| 103 |
```
|
| 104 |
|
| 105 |
### Vector store not loading
|
| 106 |
```powershell
|
| 107 |
# From RagBot root
|
| 108 |
-
python scripts/setup_embeddings.py
|
| 109 |
```
|
| 110 |
|
| 111 |
---
|
|
@@ -199,5 +198,5 @@ curl http://localhost:8000/api/v1/example
|
|
| 199 |
|
| 200 |
---
|
| 201 |
|
| 202 |
-
**Last Updated:**
|
| 203 |
**API Version:** 1.0.0
|
|
|
|
| 6 |
```powershell
|
| 7 |
# From api/ directory
|
| 8 |
cd C:\Users\admin\OneDrive\Documents\GitHub\RagBot\api
|
| 9 |
+
..\.\.venv\Scripts\python.exe -m uvicorn app.main:app --reload --port 8000
|
| 10 |
```
|
| 11 |
|
| 12 |
### Start API (Docker)
|
|
|
|
| 93 |
taskkill /PID <PID> /F
|
| 94 |
```
|
| 95 |
|
| 96 |
+
### LLM provider errors
|
| 97 |
```powershell
|
| 98 |
+
# Check your .env has the right keys
|
| 99 |
+
# Default provider is Groq (GROQ_API_KEY required)
|
| 100 |
+
# Alternative: Google Gemini (GOOGLE_API_KEY)
|
| 101 |
+
# Optional: Ollama (local, no key needed)
|
|
|
|
| 102 |
```
|
| 103 |
|
| 104 |
### Vector store not loading
|
| 105 |
```powershell
|
| 106 |
# From RagBot root
|
| 107 |
+
.\.venv\Scripts\python.exe scripts/setup_embeddings.py
|
| 108 |
```
|
| 109 |
|
| 110 |
---
|
|
|
|
| 198 |
|
| 199 |
---
|
| 200 |
|
| 201 |
+
**Last Updated:** February 2026
|
| 202 |
**API Version:** 1.0.0
|
api/README.md
CHANGED
|
@@ -31,17 +31,11 @@ This API wraps the RagBot clinical analysis system, providing:
|
|
| 31 |
|
| 32 |
### Prerequisites
|
| 33 |
|
| 34 |
-
1. **
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
2. **Required models**:
|
| 40 |
-
```bash
|
| 41 |
-
ollama pull llama3.1:8b-instruct
|
| 42 |
-
ollama pull qwen2:7b
|
| 43 |
-
ollama pull nomic-embed-text
|
| 44 |
-
```
|
| 45 |
|
| 46 |
### Option 1: Run Locally (Development)
|
| 47 |
|
|
@@ -53,8 +47,9 @@ cd api
|
|
| 53 |
pip install -r ../requirements.txt
|
| 54 |
pip install -r requirements.txt
|
| 55 |
|
| 56 |
-
#
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
# Run server
|
| 60 |
python -m uvicorn app.main:app --reload --port 8000
|
|
@@ -82,10 +77,10 @@ GET /api/v1/health
|
|
| 82 |
```json
|
| 83 |
{
|
| 84 |
"status": "healthy",
|
| 85 |
-
"timestamp": "
|
| 86 |
-
"
|
| 87 |
"vector_store_loaded": true,
|
| 88 |
-
"available_models": ["
|
| 89 |
"uptime_seconds": 3600.0,
|
| 90 |
"version": "1.0.0"
|
| 91 |
}
|
|
@@ -406,10 +401,10 @@ api/
|
|
| 406 |
# Test health endpoint
|
| 407 |
curl http://localhost:8000/api/v1/health
|
| 408 |
|
| 409 |
-
# Test example case
|
| 410 |
curl http://localhost:8000/api/v1/example
|
| 411 |
|
| 412 |
-
# Test natural language
|
| 413 |
curl -X POST http://localhost:8000/api/v1/analyze/natural \
|
| 414 |
-H "Content-Type: application/json" \
|
| 415 |
-d '{"message": "glucose 140, HbA1c 7.5"}'
|
|
@@ -427,17 +422,18 @@ uvicorn app.main:app --reload --port 8000
|
|
| 427 |
|
| 428 |
## π§ Troubleshooting
|
| 429 |
|
| 430 |
-
### Issue: "
|
| 431 |
|
| 432 |
-
**Symptom:** Health check shows `
|
| 433 |
|
| 434 |
**Solutions:**
|
| 435 |
-
1.
|
| 436 |
-
2. Check Ollama is running: `curl http://localhost:11434/api/version`
|
| 437 |
-
3. Verify models are pulled:
|
| 438 |
```bash
|
| 439 |
-
|
|
|
|
| 440 |
```
|
|
|
|
|
|
|
| 441 |
|
| 442 |
---
|
| 443 |
|
|
@@ -466,25 +462,30 @@ uvicorn app.main:app --reload --port 8000
|
|
| 466 |
|
| 467 |
---
|
| 468 |
|
| 469 |
-
### Issue: Docker container can't reach
|
| 470 |
|
| 471 |
**Symptom:** Container health check fails
|
| 472 |
|
| 473 |
**Solutions:**
|
| 474 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
**Windows/Mac (Docker Desktop):**
|
| 476 |
```yaml
|
| 477 |
-
# In docker-compose.yml
|
| 478 |
environment:
|
| 479 |
- OLLAMA_BASE_URL=http://host.docker.internal:11434
|
| 480 |
```
|
| 481 |
|
| 482 |
**Linux:**
|
| 483 |
```yaml
|
| 484 |
-
# In docker-compose.yml
|
| 485 |
network_mode: "host"
|
| 486 |
-
environment:
|
| 487 |
-
- OLLAMA_BASE_URL=http://localhost:11434
|
| 488 |
```
|
| 489 |
|
| 490 |
---
|
|
@@ -568,9 +569,9 @@ For issues or questions:
|
|
| 568 |
## π Performance Notes
|
| 569 |
|
| 570 |
- **Initial startup:** 10-30 seconds (loads vector store)
|
| 571 |
-
- **Analysis time:**
|
| 572 |
- **Concurrent requests:** Supported (FastAPI async)
|
| 573 |
-
- **Memory usage:** ~2-4GB (vector store +
|
| 574 |
|
| 575 |
---
|
| 576 |
|
|
|
|
| 31 |
|
| 32 |
### Prerequisites
|
| 33 |
|
| 34 |
+
1. **Python 3.11+** installed
|
| 35 |
+
2. **Free API key** from one of:
|
| 36 |
+
- [Groq](https://console.groq.com/keys) β Recommended (fast, free)
|
| 37 |
+
- [Google Gemini](https://aistudio.google.com/app/apikey) β Alternative
|
| 38 |
+
3. **RagBot dependencies installed** (see root README)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
### Option 1: Run Locally (Development)
|
| 41 |
|
|
|
|
| 47 |
pip install -r ../requirements.txt
|
| 48 |
pip install -r requirements.txt
|
| 49 |
|
| 50 |
+
# Ensure .env is configured in project root with your API keys
|
| 51 |
+
# GROQ_API_KEY=gsk_...
|
| 52 |
+
# LLM_PROVIDER=groq
|
| 53 |
|
| 54 |
# Run server
|
| 55 |
python -m uvicorn app.main:app --reload --port 8000
|
|
|
|
| 77 |
```json
|
| 78 |
{
|
| 79 |
"status": "healthy",
|
| 80 |
+
"timestamp": "2026-02-23T10:30:00Z",
|
| 81 |
+
"llm_status": "connected",
|
| 82 |
"vector_store_loaded": true,
|
| 83 |
+
"available_models": ["llama-3.3-70b-versatile (Groq)"],
|
| 84 |
"uptime_seconds": 3600.0,
|
| 85 |
"version": "1.0.0"
|
| 86 |
}
|
|
|
|
| 401 |
# Test health endpoint
|
| 402 |
curl http://localhost:8000/api/v1/health
|
| 403 |
|
| 404 |
+
# Test example case
|
| 405 |
curl http://localhost:8000/api/v1/example
|
| 406 |
|
| 407 |
+
# Test natural language
|
| 408 |
curl -X POST http://localhost:8000/api/v1/analyze/natural \
|
| 409 |
-H "Content-Type: application/json" \
|
| 410 |
-d '{"message": "glucose 140, HbA1c 7.5"}'
|
|
|
|
| 422 |
|
| 423 |
## π§ Troubleshooting
|
| 424 |
|
| 425 |
+
### Issue: "API key not found"
|
| 426 |
|
| 427 |
+
**Symptom:** Health check shows `llm_status: "disconnected"`
|
| 428 |
|
| 429 |
**Solutions:**
|
| 430 |
+
1. Ensure `.env` in project root has your API key:
|
|
|
|
|
|
|
| 431 |
```bash
|
| 432 |
+
GROQ_API_KEY=gsk_...
|
| 433 |
+
LLM_PROVIDER=groq
|
| 434 |
```
|
| 435 |
+
2. Get a free key at https://console.groq.com/keys
|
| 436 |
+
3. Restart the API server after editing `.env`
|
| 437 |
|
| 438 |
---
|
| 439 |
|
|
|
|
| 462 |
|
| 463 |
---
|
| 464 |
|
| 465 |
+
### Issue: Docker container can't reach LLM API
|
| 466 |
|
| 467 |
**Symptom:** Container health check fails
|
| 468 |
|
| 469 |
**Solutions:**
|
| 470 |
|
| 471 |
+
Ensure your API keys are passed as environment variables in `docker-compose.yml`:
|
| 472 |
+
```yaml
|
| 473 |
+
environment:
|
| 474 |
+
- GROQ_API_KEY=${GROQ_API_KEY}
|
| 475 |
+
- LLM_PROVIDER=groq
|
| 476 |
+
```
|
| 477 |
+
|
| 478 |
+
For local Ollama (optional):
|
| 479 |
+
|
| 480 |
**Windows/Mac (Docker Desktop):**
|
| 481 |
```yaml
|
|
|
|
| 482 |
environment:
|
| 483 |
- OLLAMA_BASE_URL=http://host.docker.internal:11434
|
| 484 |
```
|
| 485 |
|
| 486 |
**Linux:**
|
| 487 |
```yaml
|
|
|
|
| 488 |
network_mode: "host"
|
|
|
|
|
|
|
| 489 |
```
|
| 490 |
|
| 491 |
---
|
|
|
|
| 569 |
## π Performance Notes
|
| 570 |
|
| 571 |
- **Initial startup:** 10-30 seconds (loads vector store)
|
| 572 |
+
- **Analysis time:** 15-25 seconds per request (6 agents + RAG retrieval)
|
| 573 |
- **Concurrent requests:** Supported (FastAPI async)
|
| 574 |
+
- **Memory usage:** ~2-4GB (vector store + embeddings model)
|
| 575 |
|
| 576 |
---
|
| 577 |
|
api/app/main.py
CHANGED
|
@@ -38,25 +38,25 @@ async def lifespan(app: FastAPI):
|
|
| 38 |
Initializes RagBot service on startup (loads vector store, models).
|
| 39 |
"""
|
| 40 |
logger.info("=" * 70)
|
| 41 |
-
logger.info("
|
| 42 |
logger.info("=" * 70)
|
| 43 |
|
| 44 |
# Startup: Initialize RagBot service
|
| 45 |
try:
|
| 46 |
ragbot_service = get_ragbot_service()
|
| 47 |
ragbot_service.initialize()
|
| 48 |
-
logger.info("
|
| 49 |
except Exception as e:
|
| 50 |
-
logger.error(f"
|
| 51 |
-
logger.warning("
|
| 52 |
|
| 53 |
-
logger.info("
|
| 54 |
logger.info("=" * 70)
|
| 55 |
|
| 56 |
yield # Server runs here
|
| 57 |
|
| 58 |
# Shutdown
|
| 59 |
-
logger.info("
|
| 60 |
|
| 61 |
|
| 62 |
# ============================================================================
|
|
|
|
| 38 |
Initializes RagBot service on startup (loads vector store, models).
|
| 39 |
"""
|
| 40 |
logger.info("=" * 70)
|
| 41 |
+
logger.info("Starting RagBot API Server")
|
| 42 |
logger.info("=" * 70)
|
| 43 |
|
| 44 |
# Startup: Initialize RagBot service
|
| 45 |
try:
|
| 46 |
ragbot_service = get_ragbot_service()
|
| 47 |
ragbot_service.initialize()
|
| 48 |
+
logger.info("RagBot service initialized successfully")
|
| 49 |
except Exception as e:
|
| 50 |
+
logger.error(f"Failed to initialize RagBot service: {e}")
|
| 51 |
+
logger.warning("API will start but health checks will fail")
|
| 52 |
|
| 53 |
+
logger.info("API server ready to accept requests")
|
| 54 |
logger.info("=" * 70)
|
| 55 |
|
| 56 |
yield # Server runs here
|
| 57 |
|
| 58 |
# Shutdown
|
| 59 |
+
logger.info("Shutting down RagBot API Server")
|
| 60 |
|
| 61 |
|
| 62 |
# ============================================================================
|
api/app/routes/analyze.py
CHANGED
|
@@ -229,11 +229,11 @@ async def get_example():
|
|
| 229 |
"Platelets": 220000.0,
|
| 230 |
"Cholesterol": 235.0,
|
| 231 |
"Triglycerides": 210.0,
|
| 232 |
-
"HDL": 38.0,
|
| 233 |
-
"LDL": 165.0,
|
| 234 |
"BMI": 31.2,
|
| 235 |
-
"Systolic
|
| 236 |
-
"Diastolic
|
| 237 |
}
|
| 238 |
|
| 239 |
patient_context = {
|
|
|
|
| 229 |
"Platelets": 220000.0,
|
| 230 |
"Cholesterol": 235.0,
|
| 231 |
"Triglycerides": 210.0,
|
| 232 |
+
"HDL Cholesterol": 38.0,
|
| 233 |
+
"LDL Cholesterol": 165.0,
|
| 234 |
"BMI": 31.2,
|
| 235 |
+
"Systolic Blood Pressure": 142.0,
|
| 236 |
+
"Diastolic Blood Pressure": 88.0
|
| 237 |
}
|
| 238 |
|
| 239 |
patient_context = {
|
api/app/routes/biomarkers.py
CHANGED
|
@@ -10,9 +10,6 @@ from fastapi import APIRouter, HTTPException
|
|
| 10 |
|
| 11 |
from app.models.schemas import BiomarkersListResponse, BiomarkerInfo, BiomarkerReferenceRange
|
| 12 |
|
| 13 |
-
# Add parent to path
|
| 14 |
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
| 15 |
-
|
| 16 |
|
| 17 |
router = APIRouter(prefix="/api/v1", tags=["biomarkers"])
|
| 18 |
|
|
|
|
| 10 |
|
| 11 |
from app.models.schemas import BiomarkersListResponse, BiomarkerInfo, BiomarkerReferenceRange
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
router = APIRouter(prefix="/api/v1", tags=["biomarkers"])
|
| 15 |
|
api/app/routes/health.py
CHANGED
|
@@ -8,9 +8,6 @@ from pathlib import Path
|
|
| 8 |
from datetime import datetime
|
| 9 |
from fastapi import APIRouter, HTTPException
|
| 10 |
|
| 11 |
-
# Add parent paths for imports
|
| 12 |
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
| 13 |
-
|
| 14 |
from app.models.schemas import HealthResponse
|
| 15 |
from app.services.ragbot import get_ragbot_service
|
| 16 |
from app import __version__
|
|
@@ -71,7 +68,7 @@ async def health_check():
|
|
| 71 |
return HealthResponse(
|
| 72 |
status=overall_status,
|
| 73 |
timestamp=datetime.now().isoformat(),
|
| 74 |
-
|
| 75 |
vector_store_loaded=vector_store_loaded,
|
| 76 |
available_models=available_models,
|
| 77 |
uptime_seconds=ragbot_service.get_uptime_seconds(),
|
|
|
|
| 8 |
from datetime import datetime
|
| 9 |
from fastapi import APIRouter, HTTPException
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
from app.models.schemas import HealthResponse
|
| 12 |
from app.services.ragbot import get_ragbot_service
|
| 13 |
from app import __version__
|
|
|
|
| 68 |
return HealthResponse(
|
| 69 |
status=overall_status,
|
| 70 |
timestamp=datetime.now().isoformat(),
|
| 71 |
+
llm_status=llm_status,
|
| 72 |
vector_store_loaded=vector_store_loaded,
|
| 73 |
available_models=available_models,
|
| 74 |
uptime_seconds=ragbot_service.get_uptime_seconds(),
|
api/app/services/extraction.py
CHANGED
|
@@ -12,6 +12,7 @@ from typing import Dict, Any, Tuple
|
|
| 12 |
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
| 13 |
|
| 14 |
from langchain_core.prompts import ChatPromptTemplate
|
|
|
|
| 15 |
from src.llm_config import get_chat_model
|
| 16 |
|
| 17 |
|
|
@@ -48,96 +49,26 @@ If you cannot find any biomarkers, return {{"biomarkers": {{}}, "patient_context
|
|
| 48 |
|
| 49 |
|
| 50 |
# ============================================================================
|
| 51 |
-
#
|
| 52 |
# ============================================================================
|
| 53 |
|
| 54 |
-
def
|
| 55 |
-
"""
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
"bloodsugar": "Glucose",
|
| 72 |
-
"bloodglucose": "Glucose",
|
| 73 |
-
|
| 74 |
-
# Lipid panel
|
| 75 |
-
"cholesterol": "Cholesterol",
|
| 76 |
-
"totalcholesterol": "Cholesterol",
|
| 77 |
-
"triglycerides": "Triglycerides",
|
| 78 |
-
"trig": "Triglycerides",
|
| 79 |
-
"ldl": "LDL",
|
| 80 |
-
"ldlcholesterol": "LDL",
|
| 81 |
-
"hdl": "HDL",
|
| 82 |
-
"hdlcholesterol": "HDL",
|
| 83 |
-
|
| 84 |
-
# Diabetes markers
|
| 85 |
-
"hba1c": "HbA1c",
|
| 86 |
-
"a1c": "HbA1c",
|
| 87 |
-
"hemoglobina1c": "HbA1c",
|
| 88 |
-
"insulin": "Insulin",
|
| 89 |
-
|
| 90 |
-
# Body metrics
|
| 91 |
-
"bmi": "BMI",
|
| 92 |
-
"bodymassindex": "BMI",
|
| 93 |
-
|
| 94 |
-
# Complete Blood Count (CBC)
|
| 95 |
-
"hemoglobin": "Hemoglobin",
|
| 96 |
-
"hgb": "Hemoglobin",
|
| 97 |
-
"hb": "Hemoglobin",
|
| 98 |
-
"platelets": "Platelets",
|
| 99 |
-
"plt": "Platelets",
|
| 100 |
-
"wbc": "WBC",
|
| 101 |
-
"whitebloodcells": "WBC",
|
| 102 |
-
"whitecells": "WBC",
|
| 103 |
-
"rbc": "RBC",
|
| 104 |
-
"redbloodcells": "RBC",
|
| 105 |
-
"redcells": "RBC",
|
| 106 |
-
"hematocrit": "Hematocrit",
|
| 107 |
-
"hct": "Hematocrit",
|
| 108 |
-
|
| 109 |
-
# Red blood cell indices
|
| 110 |
-
"mcv": "MCV",
|
| 111 |
-
"meancorpuscularvolume": "MCV",
|
| 112 |
-
"mch": "MCH",
|
| 113 |
-
"meancorpuscularhemoglobin": "MCH",
|
| 114 |
-
"mchc": "MCHC",
|
| 115 |
-
|
| 116 |
-
# Cardiovascular
|
| 117 |
-
"heartrate": "Heart Rate",
|
| 118 |
-
"hr": "Heart Rate",
|
| 119 |
-
"pulse": "Heart Rate",
|
| 120 |
-
"systolicbp": "Systolic BP",
|
| 121 |
-
"systolic": "Systolic BP",
|
| 122 |
-
"sbp": "Systolic BP",
|
| 123 |
-
"diastolicbp": "Diastolic BP",
|
| 124 |
-
"diastolic": "Diastolic BP",
|
| 125 |
-
"dbp": "Diastolic BP",
|
| 126 |
-
"troponin": "Troponin",
|
| 127 |
-
|
| 128 |
-
# Inflammation and liver
|
| 129 |
-
"creactiveprotein": "C-reactive Protein",
|
| 130 |
-
"crp": "C-reactive Protein",
|
| 131 |
-
"alt": "ALT",
|
| 132 |
-
"alanineaminotransferase": "ALT",
|
| 133 |
-
"ast": "AST",
|
| 134 |
-
"aspartateaminotransferase": "AST",
|
| 135 |
-
|
| 136 |
-
# Kidney
|
| 137 |
-
"creatinine": "Creatinine",
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
return mappings.get(name_lower, name)
|
| 141 |
|
| 142 |
|
| 143 |
# ============================================================================
|
|
@@ -177,13 +108,7 @@ def extract_biomarkers(
|
|
| 177 |
response = chain.invoke({"user_message": user_message})
|
| 178 |
content = response.content.strip()
|
| 179 |
|
| 180 |
-
|
| 181 |
-
if "```json" in content:
|
| 182 |
-
content = content.split("```json")[1].split("```")[0].strip()
|
| 183 |
-
elif "```" in content:
|
| 184 |
-
content = content.split("```")[1].split("```")[0].strip()
|
| 185 |
-
|
| 186 |
-
extracted = json.loads(content)
|
| 187 |
biomarkers = extracted.get("biomarkers", {})
|
| 188 |
patient_context = extracted.get("patient_context", {})
|
| 189 |
|
|
@@ -235,63 +160,73 @@ def predict_disease_simple(biomarkers: Dict[str, float]) -> Dict[str, Any]:
|
|
| 235 |
"Thalassemia": 0.0
|
| 236 |
}
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
# Diabetes indicators
|
| 239 |
-
glucose =
|
| 240 |
-
hba1c =
|
| 241 |
-
if glucose > 126:
|
| 242 |
scores["Diabetes"] += 0.4
|
| 243 |
-
if glucose > 180:
|
| 244 |
scores["Diabetes"] += 0.2
|
| 245 |
-
if hba1c >= 6.5:
|
| 246 |
scores["Diabetes"] += 0.5
|
| 247 |
|
| 248 |
# Anemia indicators
|
| 249 |
-
hemoglobin =
|
| 250 |
-
mcv =
|
| 251 |
-
if hemoglobin < 12.0:
|
| 252 |
scores["Anemia"] += 0.6
|
| 253 |
-
if hemoglobin < 10.0:
|
| 254 |
scores["Anemia"] += 0.2
|
| 255 |
-
if mcv < 80:
|
| 256 |
scores["Anemia"] += 0.2
|
| 257 |
|
| 258 |
# Heart disease indicators
|
| 259 |
-
cholesterol =
|
| 260 |
-
troponin =
|
| 261 |
-
ldl =
|
| 262 |
-
if cholesterol > 240:
|
| 263 |
scores["Heart Disease"] += 0.3
|
| 264 |
-
if troponin > 0.04:
|
| 265 |
scores["Heart Disease"] += 0.6
|
| 266 |
-
if ldl > 190:
|
| 267 |
scores["Heart Disease"] += 0.2
|
| 268 |
|
| 269 |
# Thrombocytopenia indicators
|
| 270 |
-
platelets =
|
| 271 |
-
if platelets < 150000:
|
| 272 |
scores["Thrombocytopenia"] += 0.6
|
| 273 |
-
if platelets < 50000:
|
| 274 |
scores["Thrombocytopenia"] += 0.3
|
| 275 |
|
| 276 |
# Thalassemia indicators (simplified)
|
| 277 |
-
if mcv < 80 and hemoglobin < 12.0:
|
| 278 |
scores["Thalassemia"] += 0.4
|
| 279 |
|
| 280 |
# Find top prediction
|
| 281 |
top_disease = max(scores, key=scores.get)
|
| 282 |
-
confidence = scores[top_disease]
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
confidence = 0.5
|
| 287 |
-
top_disease = "Diabetes" # Default
|
| 288 |
|
| 289 |
# Normalize probabilities to sum to 1.0
|
| 290 |
total = sum(scores.values())
|
| 291 |
if total > 0:
|
| 292 |
-
probabilities = {k: v/total for k, v in scores.items()}
|
| 293 |
else:
|
| 294 |
-
probabilities = {k: 1.0/len(scores) for k in scores}
|
| 295 |
|
| 296 |
return {
|
| 297 |
"disease": top_disease,
|
|
|
|
| 12 |
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
| 13 |
|
| 14 |
from langchain_core.prompts import ChatPromptTemplate
|
| 15 |
+
from src.biomarker_normalization import normalize_biomarker_name
|
| 16 |
from src.llm_config import get_chat_model
|
| 17 |
|
| 18 |
|
|
|
|
| 49 |
|
| 50 |
|
| 51 |
# ============================================================================
|
| 52 |
+
# EXTRACTION HELPERS
|
| 53 |
# ============================================================================
|
| 54 |
|
| 55 |
+
def _parse_llm_json(content: str) -> Dict[str, Any]:
|
| 56 |
+
"""Parse JSON payload from LLM output with fallback recovery."""
|
| 57 |
+
text = content.strip()
|
| 58 |
+
|
| 59 |
+
if "```json" in text:
|
| 60 |
+
text = text.split("```json")[1].split("```")[0].strip()
|
| 61 |
+
elif "```" in text:
|
| 62 |
+
text = text.split("```")[1].split("```")[0].strip()
|
| 63 |
+
|
| 64 |
+
try:
|
| 65 |
+
return json.loads(text)
|
| 66 |
+
except json.JSONDecodeError:
|
| 67 |
+
left = text.find("{")
|
| 68 |
+
right = text.rfind("}")
|
| 69 |
+
if left != -1 and right != -1 and right > left:
|
| 70 |
+
return json.loads(text[left:right + 1])
|
| 71 |
+
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
|
| 74 |
# ============================================================================
|
|
|
|
| 108 |
response = chain.invoke({"user_message": user_message})
|
| 109 |
content = response.content.strip()
|
| 110 |
|
| 111 |
+
extracted = _parse_llm_json(content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
biomarkers = extracted.get("biomarkers", {})
|
| 113 |
patient_context = extracted.get("patient_context", {})
|
| 114 |
|
|
|
|
| 160 |
"Thalassemia": 0.0
|
| 161 |
}
|
| 162 |
|
| 163 |
+
# Helper: check both abbreviated and normalized biomarker names
|
| 164 |
+
# Returns None when biomarker is not present (avoids false triggers)
|
| 165 |
+
def _get(name, *alt_names):
|
| 166 |
+
val = biomarkers.get(name, None)
|
| 167 |
+
if val is not None:
|
| 168 |
+
return val
|
| 169 |
+
for alt in alt_names:
|
| 170 |
+
val = biomarkers.get(alt, None)
|
| 171 |
+
if val is not None:
|
| 172 |
+
return val
|
| 173 |
+
return None
|
| 174 |
+
|
| 175 |
# Diabetes indicators
|
| 176 |
+
glucose = _get("Glucose")
|
| 177 |
+
hba1c = _get("HbA1c")
|
| 178 |
+
if glucose is not None and glucose > 126:
|
| 179 |
scores["Diabetes"] += 0.4
|
| 180 |
+
if glucose is not None and glucose > 180:
|
| 181 |
scores["Diabetes"] += 0.2
|
| 182 |
+
if hba1c is not None and hba1c >= 6.5:
|
| 183 |
scores["Diabetes"] += 0.5
|
| 184 |
|
| 185 |
# Anemia indicators
|
| 186 |
+
hemoglobin = _get("Hemoglobin")
|
| 187 |
+
mcv = _get("Mean Corpuscular Volume", "MCV")
|
| 188 |
+
if hemoglobin is not None and hemoglobin < 12.0:
|
| 189 |
scores["Anemia"] += 0.6
|
| 190 |
+
if hemoglobin is not None and hemoglobin < 10.0:
|
| 191 |
scores["Anemia"] += 0.2
|
| 192 |
+
if mcv is not None and mcv < 80:
|
| 193 |
scores["Anemia"] += 0.2
|
| 194 |
|
| 195 |
# Heart disease indicators
|
| 196 |
+
cholesterol = _get("Cholesterol")
|
| 197 |
+
troponin = _get("Troponin")
|
| 198 |
+
ldl = _get("LDL Cholesterol", "LDL")
|
| 199 |
+
if cholesterol is not None and cholesterol > 240:
|
| 200 |
scores["Heart Disease"] += 0.3
|
| 201 |
+
if troponin is not None and troponin > 0.04:
|
| 202 |
scores["Heart Disease"] += 0.6
|
| 203 |
+
if ldl is not None and ldl > 190:
|
| 204 |
scores["Heart Disease"] += 0.2
|
| 205 |
|
| 206 |
# Thrombocytopenia indicators
|
| 207 |
+
platelets = _get("Platelets")
|
| 208 |
+
if platelets is not None and platelets < 150000:
|
| 209 |
scores["Thrombocytopenia"] += 0.6
|
| 210 |
+
if platelets is not None and platelets < 50000:
|
| 211 |
scores["Thrombocytopenia"] += 0.3
|
| 212 |
|
| 213 |
# Thalassemia indicators (simplified)
|
| 214 |
+
if mcv is not None and hemoglobin is not None and mcv < 80 and hemoglobin < 12.0:
|
| 215 |
scores["Thalassemia"] += 0.4
|
| 216 |
|
| 217 |
# Find top prediction
|
| 218 |
top_disease = max(scores, key=scores.get)
|
| 219 |
+
confidence = min(scores[top_disease], 1.0) # Cap at 1.0 for Pydantic validation
|
| 220 |
+
|
| 221 |
+
if confidence == 0.0:
|
| 222 |
+
top_disease = "Undetermined"
|
|
|
|
|
|
|
| 223 |
|
| 224 |
# Normalize probabilities to sum to 1.0
|
| 225 |
total = sum(scores.values())
|
| 226 |
if total > 0:
|
| 227 |
+
probabilities = {k: v / total for k, v in scores.items()}
|
| 228 |
else:
|
| 229 |
+
probabilities = {k: 1.0 / len(scores) for k in scores}
|
| 230 |
|
| 231 |
return {
|
| 232 |
"disease": top_disease,
|
api/app/services/ragbot.py
CHANGED
|
@@ -39,7 +39,7 @@ class RagBotService:
|
|
| 39 |
if self.initialized:
|
| 40 |
return
|
| 41 |
|
| 42 |
-
print("
|
| 43 |
start_time = time.time()
|
| 44 |
|
| 45 |
# Save current directory
|
|
@@ -51,17 +51,17 @@ class RagBotService:
|
|
| 51 |
# This ensures vector store paths resolve correctly
|
| 52 |
ragbot_root = Path(__file__).parent.parent.parent.parent
|
| 53 |
os.chdir(ragbot_root)
|
| 54 |
-
print(f"
|
| 55 |
|
| 56 |
self.guild = create_guild()
|
| 57 |
self.initialized = True
|
| 58 |
self.init_time = datetime.now()
|
| 59 |
|
| 60 |
elapsed = (time.time() - start_time) * 1000
|
| 61 |
-
print(f"
|
| 62 |
|
| 63 |
except Exception as e:
|
| 64 |
-
print(f"
|
| 65 |
raise
|
| 66 |
|
| 67 |
finally:
|
|
@@ -132,7 +132,7 @@ class RagBotService:
|
|
| 132 |
|
| 133 |
except Exception as e:
|
| 134 |
# Re-raise with context
|
| 135 |
-
raise RuntimeError(f"Analysis failed: {str(e)}") from e
|
| 136 |
|
| 137 |
def _format_response(
|
| 138 |
self,
|
|
@@ -147,8 +147,18 @@ class RagBotService:
|
|
| 147 |
"""
|
| 148 |
Format complete detailed response from workflow result.
|
| 149 |
Preserves ALL data from workflow execution.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
"""
|
| 151 |
|
|
|
|
|
|
|
|
|
|
| 152 |
# Extract main prediction
|
| 153 |
prediction = Prediction(
|
| 154 |
disease=model_prediction["disease"],
|
|
@@ -156,35 +166,68 @@ class RagBotService:
|
|
| 156 |
probabilities=model_prediction.get("probabilities", {})
|
| 157 |
)
|
| 158 |
|
| 159 |
-
#
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
key_drivers = []
|
| 174 |
for driver in key_drivers_data:
|
| 175 |
if isinstance(driver, dict):
|
| 176 |
key_drivers.append(KeyDriver(**driver))
|
| 177 |
|
| 178 |
-
# Disease explanation
|
| 179 |
-
disease_exp_data =
|
|
|
|
|
|
|
| 180 |
disease_explanation = DiseaseExplanation(
|
| 181 |
pathophysiology=disease_exp_data.get("pathophysiology", ""),
|
| 182 |
citations=disease_exp_data.get("citations", []),
|
| 183 |
retrieved_chunks=disease_exp_data.get("retrieved_chunks")
|
| 184 |
)
|
| 185 |
|
| 186 |
-
# Recommendations
|
| 187 |
-
recs_data =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
recommendations = Recommendations(
|
| 189 |
immediate_actions=recs_data.get("immediate_actions", []),
|
| 190 |
lifestyle_changes=recs_data.get("lifestyle_changes", []),
|
|
@@ -192,8 +235,10 @@ class RagBotService:
|
|
| 192 |
follow_up=recs_data.get("follow_up")
|
| 193 |
)
|
| 194 |
|
| 195 |
-
# Confidence assessment
|
| 196 |
-
conf_data =
|
|
|
|
|
|
|
| 197 |
confidence_assessment = ConfidenceAssessment(
|
| 198 |
prediction_reliability=conf_data.get("prediction_reliability", "UNKNOWN"),
|
| 199 |
evidence_strength=conf_data.get("evidence_strength", "UNKNOWN"),
|
|
@@ -202,7 +247,9 @@ class RagBotService:
|
|
| 202 |
)
|
| 203 |
|
| 204 |
# Alternative diagnoses
|
| 205 |
-
alternative_diagnoses =
|
|
|
|
|
|
|
| 206 |
|
| 207 |
# Assemble complete analysis
|
| 208 |
analysis = Analysis(
|
|
@@ -215,11 +262,13 @@ class RagBotService:
|
|
| 215 |
alternative_diagnoses=alternative_diagnoses
|
| 216 |
)
|
| 217 |
|
| 218 |
-
# Agent outputs (
|
| 219 |
agent_outputs_data = workflow_result.get("agent_outputs", [])
|
| 220 |
agent_outputs = []
|
| 221 |
for agent_out in agent_outputs_data:
|
| 222 |
-
if
|
|
|
|
|
|
|
| 223 |
agent_outputs.append(AgentOutput(**agent_out))
|
| 224 |
|
| 225 |
# Workflow metadata
|
|
@@ -231,7 +280,9 @@ class RagBotService:
|
|
| 231 |
}
|
| 232 |
|
| 233 |
# Conversational summary (if available)
|
| 234 |
-
conversational_summary =
|
|
|
|
|
|
|
| 235 |
|
| 236 |
# Generate conversational summary if not present
|
| 237 |
if not conversational_summary:
|
|
@@ -271,34 +322,33 @@ class RagBotService:
|
|
| 271 |
"""Generate a simple conversational summary"""
|
| 272 |
|
| 273 |
summary_parts = []
|
| 274 |
-
summary_parts.append("Hi there!
|
| 275 |
summary_parts.append("Based on your biomarkers, I analyzed your results.\n")
|
| 276 |
|
| 277 |
# Prediction
|
| 278 |
-
|
| 279 |
-
summary_parts.append(f"\n{confidence_emoji} **Primary Finding:** {prediction.disease}")
|
| 280 |
summary_parts.append(f" Confidence: {prediction.confidence:.0%}\n")
|
| 281 |
|
| 282 |
# Safety alerts
|
| 283 |
if safety_alerts:
|
| 284 |
-
summary_parts.append("\
|
| 285 |
for alert in safety_alerts[:3]: # Top 3
|
| 286 |
-
summary_parts.append(f"
|
| 287 |
-
summary_parts.append(f"
|
| 288 |
|
| 289 |
# Key drivers
|
| 290 |
if key_drivers:
|
| 291 |
-
summary_parts.append("\
|
| 292 |
for driver in key_drivers[:3]: # Top 3
|
| 293 |
-
summary_parts.append(f"
|
| 294 |
|
| 295 |
# Recommendations
|
| 296 |
if recommendations.immediate_actions:
|
| 297 |
-
summary_parts.append("\
|
| 298 |
for i, action in enumerate(recommendations.immediate_actions[:3], 1):
|
| 299 |
summary_parts.append(f" {i}. {action}")
|
| 300 |
|
| 301 |
-
summary_parts.append("\
|
| 302 |
summary_parts.append(" Please consult a healthcare professional for proper diagnosis and treatment.")
|
| 303 |
|
| 304 |
return "\n".join(summary_parts)
|
|
|
|
| 39 |
if self.initialized:
|
| 40 |
return
|
| 41 |
|
| 42 |
+
print("INFO: Initializing RagBot workflow...")
|
| 43 |
start_time = time.time()
|
| 44 |
|
| 45 |
# Save current directory
|
|
|
|
| 51 |
# This ensures vector store paths resolve correctly
|
| 52 |
ragbot_root = Path(__file__).parent.parent.parent.parent
|
| 53 |
os.chdir(ragbot_root)
|
| 54 |
+
print(f"INFO: Working directory: {ragbot_root}")
|
| 55 |
|
| 56 |
self.guild = create_guild()
|
| 57 |
self.initialized = True
|
| 58 |
self.init_time = datetime.now()
|
| 59 |
|
| 60 |
elapsed = (time.time() - start_time) * 1000
|
| 61 |
+
print(f"OK: RagBot initialized successfully ({elapsed:.0f}ms)")
|
| 62 |
|
| 63 |
except Exception as e:
|
| 64 |
+
print(f"ERROR: Failed to initialize RagBot: {e}")
|
| 65 |
raise
|
| 66 |
|
| 67 |
finally:
|
|
|
|
| 132 |
|
| 133 |
except Exception as e:
|
| 134 |
# Re-raise with context
|
| 135 |
+
raise RuntimeError(f"Analysis failed during workflow execution: {str(e)}") from e
|
| 136 |
|
| 137 |
def _format_response(
|
| 138 |
self,
|
|
|
|
| 147 |
"""
|
| 148 |
Format complete detailed response from workflow result.
|
| 149 |
Preserves ALL data from workflow execution.
|
| 150 |
+
|
| 151 |
+
workflow_result is now the full LangGraph state dict containing:
|
| 152 |
+
- final_response: dict from response_synthesizer
|
| 153 |
+
- agent_outputs: list of AgentOutput objects
|
| 154 |
+
- biomarker_flags: list of BiomarkerFlag objects
|
| 155 |
+
- safety_alerts: list of SafetyAlert objects
|
| 156 |
+
- sop_version, processing_timestamp, etc.
|
| 157 |
"""
|
| 158 |
|
| 159 |
+
# The synthesizer output is nested inside final_response
|
| 160 |
+
final_response = workflow_result.get("final_response", {}) or {}
|
| 161 |
+
|
| 162 |
# Extract main prediction
|
| 163 |
prediction = Prediction(
|
| 164 |
disease=model_prediction["disease"],
|
|
|
|
| 166 |
probabilities=model_prediction.get("probabilities", {})
|
| 167 |
)
|
| 168 |
|
| 169 |
+
# Biomarker flags: prefer state-level data (BiomarkerFlag objects from validator),
|
| 170 |
+
# fall back to synthesizer output
|
| 171 |
+
state_flags = workflow_result.get("biomarker_flags", [])
|
| 172 |
+
if state_flags:
|
| 173 |
+
biomarker_flags = []
|
| 174 |
+
for flag in state_flags:
|
| 175 |
+
if hasattr(flag, 'model_dump'):
|
| 176 |
+
biomarker_flags.append(BiomarkerFlag(**flag.model_dump()))
|
| 177 |
+
elif isinstance(flag, dict):
|
| 178 |
+
biomarker_flags.append(BiomarkerFlag(**flag))
|
| 179 |
+
else:
|
| 180 |
+
biomarker_flags_source = final_response.get("biomarker_flags", [])
|
| 181 |
+
if not biomarker_flags_source:
|
| 182 |
+
biomarker_flags_source = final_response.get("analysis", {}).get("biomarker_flags", [])
|
| 183 |
+
biomarker_flags = [
|
| 184 |
+
BiomarkerFlag(**flag) if isinstance(flag, dict) else BiomarkerFlag(**flag.model_dump())
|
| 185 |
+
for flag in biomarker_flags_source
|
| 186 |
+
]
|
| 187 |
+
|
| 188 |
+
# Safety alerts: prefer state-level data, fall back to synthesizer
|
| 189 |
+
state_alerts = workflow_result.get("safety_alerts", [])
|
| 190 |
+
if state_alerts:
|
| 191 |
+
safety_alerts = []
|
| 192 |
+
for alert in state_alerts:
|
| 193 |
+
if hasattr(alert, 'model_dump'):
|
| 194 |
+
safety_alerts.append(SafetyAlert(**alert.model_dump()))
|
| 195 |
+
elif isinstance(alert, dict):
|
| 196 |
+
safety_alerts.append(SafetyAlert(**alert))
|
| 197 |
+
else:
|
| 198 |
+
safety_alerts_source = final_response.get("safety_alerts", [])
|
| 199 |
+
if not safety_alerts_source:
|
| 200 |
+
safety_alerts_source = final_response.get("analysis", {}).get("safety_alerts", [])
|
| 201 |
+
safety_alerts = [
|
| 202 |
+
SafetyAlert(**alert) if isinstance(alert, dict) else SafetyAlert(**alert.model_dump())
|
| 203 |
+
for alert in safety_alerts_source
|
| 204 |
+
]
|
| 205 |
+
|
| 206 |
+
# Extract key drivers from synthesizer output
|
| 207 |
+
key_drivers_data = final_response.get("key_drivers", [])
|
| 208 |
+
if not key_drivers_data:
|
| 209 |
+
key_drivers_data = final_response.get("analysis", {}).get("key_drivers", [])
|
| 210 |
key_drivers = []
|
| 211 |
for driver in key_drivers_data:
|
| 212 |
if isinstance(driver, dict):
|
| 213 |
key_drivers.append(KeyDriver(**driver))
|
| 214 |
|
| 215 |
+
# Disease explanation from synthesizer
|
| 216 |
+
disease_exp_data = final_response.get("disease_explanation", {})
|
| 217 |
+
if not disease_exp_data:
|
| 218 |
+
disease_exp_data = final_response.get("analysis", {}).get("disease_explanation", {})
|
| 219 |
disease_explanation = DiseaseExplanation(
|
| 220 |
pathophysiology=disease_exp_data.get("pathophysiology", ""),
|
| 221 |
citations=disease_exp_data.get("citations", []),
|
| 222 |
retrieved_chunks=disease_exp_data.get("retrieved_chunks")
|
| 223 |
)
|
| 224 |
|
| 225 |
+
# Recommendations from synthesizer
|
| 226 |
+
recs_data = final_response.get("recommendations", {})
|
| 227 |
+
if not recs_data:
|
| 228 |
+
recs_data = final_response.get("clinical_recommendations", {})
|
| 229 |
+
if not recs_data:
|
| 230 |
+
recs_data = final_response.get("analysis", {}).get("recommendations", {})
|
| 231 |
recommendations = Recommendations(
|
| 232 |
immediate_actions=recs_data.get("immediate_actions", []),
|
| 233 |
lifestyle_changes=recs_data.get("lifestyle_changes", []),
|
|
|
|
| 235 |
follow_up=recs_data.get("follow_up")
|
| 236 |
)
|
| 237 |
|
| 238 |
+
# Confidence assessment from synthesizer
|
| 239 |
+
conf_data = final_response.get("confidence_assessment", {})
|
| 240 |
+
if not conf_data:
|
| 241 |
+
conf_data = final_response.get("analysis", {}).get("confidence_assessment", {})
|
| 242 |
confidence_assessment = ConfidenceAssessment(
|
| 243 |
prediction_reliability=conf_data.get("prediction_reliability", "UNKNOWN"),
|
| 244 |
evidence_strength=conf_data.get("evidence_strength", "UNKNOWN"),
|
|
|
|
| 247 |
)
|
| 248 |
|
| 249 |
# Alternative diagnoses
|
| 250 |
+
alternative_diagnoses = final_response.get("alternative_diagnoses")
|
| 251 |
+
if alternative_diagnoses is None:
|
| 252 |
+
alternative_diagnoses = final_response.get("analysis", {}).get("alternative_diagnoses")
|
| 253 |
|
| 254 |
# Assemble complete analysis
|
| 255 |
analysis = Analysis(
|
|
|
|
| 262 |
alternative_diagnoses=alternative_diagnoses
|
| 263 |
)
|
| 264 |
|
| 265 |
+
# Agent outputs from state (these are src.state.AgentOutput objects)
|
| 266 |
agent_outputs_data = workflow_result.get("agent_outputs", [])
|
| 267 |
agent_outputs = []
|
| 268 |
for agent_out in agent_outputs_data:
|
| 269 |
+
if hasattr(agent_out, 'model_dump'):
|
| 270 |
+
agent_outputs.append(AgentOutput(**agent_out.model_dump()))
|
| 271 |
+
elif isinstance(agent_out, dict):
|
| 272 |
agent_outputs.append(AgentOutput(**agent_out))
|
| 273 |
|
| 274 |
# Workflow metadata
|
|
|
|
| 280 |
}
|
| 281 |
|
| 282 |
# Conversational summary (if available)
|
| 283 |
+
conversational_summary = final_response.get("conversational_summary")
|
| 284 |
+
if not conversational_summary:
|
| 285 |
+
conversational_summary = final_response.get("patient_summary", {}).get("narrative")
|
| 286 |
|
| 287 |
# Generate conversational summary if not present
|
| 288 |
if not conversational_summary:
|
|
|
|
| 322 |
"""Generate a simple conversational summary"""
|
| 323 |
|
| 324 |
summary_parts = []
|
| 325 |
+
summary_parts.append("Hi there!\n")
|
| 326 |
summary_parts.append("Based on your biomarkers, I analyzed your results.\n")
|
| 327 |
|
| 328 |
# Prediction
|
| 329 |
+
summary_parts.append(f"\nPrimary Finding: {prediction.disease}")
|
|
|
|
| 330 |
summary_parts.append(f" Confidence: {prediction.confidence:.0%}\n")
|
| 331 |
|
| 332 |
# Safety alerts
|
| 333 |
if safety_alerts:
|
| 334 |
+
summary_parts.append("\nIMPORTANT SAFETY ALERTS:")
|
| 335 |
for alert in safety_alerts[:3]: # Top 3
|
| 336 |
+
summary_parts.append(f" - {alert.biomarker}: {alert.message}")
|
| 337 |
+
summary_parts.append(f" Action: {alert.action}")
|
| 338 |
|
| 339 |
# Key drivers
|
| 340 |
if key_drivers:
|
| 341 |
+
summary_parts.append("\nWhy this prediction?")
|
| 342 |
for driver in key_drivers[:3]: # Top 3
|
| 343 |
+
summary_parts.append(f" - {driver.biomarker} ({driver.value}): {driver.explanation[:100]}...")
|
| 344 |
|
| 345 |
# Recommendations
|
| 346 |
if recommendations.immediate_actions:
|
| 347 |
+
summary_parts.append("\nWhat You Should Do:")
|
| 348 |
for i, action in enumerate(recommendations.immediate_actions[:3], 1):
|
| 349 |
summary_parts.append(f" {i}. {action}")
|
| 350 |
|
| 351 |
+
summary_parts.append("\nImportant: This is an AI-assisted analysis, NOT medical advice.")
|
| 352 |
summary_parts.append(" Please consult a healthcare professional for proper diagnosis and treatment.")
|
| 353 |
|
| 354 |
return "\n".join(summary_parts)
|
config/biomarker_references.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
| 3 |
"Glucose": {
|
| 4 |
"unit": "mg/dL",
|
| 5 |
"normal_range": {"min": 70, "max": 100},
|
| 6 |
-
"critical_low":
|
| 7 |
-
"critical_high":
|
| 8 |
"type": "fasting",
|
| 9 |
"gender_specific": false,
|
| 10 |
"description": "Fasting blood glucose level",
|
|
@@ -142,8 +142,8 @@
|
|
| 142 |
"BMI": {
|
| 143 |
"unit": "kg/mΒ²",
|
| 144 |
"normal_range": {"min": 18.5, "max": 24.9},
|
| 145 |
-
"critical_low":
|
| 146 |
-
"critical_high":
|
| 147 |
"gender_specific": false,
|
| 148 |
"description": "Body Mass Index",
|
| 149 |
"clinical_significance": {
|
|
@@ -154,8 +154,8 @@
|
|
| 154 |
"Systolic Blood Pressure": {
|
| 155 |
"unit": "mmHg",
|
| 156 |
"normal_range": {"min": 90, "max": 120},
|
| 157 |
-
"critical_low":
|
| 158 |
-
"critical_high":
|
| 159 |
"gender_specific": false,
|
| 160 |
"description": "Blood pressure during heart contraction",
|
| 161 |
"clinical_significance": {
|
|
@@ -166,8 +166,8 @@
|
|
| 166 |
"Diastolic Blood Pressure": {
|
| 167 |
"unit": "mmHg",
|
| 168 |
"normal_range": {"min": 60, "max": 80},
|
| 169 |
-
"critical_low":
|
| 170 |
-
"critical_high":
|
| 171 |
"gender_specific": false,
|
| 172 |
"description": "Blood pressure during heart relaxation",
|
| 173 |
"clinical_significance": {
|
|
@@ -190,7 +190,7 @@
|
|
| 190 |
"unit": "%",
|
| 191 |
"normal_range": {"min": 0, "max": 5.7},
|
| 192 |
"critical_low": null,
|
| 193 |
-
"critical_high":
|
| 194 |
"gender_specific": false,
|
| 195 |
"description": "3-month average blood glucose",
|
| 196 |
"clinical_significance": {
|
|
@@ -274,7 +274,7 @@
|
|
| 274 |
"unit": "ng/mL",
|
| 275 |
"normal_range": {"min": 0, "max": 0.04},
|
| 276 |
"critical_low": null,
|
| 277 |
-
"critical_high": 0.
|
| 278 |
"gender_specific": false,
|
| 279 |
"description": "Cardiac muscle damage marker",
|
| 280 |
"clinical_significance": {
|
|
|
|
| 3 |
"Glucose": {
|
| 4 |
"unit": "mg/dL",
|
| 5 |
"normal_range": {"min": 70, "max": 100},
|
| 6 |
+
"critical_low": 54,
|
| 7 |
+
"critical_high": 400,
|
| 8 |
"type": "fasting",
|
| 9 |
"gender_specific": false,
|
| 10 |
"description": "Fasting blood glucose level",
|
|
|
|
| 142 |
"BMI": {
|
| 143 |
"unit": "kg/mΒ²",
|
| 144 |
"normal_range": {"min": 18.5, "max": 24.9},
|
| 145 |
+
"critical_low": 15,
|
| 146 |
+
"critical_high": 50,
|
| 147 |
"gender_specific": false,
|
| 148 |
"description": "Body Mass Index",
|
| 149 |
"clinical_significance": {
|
|
|
|
| 154 |
"Systolic Blood Pressure": {
|
| 155 |
"unit": "mmHg",
|
| 156 |
"normal_range": {"min": 90, "max": 120},
|
| 157 |
+
"critical_low": 70,
|
| 158 |
+
"critical_high": 180,
|
| 159 |
"gender_specific": false,
|
| 160 |
"description": "Blood pressure during heart contraction",
|
| 161 |
"clinical_significance": {
|
|
|
|
| 166 |
"Diastolic Blood Pressure": {
|
| 167 |
"unit": "mmHg",
|
| 168 |
"normal_range": {"min": 60, "max": 80},
|
| 169 |
+
"critical_low": 40,
|
| 170 |
+
"critical_high": 120,
|
| 171 |
"gender_specific": false,
|
| 172 |
"description": "Blood pressure during heart relaxation",
|
| 173 |
"clinical_significance": {
|
|
|
|
| 190 |
"unit": "%",
|
| 191 |
"normal_range": {"min": 0, "max": 5.7},
|
| 192 |
"critical_low": null,
|
| 193 |
+
"critical_high": 14,
|
| 194 |
"gender_specific": false,
|
| 195 |
"description": "3-month average blood glucose",
|
| 196 |
"clinical_significance": {
|
|
|
|
| 274 |
"unit": "ng/mL",
|
| 275 |
"normal_range": {"min": 0, "max": 0.04},
|
| 276 |
"critical_low": null,
|
| 277 |
+
"critical_high": 0.4,
|
| 278 |
"gender_specific": false,
|
| 279 |
"description": "Cardiac muscle damage marker",
|
| 280 |
"clinical_significance": {
|
data/chat_reports/report_Diabetes_20260223_124903.json
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timestamp": "20260223_124903",
|
| 3 |
+
"biomarkers_input": {
|
| 4 |
+
"Glucose": 140.0,
|
| 5 |
+
"HbA1c": 7.5
|
| 6 |
+
},
|
| 7 |
+
"final_response": {
|
| 8 |
+
"patient_summary": {
|
| 9 |
+
"total_biomarkers_tested": 2,
|
| 10 |
+
"biomarkers_in_normal_range": 0,
|
| 11 |
+
"biomarkers_out_of_range": 2,
|
| 12 |
+
"critical_values": 0,
|
| 13 |
+
"overall_risk_profile": "The patient's biomarker results indicate a high risk profile for diabetes, with both glucose and HbA1c levels exceeding normal ranges. The most concerning findings are the elevated glucose level of 140.0 mg/dL and HbA1c level of 7.5%, which suggest impaired glucose regulation. These results align with the predicted disease of diabetes, supporting the likelihood of an underlying diabetic condition.",
|
| 14 |
+
"narrative": "Based on your test results, it's likely that you may have diabetes, with our system showing an 85% confidence level in this prediction. Your glucose and HbA1c levels, which are important indicators of blood sugar control, are higher than normal, suggesting that your body may be having trouble regulating its blood sugar levels. I want to emphasize that it's essential to discuss these results with your doctor, who can provide a definitive diagnosis and guidance on the best course of action. Please know that while these results may be concerning, many people with diabetes are able to manage their condition and lead healthy, active lives with the right treatment and support."
|
| 15 |
+
},
|
| 16 |
+
"prediction_explanation": {
|
| 17 |
+
"primary_disease": "Diabetes",
|
| 18 |
+
"confidence": 0.85,
|
| 19 |
+
"key_drivers": [
|
| 20 |
+
{
|
| 21 |
+
"biomarker": "Glucose",
|
| 22 |
+
"value": 140.0,
|
| 23 |
+
"contribution": "31%",
|
| 24 |
+
"explanation": "Your glucose level is 140.0 mg/dL, which is higher than normal, indicating that you may have hyperglycemia, a condition where there is too much sugar in the blood, which is a key characteristic of diabetes. This result suggests that you may be at risk for diabetes or may already have the condition, and further evaluation and management may be necessary to prevent complications.",
|
| 25 |
+
"evidence": "3 Prevention and management \nof complications of diabetes \nAcute complications of diabetes\nTwo important acute complications are hypoglycaemia and hyperglycaemic \nemergencies. Hypoglycaemia\nHypoglycae"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"biomarker": "HbA1c",
|
| 29 |
+
"value": 7.5,
|
| 30 |
+
"contribution": "31%",
|
| 31 |
+
"explanation": "Your HbA1c result of 7.5% is higher than the target level of 7%, which may indicate that your blood sugar levels are not well-controlled, suggesting a possible diagnosis of Type 2 Diabetes. This means that your body may not be producing or using insulin properly, leading to elevated blood glucose levels, and your doctor may use this result as part of a comprehensive evaluation to determine the best course of treatment.",
|
| 32 |
+
"evidence": "Diabetes (Type 2) \u2014 Extensive RAG Reference\nGenerated for MediGuard AI RAG-Helper \u007f 2025-11-22\n1. What diabetes is (focused on Type 2)\nDiabetes mellitus is a chronic metabolic disease characterized by"
|
| 33 |
+
}
|
| 34 |
+
],
|
| 35 |
+
"mechanism_summary": "",
|
| 36 |
+
"pathophysiology": "Diabetes mellitus is a group of metabolic disorders characterized by the presence of hyperglycemia due to defects in insulin secretion, insulin action, or both. The underlying biological mechanisms involve impaired insulin secretion, insulin resistance, or a combination of both, leading to elevated blood glucose levels. This can result from various factors, including genetic predisposition, autoimmune destruction of beta-cells, infection-related beta-cell destruction, and other rare immune-mediated diseases. The persistent hyperglycemia can damage blood vessels and nerves, increasing the risk of cardiovascular disease, kidney failure, vision loss, and neuropathy.\n",
|
| 37 |
+
"pdf_references": [
|
| 38 |
+
"diabetes.pdf (Page 8)",
|
| 39 |
+
"diabetes.pdf (Page 4)",
|
| 40 |
+
"diabetes.pdf (Page 11)",
|
| 41 |
+
"MediGuard_Diabetes_Guidelines_Extensive.pdf (Page 0)",
|
| 42 |
+
"diabetes.pdf (Page 10)"
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
"confidence_assessment": {
|
| 46 |
+
"prediction_reliability": "MODERATE",
|
| 47 |
+
"evidence_strength": "MODERATE",
|
| 48 |
+
"limitations": [
|
| 49 |
+
"Missing data: 22 biomarker(s) not provided",
|
| 50 |
+
"Multiple critical values detected; professional evaluation essential"
|
| 51 |
+
],
|
| 52 |
+
"recommendation": "Moderate confidence prediction. Medical consultation recommended for professional evaluation and additional testing if needed.",
|
| 53 |
+
"assessment_summary": "The overall reliability of this prediction is moderate, with an 85% confidence level from the ML model, indicating a reasonable likelihood of diabetes but also some degree of uncertainty. Key limitations, including two identified, suggest that while the evidence strength is moderate, there are potential weaknesses in the assessment that could impact accuracy. Therefore, it is essential to consult a professional medical practitioner to confirm the diagnosis and develop an appropriate treatment plan, as patient safety and accurate diagnosis are paramount.",
|
| 54 |
+
"alternative_diagnoses": [
|
| 55 |
+
{
|
| 56 |
+
"disease": "Anemia",
|
| 57 |
+
"probability": 0.08,
|
| 58 |
+
"note": "Consider discussing with healthcare provider"
|
| 59 |
+
}
|
| 60 |
+
]
|
| 61 |
+
},
|
| 62 |
+
"safety_alerts": [
|
| 63 |
+
{
|
| 64 |
+
"severity": "MEDIUM",
|
| 65 |
+
"biomarker": "Glucose",
|
| 66 |
+
"message": "Glucose is 140.0 mg/dL, above normal range (70-100 mg/dL). Hyperglycemia - diabetes risk, requires further testing",
|
| 67 |
+
"action": "Consult with healthcare provider"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"severity": "MEDIUM",
|
| 71 |
+
"biomarker": "HbA1c",
|
| 72 |
+
"message": "HbA1c is 7.5 %, above normal range (0-5.7 %). Diabetes (\u00e2\u2030\u00a56.5%), Prediabetes (5.7-6.4%)",
|
| 73 |
+
"action": "Consult with healthcare provider"
|
| 74 |
+
}
|
| 75 |
+
],
|
| 76 |
+
"metadata": {
|
| 77 |
+
"timestamp": "2026-02-23T12:46:39.146732",
|
| 78 |
+
"system_version": "MediGuard AI RAG-Helper v1.0",
|
| 79 |
+
"sop_version": "Baseline",
|
| 80 |
+
"agents_executed": [
|
| 81 |
+
"Biomarker Analyzer",
|
| 82 |
+
"Biomarker-Disease Linker",
|
| 83 |
+
"Clinical Guidelines",
|
| 84 |
+
"Disease Explainer",
|
| 85 |
+
"Confidence Assessor"
|
| 86 |
+
],
|
| 87 |
+
"disclaimer": "This is an AI-assisted analysis tool for patient self-assessment. It is NOT a substitute for professional medical advice, diagnosis, or treatment. Always consult qualified healthcare providers for medical decisions."
|
| 88 |
+
},
|
| 89 |
+
"biomarker_flags": [
|
| 90 |
+
{
|
| 91 |
+
"name": "Glucose",
|
| 92 |
+
"value": 140.0,
|
| 93 |
+
"unit": "mg/dL",
|
| 94 |
+
"status": "HIGH",
|
| 95 |
+
"reference_range": "70-100 mg/dL",
|
| 96 |
+
"warning": "Glucose is 140.0 mg/dL, above normal range (70-100 mg/dL). Hyperglycemia - diabetes risk, requires further testing"
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"name": "HbA1c",
|
| 100 |
+
"value": 7.5,
|
| 101 |
+
"unit": "%",
|
| 102 |
+
"status": "HIGH",
|
| 103 |
+
"reference_range": "0-5.7 %",
|
| 104 |
+
"warning": "HbA1c is 7.5 %, above normal range (0-5.7 %). Diabetes (\u00e2\u2030\u00a56.5%), Prediabetes (5.7-6.4%)"
|
| 105 |
+
}
|
| 106 |
+
],
|
| 107 |
+
"key_drivers": [
|
| 108 |
+
{
|
| 109 |
+
"biomarker": "Glucose",
|
| 110 |
+
"value": 140.0,
|
| 111 |
+
"contribution": "31%",
|
| 112 |
+
"explanation": "Your glucose level is 140.0 mg/dL, which is higher than normal, indicating that you may have hyperglycemia, a condition where there is too much sugar in the blood, which is a key characteristic of diabetes. This result suggests that you may be at risk for diabetes or may already have the condition, and further evaluation and management may be necessary to prevent complications.",
|
| 113 |
+
"evidence": "3 Prevention and management \nof complications of diabetes \nAcute complications of diabetes\nTwo important acute complications are hypoglycaemia and hyperglycaemic \nemergencies. Hypoglycaemia\nHypoglycaemia (abnormally low blood glucose) is a frequent iatrogenic \ncomplication in diabetic patients, occurring particularly in patients receiving \nsulfonylurea or insulin. Introduction\nDefinition of diabetes\nDiabetes mellitus, commonly known as diabetes, is a group of metabolic disorders \ncharacterized b"
|
| 114 |
+
},
|
| 115 |
+
{
|
| 116 |
+
"biomarker": "HbA1c",
|
| 117 |
+
"value": 7.5,
|
| 118 |
+
"contribution": "31%",
|
| 119 |
+
"explanation": "Your HbA1c result of 7.5% is higher than the target level of 7%, which may indicate that your blood sugar levels are not well-controlled, suggesting a possible diagnosis of Type 2 Diabetes. This means that your body may not be producing or using insulin properly, leading to elevated blood glucose levels, and your doctor may use this result as part of a comprehensive evaluation to determine the best course of treatment.",
|
| 120 |
+
"evidence": "Diabetes (Type 2) \u2014 Extensive RAG Reference\nGenerated for MediGuard AI RAG-Helper \u007f 2025-11-22\n1. What diabetes is (focused on Type 2)\nDiabetes mellitus is a chronic metabolic disease characterized by elevated blood glucose due to impaired\ninsulin secretion, insulin action, or both. \u2022 The majority of patients can be expected to aim for an HbA1c of 7."
|
| 121 |
+
}
|
| 122 |
+
],
|
| 123 |
+
"disease_explanation": {
|
| 124 |
+
"pathophysiology": "Diabetes mellitus is a group of metabolic disorders characterized by the presence of hyperglycemia due to defects in insulin secretion, insulin action, or both. The underlying biological mechanisms involve impaired insulin secretion, insulin resistance, or a combination of both, leading to elevated blood glucose levels. This can result from various factors, including genetic predisposition, autoimmune destruction of beta-cells, infection-related beta-cell destruction, and other rare immune-mediated diseases. The persistent hyperglycemia can damage blood vessels and nerves, increasing the risk of cardiovascular disease, kidney failure, vision loss, and neuropathy.\n",
|
| 125 |
+
"citations": [
|
| 126 |
+
"diabetes.pdf (Page 8)",
|
| 127 |
+
"diabetes.pdf (Page 4)",
|
| 128 |
+
"diabetes.pdf (Page 11)",
|
| 129 |
+
"MediGuard_Diabetes_Guidelines_Extensive.pdf (Page 0)",
|
| 130 |
+
"diabetes.pdf (Page 10)"
|
| 131 |
+
],
|
| 132 |
+
"retrieved_chunks": null
|
| 133 |
+
},
|
| 134 |
+
"recommendations": {
|
| 135 |
+
"immediate_actions": [
|
| 136 |
+
"Consult a healthcare professional** as soon as possible for a comprehensive diagnosis and to discuss treatment options.",
|
| 137 |
+
"Monitor blood glucose levels** frequently, as advised by your healthcare provider, to understand patterns and the impact of any interventions.",
|
| 138 |
+
"Stay hydrated** by drinking plenty of water to help your body absorb glucose."
|
| 139 |
+
],
|
| 140 |
+
"lifestyle_changes": [
|
| 141 |
+
"Exercise:** Engage in at least 150 minutes of moderate-intensity aerobic exercise, or 75 minutes of vigorous-intensity aerobic exercise, or a combination of both, per week. Additionally, incorporate strength-training activities at least twice a week.",
|
| 142 |
+
"Stress Management:** Practice stress-reducing techniques such as meditation, yoga, or deep breathing exercises."
|
| 143 |
+
],
|
| 144 |
+
"monitoring": [
|
| 145 |
+
"Blood Glucose:** Monitor your blood glucose levels as advised by your healthcare provider, typically before meals and at bedtime.",
|
| 146 |
+
"HbA1c:** Have your HbA1c levels checked at least twice a year to assess your average blood glucose control over the past 2-3 months.",
|
| 147 |
+
"Blood Pressure and Lipids:** Regularly check your blood pressure and lipid profiles, as diabetes increases the risk of cardiovascular diseases.",
|
| 148 |
+
"Foot Care:** Daily inspect your feet for any signs of injury or infection, and have a comprehensive foot exam by a healthcare professional at least once a year.",
|
| 149 |
+
"Remember:** These recommendations are based on general guidelines and may need to be tailored to your specific situation by a healthcare professional. Always consult with your doctor or a qualified healthcare provider for personalized advice on managing diabetes."
|
| 150 |
+
],
|
| 151 |
+
"guideline_citations": [
|
| 152 |
+
"diabetes.pdf"
|
| 153 |
+
]
|
| 154 |
+
},
|
| 155 |
+
"clinical_recommendations": {
|
| 156 |
+
"immediate_actions": [
|
| 157 |
+
"Consult a healthcare professional** as soon as possible for a comprehensive diagnosis and to discuss treatment options.",
|
| 158 |
+
"Monitor blood glucose levels** frequently, as advised by your healthcare provider, to understand patterns and the impact of any interventions.",
|
| 159 |
+
"Stay hydrated** by drinking plenty of water to help your body absorb glucose."
|
| 160 |
+
],
|
| 161 |
+
"lifestyle_changes": [
|
| 162 |
+
"Exercise:** Engage in at least 150 minutes of moderate-intensity aerobic exercise, or 75 minutes of vigorous-intensity aerobic exercise, or a combination of both, per week. Additionally, incorporate strength-training activities at least twice a week.",
|
| 163 |
+
"Stress Management:** Practice stress-reducing techniques such as meditation, yoga, or deep breathing exercises."
|
| 164 |
+
],
|
| 165 |
+
"monitoring": [
|
| 166 |
+
"Blood Glucose:** Monitor your blood glucose levels as advised by your healthcare provider, typically before meals and at bedtime.",
|
| 167 |
+
"HbA1c:** Have your HbA1c levels checked at least twice a year to assess your average blood glucose control over the past 2-3 months.",
|
| 168 |
+
"Blood Pressure and Lipids:** Regularly check your blood pressure and lipid profiles, as diabetes increases the risk of cardiovascular diseases.",
|
| 169 |
+
"Foot Care:** Daily inspect your feet for any signs of injury or infection, and have a comprehensive foot exam by a healthcare professional at least once a year.",
|
| 170 |
+
"Remember:** These recommendations are based on general guidelines and may need to be tailored to your specific situation by a healthcare professional. Always consult with your doctor or a qualified healthcare provider for personalized advice on managing diabetes."
|
| 171 |
+
],
|
| 172 |
+
"guideline_citations": [
|
| 173 |
+
"diabetes.pdf"
|
| 174 |
+
]
|
| 175 |
+
},
|
| 176 |
+
"alternative_diagnoses": [
|
| 177 |
+
{
|
| 178 |
+
"disease": "Anemia",
|
| 179 |
+
"probability": 0.08,
|
| 180 |
+
"note": "Consider discussing with healthcare provider"
|
| 181 |
+
}
|
| 182 |
+
],
|
| 183 |
+
"analysis": {
|
| 184 |
+
"biomarker_flags": [
|
| 185 |
+
{
|
| 186 |
+
"name": "Glucose",
|
| 187 |
+
"value": 140.0,
|
| 188 |
+
"unit": "mg/dL",
|
| 189 |
+
"status": "HIGH",
|
| 190 |
+
"reference_range": "70-100 mg/dL",
|
| 191 |
+
"warning": "Glucose is 140.0 mg/dL, above normal range (70-100 mg/dL). Hyperglycemia - diabetes risk, requires further testing"
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
"name": "HbA1c",
|
| 195 |
+
"value": 7.5,
|
| 196 |
+
"unit": "%",
|
| 197 |
+
"status": "HIGH",
|
| 198 |
+
"reference_range": "0-5.7 %",
|
| 199 |
+
"warning": "HbA1c is 7.5 %, above normal range (0-5.7 %). Diabetes (\u00e2\u2030\u00a56.5%), Prediabetes (5.7-6.4%)"
|
| 200 |
+
}
|
| 201 |
+
],
|
| 202 |
+
"safety_alerts": [
|
| 203 |
+
{
|
| 204 |
+
"severity": "MEDIUM",
|
| 205 |
+
"biomarker": "Glucose",
|
| 206 |
+
"message": "Glucose is 140.0 mg/dL, above normal range (70-100 mg/dL). Hyperglycemia - diabetes risk, requires further testing",
|
| 207 |
+
"action": "Consult with healthcare provider"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"severity": "MEDIUM",
|
| 211 |
+
"biomarker": "HbA1c",
|
| 212 |
+
"message": "HbA1c is 7.5 %, above normal range (0-5.7 %). Diabetes (\u00e2\u2030\u00a56.5%), Prediabetes (5.7-6.4%)",
|
| 213 |
+
"action": "Consult with healthcare provider"
|
| 214 |
+
}
|
| 215 |
+
],
|
| 216 |
+
"key_drivers": [
|
| 217 |
+
{
|
| 218 |
+
"biomarker": "Glucose",
|
| 219 |
+
"value": 140.0,
|
| 220 |
+
"contribution": "31%",
|
| 221 |
+
"explanation": "Your glucose level is 140.0 mg/dL, which is higher than normal, indicating that you may have hyperglycemia, a condition where there is too much sugar in the blood, which is a key characteristic of diabetes. This result suggests that you may be at risk for diabetes or may already have the condition, and further evaluation and management may be necessary to prevent complications.",
|
| 222 |
+
"evidence": "3 Prevention and management \nof complications of diabetes \nAcute complications of diabetes\nTwo important acute complications are hypoglycaemia and hyperglycaemic \nemergencies. Hypoglycaemia\nHypoglycaemia (abnormally low blood glucose) is a frequent iatrogenic \ncomplication in diabetic patients, occurring particularly in patients receiving \nsulfonylurea or insulin. Introduction\nDefinition of diabetes\nDiabetes mellitus, commonly known as diabetes, is a group of metabolic disorders \ncharacterized b"
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"biomarker": "HbA1c",
|
| 226 |
+
"value": 7.5,
|
| 227 |
+
"contribution": "31%",
|
| 228 |
+
"explanation": "Your HbA1c result of 7.5% is higher than the target level of 7%, which may indicate that your blood sugar levels are not well-controlled, suggesting a possible diagnosis of Type 2 Diabetes. This means that your body may not be producing or using insulin properly, leading to elevated blood glucose levels, and your doctor may use this result as part of a comprehensive evaluation to determine the best course of treatment.",
|
| 229 |
+
"evidence": "Diabetes (Type 2) \u2014 Extensive RAG Reference\nGenerated for MediGuard AI RAG-Helper \u007f 2025-11-22\n1. What diabetes is (focused on Type 2)\nDiabetes mellitus is a chronic metabolic disease characterized by elevated blood glucose due to impaired\ninsulin secretion, insulin action, or both. \u2022 The majority of patients can be expected to aim for an HbA1c of 7."
|
| 230 |
+
}
|
| 231 |
+
],
|
| 232 |
+
"disease_explanation": {
|
| 233 |
+
"pathophysiology": "Diabetes mellitus is a group of metabolic disorders characterized by the presence of hyperglycemia due to defects in insulin secretion, insulin action, or both. The underlying biological mechanisms involve impaired insulin secretion, insulin resistance, or a combination of both, leading to elevated blood glucose levels. This can result from various factors, including genetic predisposition, autoimmune destruction of beta-cells, infection-related beta-cell destruction, and other rare immune-mediated diseases. The persistent hyperglycemia can damage blood vessels and nerves, increasing the risk of cardiovascular disease, kidney failure, vision loss, and neuropathy.\n",
|
| 234 |
+
"citations": [
|
| 235 |
+
"diabetes.pdf (Page 8)",
|
| 236 |
+
"diabetes.pdf (Page 4)",
|
| 237 |
+
"diabetes.pdf (Page 11)",
|
| 238 |
+
"MediGuard_Diabetes_Guidelines_Extensive.pdf (Page 0)",
|
| 239 |
+
"diabetes.pdf (Page 10)"
|
| 240 |
+
],
|
| 241 |
+
"retrieved_chunks": null
|
| 242 |
+
},
|
| 243 |
+
"recommendations": {
|
| 244 |
+
"immediate_actions": [
|
| 245 |
+
"Consult a healthcare professional** as soon as possible for a comprehensive diagnosis and to discuss treatment options.",
|
| 246 |
+
"Monitor blood glucose levels** frequently, as advised by your healthcare provider, to understand patterns and the impact of any interventions.",
|
| 247 |
+
"Stay hydrated** by drinking plenty of water to help your body absorb glucose."
|
| 248 |
+
],
|
| 249 |
+
"lifestyle_changes": [
|
| 250 |
+
"Exercise:** Engage in at least 150 minutes of moderate-intensity aerobic exercise, or 75 minutes of vigorous-intensity aerobic exercise, or a combination of both, per week. Additionally, incorporate strength-training activities at least twice a week.",
|
| 251 |
+
"Stress Management:** Practice stress-reducing techniques such as meditation, yoga, or deep breathing exercises."
|
| 252 |
+
],
|
| 253 |
+
"monitoring": [
|
| 254 |
+
"Blood Glucose:** Monitor your blood glucose levels as advised by your healthcare provider, typically before meals and at bedtime.",
|
| 255 |
+
"HbA1c:** Have your HbA1c levels checked at least twice a year to assess your average blood glucose control over the past 2-3 months.",
|
| 256 |
+
"Blood Pressure and Lipids:** Regularly check your blood pressure and lipid profiles, as diabetes increases the risk of cardiovascular diseases.",
|
| 257 |
+
"Foot Care:** Daily inspect your feet for any signs of injury or infection, and have a comprehensive foot exam by a healthcare professional at least once a year.",
|
| 258 |
+
"Remember:** These recommendations are based on general guidelines and may need to be tailored to your specific situation by a healthcare professional. Always consult with your doctor or a qualified healthcare provider for personalized advice on managing diabetes."
|
| 259 |
+
],
|
| 260 |
+
"guideline_citations": [
|
| 261 |
+
"diabetes.pdf"
|
| 262 |
+
]
|
| 263 |
+
},
|
| 264 |
+
"confidence_assessment": {
|
| 265 |
+
"prediction_reliability": "MODERATE",
|
| 266 |
+
"evidence_strength": "MODERATE",
|
| 267 |
+
"limitations": [
|
| 268 |
+
"Missing data: 22 biomarker(s) not provided",
|
| 269 |
+
"Multiple critical values detected; professional evaluation essential"
|
| 270 |
+
],
|
| 271 |
+
"recommendation": "Moderate confidence prediction. Medical consultation recommended for professional evaluation and additional testing if needed.",
|
| 272 |
+
"assessment_summary": "The overall reliability of this prediction is moderate, with an 85% confidence level from the ML model, indicating a reasonable likelihood of diabetes but also some degree of uncertainty. Key limitations, including two identified, suggest that while the evidence strength is moderate, there are potential weaknesses in the assessment that could impact accuracy. Therefore, it is essential to consult a professional medical practitioner to confirm the diagnosis and develop an appropriate treatment plan, as patient safety and accurate diagnosis are paramount.",
|
| 273 |
+
"alternative_diagnoses": [
|
| 274 |
+
{
|
| 275 |
+
"disease": "Anemia",
|
| 276 |
+
"probability": 0.08,
|
| 277 |
+
"note": "Consider discussing with healthcare provider"
|
| 278 |
+
}
|
| 279 |
+
]
|
| 280 |
+
},
|
| 281 |
+
"alternative_diagnoses": [
|
| 282 |
+
{
|
| 283 |
+
"disease": "Anemia",
|
| 284 |
+
"probability": 0.08,
|
| 285 |
+
"note": "Consider discussing with healthcare provider"
|
| 286 |
+
}
|
| 287 |
+
]
|
| 288 |
+
}
|
| 289 |
+
},
|
| 290 |
+
"biomarker_flags": [
|
| 291 |
+
{
|
| 292 |
+
"name": "Glucose",
|
| 293 |
+
"value": 140.0,
|
| 294 |
+
"unit": "mg/dL",
|
| 295 |
+
"status": "HIGH",
|
| 296 |
+
"reference_range": "70-100 mg/dL",
|
| 297 |
+
"warning": "Glucose is 140.0 mg/dL, above normal range (70-100 mg/dL). Hyperglycemia - diabetes risk, requires further testing"
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"name": "HbA1c",
|
| 301 |
+
"value": 7.5,
|
| 302 |
+
"unit": "%",
|
| 303 |
+
"status": "HIGH",
|
| 304 |
+
"reference_range": "0-5.7 %",
|
| 305 |
+
"warning": "HbA1c is 7.5 %, above normal range (0-5.7 %). Diabetes (\u00e2\u2030\u00a56.5%), Prediabetes (5.7-6.4%)"
|
| 306 |
+
}
|
| 307 |
+
],
|
| 308 |
+
"safety_alerts": [
|
| 309 |
+
{
|
| 310 |
+
"severity": "MEDIUM",
|
| 311 |
+
"biomarker": "Glucose",
|
| 312 |
+
"message": "Glucose is 140.0 mg/dL, above normal range (70-100 mg/dL). Hyperglycemia - diabetes risk, requires further testing",
|
| 313 |
+
"action": "Consult with healthcare provider"
|
| 314 |
+
},
|
| 315 |
+
{
|
| 316 |
+
"severity": "MEDIUM",
|
| 317 |
+
"biomarker": "HbA1c",
|
| 318 |
+
"message": "HbA1c is 7.5 %, above normal range (0-5.7 %). Diabetes (\u00e2\u2030\u00a56.5%), Prediabetes (5.7-6.4%)",
|
| 319 |
+
"action": "Consult with healthcare provider"
|
| 320 |
+
}
|
| 321 |
+
]
|
| 322 |
+
}
|
data/chat_reports/report_unknown_20260223_124439.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"timestamp": "20260223_124439",
|
| 3 |
+
"biomarkers_input": {
|
| 4 |
+
"Glucose": 140.0,
|
| 5 |
+
"HbA1c": 10.0
|
| 6 |
+
},
|
| 7 |
+
"analysis_result": {
|
| 8 |
+
"patient_biomarkers": {
|
| 9 |
+
"Glucose": 140.0,
|
| 10 |
+
"HbA1c": 10.0
|
| 11 |
+
},
|
| 12 |
+
"model_prediction": {
|
| 13 |
+
"disease": "Diabetes",
|
| 14 |
+
"confidence": 0.85,
|
| 15 |
+
"probabilities": {
|
| 16 |
+
"Diabetes": 0.85,
|
| 17 |
+
"Anemia": 0.08,
|
| 18 |
+
"Heart Disease": 0.04,
|
| 19 |
+
"Thrombocytopenia": 0.02,
|
| 20 |
+
"Thalassemia": 0.01
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"patient_context": {
|
| 24 |
+
"source": "chat"
|
| 25 |
+
},
|
| 26 |
+
"plan": null,
|
| 27 |
+
"sop":
|
docs/API.md
CHANGED
|
@@ -36,7 +36,7 @@ Currently no authentication required. For production deployment, add:
|
|
| 36 |
|
| 37 |
**Request:**
|
| 38 |
```http
|
| 39 |
-
GET /health
|
| 40 |
```
|
| 41 |
|
| 42 |
**Response:**
|
|
@@ -44,29 +44,62 @@ GET /health
|
|
| 44 |
{
|
| 45 |
"status": "healthy",
|
| 46 |
"timestamp": "2026-02-07T01:30:00Z",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
"version": "1.0.0"
|
| 48 |
}
|
| 49 |
```
|
| 50 |
|
| 51 |
---
|
| 52 |
|
| 53 |
-
### 2. Analyze Biomarkers
|
|
|
|
|
|
|
| 54 |
|
| 55 |
**Request:**
|
| 56 |
```http
|
| 57 |
-
POST /api/v1/analyze
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
Content-Type: application/json
|
| 59 |
|
| 60 |
{
|
| 61 |
"biomarkers": {
|
| 62 |
-
"Glucose":
|
| 63 |
-
"HbA1c":
|
| 64 |
-
"LDL Cholesterol":
|
|
|
|
| 65 |
},
|
| 66 |
"patient_context": {
|
| 67 |
-
"age":
|
| 68 |
-
"gender": "
|
| 69 |
-
"bmi":
|
| 70 |
}
|
| 71 |
}
|
| 72 |
```
|
|
@@ -154,60 +187,35 @@ Content-Type: application/json
|
|
| 154 |
|
| 155 |
| Field | Type | Required | Description |
|
| 156 |
|-------|------|----------|-------------|
|
| 157 |
-
| `biomarkers` |
|
| 158 |
-
| `patient_context` |
|
| 159 |
|
| 160 |
-
**Biomarker Names** (normalized):
|
| 161 |
-
Glucose, HbA1c, Triglycerides, Total Cholesterol, LDL Cholesterol, HDL Cholesterol,
|
| 162 |
|
| 163 |
-
See `config/biomarker_references.json` for full list.
|
|
|
|
| 164 |
|
| 165 |
---
|
| 166 |
|
| 167 |
-
###
|
|
|
|
|
|
|
| 168 |
|
| 169 |
**Request:**
|
| 170 |
```http
|
| 171 |
-
|
| 172 |
-
Content-Type: application/json
|
| 173 |
-
|
| 174 |
-
{
|
| 175 |
-
"biomarkers": {
|
| 176 |
-
"Glucose": 140,
|
| 177 |
-
"HbA1c": 10.0
|
| 178 |
-
}
|
| 179 |
-
}
|
| 180 |
```
|
| 181 |
|
| 182 |
-
**Response:**
|
| 183 |
-
```json
|
| 184 |
-
{
|
| 185 |
-
"valid_biomarkers": {
|
| 186 |
-
"Glucose": {
|
| 187 |
-
"value": 140,
|
| 188 |
-
"reference_range": "70-100",
|
| 189 |
-
"status": "out-of-range",
|
| 190 |
-
"severity": "high"
|
| 191 |
-
},
|
| 192 |
-
"HbA1c": {
|
| 193 |
-
"value": 10.0,
|
| 194 |
-
"reference_range": "4.0-6.4%",
|
| 195 |
-
"status": "out-of-range",
|
| 196 |
-
"severity": "high"
|
| 197 |
-
}
|
| 198 |
-
},
|
| 199 |
-
"invalid_biomarkers": [],
|
| 200 |
-
"alerts": [...]
|
| 201 |
-
}
|
| 202 |
-
```
|
| 203 |
|
| 204 |
---
|
| 205 |
|
| 206 |
-
###
|
| 207 |
|
| 208 |
**Request:**
|
| 209 |
```http
|
| 210 |
-
GET /api/v1/biomarkers
|
| 211 |
```
|
| 212 |
|
| 213 |
**Response:**
|
|
@@ -218,44 +226,20 @@ GET /api/v1/biomarkers/reference-ranges
|
|
| 218 |
"min": 70,
|
| 219 |
"max": 100,
|
| 220 |
"unit": "mg/dL",
|
| 221 |
-
"
|
|
|
|
|
|
|
| 222 |
},
|
| 223 |
"HbA1c": {
|
| 224 |
"min": 4.0,
|
| 225 |
-
"max":
|
| 226 |
"unit": "%",
|
| 227 |
-
"
|
| 228 |
-
|
| 229 |
-
|
|
|
|
| 230 |
},
|
| 231 |
-
"
|
| 232 |
-
}
|
| 233 |
-
```
|
| 234 |
-
|
| 235 |
-
---
|
| 236 |
-
|
| 237 |
-
### 5. Get Analysis History
|
| 238 |
-
|
| 239 |
-
**Request:**
|
| 240 |
-
```http
|
| 241 |
-
GET /api/v1/history?limit=10
|
| 242 |
-
```
|
| 243 |
-
|
| 244 |
-
**Response:**
|
| 245 |
-
```json
|
| 246 |
-
{
|
| 247 |
-
"analyses": [
|
| 248 |
-
{
|
| 249 |
-
"id": "report_Diabetes_20260207_012151",
|
| 250 |
-
"disease": "Diabetes",
|
| 251 |
-
"confidence": 0.85,
|
| 252 |
-
"timestamp": "2026-02-07T01:21:51Z",
|
| 253 |
-
"biomarker_count": 2
|
| 254 |
-
},
|
| 255 |
-
...
|
| 256 |
-
],
|
| 257 |
-
"total": 12,
|
| 258 |
-
"limit": 10
|
| 259 |
}
|
| 260 |
```
|
| 261 |
|
|
@@ -263,24 +247,17 @@ GET /api/v1/history?limit=10
|
|
| 263 |
|
| 264 |
## Error Handling
|
| 265 |
|
| 266 |
-
### Invalid
|
| 267 |
-
|
| 268 |
-
**Request:**
|
| 269 |
-
```http
|
| 270 |
-
POST /api/v1/analyze
|
| 271 |
-
{
|
| 272 |
-
"biomarkers": {
|
| 273 |
-
"InvalidBiomarker": 100
|
| 274 |
-
}
|
| 275 |
-
}
|
| 276 |
-
```
|
| 277 |
|
| 278 |
**Response:** `400 Bad Request`
|
| 279 |
```json
|
| 280 |
{
|
| 281 |
-
"
|
| 282 |
-
|
| 283 |
-
|
|
|
|
|
|
|
|
|
|
| 284 |
}
|
| 285 |
```
|
| 286 |
|
|
@@ -292,8 +269,8 @@ POST /api/v1/analyze
|
|
| 292 |
"detail": [
|
| 293 |
{
|
| 294 |
"loc": ["body", "biomarkers"],
|
| 295 |
-
"msg": "
|
| 296 |
-
"type": "value_error
|
| 297 |
}
|
| 298 |
]
|
| 299 |
}
|
|
@@ -329,14 +306,13 @@ biomarkers = {
|
|
| 329 |
}
|
| 330 |
|
| 331 |
response = requests.post(
|
| 332 |
-
f"{API_URL}/analyze",
|
| 333 |
json={"biomarkers": biomarkers}
|
| 334 |
)
|
| 335 |
|
| 336 |
result = response.json()
|
| 337 |
print(f"Disease: {result['prediction']['disease']}")
|
| 338 |
print(f"Confidence: {result['prediction']['confidence']}")
|
| 339 |
-
print(f"Recommendations: {result['recommendations']['immediate_actions']}")
|
| 340 |
```
|
| 341 |
|
| 342 |
### JavaScript/Node.js
|
|
@@ -348,7 +324,7 @@ const biomarkers = {
|
|
| 348 |
Triglycerides: 200
|
| 349 |
};
|
| 350 |
|
| 351 |
-
fetch('http://localhost:8000/api/v1/analyze', {
|
| 352 |
method: 'POST',
|
| 353 |
headers: {'Content-Type': 'application/json'},
|
| 354 |
body: JSON.stringify({biomarkers})
|
|
@@ -363,7 +339,7 @@ fetch('http://localhost:8000/api/v1/analyze', {
|
|
| 363 |
### cURL
|
| 364 |
|
| 365 |
```bash
|
| 366 |
-
curl -X POST http://localhost:8000/api/v1/analyze \
|
| 367 |
-H "Content-Type: application/json" \
|
| 368 |
-d '{
|
| 369 |
"biomarkers": {
|
|
@@ -406,7 +382,7 @@ app.add_middleware(
|
|
| 406 |
- **95th percentile**: < 25 seconds
|
| 407 |
- **99th percentile**: < 40 seconds
|
| 408 |
|
| 409 |
-
(
|
| 410 |
|
| 411 |
---
|
| 412 |
|
|
|
|
| 36 |
|
| 37 |
**Request:**
|
| 38 |
```http
|
| 39 |
+
GET /api/v1/health
|
| 40 |
```
|
| 41 |
|
| 42 |
**Response:**
|
|
|
|
| 44 |
{
|
| 45 |
"status": "healthy",
|
| 46 |
"timestamp": "2026-02-07T01:30:00Z",
|
| 47 |
+
"llm_status": "connected",
|
| 48 |
+
"vector_store_loaded": true,
|
| 49 |
+
"available_models": ["llama-3.3-70b-versatile (Groq)"],
|
| 50 |
+
"uptime_seconds": 3600.0,
|
| 51 |
"version": "1.0.0"
|
| 52 |
}
|
| 53 |
```
|
| 54 |
|
| 55 |
---
|
| 56 |
|
| 57 |
+
### 2. Analyze Biomarkers (Natural Language)
|
| 58 |
+
|
| 59 |
+
Parse biomarkers from free-text input, predict disease, and run the full RAG workflow.
|
| 60 |
|
| 61 |
**Request:**
|
| 62 |
```http
|
| 63 |
+
POST /api/v1/analyze/natural
|
| 64 |
+
Content-Type: application/json
|
| 65 |
+
|
| 66 |
+
{
|
| 67 |
+
"message": "My glucose is 185, HbA1c is 8.2 and cholesterol is 210",
|
| 68 |
+
"patient_context": {
|
| 69 |
+
"age": 52,
|
| 70 |
+
"gender": "male",
|
| 71 |
+
"bmi": 31.2
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
| Field | Type | Required | Description |
|
| 77 |
+
|-------|------|----------|-------------|
|
| 78 |
+
| `message` | string | Yes | Free-text describing biomarker values |
|
| 79 |
+
| `patient_context` | object | No | Age, gender, BMI for context |
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
### 3. Analyze Biomarkers (Structured)
|
| 84 |
+
|
| 85 |
+
Provide biomarkers as a dictionary (skips LLM extraction step).
|
| 86 |
+
|
| 87 |
+
**Request:**
|
| 88 |
+
```http
|
| 89 |
+
POST /api/v1/analyze/structured
|
| 90 |
Content-Type: application/json
|
| 91 |
|
| 92 |
{
|
| 93 |
"biomarkers": {
|
| 94 |
+
"Glucose": 185.0,
|
| 95 |
+
"HbA1c": 8.2,
|
| 96 |
+
"LDL Cholesterol": 165.0,
|
| 97 |
+
"HDL Cholesterol": 38.0
|
| 98 |
},
|
| 99 |
"patient_context": {
|
| 100 |
+
"age": 52,
|
| 101 |
+
"gender": "male",
|
| 102 |
+
"bmi": 31.2
|
| 103 |
}
|
| 104 |
}
|
| 105 |
```
|
|
|
|
| 187 |
|
| 188 |
| Field | Type | Required | Description |
|
| 189 |
|-------|------|----------|-------------|
|
| 190 |
+
| `biomarkers` | object | Yes | Key-value pairs of biomarker names and numeric values (at least 1) |
|
| 191 |
+
| `patient_context` | object | No | Age, gender, BMI for context |
|
| 192 |
|
| 193 |
+
**Biomarker Names** (canonical, with 80+ aliases auto-normalized):
|
| 194 |
+
Glucose, HbA1c, Triglycerides, Total Cholesterol, LDL Cholesterol, HDL Cholesterol, Hemoglobin, Platelets, White Blood Cells, Red Blood Cells, BMI, Systolic Blood Pressure, Diastolic Blood Pressure, and more.
|
| 195 |
|
| 196 |
+
See `config/biomarker_references.json` for the full list of 24 supported biomarkers.
|
| 197 |
+
```
|
| 198 |
|
| 199 |
---
|
| 200 |
|
| 201 |
+
### 4. Get Example Analysis
|
| 202 |
+
|
| 203 |
+
Returns a pre-built diabetes example case (useful for testing and understanding the response format).
|
| 204 |
|
| 205 |
**Request:**
|
| 206 |
```http
|
| 207 |
+
GET /api/v1/example
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
```
|
| 209 |
|
| 210 |
+
**Response:** Same schema as the analyze endpoints above.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
---
|
| 213 |
|
| 214 |
+
### 5. List Biomarker Reference Ranges
|
| 215 |
|
| 216 |
**Request:**
|
| 217 |
```http
|
| 218 |
+
GET /api/v1/biomarkers
|
| 219 |
```
|
| 220 |
|
| 221 |
**Response:**
|
|
|
|
| 226 |
"min": 70,
|
| 227 |
"max": 100,
|
| 228 |
"unit": "mg/dL",
|
| 229 |
+
"normal_range": "70-100",
|
| 230 |
+
"critical_low": 54,
|
| 231 |
+
"critical_high": 400
|
| 232 |
},
|
| 233 |
"HbA1c": {
|
| 234 |
"min": 4.0,
|
| 235 |
+
"max": 5.6,
|
| 236 |
"unit": "%",
|
| 237 |
+
"normal_range": "4.0-5.6",
|
| 238 |
+
"critical_low": -1,
|
| 239 |
+
"critical_high": 14
|
| 240 |
+
}
|
| 241 |
},
|
| 242 |
+
"count": 24
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
}
|
| 244 |
```
|
| 245 |
|
|
|
|
| 247 |
|
| 248 |
## Error Handling
|
| 249 |
|
| 250 |
+
### Invalid Input (Natural Language)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
**Response:** `400 Bad Request`
|
| 253 |
```json
|
| 254 |
{
|
| 255 |
+
"detail": {
|
| 256 |
+
"error_code": "EXTRACTION_FAILED",
|
| 257 |
+
"message": "Could not extract biomarkers from input",
|
| 258 |
+
"input_received": "...",
|
| 259 |
+
"suggestion": "Try: 'My glucose is 140 and HbA1c is 7.5'"
|
| 260 |
+
}
|
| 261 |
}
|
| 262 |
```
|
| 263 |
|
|
|
|
| 269 |
"detail": [
|
| 270 |
{
|
| 271 |
"loc": ["body", "biomarkers"],
|
| 272 |
+
"msg": "Biomarkers dictionary must not be empty",
|
| 273 |
+
"type": "value_error"
|
| 274 |
}
|
| 275 |
]
|
| 276 |
}
|
|
|
|
| 306 |
}
|
| 307 |
|
| 308 |
response = requests.post(
|
| 309 |
+
f"{API_URL}/analyze/structured",
|
| 310 |
json={"biomarkers": biomarkers}
|
| 311 |
)
|
| 312 |
|
| 313 |
result = response.json()
|
| 314 |
print(f"Disease: {result['prediction']['disease']}")
|
| 315 |
print(f"Confidence: {result['prediction']['confidence']}")
|
|
|
|
| 316 |
```
|
| 317 |
|
| 318 |
### JavaScript/Node.js
|
|
|
|
| 324 |
Triglycerides: 200
|
| 325 |
};
|
| 326 |
|
| 327 |
+
fetch('http://localhost:8000/api/v1/analyze/structured', {
|
| 328 |
method: 'POST',
|
| 329 |
headers: {'Content-Type': 'application/json'},
|
| 330 |
body: JSON.stringify({biomarkers})
|
|
|
|
| 339 |
### cURL
|
| 340 |
|
| 341 |
```bash
|
| 342 |
+
curl -X POST http://localhost:8000/api/v1/analyze/structured \
|
| 343 |
-H "Content-Type: application/json" \
|
| 344 |
-d '{
|
| 345 |
"biomarkers": {
|
|
|
|
| 382 |
- **95th percentile**: < 25 seconds
|
| 383 |
- **99th percentile**: < 40 seconds
|
| 384 |
|
| 385 |
+
(Includes all 6 agent processing steps and RAG retrieval)
|
| 386 |
|
| 387 |
---
|
| 388 |
|
docs/ARCHITECTURE.md
CHANGED
|
@@ -45,11 +45,12 @@ RagBot is a Multi-Agent RAG (Retrieval-Augmented Generation) system for medical
|
|
| 45 |
|
| 46 |
## Core Components
|
| 47 |
|
| 48 |
-
### 1. **Biomarker Extraction & Validation** (`src/biomarker_validator.py`)
|
| 49 |
- Parses user input for blood test results
|
| 50 |
-
- Normalizes biomarker names to
|
| 51 |
-
- Validates values against established reference ranges
|
| 52 |
- Generates safety alerts for critical values
|
|
|
|
| 53 |
|
| 54 |
### 2. **Multi-Agent Workflow** (`src/workflow.py` using LangGraph)
|
| 55 |
The system processes each patient case through 6 specialist agents:
|
|
@@ -93,11 +94,13 @@ The system processes each patient case through 6 specialist agents:
|
|
| 93 |
### 3. **Knowledge Base** (`src/pdf_processor.py`)
|
| 94 |
- **Source**: 8 medical PDF documents (750 pages total)
|
| 95 |
- **Storage**: FAISS vector database (2,609 document chunks)
|
| 96 |
-
- **Embeddings**: HuggingFace sentence-transformers (
|
| 97 |
- **Format**: Chunked with 1000 char overlap for context preservation
|
| 98 |
|
| 99 |
### 4. **LLM Configuration** (`src/llm_config.py`)
|
| 100 |
-
- **Primary LLM**: Groq LLaMA 3.3-70B
|
|
|
|
|
|
|
| 101 |
- Fast inference (~1-2 sec per agent output)
|
| 102 |
- Free API tier available
|
| 103 |
- No rate limiting for reasonable usage
|
|
@@ -126,23 +129,24 @@ User Input
|
|
| 126 |
|
| 127 |
## Key Design Decisions
|
| 128 |
|
| 129 |
-
1. **
|
| 130 |
2. **Groq LLM**: Free, fast inference for real-time interaction
|
| 131 |
-
3. **
|
| 132 |
-
4. **
|
| 133 |
-
5. **
|
| 134 |
-
6. **
|
|
|
|
|
|
|
| 135 |
|
| 136 |
## Technologies Used
|
| 137 |
|
| 138 |
| Component | Technology | Purpose |
|
| 139 |
|-----------|-----------|---------|
|
| 140 |
| Orchestration | LangGraph | Workflow management |
|
| 141 |
-
| LLM | Groq API | Fast inference |
|
| 142 |
-
| Embeddings | HuggingFace | Vector representations |
|
| 143 |
| Vector DB | FAISS | Similarity search |
|
| 144 |
| Data Validation | Pydantic V2 | Type safety & schemas |
|
| 145 |
-
| Async | Python asyncio | Parallel processing |
|
| 146 |
| REST API | FastAPI | Web interface |
|
| 147 |
|
| 148 |
## Performance Characteristics
|
|
@@ -157,7 +161,7 @@ User Input
|
|
| 157 |
|
| 158 |
### Adding New Biomarkers
|
| 159 |
1. Update `config/biomarker_references.json` with reference ranges
|
| 160 |
-
2. Add to `
|
| 161 |
3. Medical guidelines automatically handle via RAG
|
| 162 |
|
| 163 |
### Adding New Medical Domains
|
|
|
|
| 45 |
|
| 46 |
## Core Components
|
| 47 |
|
| 48 |
+
### 1. **Biomarker Extraction & Validation** (`src/biomarker_validator.py`, `src/biomarker_normalization.py`)
|
| 49 |
- Parses user input for blood test results
|
| 50 |
+
- Normalizes biomarker names via 80+ alias mappings to 24 canonical names
|
| 51 |
+
- Validates values against established reference ranges (with clinically appropriate critical thresholds)
|
| 52 |
- Generates safety alerts for critical values
|
| 53 |
+
- Flags all out-of-range values (no suppression threshold)
|
| 54 |
|
| 55 |
### 2. **Multi-Agent Workflow** (`src/workflow.py` using LangGraph)
|
| 56 |
The system processes each patient case through 6 specialist agents:
|
|
|
|
| 94 |
### 3. **Knowledge Base** (`src/pdf_processor.py`)
|
| 95 |
- **Source**: 8 medical PDF documents (750 pages total)
|
| 96 |
- **Storage**: FAISS vector database (2,609 document chunks)
|
| 97 |
+
- **Embeddings**: Google Gemini (default, free) or HuggingFace sentence-transformers (local, offline)
|
| 98 |
- **Format**: Chunked with 1000 char overlap for context preservation
|
| 99 |
|
| 100 |
### 4. **LLM Configuration** (`src/llm_config.py`)
|
| 101 |
+
- **Primary LLM**: Groq LLaMA 3.3-70B (fast, free)
|
| 102 |
+
- **Alternative LLM**: Google Gemini 2.0 Flash (free)
|
| 103 |
+
- **Local LLM**: Ollama (for offline use)
|
| 104 |
- Fast inference (~1-2 sec per agent output)
|
| 105 |
- Free API tier available
|
| 106 |
- No rate limiting for reasonable usage
|
|
|
|
| 129 |
|
| 130 |
## Key Design Decisions
|
| 131 |
|
| 132 |
+
1. **Cloud Embeddings**: Google Gemini embeddings (free tier) with HuggingFace fallback for offline use
|
| 133 |
2. **Groq LLM**: Free, fast inference for real-time interaction
|
| 134 |
+
3. **Multiple Providers**: Support for Groq, Google Gemini, and Ollama (local)
|
| 135 |
+
4. **LangGraph**: Manages complex multi-agent workflows with state management
|
| 136 |
+
5. **FAISS**: Efficient similarity search on large medical document collection
|
| 137 |
+
6. **Modular Agents**: Each agent has clear responsibility, enabling parallel execution
|
| 138 |
+
7. **RAG Integration**: Medical knowledge grounds responses in evidence
|
| 139 |
+
8. **Biomarker Normalization**: 80+ aliases ensure robust input handling
|
| 140 |
|
| 141 |
## Technologies Used
|
| 142 |
|
| 143 |
| Component | Technology | Purpose |
|
| 144 |
|-----------|-----------|---------|
|
| 145 |
| Orchestration | LangGraph | Workflow management |
|
| 146 |
+
| LLM | Groq API / Google Gemini | Fast inference |
|
| 147 |
+
| Embeddings | Google Gemini / HuggingFace | Vector representations |
|
| 148 |
| Vector DB | FAISS | Similarity search |
|
| 149 |
| Data Validation | Pydantic V2 | Type safety & schemas |
|
|
|
|
| 150 |
| REST API | FastAPI | Web interface |
|
| 151 |
|
| 152 |
## Performance Characteristics
|
|
|
|
| 161 |
|
| 162 |
### Adding New Biomarkers
|
| 163 |
1. Update `config/biomarker_references.json` with reference ranges
|
| 164 |
+
2. Add aliases to `src/biomarker_normalization.py` (NORMALIZATION_MAP)
|
| 165 |
3. Medical guidelines automatically handle via RAG
|
| 166 |
|
| 167 |
### Adding New Medical Domains
|
docs/DEEP_REVIEW.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# RagBot Deep Review
|
| 2 |
+
|
| 3 |
+
> **Last updated**: February 2026
|
| 4 |
+
> Items marked **[RESOLVED]** have been fixed. Items marked **[OPEN]** remain as future work.
|
| 5 |
+
|
| 6 |
+
## Scope
|
| 7 |
+
|
| 8 |
+
This review covers the end-to-end workflow and supporting services for RagBot, focusing on design correctness, reliability, safety guardrails, and maintainability. The review is based on a close reading of the workflow orchestration, agent implementations, API wiring, extraction and prediction logic, and the knowledge base pipeline.
|
| 9 |
+
|
| 10 |
+
Primary files reviewed:
|
| 11 |
+
- `src/workflow.py`
|
| 12 |
+
- `src/state.py`
|
| 13 |
+
- `src/config.py`
|
| 14 |
+
- `src/agents/*`
|
| 15 |
+
- `src/biomarker_validator.py`
|
| 16 |
+
- `src/pdf_processor.py`
|
| 17 |
+
- `api/app/main.py`
|
| 18 |
+
- `api/app/routes/analyze.py`
|
| 19 |
+
- `api/app/services/extraction.py`
|
| 20 |
+
- `api/app/services/ragbot.py`
|
| 21 |
+
- `scripts/chat.py`
|
| 22 |
+
|
| 23 |
+
## Architectural Understanding (Condensed)
|
| 24 |
+
|
| 25 |
+
### End-to-End Flow
|
| 26 |
+
1. Input arrives via CLI (`scripts/chat.py`) or REST API (`api/app/routes/analyze.py`).
|
| 27 |
+
2. Natural language inputs are parsed by the extraction service (`api/app/services/extraction.py`) to produce normalized biomarkers and patient context.
|
| 28 |
+
3. A rule-based prediction (`predict_disease_simple`) produces a disease hypothesis and probabilities.
|
| 29 |
+
4. The LangGraph workflow (`src/workflow.py`) orchestrates six agents: Biomarker Analyzer, Disease Explainer, Biomarker Linker, Clinical Guidelines, Confidence Assessor, Response Synthesizer.
|
| 30 |
+
5. The synthesized output is formatted into API schemas (`api/app/services/ragbot.py`) or into CLI-friendly responses (`scripts/chat.py`).
|
| 31 |
+
|
| 32 |
+
### Key Data Structures
|
| 33 |
+
- `GuildState` in `src/state.py` is the shared workflow state; it depends on additive accumulation for parallel outputs.
|
| 34 |
+
- `PatientInput` holds structured biomarkers, prediction data, and patient context.
|
| 35 |
+
- The response format is built in `ResponseSynthesizerAgent` and then translated into API schemas in `RagBotService`.
|
| 36 |
+
|
| 37 |
+
### Knowledge Base
|
| 38 |
+
- PDFs are chunked and embedded into FAISS (`src/pdf_processor.py`).
|
| 39 |
+
- Three retrievers (disease explainer, biomarker linker, clinical guidelines) share the same FAISS index with varying `k` values.
|
| 40 |
+
|
| 41 |
+
## Deep Review Findings
|
| 42 |
+
|
| 43 |
+
### Critical Issues
|
| 44 |
+
|
| 45 |
+
1. **[OPEN] State propagation is incomplete across the workflow.**
|
| 46 |
+
- `src/agents/biomarker_analyzer.py` returns only `agent_outputs` and not the computed `biomarker_flags` or `safety_alerts` into the top-level `GuildState` keys that the workflow expects to accumulate.
|
| 47 |
+
- `src/workflow.py` initializes `biomarker_flags` and `safety_alerts` in the state, but none of the agents return updates to those keys. As a result, `workflow_result.get("biomarker_flags")` and `workflow_result.get("safety_alerts")` are likely empty when the API response is formatted in `api/app/services/ragbot.py`.
|
| 48 |
+
- Effect: API output will frequently miss biomarkers and alerts, and downstream consumers will incorrectly assume a clean result set.
|
| 49 |
+
- Recommendation: return `biomarker_flags` and `safety_alerts` from the Biomarker Analyzer agent so they accumulate in the state. Ensure the response synth uses those same keys.
|
| 50 |
+
|
| 51 |
+
2. **[OPEN] LangGraph merge behavior is unsafe for parallel outputs.**
|
| 52 |
+
- `GuildState` uses `Annotated[List[AgentOutput], operator.add]` for additive merging, but the nodes return only `{ 'agent_outputs': [output] }` and nothing else. This is okay for `agent_outputs`, but parallel agents also read from the full `agent_outputs` list inside the state to infer prior results.
|
| 53 |
+
- In parallel branches, a given agent might read a partial `agent_outputs` list depending on execution order. This is visible in the `BiomarkerDiseaseLinkerAgent` and `ClinicalGuidelinesAgent` which read the prior Biomarker Analyzer output by searching `agent_outputs`.
|
| 54 |
+
- Effect: nondeterministic behavior if LangGraph schedules a branch before the Biomarker Analyzer output is merged, or if merges occur after the branch starts. This can degrade evidence selection and recommendations.
|
| 55 |
+
- Recommendation: explicitly pass relevant artifacts as dedicated state fields updated by the Biomarker Analyzer, and read those fields directly instead of scanning `agent_outputs`.
|
| 56 |
+
|
| 57 |
+
3. **[RESOLVED] Schema mismatch between workflow output and API formatter.**
|
| 58 |
+
- `ResponseSynthesizerAgent` returns a structured response with keys like `patient_summary`, `prediction_explanation`, `clinical_recommendations`, `confidence_assessment`, and `safety_alerts`.
|
| 59 |
+
- `RagBotService._format_response()` now correctly reads from `final_response` and handles both Pydantic objects and dicts.
|
| 60 |
+
- The CLI (`scripts/chat.py`) uses `_coerce_to_dict()` and `format_conversational()` to safely handle all output types.
|
| 61 |
+
- **Fix applied**: `_format_response()` updated + `_coerce_to_dict()` helper added.
|
| 62 |
+
|
| 63 |
+
### High Priority Issues
|
| 64 |
+
|
| 65 |
+
1. **[OPEN] Prediction confidence is forced to 0.5 and default disease is always Diabetes.**
|
| 66 |
+
- Both the API and CLI `predict_disease_simple` functions enforce a minimum confidence of 0.5 and default to Diabetes when confidence is low.
|
| 67 |
+
- Effect: leads to biased predictions and false confidence. This is risky in a medical domain and undermines reliability assessments.
|
| 68 |
+
- Recommendation: return a low-confidence prediction explicitly and mark reliability as low; avoid forcing a disease when evidence is insufficient.
|
| 69 |
+
|
| 70 |
+
2. **[RESOLVED] Different biomarker naming schemes across extraction modules.**
|
| 71 |
+
- Both CLI and API now use the shared `src/biomarker_normalization.py` module with 80+ aliases mapped to 24 canonical names.
|
| 72 |
+
- **Fix applied**: unified normalization in both `scripts/chat.py` and `api/app/services/extraction.py`.
|
| 73 |
+
|
| 74 |
+
3. **[RESOLVED] Use of console glyphs and non-ASCII prefixes in logs and output.**
|
| 75 |
+
- Debug prints removed from CLI. Logging suppressed for noisy HuggingFace/transformers output.
|
| 76 |
+
- API responses use clean JSON only; CLI uses UTF-8 emojis only in user-facing output.
|
| 77 |
+
- **Fix applied**: `[DEBUG]` prints removed, `BertModel LOAD REPORT` suppressed, HuggingFace deprecation warnings filtered.
|
| 78 |
+
|
| 79 |
+
### Medium Priority Issues
|
| 80 |
+
|
| 81 |
+
1. **[RESOLVED] Inconsistent model selection between agents.**
|
| 82 |
+
- All agents now use `llm_config` centralized configuration (planner, analyzer, explainer, synthesizer properties).
|
| 83 |
+
- **Fix applied**: `src/llm_config.py` provides `LLMConfig` singleton with per-role properties.
|
| 84 |
+
|
| 85 |
+
2. **[RESOLVED] Potential JSON parsing fragility in extraction.**
|
| 86 |
+
- `_parse_llm_json()` now handles markdown fences, trailing text, and partial JSON recovery.
|
| 87 |
+
- **Fix applied**: robust JSON parser in `api/app/services/extraction.py` with test coverage (`test_json_parsing.py`).
|
| 88 |
+
|
| 89 |
+
3. **[RESOLVED] Knowledge base retrieval does not enforce citations.**
|
| 90 |
+
- Disease Explainer agent now checks `sop.require_pdf_citations` and returns "insufficient evidence" when no documents are retrieved.
|
| 91 |
+
- **Fix applied**: citation guardrail in `src/agents/disease_explainer.py` with test (`test_citation_guardrails.py`).
|
| 92 |
+
|
| 93 |
+
### Low Priority Issues
|
| 94 |
+
|
| 95 |
+
1. **[OPEN] Error handling does not preserve original exceptions cleanly in API layer.**
|
| 96 |
+
- Exceptions are wrapped in `RuntimeError` without detail separation; `RagBotService.analyze()` does not attach contextual hints (e.g., which agent failed).
|
| 97 |
+
- Recommendation: wrap exceptions with agent name and error classification to improve observability.
|
| 98 |
+
|
| 99 |
+
2. **[RESOLVED] Hard-coded expected biomarker count (24) in Confidence Assessor.**
|
| 100 |
+
- Now uses `BiomarkerValidator().expected_biomarker_count()` which reads from `config/biomarker_references.json`.
|
| 101 |
+
- Test: `test_validator_count.py` verifies count matches reference config.
|
| 102 |
+
|
| 103 |
+
## Suggested Improvements (Summary)
|
| 104 |
+
|
| 105 |
+
1. ~~Align workflow output and API schema.~~ **[RESOLVED]**
|
| 106 |
+
2. Promote biomarker flags and safety alerts to first-class state fields in the workflow. **[OPEN]**
|
| 107 |
+
3. ~~Use a shared normalization utility.~~ **[RESOLVED]**
|
| 108 |
+
4. Remove forced minimum confidence and default disease; permit "low confidence" results. **[OPEN]**
|
| 109 |
+
5. ~~Introduce citation enforcement as a guardrail for RAG outputs.~~ **[RESOLVED]**
|
| 110 |
+
6. ~~Centralize model selection and logging format.~~ **[RESOLVED]**
|
| 111 |
+
|
| 112 |
+
## Verification Gaps
|
| 113 |
+
|
| 114 |
+
The following should be tested once fixes are made:
|
| 115 |
+
- Natural language extraction with partial and noisy inputs.
|
| 116 |
+
- Workflow run where no abnormal biomarkers are detected.
|
| 117 |
+
- API response schema validation for both natural and structured routes.
|
| 118 |
+
- Parallel agent execution determinism (state access to biomarker analysis).
|
| 119 |
+
- CLI behavior for biomarker names that differ from API normalization.
|
docs/DEVELOPMENT.md
CHANGED
|
@@ -9,14 +9,17 @@ This guide covers extending, customizing, and contributing to RagBot.
|
|
| 9 |
```
|
| 10 |
RagBot/
|
| 11 |
βββ src/ # Core application code
|
|
|
|
| 12 |
β βββ workflow.py # Multi-agent workflow orchestration
|
| 13 |
β βββ state.py # Pydantic data models & state
|
| 14 |
β βββ biomarker_validator.py # Biomarker validation logic
|
|
|
|
| 15 |
β βββ llm_config.py # LLM & embedding configuration
|
| 16 |
β βββ pdf_processor.py # PDF loading & vector store
|
| 17 |
β βββ config.py # Global configuration
|
| 18 |
β β
|
| 19 |
β βββ agents/ # Specialist agents
|
|
|
|
| 20 |
β β βββ biomarker_analyzer.py # Validates biomarkers
|
| 21 |
β β βββ disease_explainer.py # Explains disease (RAG)
|
| 22 |
β β βββ biomarker_linker.py # Links biomarkers to disease (RAG)
|
|
@@ -24,7 +27,12 @@ RagBot/
|
|
| 24 |
β β βββ confidence_assessor.py # Assesses prediction confidence
|
| 25 |
β β βββ response_synthesizer.py # Synthesizes findings
|
| 26 |
β β
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
β βββ evolution/ # Experimental components
|
|
|
|
| 28 |
β βββ director.py # Evolution orchestration
|
| 29 |
β βββ pareto.py # Pareto optimization
|
| 30 |
β
|
|
@@ -127,17 +135,18 @@ pytest tests/
|
|
| 127 |
}
|
| 128 |
```
|
| 129 |
|
| 130 |
-
**Step 2:**
|
| 131 |
|
| 132 |
```python
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
return mapping.get(name.lower(), name)
|
| 139 |
```
|
| 140 |
|
|
|
|
|
|
|
| 141 |
**Step 3:** Add validation test in `tests/test_basic.py`:
|
| 142 |
|
| 143 |
```python
|
|
@@ -181,13 +190,13 @@ python scripts/chat.py
|
|
| 181 |
**Step 1:** Create `src/agents/medication_checker.py`:
|
| 182 |
|
| 183 |
```python
|
| 184 |
-
from
|
| 185 |
-
from
|
| 186 |
-
from src.state import PatientInput, DiseasePrediction
|
| 187 |
|
| 188 |
class MedicationChecker:
|
| 189 |
def __init__(self):
|
| 190 |
-
|
|
|
|
| 191 |
|
| 192 |
def check_interactions(self, state: PatientInput) -> dict:
|
| 193 |
"""Check medication interactions based on biomarkers."""
|
|
@@ -226,52 +235,42 @@ medication_info = state.get("medication_interactions", {})
|
|
| 226 |
|
| 227 |
### Switching LLM Providers
|
| 228 |
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
**To use OpenAI GPT-4:**
|
| 232 |
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
|
|
|
|
|
|
| 236 |
|
| 237 |
-
def create_llm():
|
| 238 |
-
return ChatOpenAI(
|
| 239 |
-
model="gpt-4",
|
| 240 |
-
api_key=os.getenv("OPENAI_API_KEY"),
|
| 241 |
-
temperature=0.1
|
| 242 |
-
)
|
| 243 |
-
```
|
| 244 |
-
|
| 245 |
-
2. Update `requirements.txt`:
|
| 246 |
-
```
|
| 247 |
-
langchain-openai>=0.1.0
|
| 248 |
-
```
|
| 249 |
-
|
| 250 |
-
3. Test:
|
| 251 |
```bash
|
| 252 |
-
|
| 253 |
-
|
|
|
|
| 254 |
|
| 255 |
-
#
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
-
|
| 258 |
|
| 259 |
-
|
| 260 |
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
```
|
| 271 |
|
| 272 |
-
|
| 273 |
```bash
|
| 274 |
-
python scripts/setup_embeddings.py
|
| 275 |
```
|
| 276 |
|
| 277 |
β οΈ **Note:** Changing embeddings requires rebuilding the vector store (dimensions must match).
|
|
@@ -281,19 +280,19 @@ python scripts/setup_embeddings.py --force-rebuild
|
|
| 281 |
### Run All Tests
|
| 282 |
|
| 283 |
```bash
|
| 284 |
-
pytest tests/ -
|
| 285 |
```
|
| 286 |
|
| 287 |
### Run Specific Test
|
| 288 |
|
| 289 |
```bash
|
| 290 |
-
pytest tests/
|
| 291 |
```
|
| 292 |
|
| 293 |
### Test Coverage
|
| 294 |
|
| 295 |
```bash
|
| 296 |
-
pytest --cov=src tests/
|
| 297 |
```
|
| 298 |
|
| 299 |
### Add New Tests
|
|
@@ -327,15 +326,16 @@ LOG_LEVEL=DEBUG
|
|
| 327 |
|
| 328 |
```bash
|
| 329 |
python -c "
|
| 330 |
-
from src.workflow import
|
| 331 |
-
from src.state import PatientInput
|
| 332 |
|
| 333 |
-
# Create
|
| 334 |
-
|
| 335 |
|
| 336 |
# Run workflow
|
| 337 |
-
|
| 338 |
-
|
|
|
|
|
|
|
| 339 |
|
| 340 |
# Inspect result
|
| 341 |
print(result)
|
|
@@ -436,24 +436,17 @@ FAISS vector store is already loaded once at startup.
|
|
| 436 |
|
| 437 |
## Troubleshooting
|
| 438 |
|
| 439 |
-
### Issue:
|
| 440 |
-
|
| 441 |
-
```bash
|
| 442 |
-
pip install torch torchvision
|
| 443 |
-
```
|
| 444 |
-
|
| 445 |
-
### Issue: "CUDA out of memory"
|
| 446 |
|
| 447 |
```bash
|
| 448 |
-
|
| 449 |
-
python scripts/chat.py
|
| 450 |
```
|
| 451 |
|
| 452 |
-
### Issue:
|
| 453 |
|
| 454 |
-
```
|
| 455 |
-
|
| 456 |
-
|
| 457 |
|
| 458 |
### Issue: Slow inference
|
| 459 |
|
|
|
|
| 9 |
```
|
| 10 |
RagBot/
|
| 11 |
βββ src/ # Core application code
|
| 12 |
+
β βββ __init__.py # Package marker
|
| 13 |
β βββ workflow.py # Multi-agent workflow orchestration
|
| 14 |
β βββ state.py # Pydantic data models & state
|
| 15 |
β βββ biomarker_validator.py # Biomarker validation logic
|
| 16 |
+
β βββ biomarker_normalization.py # Alias-to-canonical name mapping (80+ aliases)
|
| 17 |
β βββ llm_config.py # LLM & embedding configuration
|
| 18 |
β βββ pdf_processor.py # PDF loading & vector store
|
| 19 |
β βββ config.py # Global configuration
|
| 20 |
β β
|
| 21 |
β βββ agents/ # Specialist agents
|
| 22 |
+
β β βββ __init__.py # Package marker
|
| 23 |
β β βββ biomarker_analyzer.py # Validates biomarkers
|
| 24 |
β β βββ disease_explainer.py # Explains disease (RAG)
|
| 25 |
β β βββ biomarker_linker.py # Links biomarkers to disease (RAG)
|
|
|
|
| 27 |
β β βββ confidence_assessor.py # Assesses prediction confidence
|
| 28 |
β β βββ response_synthesizer.py # Synthesizes findings
|
| 29 |
β β
|
| 30 |
+
β βββ evaluation/ # Evaluation framework
|
| 31 |
+
β β βββ __init__.py
|
| 32 |
+
β β βββ evaluators.py # Quality evaluators
|
| 33 |
+
β β
|
| 34 |
β βββ evolution/ # Experimental components
|
| 35 |
+
β βββ __init__.py
|
| 36 |
β βββ director.py # Evolution orchestration
|
| 37 |
β βββ pareto.py # Pareto optimization
|
| 38 |
β
|
|
|
|
| 135 |
}
|
| 136 |
```
|
| 137 |
|
| 138 |
+
**Step 2:** Add aliases in `src/biomarker_normalization.py`:
|
| 139 |
|
| 140 |
```python
|
| 141 |
+
NORMALIZATION_MAP = {
|
| 142 |
+
# ... existing entries ...
|
| 143 |
+
"your alias": "New Biomarker",
|
| 144 |
+
"other name": "New Biomarker",
|
| 145 |
+
}
|
|
|
|
| 146 |
```
|
| 147 |
|
| 148 |
+
All consumers (CLI, API, workflow) use this shared map automatically.
|
| 149 |
+
|
| 150 |
**Step 3:** Add validation test in `tests/test_basic.py`:
|
| 151 |
|
| 152 |
```python
|
|
|
|
| 190 |
**Step 1:** Create `src/agents/medication_checker.py`:
|
| 191 |
|
| 192 |
```python
|
| 193 |
+
from src.llm_config import LLMConfig
|
| 194 |
+
from src.state import PatientInput
|
|
|
|
| 195 |
|
| 196 |
class MedicationChecker:
|
| 197 |
def __init__(self):
|
| 198 |
+
config = LLMConfig()
|
| 199 |
+
self.llm = config.analyzer # Uses centralized LLM config
|
| 200 |
|
| 201 |
def check_interactions(self, state: PatientInput) -> dict:
|
| 202 |
"""Check medication interactions based on biomarkers."""
|
|
|
|
| 235 |
|
| 236 |
### Switching LLM Providers
|
| 237 |
|
| 238 |
+
RagBot supports three LLM providers out of the box. Set via `LLM_PROVIDER` in `.env`:
|
|
|
|
|
|
|
| 239 |
|
| 240 |
+
| Provider | Model | Cost | Speed |
|
| 241 |
+
|----------|-------|------|-------|
|
| 242 |
+
| `groq` (default) | llama-3.3-70b-versatile | Free | Fast |
|
| 243 |
+
| `gemini` | gemini-2.0-flash | Free | Medium |
|
| 244 |
+
| `ollama` | configurable | Free (local) | Varies |
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
```bash
|
| 247 |
+
# .env
|
| 248 |
+
LLM_PROVIDER="groq"
|
| 249 |
+
GROQ_API_KEY="gsk_..."
|
| 250 |
|
| 251 |
+
# Or
|
| 252 |
+
LLM_PROVIDER="gemini"
|
| 253 |
+
GOOGLE_API_KEY="..."
|
| 254 |
+
```
|
| 255 |
|
| 256 |
+
No code changes needed β `src/llm_config.py` handles provider selection automatically.
|
| 257 |
|
| 258 |
+
### Modifying Embedding Provider
|
| 259 |
|
| 260 |
+
**Current default:** Google Gemini (`models/embedding-001`, free)
|
| 261 |
+
**Fallback:** HuggingFace sentence-transformers (local, no API key needed)
|
| 262 |
+
**Optional:** Ollama (local)
|
| 263 |
|
| 264 |
+
Set via `EMBEDDING_PROVIDER` in `.env`:
|
| 265 |
+
```bash
|
| 266 |
+
EMBEDDING_PROVIDER="google" # Default - Google Gemini
|
| 267 |
+
EMBEDDING_PROVIDER="huggingface" # Fallback - local
|
| 268 |
+
EMBEDDING_PROVIDER="ollama" # Local Ollama
|
| 269 |
```
|
| 270 |
|
| 271 |
+
After changing, rebuild the vector store:
|
| 272 |
```bash
|
| 273 |
+
python scripts/setup_embeddings.py
|
| 274 |
```
|
| 275 |
|
| 276 |
β οΈ **Note:** Changing embeddings requires rebuilding the vector store (dimensions must match).
|
|
|
|
| 280 |
### Run All Tests
|
| 281 |
|
| 282 |
```bash
|
| 283 |
+
.venv\Scripts\python.exe -m pytest tests/ -q --ignore=tests/test_basic.py --ignore=tests/test_diabetes_patient.py --ignore=tests/test_evolution_loop.py --ignore=tests/test_evolution_quick.py --ignore=tests/test_evaluation_system.py
|
| 284 |
```
|
| 285 |
|
| 286 |
### Run Specific Test
|
| 287 |
|
| 288 |
```bash
|
| 289 |
+
.venv\Scripts\python.exe -m pytest tests/test_normalization.py -v
|
| 290 |
```
|
| 291 |
|
| 292 |
### Test Coverage
|
| 293 |
|
| 294 |
```bash
|
| 295 |
+
.venv\Scripts\python.exe -m pytest --cov=src tests/
|
| 296 |
```
|
| 297 |
|
| 298 |
### Add New Tests
|
|
|
|
| 326 |
|
| 327 |
```bash
|
| 328 |
python -c "
|
| 329 |
+
from src.workflow import create_guild
|
|
|
|
| 330 |
|
| 331 |
+
# Create the guild
|
| 332 |
+
guild = create_guild()
|
| 333 |
|
| 334 |
# Run workflow
|
| 335 |
+
result = guild.run({
|
| 336 |
+
'biomarkers': {'Glucose': 185, 'HbA1c': 8.2},
|
| 337 |
+
'model_prediction': {'disease': 'Diabetes', 'confidence': 0.87}
|
| 338 |
+
})
|
| 339 |
|
| 340 |
# Inspect result
|
| 341 |
print(result)
|
|
|
|
| 436 |
|
| 437 |
## Troubleshooting
|
| 438 |
|
| 439 |
+
### Issue: Vector store not found
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
|
| 441 |
```bash
|
| 442 |
+
.venv\Scripts\python.exe scripts/setup_embeddings.py
|
|
|
|
| 443 |
```
|
| 444 |
|
| 445 |
+
### Issue: LLM provider not responding
|
| 446 |
|
| 447 |
+
- Check your `.env` has valid API keys (`GROQ_API_KEY` or `GOOGLE_API_KEY`)
|
| 448 |
+
- Verify internet connection
|
| 449 |
+
- Check provider status pages (Groq Console, Google AI Studio)
|
| 450 |
|
| 451 |
### Issue: Slow inference
|
| 452 |
|