Spaces:
Running
Running
MugdhaV commited on
Commit ·
e1e9580
0
Parent(s):
Initial deployment: Gradio frontend with Modal backend - Multi-language security scanner with parallel processing
Browse files- .gitattributes +35 -0
- .gitignore +33 -0
- README.md +75 -0
- app.py +10 -0
- gradio_app.py +1611 -0
- help.html +539 -0
- requirements.txt +34 -0
- security_checker.py +1945 -0
- theme.py +84 -0
- ui_components.py +474 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
env/
|
| 8 |
+
venv/
|
| 9 |
+
ENV/
|
| 10 |
+
.venv
|
| 11 |
+
|
| 12 |
+
# Gradio
|
| 13 |
+
flagged/
|
| 14 |
+
gradio_cached_examples/
|
| 15 |
+
|
| 16 |
+
# Testing
|
| 17 |
+
.pytest_cache/
|
| 18 |
+
*.log
|
| 19 |
+
|
| 20 |
+
# IDE
|
| 21 |
+
.vscode/
|
| 22 |
+
.idea/
|
| 23 |
+
*.swp
|
| 24 |
+
*.swo
|
| 25 |
+
*~
|
| 26 |
+
|
| 27 |
+
# OS
|
| 28 |
+
.DS_Store
|
| 29 |
+
Thumbs.db
|
| 30 |
+
|
| 31 |
+
# Local environment files (example only, actual secrets in HF Spaces UI)
|
| 32 |
+
.env
|
| 33 |
+
.env.local
|
README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Security Auditor
|
| 3 |
+
emoji: 🔒
|
| 4 |
+
colorFrom: red
|
| 5 |
+
colorTo: orange
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 4.0.0
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
+
short_description: AI-powered security vulnerability scanner with serverless Modal.com backend
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# Security Auditor 🔒
|
| 15 |
+
|
| 16 |
+
AI-powered security vulnerability scanner with serverless backend powered by Modal.com.
|
| 17 |
+
|
| 18 |
+
## Features
|
| 19 |
+
|
| 20 |
+
✨ **Multi-Language Support**: Python, JavaScript, Java, C/C++, Go, Ruby, PHP, and more
|
| 21 |
+
|
| 22 |
+
⚡ **Parallel Processing**: Scan hundreds of files in seconds using Modal.com serverless containers
|
| 23 |
+
|
| 24 |
+
📊 **Comprehensive Reports**: Export findings as JSON or Markdown
|
| 25 |
+
|
| 26 |
+
🔍 **100+ Vulnerability Patterns**: Detects SQL injection, XSS, hardcoded secrets, insecure crypto, and more
|
| 27 |
+
|
| 28 |
+
## Architecture
|
| 29 |
+
|
| 30 |
+
- **Frontend**: Gradio (hosted on HuggingFace Spaces)
|
| 31 |
+
- **Backend**: FastAPI + Modal.com (serverless infrastructure)
|
| 32 |
+
- **Parallel Processing**: Up to 100+ concurrent scans via Modal's elastic scaling
|
| 33 |
+
|
| 34 |
+
## How It Works
|
| 35 |
+
|
| 36 |
+
1. **Single File Scan**: Paste code directly into the interface
|
| 37 |
+
2. **Batch Scan**: Upload multiple files or entire project directories
|
| 38 |
+
3. **Results**: View vulnerabilities organized by severity (Critical, High, Medium, Low)
|
| 39 |
+
4. **Export**: Download detailed reports in JSON or Markdown format
|
| 40 |
+
|
| 41 |
+
## Performance
|
| 42 |
+
|
| 43 |
+
- **Single File**: ~2-5 seconds
|
| 44 |
+
- **100 Files**: ~5-15 seconds (10-100x faster than sequential)
|
| 45 |
+
- **500 Files**: ~10-30 seconds
|
| 46 |
+
- **1500+ Files**: ~30-90 seconds
|
| 47 |
+
|
| 48 |
+
## Security & Privacy
|
| 49 |
+
|
| 50 |
+
- No persistent storage of uploaded code
|
| 51 |
+
- All scans run in isolated, ephemeral containers
|
| 52 |
+
- Containers destroyed immediately after scanning
|
| 53 |
+
- HTTPS encryption for all communications
|
| 54 |
+
|
| 55 |
+
## Usage
|
| 56 |
+
|
| 57 |
+
1. **Quick Scan**: Paste code in the "Scan Code" tab
|
| 58 |
+
2. **Project Scan**: Upload files in the "Scan Directory" tab
|
| 59 |
+
3. **Review**: Check findings organized by severity
|
| 60 |
+
4. **Export**: Download reports for documentation
|
| 61 |
+
|
| 62 |
+
## Technology Stack
|
| 63 |
+
|
| 64 |
+
- **Static Analysis**: Custom SAST engine with pattern matching
|
| 65 |
+
- **Backend**: FastAPI on Modal.com serverless platform
|
| 66 |
+
- **Frontend**: Gradio for interactive web interface
|
| 67 |
+
- **Deployment**: HuggingFace Spaces (frontend) + Modal.com (backend)
|
| 68 |
+
|
| 69 |
+
## License
|
| 70 |
+
|
| 71 |
+
MIT License - Feel free to use and modify for your projects!
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
**Powered by Modal.com serverless infrastructure** ⚡
|
app.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HuggingFace Spaces Entry Point for Security Auditor
|
| 3 |
+
Launches the Gradio interface with Modal backend integration
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from gradio_app import SecurityAuditorApp
|
| 7 |
+
|
| 8 |
+
if __name__ == "__main__":
|
| 9 |
+
app = SecurityAuditorApp()
|
| 10 |
+
app.interface.launch()
|
gradio_app.py
ADDED
|
@@ -0,0 +1,1611 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Modern Gradio Web Interface for Security Auditor
|
| 3 |
+
Based on design mockups in assets/ folder
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import asyncio
|
| 8 |
+
import tempfile
|
| 9 |
+
import shutil
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
from typing import List, Dict
|
| 13 |
+
import uuid
|
| 14 |
+
import os
|
| 15 |
+
import sys
|
| 16 |
+
import time
|
| 17 |
+
import threading
|
| 18 |
+
import json
|
| 19 |
+
import base64
|
| 20 |
+
import re
|
| 21 |
+
import httpx
|
| 22 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 23 |
+
import multiprocessing
|
| 24 |
+
|
| 25 |
+
# Import existing security checker (for local mode compatibility)
|
| 26 |
+
from security_checker import SecurityChecker, RiskLevel
|
| 27 |
+
|
| 28 |
+
# Modal backend URL - set via environment variable or use local default
|
| 29 |
+
MODAL_BACKEND_URL = os.getenv("MODAL_BACKEND_URL", "http://localhost:8000")
|
| 30 |
+
USE_MODAL_BACKEND = os.getenv("USE_MODAL_BACKEND", "false").lower() == "true"
|
| 31 |
+
|
| 32 |
+
# Configuration for hybrid parallel processing
|
| 33 |
+
PARALLEL_FILE_READING_WORKERS = min(multiprocessing.cpu_count() * 2, 16)
|
| 34 |
+
PARALLEL_CHUNK_LIMIT = 3 # Max concurrent Modal API calls
|
| 35 |
+
CHUNK_SIZE = 500 # Files per chunk
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class ModalScanResult:
|
| 39 |
+
"""
|
| 40 |
+
Adapter class to convert Modal backend API responses to ScanResult-like format
|
| 41 |
+
for compatibility with existing UI code
|
| 42 |
+
"""
|
| 43 |
+
def __init__(self, modal_response: dict, target: str, files_scanned: int = 1):
|
| 44 |
+
self.target = target
|
| 45 |
+
self.scan_type = "local"
|
| 46 |
+
self.files_scanned = files_scanned
|
| 47 |
+
self.errors = []
|
| 48 |
+
|
| 49 |
+
# Convert Modal response vulnerabilities to Vulnerability objects
|
| 50 |
+
if modal_response.get("success") and "findings" in modal_response:
|
| 51 |
+
vuln_data = modal_response["findings"].get("vulnerabilities", [])
|
| 52 |
+
self.vulnerabilities = []
|
| 53 |
+
for v in vuln_data:
|
| 54 |
+
# Create a simple object with attributes matching Vulnerability
|
| 55 |
+
vuln_obj = type('Vulnerability', (), v)()
|
| 56 |
+
vuln_obj.risk_level = type('RiskLevel', (), {'value': v.get('risk_level', 'INFO'), 'name': v.get('risk_level', 'INFO')})()
|
| 57 |
+
self.vulnerabilities.append(vuln_obj)
|
| 58 |
+
else:
|
| 59 |
+
self.vulnerabilities = []
|
| 60 |
+
if not modal_response.get("success"):
|
| 61 |
+
self.errors.append(modal_response.get("summary", {}).get("error", "Unknown error"))
|
| 62 |
+
|
| 63 |
+
def summary(self) -> dict:
|
| 64 |
+
"""Generate a summary compatible with UI expectations"""
|
| 65 |
+
summary = {level.value: 0 for level in RiskLevel}
|
| 66 |
+
for vuln in self.vulnerabilities:
|
| 67 |
+
if hasattr(vuln, 'risk_level'):
|
| 68 |
+
summary[vuln.risk_level.value] += 1
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
"target": self.target,
|
| 72 |
+
"scan_type": self.scan_type,
|
| 73 |
+
"duration_seconds": 0, # Not tracked in this simple adapter
|
| 74 |
+
"files_scanned": self.files_scanned,
|
| 75 |
+
"total_vulnerabilities": len(self.vulnerabilities),
|
| 76 |
+
**summary
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
# Import custom theme and UI components
|
| 80 |
+
from theme import create_security_auditor_theme
|
| 81 |
+
from ui_components import (
|
| 82 |
+
create_severity_badge,
|
| 83 |
+
create_finding_card,
|
| 84 |
+
create_summary_section,
|
| 85 |
+
create_empty_state,
|
| 86 |
+
create_loading_state
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
class ModernSecurityAuditorApp:
|
| 91 |
+
"""
|
| 92 |
+
Modern Gradio web interface for Security Auditor.
|
| 93 |
+
Based on design mockups in assets/ folder.
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
def __init__(self):
|
| 97 |
+
# Only instantiate SecurityChecker if not using Modal backend
|
| 98 |
+
self.use_modal_backend = USE_MODAL_BACKEND
|
| 99 |
+
self.modal_backend_url = MODAL_BACKEND_URL
|
| 100 |
+
if not self.use_modal_backend:
|
| 101 |
+
self.checker = SecurityChecker()
|
| 102 |
+
else:
|
| 103 |
+
self.checker = None # Not needed when using Modal backend
|
| 104 |
+
self.active_sessions = {}
|
| 105 |
+
self.cleanup_interval = 3600 # 1 hour
|
| 106 |
+
self.max_upload_size_mb = 100
|
| 107 |
+
self._help_html = self._prepare_help_content()
|
| 108 |
+
|
| 109 |
+
@staticmethod
|
| 110 |
+
def read_single_file(file_path: Path, target_path: Path, supported_extensions: set) -> dict:
|
| 111 |
+
"""Read a single file - designed for parallel execution"""
|
| 112 |
+
try:
|
| 113 |
+
if file_path.suffix not in supported_extensions:
|
| 114 |
+
return None
|
| 115 |
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
| 116 |
+
code = f.read()
|
| 117 |
+
return {
|
| 118 |
+
"code": code,
|
| 119 |
+
"language": file_path.suffix[1:], # Remove the dot
|
| 120 |
+
"filename": str(file_path.relative_to(target_path))
|
| 121 |
+
}
|
| 122 |
+
except Exception as e:
|
| 123 |
+
print(f"Error reading {file_path}: {e}")
|
| 124 |
+
return None
|
| 125 |
+
|
| 126 |
+
async def read_files_parallel(self, file_paths: List[Path], target_path: Path, max_workers: int = 8) -> List[dict]:
|
| 127 |
+
"""Read multiple files in parallel using ThreadPoolExecutor"""
|
| 128 |
+
supported_extensions = {'.py', '.js', '.java', '.go', '.php', '.rb', '.rs', '.cpp', '.c', '.ts', '.jsx', '.tsx'}
|
| 129 |
+
loop = asyncio.get_event_loop()
|
| 130 |
+
|
| 131 |
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 132 |
+
tasks = [
|
| 133 |
+
loop.run_in_executor(
|
| 134 |
+
executor,
|
| 135 |
+
self.read_single_file,
|
| 136 |
+
file_path,
|
| 137 |
+
target_path,
|
| 138 |
+
supported_extensions
|
| 139 |
+
)
|
| 140 |
+
for file_path in file_paths
|
| 141 |
+
]
|
| 142 |
+
results = await asyncio.gather(*tasks)
|
| 143 |
+
|
| 144 |
+
# Filter out None results (errors or unsupported files)
|
| 145 |
+
return [r for r in results if r is not None]
|
| 146 |
+
|
| 147 |
+
async def process_chunks_parallel(self, chunks: List[List[dict]]) -> List[dict]:
|
| 148 |
+
"""Process multiple chunks in parallel via Modal backend with rate limiting"""
|
| 149 |
+
semaphore = asyncio.Semaphore(PARALLEL_CHUNK_LIMIT)
|
| 150 |
+
|
| 151 |
+
async def _scan_with_limit(chunk):
|
| 152 |
+
async with semaphore:
|
| 153 |
+
return await self._scan_batch_via_modal(chunk)
|
| 154 |
+
|
| 155 |
+
tasks = [_scan_with_limit(chunk) for chunk in chunks]
|
| 156 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 157 |
+
|
| 158 |
+
# Handle any exceptions
|
| 159 |
+
valid_results = []
|
| 160 |
+
for i, result in enumerate(results):
|
| 161 |
+
if isinstance(result, Exception):
|
| 162 |
+
print(f"Chunk {i+1} failed: {result}")
|
| 163 |
+
else:
|
| 164 |
+
valid_results.append(result)
|
| 165 |
+
|
| 166 |
+
return valid_results
|
| 167 |
+
|
| 168 |
+
async def _scan_code_via_modal(self, code: str, language: str = "python") -> dict:
|
| 169 |
+
"""
|
| 170 |
+
Call Modal backend API to scan code
|
| 171 |
+
|
| 172 |
+
Args:
|
| 173 |
+
code: Source code to scan
|
| 174 |
+
language: Programming language (default: python)
|
| 175 |
+
|
| 176 |
+
Returns:
|
| 177 |
+
Dict with scan results from Modal backend
|
| 178 |
+
"""
|
| 179 |
+
try:
|
| 180 |
+
async with httpx.AsyncClient(timeout=300.0) as client:
|
| 181 |
+
response = await client.post(
|
| 182 |
+
f"{self.modal_backend_url}/scan",
|
| 183 |
+
json={
|
| 184 |
+
"code": code,
|
| 185 |
+
"language": language,
|
| 186 |
+
"scan_type": "all"
|
| 187 |
+
}
|
| 188 |
+
)
|
| 189 |
+
response.raise_for_status()
|
| 190 |
+
return response.json()
|
| 191 |
+
except httpx.HTTPError as e:
|
| 192 |
+
raise Exception(f"Modal backend error: {str(e)}")
|
| 193 |
+
except Exception as e:
|
| 194 |
+
raise Exception(f"Failed to connect to Modal backend: {str(e)}")
|
| 195 |
+
|
| 196 |
+
async def _scan_batch_via_modal(self, batch_files: List[Dict]) -> dict:
|
| 197 |
+
"""
|
| 198 |
+
Call Modal backend API to scan multiple files in parallel using batch endpoint
|
| 199 |
+
|
| 200 |
+
Args:
|
| 201 |
+
batch_files: List of dicts with 'code', 'language', and 'filename' keys
|
| 202 |
+
|
| 203 |
+
Returns:
|
| 204 |
+
Dict with batch scan results from Modal backend
|
| 205 |
+
"""
|
| 206 |
+
try:
|
| 207 |
+
# Extended timeout for batch processing (10 minutes)
|
| 208 |
+
async with httpx.AsyncClient(timeout=600.0) as client:
|
| 209 |
+
response = await client.post(
|
| 210 |
+
f"{self.modal_backend_url}/scan/batch",
|
| 211 |
+
json=batch_files
|
| 212 |
+
)
|
| 213 |
+
response.raise_for_status()
|
| 214 |
+
return response.json()
|
| 215 |
+
except httpx.HTTPError as e:
|
| 216 |
+
raise Exception(f"Modal batch backend error: {str(e)}")
|
| 217 |
+
except Exception as e:
|
| 218 |
+
raise Exception(f"Failed to connect to Modal batch backend: {str(e)}")
|
| 219 |
+
|
| 220 |
+
def _prepare_help_content(self):
|
| 221 |
+
"""Read help.html and embed images as base64 data URIs for self-contained display."""
|
| 222 |
+
help_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "help.html")
|
| 223 |
+
img_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "docimages")
|
| 224 |
+
|
| 225 |
+
try:
|
| 226 |
+
with open(help_path, "r", encoding="utf-8") as f:
|
| 227 |
+
html = f.read()
|
| 228 |
+
except FileNotFoundError:
|
| 229 |
+
return "<html><body><h1>Help file not found</h1></body></html>"
|
| 230 |
+
|
| 231 |
+
def replace_img_src(match):
|
| 232 |
+
filename = match.group(1)
|
| 233 |
+
img_path = os.path.join(img_dir, filename)
|
| 234 |
+
try:
|
| 235 |
+
with open(img_path, "rb") as img_f:
|
| 236 |
+
b64 = base64.b64encode(img_f.read()).decode("ascii")
|
| 237 |
+
return f'src="data:image/png;base64,{b64}"'
|
| 238 |
+
except FileNotFoundError:
|
| 239 |
+
return match.group(0)
|
| 240 |
+
|
| 241 |
+
html = re.sub(r'src="/helpimg/([^"]+)"', replace_img_src, html)
|
| 242 |
+
return html
|
| 243 |
+
|
| 244 |
+
def create_interface(self) -> gr.Blocks:
|
| 245 |
+
"""Create modern Gradio interface matching design mockups."""
|
| 246 |
+
|
| 247 |
+
self.theme = create_security_auditor_theme()
|
| 248 |
+
|
| 249 |
+
# Prepare help HTML as JSON-safe string for client-side Blob URL
|
| 250 |
+
self._help_js = json.dumps(self._help_html)
|
| 251 |
+
|
| 252 |
+
# Custom CSS for additional styling
|
| 253 |
+
self.custom_css = """
|
| 254 |
+
/* Tabler Icons CDN */
|
| 255 |
+
@import url('https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css');
|
| 256 |
+
|
| 257 |
+
/* Anthropic-inspired Theme Overrides */
|
| 258 |
+
:root {
|
| 259 |
+
--anthropic-cream: #faf9f6;
|
| 260 |
+
--anthropic-slate: #131314;
|
| 261 |
+
--anthropic-terracotta: #d97757;
|
| 262 |
+
--anthropic-terracotta-hover: #cc6944;
|
| 263 |
+
--anthropic-gray: #6b7280;
|
| 264 |
+
--anthropic-border: #e5e7eb;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
/* Logo and branding - Anthropic style */
|
| 268 |
+
.logo-container {
|
| 269 |
+
display: flex;
|
| 270 |
+
align-items: center;
|
| 271 |
+
gap: 12px;
|
| 272 |
+
padding: 20px 24px;
|
| 273 |
+
border-bottom: 1px solid var(--anthropic-border);
|
| 274 |
+
background: var(--anthropic-cream);
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
.logo-icon {
|
| 278 |
+
width: 40px;
|
| 279 |
+
height: 40px;
|
| 280 |
+
background: var(--anthropic-slate);
|
| 281 |
+
border-radius: 6px;
|
| 282 |
+
display: flex;
|
| 283 |
+
align-items: center;
|
| 284 |
+
justify-content: center;
|
| 285 |
+
color: white;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.logo-icon svg {
|
| 289 |
+
width: 24px;
|
| 290 |
+
height: 24px;
|
| 291 |
+
stroke: white;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
/* Section headers - cleaner, less shouty */
|
| 295 |
+
.section-header {
|
| 296 |
+
font-size: 13px;
|
| 297 |
+
font-weight: 600;
|
| 298 |
+
color: var(--anthropic-gray);
|
| 299 |
+
text-transform: none;
|
| 300 |
+
letter-spacing: normal;
|
| 301 |
+
margin: 20px 0 12px 0;
|
| 302 |
+
display: flex;
|
| 303 |
+
align-items: center;
|
| 304 |
+
gap: 8px;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.section-header svg {
|
| 308 |
+
width: 18px;
|
| 309 |
+
height: 18px;
|
| 310 |
+
stroke: var(--anthropic-gray);
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
/* Mode selector header */
|
| 314 |
+
.mode-selector-header {
|
| 315 |
+
font-size: 13px;
|
| 316 |
+
font-weight: 600;
|
| 317 |
+
color: var(--anthropic-gray);
|
| 318 |
+
text-transform: none;
|
| 319 |
+
letter-spacing: normal;
|
| 320 |
+
margin-bottom: 12px;
|
| 321 |
+
display: flex;
|
| 322 |
+
align-items: center;
|
| 323 |
+
gap: 8px;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.mode-selector-header svg {
|
| 327 |
+
width: 18px;
|
| 328 |
+
height: 18px;
|
| 329 |
+
stroke: var(--anthropic-gray);
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
/* Gradio overrides */
|
| 333 |
+
.gradio-container {
|
| 334 |
+
max-width: 1400px !important;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
/* Card styling */
|
| 338 |
+
.card-title {
|
| 339 |
+
margin: 0 0 8px 0;
|
| 340 |
+
font-size: 16px;
|
| 341 |
+
font-weight: 600;
|
| 342 |
+
color: var(--anthropic-slate);
|
| 343 |
+
display: flex;
|
| 344 |
+
align-items: center;
|
| 345 |
+
gap: 8px;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.card-title svg {
|
| 349 |
+
width: 20px;
|
| 350 |
+
height: 20px;
|
| 351 |
+
stroke: var(--anthropic-terracotta);
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.card-description {
|
| 355 |
+
margin: 0 0 16px 0;
|
| 356 |
+
color: var(--anthropic-gray);
|
| 357 |
+
font-size: 14px;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/* NVD Toggle Switch Styling */
|
| 361 |
+
.nvd-toggle {
|
| 362 |
+
margin: 0 !important;
|
| 363 |
+
padding: 0 !important;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.nvd-toggle input[type="checkbox"] {
|
| 367 |
+
appearance: none;
|
| 368 |
+
-webkit-appearance: none;
|
| 369 |
+
width: 48px;
|
| 370 |
+
height: 26px;
|
| 371 |
+
background: #e5e7eb;
|
| 372 |
+
border-radius: 13px;
|
| 373 |
+
position: relative;
|
| 374 |
+
cursor: pointer;
|
| 375 |
+
transition: background 0.3s ease;
|
| 376 |
+
margin: 0;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.nvd-toggle input[type="checkbox"]:checked {
|
| 380 |
+
background: var(--anthropic-terracotta);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.nvd-toggle input[type="checkbox"]::before {
|
| 384 |
+
content: "";
|
| 385 |
+
position: absolute;
|
| 386 |
+
width: 22px;
|
| 387 |
+
height: 22px;
|
| 388 |
+
border-radius: 50%;
|
| 389 |
+
background: white;
|
| 390 |
+
top: 2px;
|
| 391 |
+
left: 2px;
|
| 392 |
+
transition: left 0.3s ease;
|
| 393 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
.nvd-toggle input[type="checkbox"]:checked::before {
|
| 397 |
+
left: 24px;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.nvd-toggle label {
|
| 401 |
+
display: flex;
|
| 402 |
+
align-items: center;
|
| 403 |
+
cursor: pointer;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/* Tooltip positioning */
|
| 407 |
+
.info-icon-tooltip {
|
| 408 |
+
position: relative;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
/* Show tooltip on hover over the NVD label row */
|
| 412 |
+
.nvd-label-row:hover .info-tooltip-text {
|
| 413 |
+
visibility: visible !important;
|
| 414 |
+
opacity: 1 !important;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
/* Info badge - subtle, minimal */
|
| 418 |
+
.info-badge {
|
| 419 |
+
display: flex;
|
| 420 |
+
align-items: center;
|
| 421 |
+
gap: 12px;
|
| 422 |
+
padding: 16px;
|
| 423 |
+
background: #ffffff;
|
| 424 |
+
border: 1px solid var(--anthropic-border);
|
| 425 |
+
border-radius: 6px;
|
| 426 |
+
margin-top: 20px;
|
| 427 |
+
}
|
| 428 |
+
|
| 429 |
+
.info-badge-icon svg {
|
| 430 |
+
width: 24px;
|
| 431 |
+
height: 24px;
|
| 432 |
+
stroke: var(--anthropic-terracotta);
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.info-badge-content {
|
| 436 |
+
flex: 1;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.info-badge-title {
|
| 440 |
+
font-size: 14px;
|
| 441 |
+
font-weight: 600;
|
| 442 |
+
color: var(--anthropic-slate);
|
| 443 |
+
margin-bottom: 2px;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.info-badge-subtitle {
|
| 447 |
+
font-size: 12px;
|
| 448 |
+
color: var(--anthropic-gray);
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
/* Analyze button - Anthropic terracotta */
|
| 452 |
+
.analyze-button {
|
| 453 |
+
height: 100% !important;
|
| 454 |
+
min-height: 56px !important;
|
| 455 |
+
font-size: 15px !important;
|
| 456 |
+
font-weight: 600 !important;
|
| 457 |
+
display: flex !important;
|
| 458 |
+
align-items: center !important;
|
| 459 |
+
justify-content: center !important;
|
| 460 |
+
gap: 8px !important;
|
| 461 |
+
background: var(--anthropic-terracotta) !important;
|
| 462 |
+
border: none !important;
|
| 463 |
+
border-radius: 6px !important;
|
| 464 |
+
transition: background 0.2s ease !important;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.analyze-button:hover {
|
| 468 |
+
background: var(--anthropic-terracotta-hover) !important;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
.analyze-button svg {
|
| 472 |
+
width: 20px;
|
| 473 |
+
height: 20px;
|
| 474 |
+
stroke: white;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
/* Reset button - prominent */
|
| 478 |
+
.reset-button {
|
| 479 |
+
min-height: 44px !important;
|
| 480 |
+
font-size: 15px !important;
|
| 481 |
+
font-weight: 600 !important;
|
| 482 |
+
background: var(--anthropic-terracotta) !important;
|
| 483 |
+
border: none !important;
|
| 484 |
+
border-radius: 6px !important;
|
| 485 |
+
color: white !important;
|
| 486 |
+
transition: background 0.2s ease !important;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
.reset-button:hover {
|
| 490 |
+
background: var(--anthropic-terracotta-hover) !important;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
/* Help button - outlined, distinct from primary actions */
|
| 494 |
+
.help-button {
|
| 495 |
+
min-height: 44px !important;
|
| 496 |
+
font-size: 15px !important;
|
| 497 |
+
font-weight: 600 !important;
|
| 498 |
+
background: transparent !important;
|
| 499 |
+
border: 2px solid var(--anthropic-border) !important;
|
| 500 |
+
border-radius: 6px !important;
|
| 501 |
+
color: var(--anthropic-gray) !important;
|
| 502 |
+
transition: all 0.2s ease !important;
|
| 503 |
+
margin-top: 8px !important;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.help-button:hover {
|
| 507 |
+
border-color: var(--anthropic-terracotta) !important;
|
| 508 |
+
color: var(--anthropic-terracotta) !important;
|
| 509 |
+
background: rgba(217, 119, 87, 0.05) !important;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
/* Export button styling */
|
| 513 |
+
.export-button {
|
| 514 |
+
display: inline-flex !important;
|
| 515 |
+
align-items: center !important;
|
| 516 |
+
gap: 6px !important;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
/* Button icons via pseudo-elements */
|
| 520 |
+
.analyze-button::before {
|
| 521 |
+
content: "";
|
| 522 |
+
display: inline-block;
|
| 523 |
+
width: 20px;
|
| 524 |
+
height: 20px;
|
| 525 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cpath d='m21 21-4.35-4.35'%3E%3C/path%3E%3C/svg%3E");
|
| 526 |
+
background-size: contain;
|
| 527 |
+
background-repeat: no-repeat;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
.export-button::before {
|
| 531 |
+
content: "";
|
| 532 |
+
display: inline-block;
|
| 533 |
+
width: 18px;
|
| 534 |
+
height: 18px;
|
| 535 |
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'%3E%3C/path%3E%3Cpolyline points='7 10 12 15 17 10'%3E%3C/polyline%3E%3Cline x1='12' y1='15' x2='12' y2='3'%3E%3C/line%3E%3C/svg%3E");
|
| 536 |
+
background-size: contain;
|
| 537 |
+
background-repeat: no-repeat;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
/* Severity badges - static visual indicators */
|
| 541 |
+
.severity-badge {
|
| 542 |
+
position: relative;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
/* JavaScript for moving NVD toggle */
|
| 546 |
+
<script>
|
| 547 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 548 |
+
// Move NVD toggle into the container
|
| 549 |
+
setTimeout(function() {
|
| 550 |
+
const container = document.getElementById('nvd-toggle-container');
|
| 551 |
+
const toggle = document.querySelector('.nvd-toggle');
|
| 552 |
+
if (container && toggle) {
|
| 553 |
+
container.appendChild(toggle);
|
| 554 |
+
}
|
| 555 |
+
}, 100);
|
| 556 |
+
});
|
| 557 |
+
</script>
|
| 558 |
+
"""
|
| 559 |
+
|
| 560 |
+
with gr.Blocks(title="Security Auditor") as app:
|
| 561 |
+
|
| 562 |
+
# Header with logo
|
| 563 |
+
gr.HTML("""
|
| 564 |
+
<div class="logo-container">
|
| 565 |
+
<div class="logo-icon">
|
| 566 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
| 567 |
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 568 |
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
| 569 |
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
| 570 |
+
</svg>
|
| 571 |
+
</div>
|
| 572 |
+
<div>
|
| 573 |
+
<h1 style="margin: 0; font-size: 18px; font-weight: 700; color: #131314;">Security Auditor</h1>
|
| 574 |
+
</div>
|
| 575 |
+
</div>
|
| 576 |
+
""")
|
| 577 |
+
|
| 578 |
+
gr.Markdown("## Scan your application code for security vulnerabilities and get remediation guidance")
|
| 579 |
+
|
| 580 |
+
# Main layout
|
| 581 |
+
with gr.Row():
|
| 582 |
+
# Sidebar
|
| 583 |
+
with gr.Column(scale=1, min_width=250):
|
| 584 |
+
# Analysis Mode with integrated settings
|
| 585 |
+
with gr.Group():
|
| 586 |
+
gr.HTML('''<div class="mode-selector-header">
|
| 587 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
|
| 588 |
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 589 |
+
<line x1="12" y1="20" x2="12" y2="10"></line>
|
| 590 |
+
<line x1="18" y1="20" x2="18" y2="4"></line>
|
| 591 |
+
<line x1="6" y1="20" x2="6" y2="16"></line>
|
| 592 |
+
</svg>
|
| 593 |
+
Analysis Mode
|
| 594 |
+
</div>''')
|
| 595 |
+
|
| 596 |
+
scan_mode = gr.Radio(
|
| 597 |
+
choices=["Local Directory", "Remote URL"],
|
| 598 |
+
value="Local Directory",
|
| 599 |
+
label="",
|
| 600 |
+
container=False,
|
| 601 |
+
interactive=True
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
# NVD Enrichment toggle with info icon - side by side layout
|
| 605 |
+
gr.HTML('''
|
| 606 |
+
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 16px; gap: 12px;">
|
| 607 |
+
<div class="nvd-label-row" style="display: flex; align-items: center; gap: 8px; flex-shrink: 1; min-width: 0; cursor: default;">
|
| 608 |
+
<span style="
|
| 609 |
+
font-size: 14px;
|
| 610 |
+
font-weight: 600;
|
| 611 |
+
color: #131314;
|
| 612 |
+
white-space: nowrap;
|
| 613 |
+
">NVD Enriched Scan Results</span>
|
| 614 |
+
<div class="info-icon-tooltip" style="position: relative; display: inline-flex; align-items: center; flex-shrink: 0;">
|
| 615 |
+
<svg class="info-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
| 616 |
+
fill="none" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 617 |
+
style="cursor: pointer; display: block;"
|
| 618 |
+
onclick="toggleTooltip(this)">
|
| 619 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 620 |
+
<line x1="12" y1="16" x2="12" y2="12"></line>
|
| 621 |
+
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
| 622 |
+
</svg>
|
| 623 |
+
<div class="info-tooltip-text" style="
|
| 624 |
+
visibility: hidden;
|
| 625 |
+
opacity: 0;
|
| 626 |
+
position: absolute;
|
| 627 |
+
bottom: calc(100% + 8px);
|
| 628 |
+
left: 50%;
|
| 629 |
+
transform: translateX(-50%);
|
| 630 |
+
background: #131314;
|
| 631 |
+
color: white;
|
| 632 |
+
padding: 12px 16px;
|
| 633 |
+
border-radius: 6px;
|
| 634 |
+
font-size: 13px;
|
| 635 |
+
font-weight: 400;
|
| 636 |
+
white-space: normal;
|
| 637 |
+
width: 280px;
|
| 638 |
+
line-height: 1.5;
|
| 639 |
+
z-index: 9999;
|
| 640 |
+
transition: opacity 0.2s, visibility 0.2s;
|
| 641 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
|
| 642 |
+
">
|
| 643 |
+
Enriches scan results with related Common Vulnerabilities and Exposures (CVE) references from the National Vulnerability Database (NVD).
|
| 644 |
+
</div>
|
| 645 |
+
</div>
|
| 646 |
+
</div>
|
| 647 |
+
<div id="nvd-toggle-container" style="flex-shrink: 0;"></div>
|
| 648 |
+
</div>
|
| 649 |
+
<script>
|
| 650 |
+
(function() {
|
| 651 |
+
var outsideClickHandler = null;
|
| 652 |
+
|
| 653 |
+
window.toggleTooltip = function(icon) {
|
| 654 |
+
var tooltip = icon.nextElementSibling;
|
| 655 |
+
var isVisible = tooltip.style.visibility === 'visible';
|
| 656 |
+
|
| 657 |
+
// Hide all tooltips first
|
| 658 |
+
document.querySelectorAll('.info-tooltip-text').forEach(function(t) {
|
| 659 |
+
t.style.visibility = 'hidden';
|
| 660 |
+
t.style.opacity = '0';
|
| 661 |
+
});
|
| 662 |
+
|
| 663 |
+
// Remove any existing outside click handler
|
| 664 |
+
if (outsideClickHandler) {
|
| 665 |
+
document.removeEventListener('click', outsideClickHandler);
|
| 666 |
+
outsideClickHandler = null;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
if (!isVisible) {
|
| 670 |
+
tooltip.style.visibility = 'visible';
|
| 671 |
+
tooltip.style.opacity = '1';
|
| 672 |
+
|
| 673 |
+
setTimeout(function() {
|
| 674 |
+
outsideClickHandler = function(e) {
|
| 675 |
+
if (!icon.contains(e.target) && !tooltip.contains(e.target)) {
|
| 676 |
+
tooltip.style.visibility = 'hidden';
|
| 677 |
+
tooltip.style.opacity = '0';
|
| 678 |
+
document.removeEventListener('click', outsideClickHandler);
|
| 679 |
+
outsideClickHandler = null;
|
| 680 |
+
}
|
| 681 |
+
};
|
| 682 |
+
document.addEventListener('click', outsideClickHandler);
|
| 683 |
+
}, 100);
|
| 684 |
+
}
|
| 685 |
+
};
|
| 686 |
+
})();
|
| 687 |
+
</script>
|
| 688 |
+
''')
|
| 689 |
+
|
| 690 |
+
nvd_enrichment = gr.Checkbox(
|
| 691 |
+
label=None,
|
| 692 |
+
value=True,
|
| 693 |
+
elem_classes=["nvd-toggle"],
|
| 694 |
+
container=False,
|
| 695 |
+
show_label=False
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
|
| 699 |
+
# Not necessary to display Actions label with custom lightbolt icon as there is only one action.
|
| 700 |
+
# gr.HTML('''<div class="section-header">
|
| 701 |
+
# <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
|
| 702 |
+
# fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 703 |
+
# <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
|
| 704 |
+
# </svg>
|
| 705 |
+
# Actions
|
| 706 |
+
# </div>''')
|
| 707 |
+
|
| 708 |
+
reset_btn = gr.Button("Reset", variant="primary", size="lg", elem_classes=["reset-button"])
|
| 709 |
+
|
| 710 |
+
help_btn = gr.Button("Help", variant="secondary", size="lg", elem_classes=["help-button"])
|
| 711 |
+
|
| 712 |
+
# Info badge - Engine information
|
| 713 |
+
gr.HTML('''
|
| 714 |
+
<div class="info-badge">
|
| 715 |
+
<div class="info-badge-icon">
|
| 716 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
|
| 717 |
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 718 |
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
| 719 |
+
</svg>
|
| 720 |
+
</div>
|
| 721 |
+
<div class="info-badge-content">
|
| 722 |
+
<div class="info-badge-title">SAST + DAST + NVD</div>
|
| 723 |
+
<div class="info-badge-subtitle">40+ Vulnerability Checks</div>
|
| 724 |
+
</div>
|
| 725 |
+
</div>
|
| 726 |
+
''')
|
| 727 |
+
|
| 728 |
+
# Main content area
|
| 729 |
+
with gr.Column(scale=3):
|
| 730 |
+
|
| 731 |
+
# Input section (changes based on mode)
|
| 732 |
+
with gr.Group():
|
| 733 |
+
# Local Directory mode
|
| 734 |
+
with gr.Column(visible=True) as local_mode:
|
| 735 |
+
gr.HTML("""
|
| 736 |
+
<div style="background: white; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; margin-bottom: 16px;">
|
| 737 |
+
<h3 class="card-title">
|
| 738 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
| 739 |
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 740 |
+
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
| 741 |
+
</svg>
|
| 742 |
+
Application Directory
|
| 743 |
+
</h3>
|
| 744 |
+
<p class="card-description">Scan local directory containing the application code.</p>
|
| 745 |
+
</div>
|
| 746 |
+
""")
|
| 747 |
+
|
| 748 |
+
file_upload = gr.File(
|
| 749 |
+
label="Upload Files - Total Size Maximum 25 MB",
|
| 750 |
+
file_count="multiple",
|
| 751 |
+
file_types=[".py", ".js", ".ts", ".java", ".php", ".go", ".rb", ".c", ".cpp", ".cs", ".swift", ".kt", ".scala", ".rs", ".jsx", ".tsx"],
|
| 752 |
+
height=120
|
| 753 |
+
)
|
| 754 |
+
|
| 755 |
+
gr.HTML("""
|
| 756 |
+
<p style="color: #9ca3af; font-size: 12px; margin: -8px 0 16px 0; line-height: 1.5;">
|
| 757 |
+
Accepted: .py, .js, .ts, .java, .php, .go, .rb, .c, .cpp, .cs, .swift, .kt, .scala, .rs, .jsx, .tsx
|
| 758 |
+
</p>
|
| 759 |
+
""")
|
| 760 |
+
|
| 761 |
+
directory_path = gr.Textbox(
|
| 762 |
+
label="Or Enter Directory Path",
|
| 763 |
+
placeholder="C:/Projects/my-application",
|
| 764 |
+
lines=2,
|
| 765 |
+
max_lines=3
|
| 766 |
+
)
|
| 767 |
+
|
| 768 |
+
analyze_btn_local = gr.Button(
|
| 769 |
+
"Analyze",
|
| 770 |
+
variant="primary",
|
| 771 |
+
size="lg",
|
| 772 |
+
elem_classes=["analyze-button"]
|
| 773 |
+
)
|
| 774 |
+
|
| 775 |
+
# Remote URL mode
|
| 776 |
+
with gr.Column(visible=False) as url_mode:
|
| 777 |
+
gr.HTML("""
|
| 778 |
+
<div style="background: white; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; margin-bottom: 16px;">
|
| 779 |
+
<h3 class="card-title">
|
| 780 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
| 781 |
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 782 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 783 |
+
<line x1="2" y1="12" x2="22" y2="12"></line>
|
| 784 |
+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
|
| 785 |
+
</svg>
|
| 786 |
+
Application URL
|
| 787 |
+
</h3>
|
| 788 |
+
<p class="card-description">Scan remote web application or deployment.</p>
|
| 789 |
+
</div>
|
| 790 |
+
""")
|
| 791 |
+
|
| 792 |
+
web_url = gr.Textbox(
|
| 793 |
+
label="Web Application URL",
|
| 794 |
+
placeholder="https://your-app.example.com",
|
| 795 |
+
lines=2,
|
| 796 |
+
max_lines=3
|
| 797 |
+
)
|
| 798 |
+
|
| 799 |
+
analyze_btn_url = gr.Button(
|
| 800 |
+
"Analyze",
|
| 801 |
+
variant="primary",
|
| 802 |
+
size="lg",
|
| 803 |
+
elem_classes=["analyze-button"]
|
| 804 |
+
)
|
| 805 |
+
|
| 806 |
+
# Progress indicator
|
| 807 |
+
progress_box = gr.HTML(value=create_empty_state())
|
| 808 |
+
|
| 809 |
+
# Results section
|
| 810 |
+
results_section = gr.Column(visible=False)
|
| 811 |
+
with results_section:
|
| 812 |
+
# Analysis Summary
|
| 813 |
+
summary_html = gr.HTML()
|
| 814 |
+
|
| 815 |
+
# Security Findings
|
| 816 |
+
gr.HTML("""
|
| 817 |
+
<h2 style="
|
| 818 |
+
margin: 16px 0 13px 0;
|
| 819 |
+
font-size: 20px;
|
| 820 |
+
font-weight: 700;
|
| 821 |
+
color: #131314;
|
| 822 |
+
display: flex;
|
| 823 |
+
align-items: center;
|
| 824 |
+
gap: 8px;
|
| 825 |
+
">
|
| 826 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
| 827 |
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 828 |
+
style="stroke: #d97757;">
|
| 829 |
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
| 830 |
+
<line x1="12" y1="9" x2="12" y2="13"></line>
|
| 831 |
+
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
| 832 |
+
</svg>
|
| 833 |
+
Security Findings
|
| 834 |
+
</h2>
|
| 835 |
+
""")
|
| 836 |
+
|
| 837 |
+
findings_count = gr.HTML()
|
| 838 |
+
findings_html = gr.HTML()
|
| 839 |
+
|
| 840 |
+
# Export button
|
| 841 |
+
with gr.Row():
|
| 842 |
+
export_json_btn = gr.Button(
|
| 843 |
+
"Export JSON Report",
|
| 844 |
+
variant="primary",
|
| 845 |
+
size="lg",
|
| 846 |
+
elem_classes=["export-button"]
|
| 847 |
+
)
|
| 848 |
+
export_md_btn = gr.Button(
|
| 849 |
+
"Export Markdown Report",
|
| 850 |
+
variant="primary",
|
| 851 |
+
size="lg",
|
| 852 |
+
elem_classes=["export-button"]
|
| 853 |
+
)
|
| 854 |
+
|
| 855 |
+
download_file = gr.File(label="Download Report", visible=False)
|
| 856 |
+
download_file_md = gr.File(label="Download Markdown Report", visible=False)
|
| 857 |
+
|
| 858 |
+
# Event handlers
|
| 859 |
+
|
| 860 |
+
def toggle_mode(mode):
|
| 861 |
+
"""Toggle visibility based on selected mode."""
|
| 862 |
+
return {
|
| 863 |
+
local_mode: gr.update(visible=(mode == "Local Directory")),
|
| 864 |
+
url_mode: gr.update(visible=(mode == "Remote URL"))
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
scan_mode.change(
|
| 868 |
+
fn=toggle_mode,
|
| 869 |
+
inputs=[scan_mode],
|
| 870 |
+
outputs=[local_mode, url_mode]
|
| 871 |
+
)
|
| 872 |
+
|
| 873 |
+
def reset_interface():
|
| 874 |
+
"""Reset the interface to initial state."""
|
| 875 |
+
return (
|
| 876 |
+
create_empty_state(), # progress_box
|
| 877 |
+
gr.update(visible=False), # results_section
|
| 878 |
+
"", # summary_html
|
| 879 |
+
"", # findings_count
|
| 880 |
+
"", # findings_html
|
| 881 |
+
None, # file_upload
|
| 882 |
+
"", # directory_path
|
| 883 |
+
"", # web_url
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
reset_btn.click(
|
| 887 |
+
fn=reset_interface,
|
| 888 |
+
outputs=[
|
| 889 |
+
progress_box,
|
| 890 |
+
results_section,
|
| 891 |
+
summary_html,
|
| 892 |
+
findings_count,
|
| 893 |
+
findings_html,
|
| 894 |
+
file_upload,
|
| 895 |
+
directory_path,
|
| 896 |
+
web_url
|
| 897 |
+
]
|
| 898 |
+
)
|
| 899 |
+
|
| 900 |
+
help_btn.click(
|
| 901 |
+
fn=None,
|
| 902 |
+
inputs=[],
|
| 903 |
+
outputs=[],
|
| 904 |
+
js=f"() => {{ const html = {self._help_js}; const blob = new Blob([html], {{type:'text/html;charset=utf-8'}}); window.open(URL.createObjectURL(blob), '_blank'); }}"
|
| 905 |
+
)
|
| 906 |
+
|
| 907 |
+
def scan_local_files(files, dir_path, nvd_check):
|
| 908 |
+
"""Handle local file/directory scanning."""
|
| 909 |
+
# Show loading state
|
| 910 |
+
yield (
|
| 911 |
+
create_loading_state("Initializing scan..."),
|
| 912 |
+
gr.update(visible=False),
|
| 913 |
+
"", "", "", None, None
|
| 914 |
+
)
|
| 915 |
+
|
| 916 |
+
# Check if we have files or directory path
|
| 917 |
+
if not files and not dir_path:
|
| 918 |
+
yield (
|
| 919 |
+
"""<div style="background: #fef2f2; border: 1px solid #fca5a5; border-radius: 12px; padding: 20px; color: #dc2626;">
|
| 920 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
| 921 |
+
fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 922 |
+
style="display: inline-block; vertical-align: middle; margin-right: 8px;">
|
| 923 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 924 |
+
<line x1="15" y1="9" x2="9" y2="15"></line>
|
| 925 |
+
<line x1="9" y1="9" x2="15" y2="15"></line>
|
| 926 |
+
</svg>
|
| 927 |
+
<strong>No input provided</strong><br/>
|
| 928 |
+
Please upload files or enter a directory path.
|
| 929 |
+
</div>""",
|
| 930 |
+
gr.update(visible=False),
|
| 931 |
+
"", "", "", None, None
|
| 932 |
+
)
|
| 933 |
+
return
|
| 934 |
+
|
| 935 |
+
# Create session
|
| 936 |
+
session_id = str(uuid.uuid4())
|
| 937 |
+
session_dir = Path(tempfile.mkdtemp(prefix=f"audit_{session_id}_"))
|
| 938 |
+
|
| 939 |
+
try:
|
| 940 |
+
target_path = session_dir
|
| 941 |
+
|
| 942 |
+
# If files uploaded, save them
|
| 943 |
+
if files:
|
| 944 |
+
yield (
|
| 945 |
+
create_loading_state(f"Uploading {len(files)} files..."),
|
| 946 |
+
gr.update(visible=False),
|
| 947 |
+
"", "", "", None, None
|
| 948 |
+
)
|
| 949 |
+
for file in files:
|
| 950 |
+
dest = session_dir / Path(file.name).name
|
| 951 |
+
shutil.copy(file.name, dest)
|
| 952 |
+
|
| 953 |
+
# If directory path provided, use it directly (if exists)
|
| 954 |
+
elif dir_path and os.path.exists(dir_path):
|
| 955 |
+
target_path = Path(dir_path)
|
| 956 |
+
|
| 957 |
+
yield (
|
| 958 |
+
create_loading_state("Scanning for vulnerabilities..."),
|
| 959 |
+
gr.update(visible=False),
|
| 960 |
+
"", "", "", None, None
|
| 961 |
+
)
|
| 962 |
+
|
| 963 |
+
# Run scan (Modal backend or local)
|
| 964 |
+
if self.use_modal_backend:
|
| 965 |
+
# Use Modal backend API with parallel batch processing
|
| 966 |
+
loop = asyncio.new_event_loop()
|
| 967 |
+
asyncio.set_event_loop(loop)
|
| 968 |
+
|
| 969 |
+
# Collect all file paths first
|
| 970 |
+
file_paths = [
|
| 971 |
+
file_path
|
| 972 |
+
for file_path in target_path.rglob("*")
|
| 973 |
+
if file_path.is_file() and file_path.suffix in ['.py', '.js', '.java', '.go', '.php', '.rb', '.rs', '.cpp', '.c', '.ts', '.jsx', '.tsx']
|
| 974 |
+
]
|
| 975 |
+
|
| 976 |
+
# Read files in parallel
|
| 977 |
+
yield (
|
| 978 |
+
create_loading_state(f"Reading {len(file_paths)} files in parallel..."),
|
| 979 |
+
gr.update(visible=False),
|
| 980 |
+
"", "", "", None, None
|
| 981 |
+
)
|
| 982 |
+
|
| 983 |
+
batch_files = loop.run_until_complete(
|
| 984 |
+
self.read_files_parallel(file_paths, target_path, max_workers=PARALLEL_FILE_READING_WORKERS)
|
| 985 |
+
)
|
| 986 |
+
|
| 987 |
+
# Process files in parallel using batch endpoint
|
| 988 |
+
# Split into chunks if batch is too large (Modal backend limit)
|
| 989 |
+
all_vulnerabilities = []
|
| 990 |
+
|
| 991 |
+
if len(batch_files) <= CHUNK_SIZE:
|
| 992 |
+
# Single batch - process all files in parallel
|
| 993 |
+
yield (
|
| 994 |
+
create_loading_state(f"Processing {len(batch_files)} files in parallel on Modal..."),
|
| 995 |
+
gr.update(visible=False),
|
| 996 |
+
"", "", "", None, None
|
| 997 |
+
)
|
| 998 |
+
|
| 999 |
+
try:
|
| 1000 |
+
batch_result = loop.run_until_complete(
|
| 1001 |
+
self._scan_batch_via_modal(batch_files)
|
| 1002 |
+
)
|
| 1003 |
+
|
| 1004 |
+
if batch_result.get("success") and "results" in batch_result:
|
| 1005 |
+
for file_result in batch_result["results"]:
|
| 1006 |
+
if file_result.get("status") == "success" and "vulnerabilities" in file_result:
|
| 1007 |
+
all_vulnerabilities.extend(file_result["vulnerabilities"])
|
| 1008 |
+
elif file_result.get("status") == "error":
|
| 1009 |
+
print(f"Error scanning {file_result.get('filename', 'unknown')}: {file_result.get('error', 'Unknown error')}")
|
| 1010 |
+
except Exception as e:
|
| 1011 |
+
print(f"Batch scan error: {e}")
|
| 1012 |
+
# Fall back to sequential processing on error
|
| 1013 |
+
for file_data in batch_files:
|
| 1014 |
+
try:
|
| 1015 |
+
modal_response = loop.run_until_complete(
|
| 1016 |
+
self._scan_code_via_modal(file_data["code"], file_data["language"])
|
| 1017 |
+
)
|
| 1018 |
+
if modal_response.get("success") and "findings" in modal_response:
|
| 1019 |
+
vulns = modal_response["findings"].get("vulnerabilities", [])
|
| 1020 |
+
for v in vulns:
|
| 1021 |
+
v["file_path"] = file_data["filename"]
|
| 1022 |
+
all_vulnerabilities.extend(vulns)
|
| 1023 |
+
except Exception as file_error:
|
| 1024 |
+
print(f"Error scanning {file_data['filename']}: {file_error}")
|
| 1025 |
+
else:
|
| 1026 |
+
# Split into chunks and process all chunks in parallel
|
| 1027 |
+
chunks = [batch_files[i:i + CHUNK_SIZE] for i in range(0, len(batch_files), CHUNK_SIZE)]
|
| 1028 |
+
|
| 1029 |
+
yield (
|
| 1030 |
+
create_loading_state(f"Processing {len(chunks)} chunks in parallel on Modal ({len(batch_files)} files total)..."),
|
| 1031 |
+
gr.update(visible=False),
|
| 1032 |
+
"", "", "", None, None
|
| 1033 |
+
)
|
| 1034 |
+
|
| 1035 |
+
try:
|
| 1036 |
+
# Process all chunks in parallel
|
| 1037 |
+
chunk_results = loop.run_until_complete(
|
| 1038 |
+
self.process_chunks_parallel(chunks)
|
| 1039 |
+
)
|
| 1040 |
+
|
| 1041 |
+
# Aggregate results from all chunks
|
| 1042 |
+
for chunk_result in chunk_results:
|
| 1043 |
+
if chunk_result.get("success") and "results" in chunk_result:
|
| 1044 |
+
for file_result in chunk_result["results"]:
|
| 1045 |
+
if file_result.get("status") == "success" and "vulnerabilities" in file_result:
|
| 1046 |
+
all_vulnerabilities.extend(file_result["vulnerabilities"])
|
| 1047 |
+
elif file_result.get("status") == "error":
|
| 1048 |
+
print(f"Error scanning {file_result.get('filename', 'unknown')}: {file_result.get('error', 'Unknown error')}")
|
| 1049 |
+
except Exception as e:
|
| 1050 |
+
print(f"Parallel chunk processing error: {e}")
|
| 1051 |
+
|
| 1052 |
+
loop.close()
|
| 1053 |
+
|
| 1054 |
+
# Create result object compatible with UI
|
| 1055 |
+
result = ModalScanResult(
|
| 1056 |
+
{"success": True, "findings": {"vulnerabilities": all_vulnerabilities}},
|
| 1057 |
+
str(target_path),
|
| 1058 |
+
len(batch_files)
|
| 1059 |
+
)
|
| 1060 |
+
else:
|
| 1061 |
+
# Use local SecurityChecker with parallel processing
|
| 1062 |
+
loop = asyncio.new_event_loop()
|
| 1063 |
+
asyncio.set_event_loop(loop)
|
| 1064 |
+
|
| 1065 |
+
# Determine number of workers based on CPU count
|
| 1066 |
+
import multiprocessing
|
| 1067 |
+
cpu_count = multiprocessing.cpu_count()
|
| 1068 |
+
max_workers = min(cpu_count * 2, 16) # Use 2x CPU cores, max 16
|
| 1069 |
+
|
| 1070 |
+
yield (
|
| 1071 |
+
create_loading_state(f"Scanning files in parallel (using {max_workers} workers)..."),
|
| 1072 |
+
gr.update(visible=False),
|
| 1073 |
+
"", "", "", None, None
|
| 1074 |
+
)
|
| 1075 |
+
|
| 1076 |
+
result = loop.run_until_complete(
|
| 1077 |
+
self.checker.scan_local(
|
| 1078 |
+
str(target_path),
|
| 1079 |
+
include_nvd=nvd_check,
|
| 1080 |
+
max_workers=max_workers,
|
| 1081 |
+
use_parallel=True
|
| 1082 |
+
)
|
| 1083 |
+
)
|
| 1084 |
+
loop.close()
|
| 1085 |
+
|
| 1086 |
+
# Generate HTML components
|
| 1087 |
+
summary = result.summary()
|
| 1088 |
+
summary_section = create_summary_section({
|
| 1089 |
+
'target': str(target_path),
|
| 1090 |
+
'files_scanned': result.files_scanned,
|
| 1091 |
+
'scan_type': 'local',
|
| 1092 |
+
'summary': summary
|
| 1093 |
+
})
|
| 1094 |
+
|
| 1095 |
+
# Create filter script for severity badges
|
| 1096 |
+
filter_script = """
|
| 1097 |
+
<script>
|
| 1098 |
+
// Filter function called by severity badges
|
| 1099 |
+
window.filterBySeverityBadge = function(severity) {
|
| 1100 |
+
const allBadges = document.querySelectorAll('.severity-badge');
|
| 1101 |
+
const allCards = document.querySelectorAll('.finding-card');
|
| 1102 |
+
|
| 1103 |
+
// Check if clicking the active badge (toggle off)
|
| 1104 |
+
const clickedBadge = document.querySelector('.severity-badge[data-severity="' + severity + '"]');
|
| 1105 |
+
const isActive = clickedBadge && clickedBadge.classList.contains('active');
|
| 1106 |
+
|
| 1107 |
+
if (isActive) {
|
| 1108 |
+
// Show all findings
|
| 1109 |
+
allBadges.forEach(function(badge) {
|
| 1110 |
+
badge.classList.remove('active');
|
| 1111 |
+
badge.classList.remove('inactive');
|
| 1112 |
+
});
|
| 1113 |
+
allCards.forEach(function(card) {
|
| 1114 |
+
card.style.display = 'block';
|
| 1115 |
+
});
|
| 1116 |
+
updateFindingsCount(allCards.length);
|
| 1117 |
+
} else {
|
| 1118 |
+
// Filter by severity
|
| 1119 |
+
allBadges.forEach(function(badge) {
|
| 1120 |
+
if (badge.getAttribute('data-severity') === severity) {
|
| 1121 |
+
badge.classList.add('active');
|
| 1122 |
+
badge.classList.remove('inactive');
|
| 1123 |
+
} else {
|
| 1124 |
+
badge.classList.remove('active');
|
| 1125 |
+
badge.classList.add('inactive');
|
| 1126 |
+
}
|
| 1127 |
+
});
|
| 1128 |
+
|
| 1129 |
+
var visibleCount = 0;
|
| 1130 |
+
allCards.forEach(function(card) {
|
| 1131 |
+
if (card.getAttribute('data-severity') === severity) {
|
| 1132 |
+
card.style.display = 'block';
|
| 1133 |
+
visibleCount++;
|
| 1134 |
+
} else {
|
| 1135 |
+
card.style.display = 'none';
|
| 1136 |
+
}
|
| 1137 |
+
});
|
| 1138 |
+
|
| 1139 |
+
updateFindingsCount(visibleCount);
|
| 1140 |
+
|
| 1141 |
+
// Scroll to findings
|
| 1142 |
+
const firstCard = document.querySelector('.finding-card[style*="display: block"]');
|
| 1143 |
+
if (firstCard) {
|
| 1144 |
+
firstCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
| 1145 |
+
}
|
| 1146 |
+
}
|
| 1147 |
+
}
|
| 1148 |
+
|
| 1149 |
+
function updateFindingsCount(count) {
|
| 1150 |
+
const countElement = document.getElementById('findings-count-display');
|
| 1151 |
+
if (countElement) {
|
| 1152 |
+
countElement.innerHTML = 'Showing <strong>' + count + '</strong> finding' + (count !== 1 ? 's' : '');
|
| 1153 |
+
}
|
| 1154 |
+
}
|
| 1155 |
+
</script>
|
| 1156 |
+
"""
|
| 1157 |
+
|
| 1158 |
+
findings = ""
|
| 1159 |
+
if result.vulnerabilities:
|
| 1160 |
+
# Deduplicate vulnerabilities based on name, file, and line
|
| 1161 |
+
seen = set()
|
| 1162 |
+
unique_vulns = []
|
| 1163 |
+
for vuln in result.vulnerabilities:
|
| 1164 |
+
# Create unique key
|
| 1165 |
+
key = f"{vuln.name}|{vuln.file_path}|{vuln.line_number}"
|
| 1166 |
+
if key not in seen:
|
| 1167 |
+
seen.add(key)
|
| 1168 |
+
unique_vulns.append(vuln)
|
| 1169 |
+
|
| 1170 |
+
findings_counter = f"""
|
| 1171 |
+
<div id="findings-count-display" style="
|
| 1172 |
+
color: #6b7280;
|
| 1173 |
+
font-size: 14px;
|
| 1174 |
+
margin: 13px 0 16px 0;
|
| 1175 |
+
padding: 12px;
|
| 1176 |
+
background: #f9fafb;
|
| 1177 |
+
border-radius: 8px;
|
| 1178 |
+
">
|
| 1179 |
+
Showing <strong>{len(unique_vulns)}</strong> findings
|
| 1180 |
+
</div>
|
| 1181 |
+
"""
|
| 1182 |
+
|
| 1183 |
+
# Sort by severity
|
| 1184 |
+
severity_order = {
|
| 1185 |
+
RiskLevel.CRITICAL: 0,
|
| 1186 |
+
RiskLevel.HIGH: 1,
|
| 1187 |
+
RiskLevel.MEDIUM: 2,
|
| 1188 |
+
RiskLevel.LOW: 3,
|
| 1189 |
+
RiskLevel.INFO: 4
|
| 1190 |
+
}
|
| 1191 |
+
sorted_vulns = sorted(
|
| 1192 |
+
unique_vulns,
|
| 1193 |
+
key=lambda v: severity_order.get(v.risk_level, 5)
|
| 1194 |
+
)
|
| 1195 |
+
|
| 1196 |
+
for vuln in sorted_vulns:
|
| 1197 |
+
findings += create_finding_card({
|
| 1198 |
+
'name': vuln.name,
|
| 1199 |
+
'risk_level': vuln.risk_level.name,
|
| 1200 |
+
'file_path': vuln.file_path,
|
| 1201 |
+
'line_number': vuln.line_number,
|
| 1202 |
+
'description': vuln.description,
|
| 1203 |
+
'cwe_id': vuln.cwe_id,
|
| 1204 |
+
'cve_ids': vuln.cve_ids,
|
| 1205 |
+
'remediation': vuln.remediation
|
| 1206 |
+
})
|
| 1207 |
+
else:
|
| 1208 |
+
findings_counter = ""
|
| 1209 |
+
findings = """
|
| 1210 |
+
<div style="
|
| 1211 |
+
background: #f0fdf4;
|
| 1212 |
+
border: 1px solid #86efac;
|
| 1213 |
+
border-radius: 12px;
|
| 1214 |
+
padding: 40px;
|
| 1215 |
+
text-align: center;
|
| 1216 |
+
color: #166534;
|
| 1217 |
+
">
|
| 1218 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"
|
| 1219 |
+
fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 1220 |
+
style="margin: 0 auto 16px; display: block;">
|
| 1221 |
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
| 1222 |
+
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
| 1223 |
+
</svg>
|
| 1224 |
+
<h3 style="margin: 0 0 8px 0; font-size: 20px; font-weight: 600;">No Vulnerabilities Found</h3>
|
| 1225 |
+
<p style="margin: 0; font-size: 14px;">Your code looks secure!</p>
|
| 1226 |
+
</div>
|
| 1227 |
+
"""
|
| 1228 |
+
|
| 1229 |
+
# Generate reports for download
|
| 1230 |
+
json_report = self.checker.generate_report(result, format="json")
|
| 1231 |
+
json_path = session_dir / "security_report.json"
|
| 1232 |
+
with open(json_path, 'w') as f:
|
| 1233 |
+
f.write(json_report)
|
| 1234 |
+
|
| 1235 |
+
md_report = self.checker.generate_report(result, format="markdown")
|
| 1236 |
+
md_path = session_dir / "security_report.md"
|
| 1237 |
+
with open(md_path, 'w') as f:
|
| 1238 |
+
f.write(md_report)
|
| 1239 |
+
|
| 1240 |
+
# Schedule cleanup
|
| 1241 |
+
self.active_sessions[session_id] = {
|
| 1242 |
+
'dir': session_dir,
|
| 1243 |
+
'created': datetime.now(),
|
| 1244 |
+
'json_path': json_path,
|
| 1245 |
+
'md_path': md_path
|
| 1246 |
+
}
|
| 1247 |
+
|
| 1248 |
+
progress_msg = f"""
|
| 1249 |
+
<div style="
|
| 1250 |
+
background: #f0fdf4;
|
| 1251 |
+
border: 1px solid #86efac;
|
| 1252 |
+
border-radius: 12px;
|
| 1253 |
+
padding: 20px;
|
| 1254 |
+
color: #166534;
|
| 1255 |
+
">
|
| 1256 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
| 1257 |
+
fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 1258 |
+
style="display: inline-block; vertical-align: middle; margin-right: 8px;">
|
| 1259 |
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
| 1260 |
+
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
| 1261 |
+
</svg>
|
| 1262 |
+
<strong>Scan complete!</strong><br/>
|
| 1263 |
+
Files scanned: {result.files_scanned} | Vulnerabilities found: {summary['total_vulnerabilities']}
|
| 1264 |
+
</div>
|
| 1265 |
+
"""
|
| 1266 |
+
|
| 1267 |
+
yield (
|
| 1268 |
+
progress_msg,
|
| 1269 |
+
gr.update(visible=True),
|
| 1270 |
+
summary_section + filter_script,
|
| 1271 |
+
findings_counter,
|
| 1272 |
+
findings,
|
| 1273 |
+
str(json_path),
|
| 1274 |
+
str(md_path)
|
| 1275 |
+
)
|
| 1276 |
+
|
| 1277 |
+
except Exception as e:
|
| 1278 |
+
error_msg = f"""
|
| 1279 |
+
<div style="
|
| 1280 |
+
background: #fef2f2;
|
| 1281 |
+
border: 1px solid #fca5a5;
|
| 1282 |
+
border-radius: 12px;
|
| 1283 |
+
padding: 20px;
|
| 1284 |
+
color: #dc2626;
|
| 1285 |
+
">
|
| 1286 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
| 1287 |
+
fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 1288 |
+
style="display: inline-block; vertical-align: middle; margin-right: 8px;">
|
| 1289 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1290 |
+
<line x1="15" y1="9" x2="9" y2="15"></line>
|
| 1291 |
+
<line x1="9" y1="9" x2="15" y2="15"></line>
|
| 1292 |
+
</svg>
|
| 1293 |
+
<strong>Scan failed</strong><br/>
|
| 1294 |
+
{str(e)}
|
| 1295 |
+
</div>
|
| 1296 |
+
"""
|
| 1297 |
+
yield (
|
| 1298 |
+
error_msg,
|
| 1299 |
+
gr.update(visible=False),
|
| 1300 |
+
"", "", "", None, None
|
| 1301 |
+
)
|
| 1302 |
+
|
| 1303 |
+
def scan_web_app(url, nvd_check):
|
| 1304 |
+
"""Handle web application scanning."""
|
| 1305 |
+
yield (
|
| 1306 |
+
create_loading_state("Scanning web application..."),
|
| 1307 |
+
gr.update(visible=False),
|
| 1308 |
+
"", "", "", None, None
|
| 1309 |
+
)
|
| 1310 |
+
|
| 1311 |
+
if not url:
|
| 1312 |
+
yield (
|
| 1313 |
+
"""<div style="background: #fef2f2; border: 1px solid #fca5a5; border-radius: 12px; padding: 20px; color: #dc2626;">
|
| 1314 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
| 1315 |
+
fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 1316 |
+
style="display: inline-block; vertical-align: middle; margin-right: 8px;">
|
| 1317 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1318 |
+
<line x1="15" y1="9" x2="9" y2="15"></line>
|
| 1319 |
+
<line x1="9" y1="9" x2="15" y2="15"></line>
|
| 1320 |
+
</svg>
|
| 1321 |
+
<strong>No URL provided</strong><br/>
|
| 1322 |
+
Please enter a web application URL.
|
| 1323 |
+
</div>""",
|
| 1324 |
+
gr.update(visible=False),
|
| 1325 |
+
"", "", "", None, None
|
| 1326 |
+
)
|
| 1327 |
+
return
|
| 1328 |
+
|
| 1329 |
+
try:
|
| 1330 |
+
# Ensure URL has protocol
|
| 1331 |
+
if not url.startswith(('http://', 'https://')):
|
| 1332 |
+
url = 'https://' + url
|
| 1333 |
+
|
| 1334 |
+
# Run scan
|
| 1335 |
+
loop = asyncio.new_event_loop()
|
| 1336 |
+
asyncio.set_event_loop(loop)
|
| 1337 |
+
result = loop.run_until_complete(
|
| 1338 |
+
self.checker.scan_web(url, include_nvd=False)
|
| 1339 |
+
)
|
| 1340 |
+
loop.close()
|
| 1341 |
+
|
| 1342 |
+
# Generate HTML components (similar to local scan)
|
| 1343 |
+
summary = result.summary()
|
| 1344 |
+
summary_section = create_summary_section({
|
| 1345 |
+
'target': url,
|
| 1346 |
+
'files_scanned': 0,
|
| 1347 |
+
'scan_type': 'web',
|
| 1348 |
+
'summary': summary
|
| 1349 |
+
})
|
| 1350 |
+
|
| 1351 |
+
# Create filter script for severity badges
|
| 1352 |
+
filter_script = """
|
| 1353 |
+
<script>
|
| 1354 |
+
// Filter function called by severity badges
|
| 1355 |
+
window.filterBySeverityBadge = function(severity) {
|
| 1356 |
+
const allBadges = document.querySelectorAll('.severity-badge');
|
| 1357 |
+
const allCards = document.querySelectorAll('.finding-card');
|
| 1358 |
+
|
| 1359 |
+
// Check if clicking the active badge (toggle off)
|
| 1360 |
+
const clickedBadge = document.querySelector('.severity-badge[data-severity="' + severity + '"]');
|
| 1361 |
+
const isActive = clickedBadge && clickedBadge.classList.contains('active');
|
| 1362 |
+
|
| 1363 |
+
if (isActive) {
|
| 1364 |
+
// Show all findings
|
| 1365 |
+
allBadges.forEach(function(badge) {
|
| 1366 |
+
badge.classList.remove('active');
|
| 1367 |
+
badge.classList.remove('inactive');
|
| 1368 |
+
});
|
| 1369 |
+
allCards.forEach(function(card) {
|
| 1370 |
+
card.style.display = 'block';
|
| 1371 |
+
});
|
| 1372 |
+
updateFindingsCount(allCards.length);
|
| 1373 |
+
} else {
|
| 1374 |
+
// Filter by severity
|
| 1375 |
+
allBadges.forEach(function(badge) {
|
| 1376 |
+
if (badge.getAttribute('data-severity') === severity) {
|
| 1377 |
+
badge.classList.add('active');
|
| 1378 |
+
badge.classList.remove('inactive');
|
| 1379 |
+
} else {
|
| 1380 |
+
badge.classList.remove('active');
|
| 1381 |
+
badge.classList.add('inactive');
|
| 1382 |
+
}
|
| 1383 |
+
});
|
| 1384 |
+
|
| 1385 |
+
var visibleCount = 0;
|
| 1386 |
+
allCards.forEach(function(card) {
|
| 1387 |
+
if (card.getAttribute('data-severity') === severity) {
|
| 1388 |
+
card.style.display = 'block';
|
| 1389 |
+
visibleCount++;
|
| 1390 |
+
} else {
|
| 1391 |
+
card.style.display = 'none';
|
| 1392 |
+
}
|
| 1393 |
+
});
|
| 1394 |
+
|
| 1395 |
+
updateFindingsCount(visibleCount);
|
| 1396 |
+
|
| 1397 |
+
// Scroll to findings
|
| 1398 |
+
const firstCard = document.querySelector('.finding-card[style*="display: block"]');
|
| 1399 |
+
if (firstCard) {
|
| 1400 |
+
firstCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
| 1401 |
+
}
|
| 1402 |
+
}
|
| 1403 |
+
}
|
| 1404 |
+
|
| 1405 |
+
function updateFindingsCount(count) {
|
| 1406 |
+
const countElement = document.getElementById('findings-count-display');
|
| 1407 |
+
if (countElement) {
|
| 1408 |
+
countElement.innerHTML = 'Showing <strong>' + count + '</strong> finding' + (count !== 1 ? 's' : '');
|
| 1409 |
+
}
|
| 1410 |
+
}
|
| 1411 |
+
</script>
|
| 1412 |
+
"""
|
| 1413 |
+
|
| 1414 |
+
# Deduplicate vulnerabilities
|
| 1415 |
+
seen = set()
|
| 1416 |
+
unique_vulns = []
|
| 1417 |
+
for vuln in result.vulnerabilities:
|
| 1418 |
+
# Create unique key (for URL scans, line_number is usually 0, so use name and description)
|
| 1419 |
+
key = f"{vuln.name}|{vuln.description}"
|
| 1420 |
+
if key not in seen:
|
| 1421 |
+
seen.add(key)
|
| 1422 |
+
unique_vulns.append(vuln)
|
| 1423 |
+
|
| 1424 |
+
findings_counter = f"""
|
| 1425 |
+
<div id="findings-count-display" style="color: #6b7280; font-size: 14px; margin: 13px 0 16px 0; padding: 12px; background: #f9fafb; border-radius: 8px;">
|
| 1426 |
+
Showing <strong>{len(unique_vulns)}</strong> findings
|
| 1427 |
+
</div>
|
| 1428 |
+
"""
|
| 1429 |
+
|
| 1430 |
+
findings = ""
|
| 1431 |
+
if unique_vulns:
|
| 1432 |
+
for vuln in unique_vulns:
|
| 1433 |
+
findings += create_finding_card({
|
| 1434 |
+
'name': vuln.name,
|
| 1435 |
+
'risk_level': vuln.risk_level.name,
|
| 1436 |
+
'file_path': url,
|
| 1437 |
+
'line_number': 0,
|
| 1438 |
+
'description': vuln.description,
|
| 1439 |
+
'cwe_id': vuln.cwe_id,
|
| 1440 |
+
'cve_ids': vuln.cve_ids,
|
| 1441 |
+
'remediation': vuln.remediation
|
| 1442 |
+
})
|
| 1443 |
+
else:
|
| 1444 |
+
findings_counter = ""
|
| 1445 |
+
findings = """
|
| 1446 |
+
<div style="background: #f0fdf4; border: 1px solid #86efac; border-radius: 12px; padding: 40px; text-align: center; color: #166534;">
|
| 1447 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"
|
| 1448 |
+
fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 1449 |
+
style="margin: 0 auto 16px; display: block;">
|
| 1450 |
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
| 1451 |
+
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
| 1452 |
+
</svg>
|
| 1453 |
+
<h3 style="margin: 0 0 8px 0;">No Issues Found</h3>
|
| 1454 |
+
<p style="margin: 0;">Web application appears to be configured securely.</p>
|
| 1455 |
+
</div>
|
| 1456 |
+
"""
|
| 1457 |
+
|
| 1458 |
+
# Generate report
|
| 1459 |
+
session_id = str(uuid.uuid4())
|
| 1460 |
+
session_dir = Path(tempfile.mkdtemp(prefix=f"audit_{session_id}_"))
|
| 1461 |
+
json_report = self.checker.generate_report(result, format="json")
|
| 1462 |
+
json_path = session_dir / "security_report.json"
|
| 1463 |
+
with open(json_path, 'w') as f:
|
| 1464 |
+
f.write(json_report)
|
| 1465 |
+
|
| 1466 |
+
md_report = self.checker.generate_report(result, format="markdown")
|
| 1467 |
+
md_path = session_dir / "security_report.md"
|
| 1468 |
+
with open(md_path, 'w') as f:
|
| 1469 |
+
f.write(md_report)
|
| 1470 |
+
|
| 1471 |
+
progress_msg = f"""
|
| 1472 |
+
<div style="background: #f0fdf4; border: 1px solid #86efac; border-radius: 12px; padding: 20px; color: #166534;">
|
| 1473 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
| 1474 |
+
fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 1475 |
+
style="display: inline-block; vertical-align: middle; margin-right: 8px;">
|
| 1476 |
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
| 1477 |
+
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
| 1478 |
+
</svg>
|
| 1479 |
+
<strong>Scan complete!</strong><br/>
|
| 1480 |
+
Vulnerabilities found: {summary['total_vulnerabilities']}
|
| 1481 |
+
</div>
|
| 1482 |
+
"""
|
| 1483 |
+
|
| 1484 |
+
yield (
|
| 1485 |
+
progress_msg,
|
| 1486 |
+
gr.update(visible=True),
|
| 1487 |
+
summary_section + filter_script,
|
| 1488 |
+
findings_counter,
|
| 1489 |
+
findings,
|
| 1490 |
+
str(json_path),
|
| 1491 |
+
str(md_path)
|
| 1492 |
+
)
|
| 1493 |
+
|
| 1494 |
+
except Exception as e:
|
| 1495 |
+
error_msg = f"""
|
| 1496 |
+
<div style="background: #fef2f2; border: 1px solid #fca5a5; border-radius: 12px; padding: 20px; color: #dc2626;">
|
| 1497 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
| 1498 |
+
fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 1499 |
+
style="display: inline-block; vertical-align: middle; margin-right: 8px;">
|
| 1500 |
+
<circle cx="12" cy="12" r="10"></circle>
|
| 1501 |
+
<line x1="15" y1="9" x2="9" y2="15"></line>
|
| 1502 |
+
<line x1="9" y1="9" x2="15" y2="15"></line>
|
| 1503 |
+
</svg>
|
| 1504 |
+
<strong>Scan failed</strong><br/>
|
| 1505 |
+
{str(e)}
|
| 1506 |
+
</div>
|
| 1507 |
+
"""
|
| 1508 |
+
yield (
|
| 1509 |
+
error_msg,
|
| 1510 |
+
gr.update(visible=False),
|
| 1511 |
+
"", "", "", None, None
|
| 1512 |
+
)
|
| 1513 |
+
|
| 1514 |
+
# Connect event handlers
|
| 1515 |
+
analyze_btn_local.click(
|
| 1516 |
+
fn=scan_local_files,
|
| 1517 |
+
inputs=[file_upload, directory_path, nvd_enrichment],
|
| 1518 |
+
outputs=[
|
| 1519 |
+
progress_box,
|
| 1520 |
+
results_section,
|
| 1521 |
+
summary_html,
|
| 1522 |
+
findings_count,
|
| 1523 |
+
findings_html,
|
| 1524 |
+
download_file,
|
| 1525 |
+
download_file_md
|
| 1526 |
+
]
|
| 1527 |
+
)
|
| 1528 |
+
|
| 1529 |
+
analyze_btn_url.click(
|
| 1530 |
+
fn=scan_web_app,
|
| 1531 |
+
inputs=[web_url, nvd_enrichment],
|
| 1532 |
+
outputs=[
|
| 1533 |
+
progress_box,
|
| 1534 |
+
results_section,
|
| 1535 |
+
summary_html,
|
| 1536 |
+
findings_count,
|
| 1537 |
+
findings_html,
|
| 1538 |
+
download_file,
|
| 1539 |
+
download_file_md
|
| 1540 |
+
]
|
| 1541 |
+
)
|
| 1542 |
+
|
| 1543 |
+
def export_json():
|
| 1544 |
+
"""Export JSON report."""
|
| 1545 |
+
return gr.update(visible=True)
|
| 1546 |
+
|
| 1547 |
+
def export_markdown():
|
| 1548 |
+
"""Export Markdown report."""
|
| 1549 |
+
return gr.update(visible=True)
|
| 1550 |
+
|
| 1551 |
+
export_json_btn.click(
|
| 1552 |
+
fn=export_json,
|
| 1553 |
+
outputs=[download_file]
|
| 1554 |
+
)
|
| 1555 |
+
|
| 1556 |
+
export_md_btn.click(
|
| 1557 |
+
fn=export_markdown,
|
| 1558 |
+
outputs=[download_file_md]
|
| 1559 |
+
)
|
| 1560 |
+
|
| 1561 |
+
return app
|
| 1562 |
+
|
| 1563 |
+
def cleanup_old_sessions(self):
|
| 1564 |
+
"""Remove sessions older than cleanup_interval."""
|
| 1565 |
+
now = datetime.now()
|
| 1566 |
+
to_remove = []
|
| 1567 |
+
|
| 1568 |
+
for session_id, session in self.active_sessions.items():
|
| 1569 |
+
age = (now - session['created']).total_seconds()
|
| 1570 |
+
if age > self.cleanup_interval:
|
| 1571 |
+
shutil.rmtree(session['dir'], ignore_errors=True)
|
| 1572 |
+
to_remove.append(session_id)
|
| 1573 |
+
|
| 1574 |
+
for session_id in to_remove:
|
| 1575 |
+
del self.active_sessions[session_id]
|
| 1576 |
+
|
| 1577 |
+
def launch(self, **kwargs):
|
| 1578 |
+
"""Launch the Gradio application."""
|
| 1579 |
+
# Start cleanup background task
|
| 1580 |
+
def cleanup_loop():
|
| 1581 |
+
while True:
|
| 1582 |
+
time.sleep(600) # Check every 10 minutes
|
| 1583 |
+
self.cleanup_old_sessions()
|
| 1584 |
+
|
| 1585 |
+
cleanup_thread = threading.Thread(target=cleanup_loop, daemon=True)
|
| 1586 |
+
cleanup_thread.start()
|
| 1587 |
+
|
| 1588 |
+
# Create and launch interface
|
| 1589 |
+
interface = self.create_interface()
|
| 1590 |
+
interface.queue()
|
| 1591 |
+
|
| 1592 |
+
# In Gradio 6.0, theme and css are passed to launch() instead of Blocks()
|
| 1593 |
+
interface.launch(
|
| 1594 |
+
theme=self.theme,
|
| 1595 |
+
css=self.custom_css,
|
| 1596 |
+
**kwargs
|
| 1597 |
+
)
|
| 1598 |
+
|
| 1599 |
+
|
| 1600 |
+
def main():
|
| 1601 |
+
"""Main entry point."""
|
| 1602 |
+
app = ModernSecurityAuditorApp()
|
| 1603 |
+
app.launch(
|
| 1604 |
+
server_name="0.0.0.0",
|
| 1605 |
+
server_port=7860,
|
| 1606 |
+
share=False
|
| 1607 |
+
)
|
| 1608 |
+
|
| 1609 |
+
|
| 1610 |
+
if __name__ == "__main__":
|
| 1611 |
+
main()
|
help.html
ADDED
|
@@ -0,0 +1,539 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Security Auditor - Help Guide</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #faf9f6;
|
| 10 |
+
--text: #131314;
|
| 11 |
+
--accent: #d97757;
|
| 12 |
+
--accent-hover: #cc6944;
|
| 13 |
+
--gray: #6b7280;
|
| 14 |
+
--light-gray: #9ca3af;
|
| 15 |
+
--border: #e5e7eb;
|
| 16 |
+
--card-bg: #ffffff;
|
| 17 |
+
--critical: #dc2626;
|
| 18 |
+
--critical-bg: #fef2f2;
|
| 19 |
+
--critical-border: #fca5a5;
|
| 20 |
+
--high: #ea580c;
|
| 21 |
+
--high-bg: #fff7ed;
|
| 22 |
+
--high-border: #fdba74;
|
| 23 |
+
--medium: #d97706;
|
| 24 |
+
--medium-bg: #fffbeb;
|
| 25 |
+
--medium-border: #fcd34d;
|
| 26 |
+
--low: #0d9488;
|
| 27 |
+
--low-bg: #f0fdfa;
|
| 28 |
+
--low-border: #5eead4;
|
| 29 |
+
--info-color: #6b7280;
|
| 30 |
+
--info-bg: #f9fafb;
|
| 31 |
+
--info-border: #d1d5db;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 35 |
+
|
| 36 |
+
body {
|
| 37 |
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 38 |
+
background: var(--bg);
|
| 39 |
+
color: var(--text);
|
| 40 |
+
line-height: 1.7;
|
| 41 |
+
max-width: 920px;
|
| 42 |
+
margin: 0 auto;
|
| 43 |
+
padding: 40px 24px 60px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
/* Header */
|
| 47 |
+
.header {
|
| 48 |
+
display: flex;
|
| 49 |
+
align-items: center;
|
| 50 |
+
gap: 14px;
|
| 51 |
+
padding-bottom: 24px;
|
| 52 |
+
border-bottom: 1px solid var(--border);
|
| 53 |
+
margin-bottom: 32px;
|
| 54 |
+
}
|
| 55 |
+
.header-icon {
|
| 56 |
+
width: 44px;
|
| 57 |
+
height: 44px;
|
| 58 |
+
background: var(--text);
|
| 59 |
+
border-radius: 8px;
|
| 60 |
+
display: flex;
|
| 61 |
+
align-items: center;
|
| 62 |
+
justify-content: center;
|
| 63 |
+
flex-shrink: 0;
|
| 64 |
+
}
|
| 65 |
+
.header-icon svg { width: 24px; height: 24px; stroke: white; fill: none; }
|
| 66 |
+
.header-title { font-size: 24px; font-weight: 700; }
|
| 67 |
+
.header-subtitle { font-size: 14px; color: var(--gray); margin-top: 2px; }
|
| 68 |
+
|
| 69 |
+
/* Headings */
|
| 70 |
+
h2 {
|
| 71 |
+
font-size: 22px;
|
| 72 |
+
font-weight: 700;
|
| 73 |
+
margin: 48px 0 16px;
|
| 74 |
+
padding-bottom: 8px;
|
| 75 |
+
border-bottom: 2px solid var(--border);
|
| 76 |
+
display: flex;
|
| 77 |
+
align-items: center;
|
| 78 |
+
gap: 10px;
|
| 79 |
+
}
|
| 80 |
+
h2 .h-num {
|
| 81 |
+
display: inline-flex;
|
| 82 |
+
align-items: center;
|
| 83 |
+
justify-content: center;
|
| 84 |
+
width: 30px;
|
| 85 |
+
height: 30px;
|
| 86 |
+
background: var(--accent);
|
| 87 |
+
color: white;
|
| 88 |
+
border-radius: 50%;
|
| 89 |
+
font-size: 14px;
|
| 90 |
+
font-weight: 700;
|
| 91 |
+
flex-shrink: 0;
|
| 92 |
+
}
|
| 93 |
+
h3 {
|
| 94 |
+
font-size: 17px;
|
| 95 |
+
font-weight: 600;
|
| 96 |
+
margin: 28px 0 12px;
|
| 97 |
+
color: var(--text);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
/* Paragraphs and lists */
|
| 101 |
+
p { margin: 0 0 14px; color: var(--text); }
|
| 102 |
+
ul, ol { margin: 0 0 16px; padding-left: 24px; }
|
| 103 |
+
li { margin: 6px 0; }
|
| 104 |
+
|
| 105 |
+
/* Links */
|
| 106 |
+
a { color: var(--accent); text-decoration: none; }
|
| 107 |
+
a:hover { color: var(--accent-hover); text-decoration: underline; }
|
| 108 |
+
|
| 109 |
+
/* Table of Contents */
|
| 110 |
+
.toc {
|
| 111 |
+
background: var(--card-bg);
|
| 112 |
+
border: 1px solid var(--border);
|
| 113 |
+
border-radius: 10px;
|
| 114 |
+
padding: 24px 28px;
|
| 115 |
+
margin: 0 0 40px;
|
| 116 |
+
}
|
| 117 |
+
.toc-title {
|
| 118 |
+
font-size: 15px;
|
| 119 |
+
font-weight: 700;
|
| 120 |
+
margin-bottom: 12px;
|
| 121 |
+
color: var(--text);
|
| 122 |
+
}
|
| 123 |
+
.toc ol { padding-left: 20px; margin: 0; }
|
| 124 |
+
.toc li { margin: 7px 0; font-size: 15px; }
|
| 125 |
+
.toc a { color: var(--accent); font-weight: 500; }
|
| 126 |
+
|
| 127 |
+
/* Cards */
|
| 128 |
+
.card {
|
| 129 |
+
background: var(--card-bg);
|
| 130 |
+
border: 1px solid var(--border);
|
| 131 |
+
border-radius: 10px;
|
| 132 |
+
padding: 20px 24px;
|
| 133 |
+
margin: 16px 0;
|
| 134 |
+
}
|
| 135 |
+
.card-label {
|
| 136 |
+
font-size: 12px;
|
| 137 |
+
font-weight: 600;
|
| 138 |
+
text-transform: uppercase;
|
| 139 |
+
letter-spacing: 0.05em;
|
| 140 |
+
color: var(--gray);
|
| 141 |
+
margin-bottom: 6px;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/* Steps */
|
| 145 |
+
.steps { counter-reset: step; list-style: none; padding-left: 0; }
|
| 146 |
+
.steps li {
|
| 147 |
+
counter-increment: step;
|
| 148 |
+
position: relative;
|
| 149 |
+
padding-left: 40px;
|
| 150 |
+
margin: 14px 0;
|
| 151 |
+
}
|
| 152 |
+
.steps li::before {
|
| 153 |
+
content: counter(step);
|
| 154 |
+
position: absolute;
|
| 155 |
+
left: 0;
|
| 156 |
+
top: 1px;
|
| 157 |
+
width: 26px;
|
| 158 |
+
height: 26px;
|
| 159 |
+
background: var(--accent);
|
| 160 |
+
color: white;
|
| 161 |
+
border-radius: 50%;
|
| 162 |
+
font-size: 13px;
|
| 163 |
+
font-weight: 700;
|
| 164 |
+
display: flex;
|
| 165 |
+
align-items: center;
|
| 166 |
+
justify-content: center;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/* Severity badges */
|
| 170 |
+
.severity-sample {
|
| 171 |
+
display: inline-flex;
|
| 172 |
+
align-items: center;
|
| 173 |
+
gap: 8px;
|
| 174 |
+
padding: 8px 16px;
|
| 175 |
+
border-radius: 6px;
|
| 176 |
+
font-size: 13px;
|
| 177 |
+
font-weight: 600;
|
| 178 |
+
border: 2px solid;
|
| 179 |
+
margin: 4px 4px 4px 0;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* Severity table */
|
| 183 |
+
.severity-table { width: 100%; border-collapse: collapse; margin: 16px 0; }
|
| 184 |
+
.severity-table th {
|
| 185 |
+
text-align: left;
|
| 186 |
+
padding: 12px 16px;
|
| 187 |
+
background: var(--bg);
|
| 188 |
+
border-bottom: 2px solid var(--border);
|
| 189 |
+
font-size: 13px;
|
| 190 |
+
font-weight: 600;
|
| 191 |
+
color: var(--gray);
|
| 192 |
+
text-transform: uppercase;
|
| 193 |
+
letter-spacing: 0.05em;
|
| 194 |
+
}
|
| 195 |
+
.severity-table td {
|
| 196 |
+
padding: 14px 16px;
|
| 197 |
+
border-bottom: 1px solid var(--border);
|
| 198 |
+
vertical-align: top;
|
| 199 |
+
font-size: 14px;
|
| 200 |
+
}
|
| 201 |
+
.severity-table tr:last-child td { border-bottom: none; }
|
| 202 |
+
.severity-tag {
|
| 203 |
+
display: inline-block;
|
| 204 |
+
padding: 3px 10px;
|
| 205 |
+
border-radius: 4px;
|
| 206 |
+
font-size: 12px;
|
| 207 |
+
font-weight: 700;
|
| 208 |
+
white-space: nowrap;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
/* Screenshots */
|
| 212 |
+
.screenshot {
|
| 213 |
+
display: block;
|
| 214 |
+
max-width: 100%;
|
| 215 |
+
border: 1px solid var(--border);
|
| 216 |
+
border-radius: 10px;
|
| 217 |
+
margin: 20px 0;
|
| 218 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
| 219 |
+
}
|
| 220 |
+
.screenshot-caption {
|
| 221 |
+
font-size: 13px;
|
| 222 |
+
color: var(--gray);
|
| 223 |
+
text-align: center;
|
| 224 |
+
margin: -10px 0 20px;
|
| 225 |
+
font-style: italic;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
/* Tip / Note boxes */
|
| 229 |
+
.tip {
|
| 230 |
+
background: var(--high-bg);
|
| 231 |
+
border-left: 4px solid var(--accent);
|
| 232 |
+
border-radius: 0 8px 8px 0;
|
| 233 |
+
padding: 14px 18px;
|
| 234 |
+
margin: 16px 0;
|
| 235 |
+
font-size: 14px;
|
| 236 |
+
}
|
| 237 |
+
.tip-label {
|
| 238 |
+
font-weight: 700;
|
| 239 |
+
color: var(--accent);
|
| 240 |
+
margin-bottom: 4px;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/* Footer */
|
| 244 |
+
.footer {
|
| 245 |
+
margin-top: 60px;
|
| 246 |
+
padding-top: 24px;
|
| 247 |
+
border-top: 1px solid var(--border);
|
| 248 |
+
text-align: center;
|
| 249 |
+
color: var(--gray);
|
| 250 |
+
font-size: 13px;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* Keyboard shortcut styling */
|
| 254 |
+
kbd {
|
| 255 |
+
background: var(--bg);
|
| 256 |
+
border: 1px solid var(--border);
|
| 257 |
+
border-radius: 4px;
|
| 258 |
+
padding: 2px 6px;
|
| 259 |
+
font-size: 13px;
|
| 260 |
+
font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
/* Print */
|
| 264 |
+
@media print {
|
| 265 |
+
body { background: white; max-width: 100%; padding: 20px; }
|
| 266 |
+
.toc { break-after: page; }
|
| 267 |
+
.card, .severity-table tr { break-inside: avoid; }
|
| 268 |
+
.screenshot { max-width: 80%; margin: 12px auto; }
|
| 269 |
+
a { color: var(--text); }
|
| 270 |
+
h2 { break-after: avoid; }
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
/* Responsive */
|
| 274 |
+
@media (max-width: 640px) {
|
| 275 |
+
body { padding: 20px 16px 40px; }
|
| 276 |
+
.header-title { font-size: 20px; }
|
| 277 |
+
h2 { font-size: 19px; }
|
| 278 |
+
.severity-table th, .severity-table td { padding: 10px 10px; font-size: 13px; }
|
| 279 |
+
}
|
| 280 |
+
</style>
|
| 281 |
+
</head>
|
| 282 |
+
<body>
|
| 283 |
+
|
| 284 |
+
<!-- Header -->
|
| 285 |
+
<div class="header">
|
| 286 |
+
<div class="header-icon">
|
| 287 |
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 288 |
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
| 289 |
+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
| 290 |
+
</svg>
|
| 291 |
+
</div>
|
| 292 |
+
<div>
|
| 293 |
+
<div class="header-title">Security Auditor</div>
|
| 294 |
+
<div class="header-subtitle">Help Guide</div>
|
| 295 |
+
</div>
|
| 296 |
+
</div>
|
| 297 |
+
|
| 298 |
+
<!-- Table of Contents -->
|
| 299 |
+
<nav class="toc">
|
| 300 |
+
<div class="toc-title">Contents</div>
|
| 301 |
+
<ol>
|
| 302 |
+
<li><a href="#getting-started">Getting Started</a></li>
|
| 303 |
+
<li><a href="#local-scan">Scanning a Local Directory</a></li>
|
| 304 |
+
<li><a href="#remote-scan">Scanning a Remote URL</a></li>
|
| 305 |
+
<li><a href="#understanding-results">Understanding Your Results</a></li>
|
| 306 |
+
<li><a href="#nvd-enrichment">National Vulnerability Database (NVD) Enrichment</a> <a href="https://nvd.nist.gov/general/cve-process" target="_blank" rel="noopener noreferrer">NVD CVE Process</a></li>
|
| 307 |
+
<li><a href="#severity-guide">Severity Levels Guide</a></li>
|
| 308 |
+
</ol>
|
| 309 |
+
</nav>
|
| 310 |
+
|
| 311 |
+
<!-- Section 1: Getting Started -->
|
| 312 |
+
<h2 id="getting-started"><span class="h-num">1</span> Getting Started</h2>
|
| 313 |
+
|
| 314 |
+
<p>
|
| 315 |
+
Security Auditor is a combined <strong>Static Application Security Testing (SAST)</strong> and
|
| 316 |
+
<strong>Dynamic Application Security Testing (DAST)</strong> platform that identifies security
|
| 317 |
+
vulnerabilities in your application code and web deployments. It performs <strong>40+ security checks</strong>
|
| 318 |
+
across two scanning modes.
|
| 319 |
+
</p>
|
| 320 |
+
|
| 321 |
+
<p>
|
| 322 |
+
The checks this tool performs overlap with the <strong><a href="https://owasp.org/www-project-top-ten/" target="_blank" rel="noopener noreferrer">Open Web Application Security Project (OWASP)</a></strong> Top Ten; many checks map to common OWASP categories such as Injection, Cross-Site Scripting (XSS), Broken Authentication, and Security Misconfiguration.
|
| 323 |
+
</p>
|
| 324 |
+
|
| 325 |
+
<div class="card">
|
| 326 |
+
<div class="card-label">Two Scanning Modes</div>
|
| 327 |
+
<ul>
|
| 328 |
+
<li><strong>Local Directory</strong> — Upload source code files or specify a directory path. The SAST engine scans your code for 28+ vulnerability patterns including SQL injection, XSS, command injection, hardcoded credentials, and more.</li>
|
| 329 |
+
<li><strong>Remote URL</strong> — Enter a web application URL. The DAST engine checks HTTP security headers, probes for exposed sensitive paths, verifies HTTPS configuration, and scans response content for information leaks.</li>
|
| 330 |
+
</ul>
|
| 331 |
+
</div>
|
| 332 |
+
|
| 333 |
+
<img src="/helpimg/NewGradioScreenshot_landingPage.png"
|
| 334 |
+
alt="Security Auditor landing page showing Local Directory mode selected"
|
| 335 |
+
class="screenshot" />
|
| 336 |
+
<p class="screenshot-caption">The Security Auditor landing page with Local Directory mode selected.</p>
|
| 337 |
+
|
| 338 |
+
<p>
|
| 339 |
+
<strong>Supported file types for local scanning:</strong><br />
|
| 340 |
+
<code>.py, .js, .ts, .java, .php, .go, .rb, .c, .cpp, .cs, .swift, .kt, .scala, .rs, .jsx, .tsx</code>
|
| 341 |
+
</p>
|
| 342 |
+
|
| 343 |
+
<!-- Section 2: Local Directory Scan -->
|
| 344 |
+
<h2 id="local-scan"><span class="h-num">2</span> Scanning a Local Directory</h2>
|
| 345 |
+
|
| 346 |
+
<p>Use this mode to scan application source code files for security vulnerabilities using static analysis.</p>
|
| 347 |
+
|
| 348 |
+
<ol class="steps">
|
| 349 |
+
<li>Select <strong>Local Directory</strong> in the Analysis Mode panel on the left sidebar.</li>
|
| 350 |
+
<li>Provide your code using one of two methods:
|
| 351 |
+
<ul>
|
| 352 |
+
<li><strong>Upload files</strong> — Drag and drop or click the upload area (total size maximum 25 MB).</li>
|
| 353 |
+
<li><strong>Enter a directory path</strong> — Type the full path to a local directory, e.g. <kbd>C:/Projects/my-application</kbd>.</li>
|
| 354 |
+
</ul>
|
| 355 |
+
</li>
|
| 356 |
+
<li>Optionally toggle <strong>NVD Enriched Scan Results</strong> on or off (see <a href="#nvd-enrichment">Section 5</a>).</li>
|
| 357 |
+
<li>Click the <strong>Analyze</strong> button.</li>
|
| 358 |
+
<li>Wait for the scan to complete. A progress indicator shows the current status.</li>
|
| 359 |
+
</ol>
|
| 360 |
+
|
| 361 |
+
<div class="tip">
|
| 362 |
+
<div class="tip-label">Tip</div>
|
| 363 |
+
When uploading files, you can select multiple files at once. The scanner analyses all uploaded files together, detecting cross-file vulnerability patterns.
|
| 364 |
+
</div>
|
| 365 |
+
|
| 366 |
+
<img src="/helpimg/NewGradioScreenshot_LocalAppResult.png"
|
| 367 |
+
alt="Local directory scan results showing Analysis Summary and Security Findings"
|
| 368 |
+
class="screenshot" />
|
| 369 |
+
<p class="screenshot-caption">Results from a local directory scan showing the Analysis Summary, severity badges, and individual finding cards.</p>
|
| 370 |
+
|
| 371 |
+
<!-- Section 3: Remote URL Scan -->
|
| 372 |
+
<h2 id="remote-scan"><span class="h-num">3</span> Scanning a Remote URL</h2>
|
| 373 |
+
|
| 374 |
+
<p>Use this mode to dynamically test a running web application for security misconfigurations and vulnerabilities.</p>
|
| 375 |
+
|
| 376 |
+
<ol class="steps">
|
| 377 |
+
<li>Select <strong>Remote URL</strong> in the Analysis Mode panel.</li>
|
| 378 |
+
<li>Enter the target web application URL in the <strong>Web Application URL</strong> field, e.g. <kbd>https://your-app.example.com</kbd>.</li>
|
| 379 |
+
<li>Click the <strong>Analyze</strong> button.</li>
|
| 380 |
+
</ol>
|
| 381 |
+
|
| 382 |
+
<h3>What Gets Checked</h3>
|
| 383 |
+
<div class="card">
|
| 384 |
+
<ul>
|
| 385 |
+
<li><strong>HTTP Security Headers</strong> — Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Strict-Transport-Security, Referrer-Policy, Permissions-Policy.</li>
|
| 386 |
+
<li><strong>Sensitive Path Exposure</strong> — Probes for common exposed paths such as <code>/.env</code>, <code>/.git/config</code>, <code>/admin</code>, <code>/phpinfo.php</code>, <code>/swagger.json</code>, and 15+ other paths.</li>
|
| 387 |
+
<li><strong>HTTPS Configuration</strong> — Verifies the application uses HTTPS rather than unencrypted HTTP.</li>
|
| 388 |
+
<li><strong>Response Content Analysis</strong> — Scans HTML responses for database error messages, stack trace disclosures, debug mode indicators, and sensitive data in comments.</li>
|
| 389 |
+
<li><strong>Server Version Disclosure</strong> — Detects if the server reveals its software version in response headers.</li>
|
| 390 |
+
</ul>
|
| 391 |
+
</div>
|
| 392 |
+
|
| 393 |
+
<img src="/helpimg/NewGradioScreenshot_RemoteURLResult.png"
|
| 394 |
+
alt="Remote URL scan results showing missing security headers and other findings"
|
| 395 |
+
class="screenshot" />
|
| 396 |
+
<p class="screenshot-caption">Results from a remote URL scan showing missing security headers and insecure HTTP connection findings.</p>
|
| 397 |
+
|
| 398 |
+
<!-- Section 4: Understanding Results -->
|
| 399 |
+
<h2 id="understanding-results"><span class="h-num">4</span> Understanding Your Results</h2>
|
| 400 |
+
|
| 401 |
+
<h3>Analysis Summary</h3>
|
| 402 |
+
<p>
|
| 403 |
+
After a scan completes, the <strong>Analysis Summary</strong> section appears at the top of the results. It
|
| 404 |
+
displays metadata about the scan and a count of findings grouped by severity level.
|
| 405 |
+
</p>
|
| 406 |
+
<div class="card">
|
| 407 |
+
<ul>
|
| 408 |
+
<li><strong>Target</strong> — The directory path or URL that was scanned.</li>
|
| 409 |
+
<li><strong>Files Analyzed</strong> — Number of source code files processed (local scans only).</li>
|
| 410 |
+
<li><strong>Total Findings</strong> — Total number of security issues detected.</li>
|
| 411 |
+
<li><strong>Analysis Type</strong> — Either <em>Local</em> (SAST) or <em>Web</em> (DAST).</li>
|
| 412 |
+
</ul>
|
| 413 |
+
</div>
|
| 414 |
+
|
| 415 |
+
<h3>Severity Badges</h3>
|
| 416 |
+
<p>
|
| 417 |
+
Below the metadata, colour-coded severity badges show how many findings fall into each severity level:
|
| 418 |
+
</p>
|
| 419 |
+
<p>
|
| 420 |
+
<span class="severity-sample" style="background: var(--critical-bg); color: var(--critical); border-color: var(--critical-border);">Critical</span>
|
| 421 |
+
<span class="severity-sample" style="background: var(--high-bg); color: var(--high); border-color: var(--high-border);">High</span>
|
| 422 |
+
<span class="severity-sample" style="background: var(--medium-bg); color: var(--medium); border-color: var(--medium-border);">Medium</span>
|
| 423 |
+
<span class="severity-sample" style="background: var(--low-bg); color: var(--low); border-color: var(--low-border);">Low</span>
|
| 424 |
+
<span class="severity-sample" style="background: var(--info-bg); color: var(--info-color); border-color: var(--info-border);">Info</span>
|
| 425 |
+
</p>
|
| 426 |
+
|
| 427 |
+
<h3>Finding Cards</h3>
|
| 428 |
+
<p>Each detected vulnerability is displayed as a finding card containing the following information:</p>
|
| 429 |
+
<div class="card">
|
| 430 |
+
<ul>
|
| 431 |
+
<li><strong>Vulnerability Name</strong> — The type of security issue (e.g. "SQL Injection", "Missing Security Header: Content-Security-Policy").</li>
|
| 432 |
+
<li><strong>Severity Tag</strong> — A colour-coded badge showing CRITICAL, HIGH, MEDIUM, LOW, or INFO.</li>
|
| 433 |
+
<li><strong>Common Weakness Enumeration (CWE) Reference</strong> — The identifier (e.g. CWE-89).</li>
|
| 434 |
+
<li><strong>CVE References</strong> — Related Common Vulnerabilities and Exposures entries (when NVD enrichment is enabled).</li>
|
| 435 |
+
<li><strong>File Path & Line Number</strong> — Exact location in the source code (local scans) or the target URL (remote scans).</li>
|
| 436 |
+
<li><strong>Description</strong> — Explanation of the vulnerability and its potential impact.</li>
|
| 437 |
+
<li><strong>Remediation Guidance</strong> — Click the expandable section to view recommended fixes and best practices.</li>
|
| 438 |
+
</ul>
|
| 439 |
+
</div>
|
| 440 |
+
|
| 441 |
+
<h3>Exporting Reports</h3>
|
| 442 |
+
<p>
|
| 443 |
+
At the bottom of the results, two export options are available:
|
| 444 |
+
</p>
|
| 445 |
+
<ul>
|
| 446 |
+
<li><strong>Export JSON Report</strong> — Downloads a structured JSON file containing all scan data, suitable for integration with CI/CD pipelines or other security tools.</li>
|
| 447 |
+
<li><strong>Export Markdown Report</strong> — Downloads a Markdown report with findings grouped by severity, including file locations, code snippets, and remediation guidance. Ideal for pasting into vibe-coding platforms (Cursor, Lovable, Bolt, etc.) to fix identified issues.</li>
|
| 448 |
+
</ul>
|
| 449 |
+
|
| 450 |
+
<!-- Section 5: NVD Enrichment -->
|
| 451 |
+
<h2 id="nvd-enrichment"><span class="h-num">5</span> NVD Enrichment</h2>
|
| 452 |
+
|
| 453 |
+
<p>
|
| 454 |
+
The <strong>NVD Enriched Scan Results</strong> toggle in the sidebar controls whether scan findings are
|
| 455 |
+
enriched with data from the <strong><a href="https://nvd.nist.gov/general/cve-process" target="_blank" rel="noopener noreferrer">NVD</a></strong>, maintained by the <strong><a href="https://www.nist.gov/" target="_blank" rel="noopener noreferrer">National Institute of Standards and Technology (NIST)</a></strong>.
|
| 456 |
+
</p>
|
| 457 |
+
|
| 458 |
+
<div class="card">
|
| 459 |
+
<div class="card-label">What NVD Enrichment Adds</div>
|
| 460 |
+
<ul>
|
| 461 |
+
<li>Related <strong><a href="https://nvd.nist.gov/general/cve-process" target="_blank" rel="noopener noreferrer">Common Vulnerabilities and Exposures (CVE)</a></strong> references for each finding.</li>
|
| 462 |
+
</div>
|
| 463 |
+
|
| 464 |
+
<h3>When to Enable</h3>
|
| 465 |
+
<ul>
|
| 466 |
+
<li>Comprehensive security audits where you need full CVE context.</li>
|
| 467 |
+
<li>Compliance reporting that requires specific vulnerability references.</li>
|
| 468 |
+
<li>When you need detailed remediation guidance for each finding.</li>
|
| 469 |
+
</ul>
|
| 470 |
+
|
| 471 |
+
<h3>When to Disable</h3>
|
| 472 |
+
<ul>
|
| 473 |
+
<li>Quick scans where speed is the priority.</li>
|
| 474 |
+
<li>Offline environments without internet access.</li>
|
| 475 |
+
<li>When the NVD API is rate-limited or unavailable.</li>
|
| 476 |
+
</ul>
|
| 477 |
+
|
| 478 |
+
<div class="tip">
|
| 479 |
+
<div class="tip-label">Note</div>
|
| 480 |
+
NVD enrichment adds processing time to the scan. The toggle is enabled by default. You can disable it for faster scans and re-run with enrichment when needed.
|
| 481 |
+
</div>
|
| 482 |
+
|
| 483 |
+
<!-- Section 6: Severity Levels Guide -->
|
| 484 |
+
<h2 id="severity-guide"><span class="h-num">6</span> Severity Levels Guide</h2>
|
| 485 |
+
|
| 486 |
+
<p>
|
| 487 |
+
Findings are classified into five severity levels. Use this guide to prioritise remediation efforts.
|
| 488 |
+
</p>
|
| 489 |
+
|
| 490 |
+
<table class="severity-table">
|
| 491 |
+
<thead>
|
| 492 |
+
<tr>
|
| 493 |
+
<th style="width: 120px;">Severity</th>
|
| 494 |
+
<th>Description</th>
|
| 495 |
+
<th style="width: 200px;">Examples</th>
|
| 496 |
+
</tr>
|
| 497 |
+
</thead>
|
| 498 |
+
<tbody>
|
| 499 |
+
<tr>
|
| 500 |
+
<td><span class="severity-tag" style="background: var(--critical-bg); color: var(--critical);">CRITICAL</span></td>
|
| 501 |
+
<td>Immediate action required. These vulnerabilities can lead to full system compromise, data breaches, or remote code execution.</td>
|
| 502 |
+
<td>SQL Injection, Command Injection, Hardcoded Credentials, Insecure Deserialization</td>
|
| 503 |
+
</tr>
|
| 504 |
+
<tr>
|
| 505 |
+
<td><span class="severity-tag" style="background: var(--high-bg); color: var(--high);">HIGH</span></td>
|
| 506 |
+
<td>Serious vulnerabilities requiring prompt attention. These can lead to significant data exposure or unauthorized access.</td>
|
| 507 |
+
<td>Cross-Site Scripting (XSS), Path Traversal, SSRF, JWT Without Verification</td>
|
| 508 |
+
</tr>
|
| 509 |
+
<tr>
|
| 510 |
+
<td><span class="severity-tag" style="background: var(--medium-bg); color: var(--medium);">MEDIUM</span></td>
|
| 511 |
+
<td>Moderate risk requiring investigation. These may enable attacks under certain conditions or weaken security posture.</td>
|
| 512 |
+
<td>CORS Misconfiguration, Weak Cryptographic Algorithm, Open Redirect, Missing Content-Security-Policy</td>
|
| 513 |
+
</tr>
|
| 514 |
+
<tr>
|
| 515 |
+
<td><span class="severity-tag" style="background: var(--low-bg); color: var(--low);">LOW</span></td>
|
| 516 |
+
<td>Minor issues with lower priority. These represent defence-in-depth concerns or best practice violations.</td>
|
| 517 |
+
<td>Debug Mode Enabled, Missing Non-Critical Headers, Verbose Error Messages, Sensitive Data in Logs</td>
|
| 518 |
+
</tr>
|
| 519 |
+
<tr>
|
| 520 |
+
<td><span class="severity-tag" style="background: var(--info-bg); color: var(--info-color);">INFO</span></td>
|
| 521 |
+
<td>Informational findings with no direct security risk. These highlight areas for awareness or potential improvement.</td>
|
| 522 |
+
<td>Technology Detection, Configuration Notes, Server Version Disclosure</td>
|
| 523 |
+
</tr>
|
| 524 |
+
</tbody>
|
| 525 |
+
</table>
|
| 526 |
+
|
| 527 |
+
<div class="tip">
|
| 528 |
+
<div class="tip-label">Prioritisation Strategy</div>
|
| 529 |
+
Address <strong>Critical</strong> and <strong>High</strong> findings first, as they pose the greatest risk. Medium findings should be reviewed and scheduled for remediation. Low and Info findings can be addressed as part of regular maintenance cycles.
|
| 530 |
+
</div>
|
| 531 |
+
|
| 532 |
+
<!-- Footer -->
|
| 533 |
+
<div class="footer">
|
| 534 |
+
Security Auditor · SAST + DAST Platform<br />
|
| 535 |
+
</div>
|
| 536 |
+
|
| 537 |
+
|
| 538 |
+
</body>
|
| 539 |
+
</html>
|
requirements.txt
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Security Checker Dependencies
|
| 2 |
+
# =============================
|
| 3 |
+
|
| 4 |
+
# Core Dependencies (Required for running the application)
|
| 5 |
+
# ---------------------------------------------------------
|
| 6 |
+
|
| 7 |
+
# Core async HTTP client for NVD API and web scanning
|
| 8 |
+
aiohttp>=3.8.0
|
| 9 |
+
|
| 10 |
+
# YAML parsing (for config files)
|
| 11 |
+
PyYAML>=6.0
|
| 12 |
+
|
| 13 |
+
# For generating HTML reports
|
| 14 |
+
jinja2>=3.1.0
|
| 15 |
+
|
| 16 |
+
# For advanced pattern matching
|
| 17 |
+
regex>=2023.0.0
|
| 18 |
+
|
| 19 |
+
# For rich console output
|
| 20 |
+
rich>=13.0.0
|
| 21 |
+
|
| 22 |
+
# Gradio web interface
|
| 23 |
+
gradio>=4.0.0
|
| 24 |
+
|
| 25 |
+
# HTTP client for Modal backend API calls
|
| 26 |
+
httpx>=0.27.0
|
| 27 |
+
|
| 28 |
+
# Development Dependencies (Optional - only needed for development/testing)
|
| 29 |
+
# ---------------------------------------------------------------------------
|
| 30 |
+
# Uncomment the lines below if you need testing or type checking:
|
| 31 |
+
#
|
| 32 |
+
# pytest>=7.0.0
|
| 33 |
+
# pytest-asyncio>=0.21.0
|
| 34 |
+
# mypy>=1.0.0
|
security_checker.py
ADDED
|
@@ -0,0 +1,1945 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Security Checker Application
|
| 4 |
+
============================
|
| 5 |
+
A comprehensive security analysis tool that combines:
|
| 6 |
+
1. Static Application Security Testing (SAST)
|
| 7 |
+
2. NIST National Vulnerability Database (NVD) integration
|
| 8 |
+
|
| 9 |
+
Think of this as a "security doctor" for your applications:
|
| 10 |
+
- SAST = X-ray machine (looks inside without running)
|
| 11 |
+
- NVD = Medical database (known vulnerabilities/diseases)
|
| 12 |
+
- Report = Diagnosis with treatment plan
|
| 13 |
+
|
| 14 |
+
Author: Security Checker Project
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import os
|
| 18 |
+
import re
|
| 19 |
+
import json
|
| 20 |
+
import hashlib
|
| 21 |
+
import asyncio
|
| 22 |
+
import aiohttp
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
from dataclasses import dataclass, field
|
| 25 |
+
from typing import List, Dict, Optional, Tuple
|
| 26 |
+
from enum import Enum
|
| 27 |
+
from datetime import datetime
|
| 28 |
+
from urllib.parse import urlparse
|
| 29 |
+
import fnmatch
|
| 30 |
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class RiskLevel(Enum):
|
| 34 |
+
"""
|
| 35 |
+
Risk levels follow CVSS (Common Vulnerability Scoring System).
|
| 36 |
+
Think of it like triage in an emergency room:
|
| 37 |
+
- CRITICAL: Life-threatening, needs immediate attention
|
| 38 |
+
- HIGH: Serious condition, treat soon
|
| 39 |
+
- MEDIUM: Concerning, schedule treatment
|
| 40 |
+
- LOW: Minor issue, monitor
|
| 41 |
+
- INFO: Just a note for awareness
|
| 42 |
+
"""
|
| 43 |
+
CRITICAL = "CRITICAL" # CVSS 9.0-10.0
|
| 44 |
+
HIGH = "HIGH" # CVSS 7.0-8.9
|
| 45 |
+
MEDIUM = "MEDIUM" # CVSS 4.0-6.9
|
| 46 |
+
LOW = "LOW" # CVSS 0.1-3.9
|
| 47 |
+
INFO = "INFO" # Informational
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
@dataclass
|
| 51 |
+
class Vulnerability:
|
| 52 |
+
"""
|
| 53 |
+
Represents a single vulnerability found in the code.
|
| 54 |
+
|
| 55 |
+
Analogy: This is like a medical diagnosis report entry:
|
| 56 |
+
- name: Disease name
|
| 57 |
+
- description: What's wrong
|
| 58 |
+
- file_path: Where in the body (code) the problem is
|
| 59 |
+
- line_number: Exact location
|
| 60 |
+
- code_snippet: The problematic tissue sample
|
| 61 |
+
- risk_level: How serious is it
|
| 62 |
+
- remediation: Treatment plan
|
| 63 |
+
- cve_ids: Reference to known disease database (NVD)
|
| 64 |
+
"""
|
| 65 |
+
name: str
|
| 66 |
+
description: str
|
| 67 |
+
file_path: str
|
| 68 |
+
line_number: int
|
| 69 |
+
code_snippet: str
|
| 70 |
+
risk_level: RiskLevel
|
| 71 |
+
remediation: str
|
| 72 |
+
cve_ids: List[str] = field(default_factory=list)
|
| 73 |
+
cwe_id: Optional[str] = None
|
| 74 |
+
confidence: str = "HIGH" # HIGH, MEDIUM, LOW
|
| 75 |
+
|
| 76 |
+
def to_dict(self) -> Dict:
|
| 77 |
+
return {
|
| 78 |
+
"name": self.name,
|
| 79 |
+
"description": self.description,
|
| 80 |
+
"file_path": self.file_path,
|
| 81 |
+
"line_number": self.line_number,
|
| 82 |
+
"code_snippet": self.code_snippet,
|
| 83 |
+
"risk_level": self.risk_level.value,
|
| 84 |
+
"remediation": self.remediation,
|
| 85 |
+
"cve_ids": self.cve_ids,
|
| 86 |
+
"cwe_id": self.cwe_id,
|
| 87 |
+
"confidence": self.confidence
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
@dataclass
|
| 92 |
+
class ScanResult:
|
| 93 |
+
"""
|
| 94 |
+
Complete scan results - like a full medical report.
|
| 95 |
+
"""
|
| 96 |
+
target: str
|
| 97 |
+
scan_type: str # "local" or "web"
|
| 98 |
+
start_time: datetime
|
| 99 |
+
end_time: Optional[datetime] = None
|
| 100 |
+
vulnerabilities: List[Vulnerability] = field(default_factory=list)
|
| 101 |
+
files_scanned: int = 0
|
| 102 |
+
errors: List[str] = field(default_factory=list)
|
| 103 |
+
|
| 104 |
+
def summary(self) -> Dict:
|
| 105 |
+
"""Generate a summary of findings by risk level."""
|
| 106 |
+
summary = {level.value: 0 for level in RiskLevel}
|
| 107 |
+
for vuln in self.vulnerabilities:
|
| 108 |
+
summary[vuln.risk_level.value] += 1
|
| 109 |
+
return {
|
| 110 |
+
"target": self.target,
|
| 111 |
+
"scan_type": self.scan_type,
|
| 112 |
+
"duration_seconds": (self.end_time - self.start_time).total_seconds() if self.end_time else None,
|
| 113 |
+
"files_scanned": self.files_scanned,
|
| 114 |
+
"total_vulnerabilities": len(self.vulnerabilities),
|
| 115 |
+
"by_severity": summary,
|
| 116 |
+
"errors": len(self.errors)
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
class SASTRule:
|
| 121 |
+
"""
|
| 122 |
+
A single SAST detection rule.
|
| 123 |
+
|
| 124 |
+
Analogy: Like a specific test in a medical lab
|
| 125 |
+
- pattern: What symptom to look for
|
| 126 |
+
- name: Name of the condition
|
| 127 |
+
- languages: Which "body types" this applies to
|
| 128 |
+
"""
|
| 129 |
+
def __init__(
|
| 130 |
+
self,
|
| 131 |
+
name: str,
|
| 132 |
+
pattern: str,
|
| 133 |
+
description: str,
|
| 134 |
+
risk_level: RiskLevel,
|
| 135 |
+
remediation: str,
|
| 136 |
+
cwe_id: str,
|
| 137 |
+
languages: List[str],
|
| 138 |
+
false_positive_patterns: List[str] = None
|
| 139 |
+
):
|
| 140 |
+
self.name = name
|
| 141 |
+
self.pattern = re.compile(pattern, re.IGNORECASE | re.MULTILINE)
|
| 142 |
+
self.description = description
|
| 143 |
+
self.risk_level = risk_level
|
| 144 |
+
self.remediation = remediation
|
| 145 |
+
self.cwe_id = cwe_id
|
| 146 |
+
self.languages = languages # File extensions: ['.py', '.js', etc.]
|
| 147 |
+
self.false_positive_patterns = [
|
| 148 |
+
re.compile(fp, re.IGNORECASE) for fp in (false_positive_patterns or [])
|
| 149 |
+
]
|
| 150 |
+
|
| 151 |
+
def matches(self, code: str, file_ext: str) -> List[Tuple[int, str]]:
|
| 152 |
+
"""
|
| 153 |
+
Find all matches in the code.
|
| 154 |
+
Returns list of (line_number, matched_snippet).
|
| 155 |
+
"""
|
| 156 |
+
if file_ext not in self.languages:
|
| 157 |
+
return []
|
| 158 |
+
|
| 159 |
+
matches = []
|
| 160 |
+
lines = code.split('\n')
|
| 161 |
+
|
| 162 |
+
for i, line in enumerate(lines, 1):
|
| 163 |
+
if self.pattern.search(line):
|
| 164 |
+
# Check for false positives
|
| 165 |
+
is_false_positive = any(
|
| 166 |
+
fp.search(line) for fp in self.false_positive_patterns
|
| 167 |
+
)
|
| 168 |
+
if not is_false_positive:
|
| 169 |
+
# Get context (line before and after)
|
| 170 |
+
start = max(0, i - 2)
|
| 171 |
+
end = min(len(lines), i + 1)
|
| 172 |
+
snippet = '\n'.join(lines[start:end])
|
| 173 |
+
matches.append((i, snippet))
|
| 174 |
+
|
| 175 |
+
return matches
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
class SASTEngine:
|
| 179 |
+
"""
|
| 180 |
+
Static Application Security Testing Engine
|
| 181 |
+
|
| 182 |
+
Analogy: This is like a diagnostic imaging department
|
| 183 |
+
- Scans code without executing it (like X-ray/MRI)
|
| 184 |
+
- Looks for known vulnerability patterns
|
| 185 |
+
- Reports findings with locations
|
| 186 |
+
|
| 187 |
+
How it works:
|
| 188 |
+
1. Load detection rules (what to look for)
|
| 189 |
+
2. Read source files
|
| 190 |
+
3. Match patterns against code
|
| 191 |
+
4. Report findings
|
| 192 |
+
"""
|
| 193 |
+
|
| 194 |
+
def __init__(self):
|
| 195 |
+
self.rules = self._load_rules()
|
| 196 |
+
self.file_extensions = {
|
| 197 |
+
'.py': 'python',
|
| 198 |
+
'.js': 'javascript',
|
| 199 |
+
'.ts': 'typescript',
|
| 200 |
+
'.jsx': 'javascript',
|
| 201 |
+
'.tsx': 'typescript',
|
| 202 |
+
'.java': 'java',
|
| 203 |
+
'.php': 'php',
|
| 204 |
+
'.rb': 'ruby',
|
| 205 |
+
'.go': 'go',
|
| 206 |
+
'.cs': 'csharp',
|
| 207 |
+
'.c': 'c',
|
| 208 |
+
'.cpp': 'cpp',
|
| 209 |
+
'.h': 'c',
|
| 210 |
+
'.hpp': 'cpp',
|
| 211 |
+
'.sql': 'sql',
|
| 212 |
+
'.html': 'html',
|
| 213 |
+
'.htm': 'html',
|
| 214 |
+
'.xml': 'xml',
|
| 215 |
+
'.yml': 'yaml',
|
| 216 |
+
'.yaml': 'yaml',
|
| 217 |
+
'.json': 'json',
|
| 218 |
+
'.sh': 'shell',
|
| 219 |
+
'.bash': 'shell',
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
# Directories to skip (like avoiding scanning healthy tissue)
|
| 223 |
+
self.skip_dirs = {
|
| 224 |
+
'node_modules', 'venv', '.venv', 'env', '.env',
|
| 225 |
+
'__pycache__', '.git', '.svn', '.hg',
|
| 226 |
+
'dist', 'build', 'target', 'vendor',
|
| 227 |
+
'.idea', '.vscode', 'coverage'
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
def _load_rules(self) -> List[SASTRule]:
|
| 231 |
+
"""
|
| 232 |
+
Load vulnerability detection rules.
|
| 233 |
+
|
| 234 |
+
These rules are like a checklist of known security problems.
|
| 235 |
+
Each rule defines:
|
| 236 |
+
- A pattern to match (regex)
|
| 237 |
+
- The type of vulnerability
|
| 238 |
+
- How severe it is
|
| 239 |
+
- How to fix it
|
| 240 |
+
"""
|
| 241 |
+
return [
|
| 242 |
+
# ============================================================
|
| 243 |
+
# INJECTION VULNERABILITIES (The "contamination" category)
|
| 244 |
+
# Like checking for contaminants in food/medicine
|
| 245 |
+
# ============================================================
|
| 246 |
+
|
| 247 |
+
SASTRule(
|
| 248 |
+
name="SQL Injection",
|
| 249 |
+
pattern=r"""(?:execute|cursor\.execute|query|raw|rawQuery|executeQuery)\s*\(\s*[f"'].*?%s.*?['"]\s*%|(?:execute|cursor\.execute)\s*\(\s*[f"'].*?\{.*?\}.*?['"]|(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER).*?['"]\s*\+\s*|f['"]\s*(?:SELECT|INSERT|UPDATE|DELETE).*?\{""",
|
| 250 |
+
description="Potential SQL Injection vulnerability. User input may be directly concatenated into SQL queries, allowing attackers to manipulate database operations.",
|
| 251 |
+
risk_level=RiskLevel.CRITICAL,
|
| 252 |
+
remediation="""Use parameterized queries or prepared statements:
|
| 253 |
+
|
| 254 |
+
VULNERABLE:
|
| 255 |
+
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
|
| 256 |
+
|
| 257 |
+
SECURE:
|
| 258 |
+
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
| 259 |
+
|
| 260 |
+
For ORMs, use built-in query builders instead of raw SQL.""",
|
| 261 |
+
cwe_id="CWE-89",
|
| 262 |
+
languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go', '.cs'],
|
| 263 |
+
false_positive_patterns=[r'#.*SQL', r'//.*SQL', r'/\*.*SQL']
|
| 264 |
+
),
|
| 265 |
+
|
| 266 |
+
SASTRule(
|
| 267 |
+
name="Command Injection",
|
| 268 |
+
pattern=r"""(?:os\.system|os\.popen|subprocess\.call|subprocess\.run|subprocess\.Popen|exec|eval|Runtime\.getRuntime\(\)\.exec|shell_exec|system|passthru|popen)\s*\([^)]*(?:\+|%|\.format|\{|\$)""",
|
| 269 |
+
description="Potential Command Injection vulnerability. User input may be passed to system commands, allowing attackers to execute arbitrary commands.",
|
| 270 |
+
risk_level=RiskLevel.CRITICAL,
|
| 271 |
+
remediation="""Avoid passing user input to shell commands. If necessary:
|
| 272 |
+
|
| 273 |
+
VULNERABLE:
|
| 274 |
+
os.system(f"ping {user_input}")
|
| 275 |
+
|
| 276 |
+
SECURE:
|
| 277 |
+
import shlex
|
| 278 |
+
subprocess.run(["ping", shlex.quote(user_input)], shell=False)
|
| 279 |
+
|
| 280 |
+
Best practice: Use libraries instead of shell commands when possible.""",
|
| 281 |
+
cwe_id="CWE-78",
|
| 282 |
+
languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go', '.sh']
|
| 283 |
+
),
|
| 284 |
+
|
| 285 |
+
SASTRule(
|
| 286 |
+
name="XSS (Cross-Site Scripting)",
|
| 287 |
+
pattern=r"""(?:innerHTML|outerHTML|document\.write|\.html\(|v-html|dangerouslySetInnerHTML|\[innerHTML\])\s*=?\s*(?:[^;]*(?:\+|`|\$\{))""",
|
| 288 |
+
description="Potential Cross-Site Scripting (XSS) vulnerability. Untrusted data may be inserted into the DOM without proper encoding.",
|
| 289 |
+
risk_level=RiskLevel.HIGH,
|
| 290 |
+
remediation="""Sanitize and encode output before inserting into HTML:
|
| 291 |
+
|
| 292 |
+
VULNERABLE:
|
| 293 |
+
element.innerHTML = userInput;
|
| 294 |
+
|
| 295 |
+
SECURE:
|
| 296 |
+
element.textContent = userInput; // For text
|
| 297 |
+
// Or use a sanitization library like DOMPurify:
|
| 298 |
+
element.innerHTML = DOMPurify.sanitize(userInput);
|
| 299 |
+
|
| 300 |
+
For React, avoid dangerouslySetInnerHTML unless absolutely necessary.""",
|
| 301 |
+
cwe_id="CWE-79",
|
| 302 |
+
languages=['.js', '.ts', '.jsx', '.tsx', '.html', '.php']
|
| 303 |
+
),
|
| 304 |
+
|
| 305 |
+
SASTRule(
|
| 306 |
+
name="Path Traversal",
|
| 307 |
+
pattern=r"""(?:open|read|write|file_get_contents|file_put_contents|include|require|fopen|readFile|writeFile|createReadStream)\s*\([^)]*(?:\+|`|\$\{|\.\./)""",
|
| 308 |
+
description="Potential Path Traversal vulnerability. User input may be used to construct file paths, allowing attackers to access unauthorized files.",
|
| 309 |
+
risk_level=RiskLevel.HIGH,
|
| 310 |
+
remediation="""Validate and sanitize file paths:
|
| 311 |
+
|
| 312 |
+
VULNERABLE:
|
| 313 |
+
with open(f"/uploads/{filename}") as f:
|
| 314 |
+
|
| 315 |
+
SECURE:
|
| 316 |
+
import os
|
| 317 |
+
safe_path = os.path.normpath(filename)
|
| 318 |
+
if '..' in safe_path or safe_path.startswith('/'):
|
| 319 |
+
raise ValueError("Invalid path")
|
| 320 |
+
full_path = os.path.join(UPLOAD_DIR, safe_path)
|
| 321 |
+
if not full_path.startswith(UPLOAD_DIR):
|
| 322 |
+
raise ValueError("Path traversal detected")""",
|
| 323 |
+
cwe_id="CWE-22",
|
| 324 |
+
languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go']
|
| 325 |
+
),
|
| 326 |
+
|
| 327 |
+
SASTRule(
|
| 328 |
+
name="LDAP Injection",
|
| 329 |
+
pattern=r"""(?:ldap_search|ldap_bind|search_s|search_ext_s)\s*\([^)]*(?:\+|%|\.format|\{)""",
|
| 330 |
+
description="Potential LDAP Injection vulnerability. User input may be used in LDAP queries without proper escaping.",
|
| 331 |
+
risk_level=RiskLevel.HIGH,
|
| 332 |
+
remediation="""Escape special LDAP characters in user input:
|
| 333 |
+
|
| 334 |
+
VULNERABLE:
|
| 335 |
+
ldap.search_s(base, scope, f"(uid={username})")
|
| 336 |
+
|
| 337 |
+
SECURE:
|
| 338 |
+
from ldap3.utils.conv import escape_filter_chars
|
| 339 |
+
safe_username = escape_filter_chars(username)
|
| 340 |
+
ldap.search_s(base, scope, f"(uid={safe_username})")""",
|
| 341 |
+
cwe_id="CWE-90",
|
| 342 |
+
languages=['.py', '.java', '.php', '.cs']
|
| 343 |
+
),
|
| 344 |
+
|
| 345 |
+
# ============================================================
|
| 346 |
+
# AUTHENTICATION & SESSION VULNERABILITIES
|
| 347 |
+
# Like checking if the locks and keys are secure
|
| 348 |
+
# ============================================================
|
| 349 |
+
|
| 350 |
+
SASTRule(
|
| 351 |
+
name="Hardcoded Credentials",
|
| 352 |
+
pattern=r"""(?:password|passwd|pwd|secret|api_key|apikey|api_secret|access_token|auth_token|private_key)\s*[=:]\s*['"]\w{8,}['"]""",
|
| 353 |
+
description="Hardcoded credentials detected. Sensitive information should not be stored in source code.",
|
| 354 |
+
risk_level=RiskLevel.HIGH,
|
| 355 |
+
remediation="""Store credentials securely:
|
| 356 |
+
|
| 357 |
+
VULNERABLE:
|
| 358 |
+
password = "MySecretPass123"
|
| 359 |
+
api_key = "sk-1234567890abcdef"
|
| 360 |
+
|
| 361 |
+
SECURE:
|
| 362 |
+
import os
|
| 363 |
+
password = os.environ.get('DB_PASSWORD')
|
| 364 |
+
api_key = os.environ.get('API_KEY')
|
| 365 |
+
|
| 366 |
+
Use environment variables, secrets managers (AWS Secrets Manager,
|
| 367 |
+
HashiCorp Vault), or encrypted configuration files.""",
|
| 368 |
+
cwe_id="CWE-798",
|
| 369 |
+
languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go', '.cs', '.yml', '.yaml', '.json'],
|
| 370 |
+
false_positive_patterns=[r'example', r'placeholder', r'your_', r'<.*>', r'xxx', r'\$\{']
|
| 371 |
+
),
|
| 372 |
+
|
| 373 |
+
SASTRule(
|
| 374 |
+
name="Weak Password Hashing",
|
| 375 |
+
pattern=r"""(?:md5|sha1)\s*\(|hashlib\.(?:md5|sha1)\(|MessageDigest\.getInstance\s*\(\s*['"](MD5|SHA-?1)['"]|password.*=.*(?:md5|sha1)""",
|
| 376 |
+
description="Weak hashing algorithm used for passwords. MD5 and SHA1 are cryptographically broken for password storage.",
|
| 377 |
+
risk_level=RiskLevel.HIGH,
|
| 378 |
+
remediation="""Use strong password hashing algorithms:
|
| 379 |
+
|
| 380 |
+
VULNERABLE:
|
| 381 |
+
hashed = hashlib.md5(password.encode()).hexdigest()
|
| 382 |
+
|
| 383 |
+
SECURE:
|
| 384 |
+
import bcrypt
|
| 385 |
+
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
|
| 386 |
+
|
| 387 |
+
# Or use argon2 (recommended):
|
| 388 |
+
from argon2 import PasswordHasher
|
| 389 |
+
ph = PasswordHasher()
|
| 390 |
+
hashed = ph.hash(password)
|
| 391 |
+
|
| 392 |
+
Recommended algorithms: Argon2, bcrypt, scrypt, PBKDF2""",
|
| 393 |
+
cwe_id="CWE-328",
|
| 394 |
+
languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go', '.cs']
|
| 395 |
+
),
|
| 396 |
+
|
| 397 |
+
SASTRule(
|
| 398 |
+
name="JWT Without Verification",
|
| 399 |
+
pattern=r"""jwt\.decode\s*\([^)]*verify\s*=\s*False|algorithms\s*=\s*\[?\s*['"](none|HS256)['"]|\.decode\(\s*token\s*\)|jsonwebtoken\.decode\s*\(""",
|
| 400 |
+
description="JWT token decoded without proper verification or using weak/no algorithm.",
|
| 401 |
+
risk_level=RiskLevel.HIGH,
|
| 402 |
+
remediation="""Always verify JWT signatures:
|
| 403 |
+
|
| 404 |
+
VULNERABLE:
|
| 405 |
+
payload = jwt.decode(token, verify=False)
|
| 406 |
+
payload = jwt.decode(token, algorithms=['none'])
|
| 407 |
+
|
| 408 |
+
SECURE:
|
| 409 |
+
payload = jwt.decode(
|
| 410 |
+
token,
|
| 411 |
+
SECRET_KEY,
|
| 412 |
+
algorithms=['RS256'], # Use asymmetric algorithms
|
| 413 |
+
options={'verify_exp': True}
|
| 414 |
+
)
|
| 415 |
+
|
| 416 |
+
Use RS256 or ES256 instead of HS256 for better security.""",
|
| 417 |
+
cwe_id="CWE-347",
|
| 418 |
+
languages=['.py', '.js', '.ts', '.java', '.go']
|
| 419 |
+
),
|
| 420 |
+
|
| 421 |
+
SASTRule(
|
| 422 |
+
name="Session Fixation Risk",
|
| 423 |
+
pattern=r"""session\s*\[\s*['"].*['"]\s*\]\s*=.*request\.|req\.session\s*=.*req\.(body|query|params)|session_id\s*=.*(?:GET|POST|request)""",
|
| 424 |
+
description="Potential session fixation vulnerability. Session identifiers should be regenerated after authentication.",
|
| 425 |
+
risk_level=RiskLevel.MEDIUM,
|
| 426 |
+
remediation="""Regenerate session after authentication:
|
| 427 |
+
|
| 428 |
+
VULNERABLE:
|
| 429 |
+
session['user_id'] = user.id # Without regenerating
|
| 430 |
+
|
| 431 |
+
SECURE (Python/Flask):
|
| 432 |
+
from flask import session
|
| 433 |
+
session.regenerate() # Regenerate session ID
|
| 434 |
+
session['user_id'] = user.id
|
| 435 |
+
|
| 436 |
+
SECURE (Node.js/Express):
|
| 437 |
+
req.session.regenerate((err) => {
|
| 438 |
+
req.session.userId = user.id;
|
| 439 |
+
});""",
|
| 440 |
+
cwe_id="CWE-384",
|
| 441 |
+
languages=['.py', '.js', '.ts', '.php', '.java']
|
| 442 |
+
),
|
| 443 |
+
|
| 444 |
+
# ============================================================
|
| 445 |
+
# CRYPTOGRAPHIC VULNERABILITIES
|
| 446 |
+
# Like checking if the safe is actually secure
|
| 447 |
+
# ============================================================
|
| 448 |
+
|
| 449 |
+
SASTRule(
|
| 450 |
+
name="Weak Cryptographic Algorithm",
|
| 451 |
+
pattern=r"""(?:DES|RC4|RC2|Blowfish|IDEA)(?:\.|\s|Cipher)|Cipher\.getInstance\s*\(\s*['"](DES|RC4|Blowfish)['"]\)|from\s+Crypto\.Cipher\s+import\s+(DES|Blowfish)|cryptography.*(?:DES|RC4|Blowfish)""",
|
| 452 |
+
description="Weak cryptographic algorithm detected. DES, RC4, RC2, and Blowfish are considered insecure.",
|
| 453 |
+
risk_level=RiskLevel.HIGH,
|
| 454 |
+
remediation="""Use modern cryptographic algorithms:
|
| 455 |
+
|
| 456 |
+
VULNERABLE:
|
| 457 |
+
from Crypto.Cipher import DES
|
| 458 |
+
cipher = DES.new(key, DES.MODE_CBC)
|
| 459 |
+
|
| 460 |
+
SECURE:
|
| 461 |
+
from cryptography.fernet import Fernet
|
| 462 |
+
# Or for low-level:
|
| 463 |
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
|
| 464 |
+
cipher = Cipher(algorithms.AES(key), modes.GCM(iv))
|
| 465 |
+
|
| 466 |
+
Recommended: AES-256-GCM, ChaCha20-Poly1305""",
|
| 467 |
+
cwe_id="CWE-327",
|
| 468 |
+
languages=['.py', '.java', '.js', '.ts', '.go', '.cs', '.php']
|
| 469 |
+
),
|
| 470 |
+
|
| 471 |
+
SASTRule(
|
| 472 |
+
name="Insecure Random Number Generator",
|
| 473 |
+
pattern=r"""(?:random\.random|random\.randint|Math\.random|rand\(\)|srand\(\)|mt_rand)\s*\(""",
|
| 474 |
+
description="Insecure random number generator used. These are not cryptographically secure and shouldn't be used for security purposes.",
|
| 475 |
+
risk_level=RiskLevel.MEDIUM,
|
| 476 |
+
remediation="""Use cryptographically secure random generators:
|
| 477 |
+
|
| 478 |
+
VULNERABLE:
|
| 479 |
+
token = ''.join(random.choices(string.ascii_letters, k=32))
|
| 480 |
+
|
| 481 |
+
SECURE (Python):
|
| 482 |
+
import secrets
|
| 483 |
+
token = secrets.token_urlsafe(32)
|
| 484 |
+
|
| 485 |
+
SECURE (JavaScript):
|
| 486 |
+
const array = new Uint8Array(32);
|
| 487 |
+
crypto.getRandomValues(array);
|
| 488 |
+
|
| 489 |
+
SECURE (Java):
|
| 490 |
+
SecureRandom random = new SecureRandom();""",
|
| 491 |
+
cwe_id="CWE-338",
|
| 492 |
+
languages=['.py', '.js', '.ts', '.java', '.php', '.c', '.cpp'],
|
| 493 |
+
false_positive_patterns=[r'random\.seed', r'shuffle', r'sample']
|
| 494 |
+
),
|
| 495 |
+
|
| 496 |
+
SASTRule(
|
| 497 |
+
name="Hardcoded Cryptographic Key",
|
| 498 |
+
pattern=r"""(?:key|iv|nonce|salt)\s*[=:]\s*(?:b?['"]\w{16,}['"]|bytes\s*\(\s*['"]\w{16,}['"])""",
|
| 499 |
+
description="Hardcoded cryptographic key detected. Encryption keys should never be stored in source code.",
|
| 500 |
+
risk_level=RiskLevel.CRITICAL,
|
| 501 |
+
remediation="""Store cryptographic keys securely:
|
| 502 |
+
|
| 503 |
+
VULNERABLE:
|
| 504 |
+
key = b'ThisIsASecretKey1234567890123456'
|
| 505 |
+
|
| 506 |
+
SECURE:
|
| 507 |
+
import os
|
| 508 |
+
key = os.environ.get('ENCRYPTION_KEY').encode()
|
| 509 |
+
|
| 510 |
+
# Or use a key management system:
|
| 511 |
+
from aws_encryption_sdk import KMSMasterKeyProvider
|
| 512 |
+
key_provider = KMSMasterKeyProvider(key_ids=[KEY_ARN])
|
| 513 |
+
|
| 514 |
+
Best practice: Use Hardware Security Modules (HSM) or
|
| 515 |
+
Key Management Services (AWS KMS, Azure Key Vault).""",
|
| 516 |
+
cwe_id="CWE-321",
|
| 517 |
+
languages=['.py', '.java', '.js', '.ts', '.go', '.cs', '.php']
|
| 518 |
+
),
|
| 519 |
+
|
| 520 |
+
# ============================================================
|
| 521 |
+
# INSECURE DESERIALIZATION
|
| 522 |
+
# Like accepting packages without checking what's inside
|
| 523 |
+
# ============================================================
|
| 524 |
+
|
| 525 |
+
SASTRule(
|
| 526 |
+
name="Insecure Deserialization (Python)",
|
| 527 |
+
pattern=r"""pickle\.loads?\s*\(|yaml\.(?:unsafe_)?load\s*\([^)]*(?!Loader\s*=\s*yaml\.SafeLoader)|marshal\.loads?\s*\(|shelve\.open\s*\(""",
|
| 528 |
+
description="Insecure deserialization detected. Deserializing untrusted data can lead to remote code execution.",
|
| 529 |
+
risk_level=RiskLevel.CRITICAL,
|
| 530 |
+
remediation="""Use safe deserialization methods:
|
| 531 |
+
|
| 532 |
+
VULNERABLE:
|
| 533 |
+
data = pickle.loads(user_input)
|
| 534 |
+
config = yaml.load(file)
|
| 535 |
+
|
| 536 |
+
SECURE:
|
| 537 |
+
import json
|
| 538 |
+
data = json.loads(user_input) # JSON is safe
|
| 539 |
+
|
| 540 |
+
# For YAML, always use SafeLoader:
|
| 541 |
+
config = yaml.load(file, Loader=yaml.SafeLoader)
|
| 542 |
+
# Or better:
|
| 543 |
+
config = yaml.safe_load(file)
|
| 544 |
+
|
| 545 |
+
Never deserialize untrusted data with pickle/marshal.""",
|
| 546 |
+
cwe_id="CWE-502",
|
| 547 |
+
languages=['.py']
|
| 548 |
+
),
|
| 549 |
+
|
| 550 |
+
SASTRule(
|
| 551 |
+
name="Insecure Deserialization (Java)",
|
| 552 |
+
pattern=r"""ObjectInputStream\s*\(|readObject\s*\(\)|XMLDecoder\s*\(|XStream\.fromXML\s*\(|JSON\.parse\s*\(.*\)\.class""",
|
| 553 |
+
description="Insecure deserialization detected in Java. Can lead to remote code execution.",
|
| 554 |
+
risk_level=RiskLevel.CRITICAL,
|
| 555 |
+
remediation="""Validate and filter deserialization:
|
| 556 |
+
|
| 557 |
+
VULNERABLE:
|
| 558 |
+
ObjectInputStream ois = new ObjectInputStream(input);
|
| 559 |
+
Object obj = ois.readObject();
|
| 560 |
+
|
| 561 |
+
SECURE:
|
| 562 |
+
// Use a whitelist filter
|
| 563 |
+
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
|
| 564 |
+
"com.myapp.SafeClass;!*"
|
| 565 |
+
);
|
| 566 |
+
ois.setObjectInputFilter(filter);
|
| 567 |
+
|
| 568 |
+
// Or use JSON/Protocol Buffers instead of Java serialization
|
| 569 |
+
|
| 570 |
+
Consider: Jackson with @JsonTypeInfo restrictions,
|
| 571 |
+
or Protocol Buffers for type-safe serialization.""",
|
| 572 |
+
cwe_id="CWE-502",
|
| 573 |
+
languages=['.java']
|
| 574 |
+
),
|
| 575 |
+
|
| 576 |
+
SASTRule(
|
| 577 |
+
name="Insecure Deserialization (JavaScript)",
|
| 578 |
+
pattern=r"""(?:eval|Function)\s*\(\s*(?:JSON\.parse|atob|decodeURIComponent)|node-serialize|serialize-javascript.*(?:eval|Function)|unserialize\s*\(""",
|
| 579 |
+
description="Insecure deserialization in JavaScript. Eval of untrusted data can lead to code execution.",
|
| 580 |
+
risk_level=RiskLevel.CRITICAL,
|
| 581 |
+
remediation="""Never eval deserialized data:
|
| 582 |
+
|
| 583 |
+
VULNERABLE:
|
| 584 |
+
eval(JSON.parse(userInput).code);
|
| 585 |
+
const obj = serialize.unserialize(userInput);
|
| 586 |
+
|
| 587 |
+
SECURE:
|
| 588 |
+
const data = JSON.parse(userInput);
|
| 589 |
+
// Validate structure before use
|
| 590 |
+
if (typeof data.name !== 'string') {
|
| 591 |
+
throw new Error('Invalid data');
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
Avoid node-serialize and similar libraries with
|
| 595 |
+
eval-based deserialization.""",
|
| 596 |
+
cwe_id="CWE-502",
|
| 597 |
+
languages=['.js', '.ts']
|
| 598 |
+
),
|
| 599 |
+
|
| 600 |
+
# ============================================================
|
| 601 |
+
# INFORMATION DISCLOSURE
|
| 602 |
+
# Like leaving sensitive documents in public view
|
| 603 |
+
# ============================================================
|
| 604 |
+
|
| 605 |
+
SASTRule(
|
| 606 |
+
name="Debug Mode Enabled",
|
| 607 |
+
pattern=r"""(?:DEBUG|debug)\s*[=:]\s*(?:True|true|1|['"](true|on|yes)['"])|app\.run\s*\([^)]*debug\s*=\s*True|FLASK_DEBUG\s*=\s*1""",
|
| 608 |
+
description="Debug mode appears to be enabled. This can expose sensitive information in production.",
|
| 609 |
+
risk_level=RiskLevel.MEDIUM,
|
| 610 |
+
remediation="""Disable debug mode in production:
|
| 611 |
+
|
| 612 |
+
VULNERABLE:
|
| 613 |
+
app.run(debug=True)
|
| 614 |
+
DEBUG = True
|
| 615 |
+
|
| 616 |
+
SECURE:
|
| 617 |
+
import os
|
| 618 |
+
DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
|
| 619 |
+
|
| 620 |
+
# In production:
|
| 621 |
+
app.run(debug=False)
|
| 622 |
+
|
| 623 |
+
Use environment variables to control debug settings.""",
|
| 624 |
+
cwe_id="CWE-215",
|
| 625 |
+
languages=['.py', '.js', '.ts', '.java', '.php', '.rb', '.yml', '.yaml', '.json'],
|
| 626 |
+
false_positive_patterns=[r'#.*DEBUG', r'//.*DEBUG', r'debug.*log']
|
| 627 |
+
),
|
| 628 |
+
|
| 629 |
+
SASTRule(
|
| 630 |
+
name="Sensitive Data in Logs",
|
| 631 |
+
pattern=r"""(?:log(?:ger)?\.(?:info|debug|warn|error|critical)|print|console\.log|System\.out\.print)\s*\([^)]*(?:password|secret|token|key|credit.?card|ssn|api.?key)""",
|
| 632 |
+
description="Sensitive data may be written to logs. This can expose credentials and personal information.",
|
| 633 |
+
risk_level=RiskLevel.MEDIUM,
|
| 634 |
+
remediation="""Never log sensitive information:
|
| 635 |
+
|
| 636 |
+
VULNERABLE:
|
| 637 |
+
logger.info(f"User login: {username}, password: {password}")
|
| 638 |
+
|
| 639 |
+
SECURE:
|
| 640 |
+
logger.info(f"User login: {username}")
|
| 641 |
+
# Or mask sensitive data:
|
| 642 |
+
logger.info(f"API key: {api_key[:4]}****")
|
| 643 |
+
|
| 644 |
+
Use structured logging with sensitive field filtering.""",
|
| 645 |
+
cwe_id="CWE-532",
|
| 646 |
+
languages=['.py', '.java', '.js', '.ts', '.rb', '.go', '.php']
|
| 647 |
+
),
|
| 648 |
+
|
| 649 |
+
SASTRule(
|
| 650 |
+
name="Stack Trace Exposure",
|
| 651 |
+
pattern=r"""(?:printStackTrace|traceback\.print_exc|console\.trace|e\.stack|err\.stack)\s*\(?\)?|except.*?:?\s*pass|rescue\s*=>\s*nil""",
|
| 652 |
+
description="Stack traces may be exposed to users or exceptions silently ignored.",
|
| 653 |
+
risk_level=RiskLevel.LOW,
|
| 654 |
+
remediation="""Handle exceptions properly without exposing internals:
|
| 655 |
+
|
| 656 |
+
VULNERABLE:
|
| 657 |
+
except Exception as e:
|
| 658 |
+
return str(e) # Exposes internal details
|
| 659 |
+
|
| 660 |
+
SECURE:
|
| 661 |
+
except Exception as e:
|
| 662 |
+
logger.exception("Operation failed") # Log internally
|
| 663 |
+
return {"error": "An error occurred"} # Generic message
|
| 664 |
+
|
| 665 |
+
Never expose full stack traces to end users.""",
|
| 666 |
+
cwe_id="CWE-209",
|
| 667 |
+
languages=['.py', '.java', '.js', '.ts', '.rb', '.php']
|
| 668 |
+
),
|
| 669 |
+
|
| 670 |
+
# ============================================================
|
| 671 |
+
# SECURITY MISCONFIGURATION
|
| 672 |
+
# Like leaving doors unlocked or windows open
|
| 673 |
+
# ============================================================
|
| 674 |
+
|
| 675 |
+
SASTRule(
|
| 676 |
+
name="CORS Wildcard",
|
| 677 |
+
pattern=r"""(?:Access-Control-Allow-Origin|cors)\s*[=:]\s*['"]\*['"]|\.allowedOrigins\s*\(\s*['"]\*['"]|cors\s*\(\s*\{[^}]*origin\s*:\s*(?:true|['"]\*['"])""",
|
| 678 |
+
description="CORS configured to allow all origins. This can enable cross-site request attacks.",
|
| 679 |
+
risk_level=RiskLevel.MEDIUM,
|
| 680 |
+
remediation="""Restrict CORS to specific trusted origins:
|
| 681 |
+
|
| 682 |
+
VULNERABLE:
|
| 683 |
+
Access-Control-Allow-Origin: *
|
| 684 |
+
cors({ origin: '*' })
|
| 685 |
+
|
| 686 |
+
SECURE:
|
| 687 |
+
cors({
|
| 688 |
+
origin: ['https://trusted-site.com'],
|
| 689 |
+
methods: ['GET', 'POST'],
|
| 690 |
+
credentials: true
|
| 691 |
+
})
|
| 692 |
+
|
| 693 |
+
Never use wildcard CORS with credentials.""",
|
| 694 |
+
cwe_id="CWE-942",
|
| 695 |
+
languages=['.py', '.java', '.js', '.ts', '.php', '.rb', '.go']
|
| 696 |
+
),
|
| 697 |
+
|
| 698 |
+
SASTRule(
|
| 699 |
+
name="SSL/TLS Verification Disabled",
|
| 700 |
+
pattern=r"""verify\s*[=:]\s*False|VERIFY_SSL\s*=\s*False|ssl\s*[=:]\s*False|rejectUnauthorized\s*[=:]\s*false|InsecureSkipVerify\s*[=:]\s*true|CURLOPT_SSL_VERIFYPEER.*false""",
|
| 701 |
+
description="SSL/TLS certificate verification is disabled. This makes the application vulnerable to man-in-the-middle attacks.",
|
| 702 |
+
risk_level=RiskLevel.HIGH,
|
| 703 |
+
remediation="""Always verify SSL/TLS certificates:
|
| 704 |
+
|
| 705 |
+
VULNERABLE:
|
| 706 |
+
requests.get(url, verify=False)
|
| 707 |
+
https.request({rejectUnauthorized: false})
|
| 708 |
+
|
| 709 |
+
SECURE:
|
| 710 |
+
requests.get(url, verify=True)
|
| 711 |
+
# Or with custom CA:
|
| 712 |
+
requests.get(url, verify='/path/to/ca-bundle.crt')
|
| 713 |
+
|
| 714 |
+
If you need to use self-signed certs in development,
|
| 715 |
+
use environment-based configuration.""",
|
| 716 |
+
cwe_id="CWE-295",
|
| 717 |
+
languages=['.py', '.java', '.js', '.ts', '.go', '.php', '.rb']
|
| 718 |
+
),
|
| 719 |
+
|
| 720 |
+
SASTRule(
|
| 721 |
+
name="Insecure HTTP",
|
| 722 |
+
pattern=r"""['"](http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)[^'"]+)['"]""",
|
| 723 |
+
description="Insecure HTTP URL detected. Data transmitted over HTTP can be intercepted.",
|
| 724 |
+
risk_level=RiskLevel.LOW,
|
| 725 |
+
remediation="""Use HTTPS for all external communications:
|
| 726 |
+
|
| 727 |
+
VULNERABLE:
|
| 728 |
+
api_url = "http://api.example.com/data"
|
| 729 |
+
|
| 730 |
+
SECURE:
|
| 731 |
+
api_url = "https://api.example.com/data"
|
| 732 |
+
|
| 733 |
+
Configure HSTS (HTTP Strict Transport Security) on your servers.""",
|
| 734 |
+
cwe_id="CWE-319",
|
| 735 |
+
languages=['.py', '.java', '.js', '.ts', '.go', '.php', '.rb', '.yml', '.yaml', '.json'],
|
| 736 |
+
false_positive_patterns=[r'#.*http://', r'//.*http://', r'example\.com', r'schema.*http://']
|
| 737 |
+
),
|
| 738 |
+
|
| 739 |
+
SASTRule(
|
| 740 |
+
name="Missing Security Headers",
|
| 741 |
+
pattern=r"""(?:Content-Security-Policy|X-Frame-Options|X-Content-Type-Options|Strict-Transport-Security)\s*[=:]\s*['"]['""]|no_header|disable.*header""",
|
| 742 |
+
description="Security headers may be missing or disabled. This can enable various attacks.",
|
| 743 |
+
risk_level=RiskLevel.LOW,
|
| 744 |
+
remediation="""Configure security headers:
|
| 745 |
+
|
| 746 |
+
Add these headers to your responses:
|
| 747 |
+
Content-Security-Policy: default-src 'self'
|
| 748 |
+
X-Frame-Options: DENY
|
| 749 |
+
X-Content-Type-Options: nosniff
|
| 750 |
+
Strict-Transport-Security: max-age=31536000; includeSubDomains
|
| 751 |
+
X-XSS-Protection: 1; mode=block
|
| 752 |
+
|
| 753 |
+
Use helmet.js (Node), django-csp, or similar libraries.""",
|
| 754 |
+
cwe_id="CWE-693",
|
| 755 |
+
languages=['.py', '.java', '.js', '.ts', '.php', '.rb']
|
| 756 |
+
),
|
| 757 |
+
|
| 758 |
+
# ============================================================
|
| 759 |
+
# XML VULNERABILITIES
|
| 760 |
+
# XML parsers can be tricked into dangerous behavior
|
| 761 |
+
# ============================================================
|
| 762 |
+
|
| 763 |
+
SASTRule(
|
| 764 |
+
name="XXE (XML External Entity)",
|
| 765 |
+
pattern=r"""(?:xml\.etree|lxml|xml\.dom|xml\.sax|XMLReader|DocumentBuilder|SAXParser|XMLParser).*(?:parse|read|load)|<!ENTITY|SYSTEM\s+['""]|resolve_entities\s*=\s*True""",
|
| 766 |
+
description="Potential XML External Entity (XXE) vulnerability. XML parsers should disable external entity processing.",
|
| 767 |
+
risk_level=RiskLevel.HIGH,
|
| 768 |
+
remediation="""Disable external entity processing:
|
| 769 |
+
|
| 770 |
+
VULNERABLE (Python):
|
| 771 |
+
tree = etree.parse(xml_file)
|
| 772 |
+
|
| 773 |
+
SECURE (Python):
|
| 774 |
+
parser = etree.XMLParser(resolve_entities=False, no_network=True)
|
| 775 |
+
tree = etree.parse(xml_file, parser)
|
| 776 |
+
|
| 777 |
+
SECURE (Java):
|
| 778 |
+
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
| 779 |
+
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
|
| 780 |
+
dbf.setExpandEntityReferences(false);""",
|
| 781 |
+
cwe_id="CWE-611",
|
| 782 |
+
languages=['.py', '.java', '.php', '.cs', '.rb']
|
| 783 |
+
),
|
| 784 |
+
|
| 785 |
+
# ============================================================
|
| 786 |
+
# SERVER-SIDE REQUEST FORGERY (SSRF)
|
| 787 |
+
# Like being tricked into making calls you shouldn't
|
| 788 |
+
# ============================================================
|
| 789 |
+
|
| 790 |
+
SASTRule(
|
| 791 |
+
name="Server-Side Request Forgery (SSRF)",
|
| 792 |
+
pattern=r"""(?:requests\.get|urllib\.request\.urlopen|http\.get|fetch|axios\.get|HttpClient)\s*\([^)]*(?:request\.|req\.|params\.|query\.|body\.|input|GET|POST)""",
|
| 793 |
+
description="Potential SSRF vulnerability. User input may control server-side HTTP requests.",
|
| 794 |
+
risk_level=RiskLevel.HIGH,
|
| 795 |
+
remediation="""Validate and whitelist URLs:
|
| 796 |
+
|
| 797 |
+
VULNERABLE:
|
| 798 |
+
url = request.args.get('url')
|
| 799 |
+
response = requests.get(url)
|
| 800 |
+
|
| 801 |
+
SECURE:
|
| 802 |
+
from urllib.parse import urlparse
|
| 803 |
+
|
| 804 |
+
ALLOWED_HOSTS = ['api.trusted.com', 'data.trusted.com']
|
| 805 |
+
|
| 806 |
+
parsed = urlparse(url)
|
| 807 |
+
if parsed.hostname not in ALLOWED_HOSTS:
|
| 808 |
+
raise ValueError("URL not allowed")
|
| 809 |
+
if parsed.scheme not in ['http', 'https']:
|
| 810 |
+
raise ValueError("Invalid scheme")
|
| 811 |
+
# Block internal IPs
|
| 812 |
+
if is_internal_ip(parsed.hostname):
|
| 813 |
+
raise ValueError("Internal URLs not allowed")""",
|
| 814 |
+
cwe_id="CWE-918",
|
| 815 |
+
languages=['.py', '.java', '.js', '.ts', '.go', '.php', '.rb']
|
| 816 |
+
),
|
| 817 |
+
|
| 818 |
+
# ============================================================
|
| 819 |
+
# ADDITIONAL COMMON VULNERABILITIES
|
| 820 |
+
# ============================================================
|
| 821 |
+
|
| 822 |
+
SASTRule(
|
| 823 |
+
name="Unsafe Regex (ReDoS)",
|
| 824 |
+
pattern=r"""(?:re\.compile|new\s+RegExp|regex|pattern)\s*\([^)]*(?:\+\*|\*\+|\.+\*|\.+\+|\(\.\*\)|\(\.\+\)|(?:\[[^\]]*\]){2,}\*|\{\d+,\}\*|\{\d+,\}\+)""",
|
| 825 |
+
description="Potentially vulnerable regular expression that could cause ReDoS (Regular Expression Denial of Service).",
|
| 826 |
+
risk_level=RiskLevel.MEDIUM,
|
| 827 |
+
remediation="""Avoid nested quantifiers in regex:
|
| 828 |
+
|
| 829 |
+
VULNERABLE:
|
| 830 |
+
pattern = re.compile(r'(a+)+b') # Catastrophic backtracking
|
| 831 |
+
|
| 832 |
+
SECURE:
|
| 833 |
+
pattern = re.compile(r'a+b') # Simple, efficient
|
| 834 |
+
|
| 835 |
+
# Or use atomic groups/possessive quantifiers where supported
|
| 836 |
+
# Set timeouts for regex operations:
|
| 837 |
+
import regex
|
| 838 |
+
regex.match(pattern, text, timeout=1.0)""",
|
| 839 |
+
cwe_id="CWE-1333",
|
| 840 |
+
languages=['.py', '.java', '.js', '.ts', '.go', '.php', '.rb']
|
| 841 |
+
),
|
| 842 |
+
|
| 843 |
+
SASTRule(
|
| 844 |
+
name="Prototype Pollution",
|
| 845 |
+
pattern=r"""(?:Object\.assign|_\.merge|_\.extend|_\.defaults|jQuery\.extend|angular\.(?:merge|extend))\s*\([^,]*,\s*(?:req\.|request\.|params\.|body\.|input)|\[['"]__proto__['"]\]|\[['"]constructor['"]\]\.prototype""",
|
| 846 |
+
description="Potential prototype pollution vulnerability. Merging user input into objects can modify Object.prototype.",
|
| 847 |
+
risk_level=RiskLevel.HIGH,
|
| 848 |
+
remediation="""Validate and sanitize object keys:
|
| 849 |
+
|
| 850 |
+
VULNERABLE:
|
| 851 |
+
Object.assign(target, req.body);
|
| 852 |
+
_.merge(config, userInput);
|
| 853 |
+
|
| 854 |
+
SECURE:
|
| 855 |
+
// Use Object.create(null) for prototype-less objects
|
| 856 |
+
const safeObj = Object.create(null);
|
| 857 |
+
|
| 858 |
+
// Whitelist allowed properties
|
| 859 |
+
const allowed = ['name', 'email'];
|
| 860 |
+
for (const key of allowed) {
|
| 861 |
+
if (key in userInput) {
|
| 862 |
+
safeObj[key] = userInput[key];
|
| 863 |
+
}
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
// Or use libraries like 'lodash' with safeguards""",
|
| 867 |
+
cwe_id="CWE-1321",
|
| 868 |
+
languages=['.js', '.ts']
|
| 869 |
+
),
|
| 870 |
+
|
| 871 |
+
SASTRule(
|
| 872 |
+
name="Open Redirect",
|
| 873 |
+
pattern=r"""(?:redirect|res\.redirect|header\s*\(\s*['""]Location|window\.location|document\.location)\s*[=(]\s*(?:req\.|request\.|params\.|query\.|input|GET|POST|\$_)""",
|
| 874 |
+
description="Potential open redirect vulnerability. User input controls redirect destination.",
|
| 875 |
+
risk_level=RiskLevel.MEDIUM,
|
| 876 |
+
remediation="""Validate redirect URLs:
|
| 877 |
+
|
| 878 |
+
VULNERABLE:
|
| 879 |
+
redirect_url = request.args.get('next')
|
| 880 |
+
return redirect(redirect_url)
|
| 881 |
+
|
| 882 |
+
SECURE:
|
| 883 |
+
from urllib.parse import urlparse
|
| 884 |
+
|
| 885 |
+
redirect_url = request.args.get('next', '/')
|
| 886 |
+
parsed = urlparse(redirect_url)
|
| 887 |
+
|
| 888 |
+
# Only allow relative URLs or specific domains
|
| 889 |
+
if parsed.netloc and parsed.netloc != 'mysite.com':
|
| 890 |
+
redirect_url = '/'
|
| 891 |
+
|
| 892 |
+
return redirect(redirect_url)""",
|
| 893 |
+
cwe_id="CWE-601",
|
| 894 |
+
languages=['.py', '.java', '.js', '.ts', '.php', '.rb']
|
| 895 |
+
),
|
| 896 |
+
|
| 897 |
+
SASTRule(
|
| 898 |
+
name="Mass Assignment",
|
| 899 |
+
pattern=r"""(?:\.update_attributes|\.update\(|\.create\(|\.build\(|Model\.create|\.save\()\s*\(?[^)]*(?:req\.|request\.|params\[|body\[|:permit\s*\(\s*!)""",
|
| 900 |
+
description="Potential mass assignment vulnerability. User input may modify unintended model attributes.",
|
| 901 |
+
risk_level=RiskLevel.MEDIUM,
|
| 902 |
+
remediation="""Whitelist allowed attributes:
|
| 903 |
+
|
| 904 |
+
VULNERABLE (Rails):
|
| 905 |
+
User.create(params[:user])
|
| 906 |
+
|
| 907 |
+
SECURE (Rails):
|
| 908 |
+
User.create(params.require(:user).permit(:name, :email))
|
| 909 |
+
|
| 910 |
+
VULNERABLE (Django):
|
| 911 |
+
User.objects.create(**request.POST)
|
| 912 |
+
|
| 913 |
+
SECURE (Django):
|
| 914 |
+
User.objects.create(
|
| 915 |
+
name=request.POST.get('name'),
|
| 916 |
+
email=request.POST.get('email')
|
| 917 |
+
)
|
| 918 |
+
|
| 919 |
+
Always explicitly specify which fields can be mass-assigned.""",
|
| 920 |
+
cwe_id="CWE-915",
|
| 921 |
+
languages=['.py', '.rb', '.java', '.js', '.ts', '.php']
|
| 922 |
+
),
|
| 923 |
+
]
|
| 924 |
+
|
| 925 |
+
def scan_file(self, file_path: str) -> List[Vulnerability]:
|
| 926 |
+
"""
|
| 927 |
+
Scan a single file for vulnerabilities.
|
| 928 |
+
|
| 929 |
+
Like running a specific diagnostic test on one tissue sample.
|
| 930 |
+
"""
|
| 931 |
+
vulnerabilities = []
|
| 932 |
+
|
| 933 |
+
try:
|
| 934 |
+
file_ext = Path(file_path).suffix.lower()
|
| 935 |
+
|
| 936 |
+
if file_ext not in self.file_extensions:
|
| 937 |
+
return vulnerabilities
|
| 938 |
+
|
| 939 |
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
| 940 |
+
code = f.read()
|
| 941 |
+
|
| 942 |
+
for rule in self.rules:
|
| 943 |
+
matches = rule.matches(code, file_ext)
|
| 944 |
+
for line_num, snippet in matches:
|
| 945 |
+
vuln = Vulnerability(
|
| 946 |
+
name=rule.name,
|
| 947 |
+
description=rule.description,
|
| 948 |
+
file_path=file_path,
|
| 949 |
+
line_number=line_num,
|
| 950 |
+
code_snippet=snippet,
|
| 951 |
+
risk_level=rule.risk_level,
|
| 952 |
+
remediation=rule.remediation,
|
| 953 |
+
cwe_id=rule.cwe_id
|
| 954 |
+
)
|
| 955 |
+
vulnerabilities.append(vuln)
|
| 956 |
+
|
| 957 |
+
except Exception as e:
|
| 958 |
+
# Log but don't fail on individual file errors
|
| 959 |
+
pass
|
| 960 |
+
|
| 961 |
+
return vulnerabilities
|
| 962 |
+
|
| 963 |
+
def scan_directory(self, directory: str, max_workers: int = 8, use_parallel: bool = True) -> Tuple[List[Vulnerability], int]:
|
| 964 |
+
"""
|
| 965 |
+
Recursively scan a directory with optional parallel processing.
|
| 966 |
+
|
| 967 |
+
Like performing a full-body scan.
|
| 968 |
+
|
| 969 |
+
Args:
|
| 970 |
+
directory: Path to directory to scan
|
| 971 |
+
max_workers: Number of parallel workers (default: 8)
|
| 972 |
+
use_parallel: Whether to use parallel processing (default: True)
|
| 973 |
+
|
| 974 |
+
Returns:
|
| 975 |
+
Tuple of (vulnerabilities, files_scanned)
|
| 976 |
+
"""
|
| 977 |
+
vulnerabilities = []
|
| 978 |
+
files_scanned = 0
|
| 979 |
+
|
| 980 |
+
# Collect all files to scan
|
| 981 |
+
files_to_scan = []
|
| 982 |
+
for root, dirs, files in os.walk(directory):
|
| 983 |
+
# Skip unwanted directories
|
| 984 |
+
dirs[:] = [d for d in dirs if d not in self.skip_dirs]
|
| 985 |
+
|
| 986 |
+
for file in files:
|
| 987 |
+
file_path = os.path.join(root, file)
|
| 988 |
+
file_ext = Path(file_path).suffix.lower()
|
| 989 |
+
|
| 990 |
+
if file_ext in self.file_extensions:
|
| 991 |
+
files_to_scan.append(file_path)
|
| 992 |
+
|
| 993 |
+
if not use_parallel or len(files_to_scan) <= 1:
|
| 994 |
+
# Sequential processing (original behavior)
|
| 995 |
+
for file_path in files_to_scan:
|
| 996 |
+
files_scanned += 1
|
| 997 |
+
vulns = self.scan_file(file_path)
|
| 998 |
+
vulnerabilities.extend(vulns)
|
| 999 |
+
else:
|
| 1000 |
+
# Parallel processing using ThreadPoolExecutor
|
| 1001 |
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
| 1002 |
+
# Submit all scan jobs
|
| 1003 |
+
future_to_file = {
|
| 1004 |
+
executor.submit(self.scan_file, file_path): file_path
|
| 1005 |
+
for file_path in files_to_scan
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
# Collect results as they complete
|
| 1009 |
+
for future in as_completed(future_to_file):
|
| 1010 |
+
file_path = future_to_file[future]
|
| 1011 |
+
try:
|
| 1012 |
+
vulns = future.result()
|
| 1013 |
+
vulnerabilities.extend(vulns)
|
| 1014 |
+
files_scanned += 1
|
| 1015 |
+
except Exception as e:
|
| 1016 |
+
# Log error but continue with other files
|
| 1017 |
+
print(f"Error scanning {file_path}: {e}")
|
| 1018 |
+
files_scanned += 1 # Count as scanned even if error
|
| 1019 |
+
|
| 1020 |
+
return vulnerabilities, files_scanned
|
| 1021 |
+
|
| 1022 |
+
|
| 1023 |
+
class NVDClient:
|
| 1024 |
+
"""
|
| 1025 |
+
NIST National Vulnerability Database Client
|
| 1026 |
+
|
| 1027 |
+
Analogy: This is like searching a medical journal database
|
| 1028 |
+
- Searches for known vulnerabilities (diseases) by keyword
|
| 1029 |
+
- Retrieves detailed information including severity scores
|
| 1030 |
+
- Provides references to official documentation
|
| 1031 |
+
|
| 1032 |
+
The NVD contains over 200,000 known vulnerabilities (CVEs)
|
| 1033 |
+
with detailed descriptions, severity scores, and references.
|
| 1034 |
+
"""
|
| 1035 |
+
|
| 1036 |
+
BASE_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
| 1037 |
+
|
| 1038 |
+
def __init__(self, api_key: Optional[str] = None):
|
| 1039 |
+
"""
|
| 1040 |
+
Initialize NVD client.
|
| 1041 |
+
|
| 1042 |
+
Args:
|
| 1043 |
+
api_key: Optional NVD API key for higher rate limits.
|
| 1044 |
+
Get one free at: https://nvd.nist.gov/developers/request-an-api-key
|
| 1045 |
+
"""
|
| 1046 |
+
self.api_key = api_key
|
| 1047 |
+
self.rate_limit_delay = 0.6 if api_key else 6.0 # NVD rate limits
|
| 1048 |
+
|
| 1049 |
+
async def search_vulnerabilities(
|
| 1050 |
+
self,
|
| 1051 |
+
keyword: Optional[str] = None,
|
| 1052 |
+
cwe_id: Optional[str] = None,
|
| 1053 |
+
severity: Optional[str] = None,
|
| 1054 |
+
limit: int = 20
|
| 1055 |
+
) -> List[Dict]:
|
| 1056 |
+
"""
|
| 1057 |
+
Search the NVD for vulnerabilities.
|
| 1058 |
+
|
| 1059 |
+
Args:
|
| 1060 |
+
keyword: Search term (e.g., "sql injection python")
|
| 1061 |
+
cwe_id: CWE ID to filter by (e.g., "CWE-89")
|
| 1062 |
+
severity: Severity level (LOW, MEDIUM, HIGH, CRITICAL)
|
| 1063 |
+
limit: Maximum results to return
|
| 1064 |
+
|
| 1065 |
+
Returns:
|
| 1066 |
+
List of CVE entries with details
|
| 1067 |
+
"""
|
| 1068 |
+
params = {"resultsPerPage": min(limit, 100)}
|
| 1069 |
+
|
| 1070 |
+
if keyword:
|
| 1071 |
+
params["keywordSearch"] = keyword
|
| 1072 |
+
|
| 1073 |
+
if cwe_id:
|
| 1074 |
+
# Format: CWE-89 -> CWE-89
|
| 1075 |
+
params["cweId"] = cwe_id
|
| 1076 |
+
|
| 1077 |
+
if severity:
|
| 1078 |
+
params["cvssV3Severity"] = severity.upper()
|
| 1079 |
+
|
| 1080 |
+
headers = {}
|
| 1081 |
+
if self.api_key:
|
| 1082 |
+
headers["apiKey"] = self.api_key
|
| 1083 |
+
|
| 1084 |
+
try:
|
| 1085 |
+
async with aiohttp.ClientSession() as session:
|
| 1086 |
+
async with session.get(
|
| 1087 |
+
self.BASE_URL,
|
| 1088 |
+
params=params,
|
| 1089 |
+
headers=headers,
|
| 1090 |
+
timeout=aiohttp.ClientTimeout(total=30)
|
| 1091 |
+
) as response:
|
| 1092 |
+
if response.status == 200:
|
| 1093 |
+
data = await response.json()
|
| 1094 |
+
return self._parse_results(data)
|
| 1095 |
+
elif response.status == 403:
|
| 1096 |
+
return [{"error": "NVD API rate limited. Consider using an API key."}]
|
| 1097 |
+
else:
|
| 1098 |
+
return [{"error": f"NVD API error: {response.status}"}]
|
| 1099 |
+
|
| 1100 |
+
except asyncio.TimeoutError:
|
| 1101 |
+
return [{"error": "NVD API request timed out"}]
|
| 1102 |
+
except Exception as e:
|
| 1103 |
+
return [{"error": f"NVD API error: {str(e)}"}]
|
| 1104 |
+
|
| 1105 |
+
def _parse_results(self, data: Dict) -> List[Dict]:
|
| 1106 |
+
"""Parse NVD API response into a cleaner format."""
|
| 1107 |
+
results = []
|
| 1108 |
+
|
| 1109 |
+
for vuln in data.get("vulnerabilities", []):
|
| 1110 |
+
cve = vuln.get("cve", {})
|
| 1111 |
+
cve_id = cve.get("id", "Unknown")
|
| 1112 |
+
|
| 1113 |
+
# Get description
|
| 1114 |
+
descriptions = cve.get("descriptions", [])
|
| 1115 |
+
description = next(
|
| 1116 |
+
(d["value"] for d in descriptions if d.get("lang") == "en"),
|
| 1117 |
+
"No description available"
|
| 1118 |
+
)
|
| 1119 |
+
|
| 1120 |
+
# Get CVSS score and severity
|
| 1121 |
+
metrics = cve.get("metrics", {})
|
| 1122 |
+
cvss_data = None
|
| 1123 |
+
severity = "UNKNOWN"
|
| 1124 |
+
score = 0.0
|
| 1125 |
+
|
| 1126 |
+
# Try CVSS v3.1, then v3.0, then v2.0
|
| 1127 |
+
for version in ["cvssMetricV31", "cvssMetricV30", "cvssMetricV2"]:
|
| 1128 |
+
if version in metrics and metrics[version]:
|
| 1129 |
+
cvss_data = metrics[version][0]
|
| 1130 |
+
if "cvssData" in cvss_data:
|
| 1131 |
+
score = cvss_data["cvssData"].get("baseScore", 0)
|
| 1132 |
+
severity = cvss_data["cvssData"].get("baseSeverity", "UNKNOWN")
|
| 1133 |
+
break
|
| 1134 |
+
|
| 1135 |
+
# Get references
|
| 1136 |
+
references = [
|
| 1137 |
+
ref.get("url") for ref in cve.get("references", [])[:5]
|
| 1138 |
+
]
|
| 1139 |
+
|
| 1140 |
+
# Get CWE IDs
|
| 1141 |
+
cwes = []
|
| 1142 |
+
for weakness in cve.get("weaknesses", []):
|
| 1143 |
+
for desc in weakness.get("description", []):
|
| 1144 |
+
if desc.get("value", "").startswith("CWE-"):
|
| 1145 |
+
cwes.append(desc["value"])
|
| 1146 |
+
|
| 1147 |
+
results.append({
|
| 1148 |
+
"cve_id": cve_id,
|
| 1149 |
+
"description": description[:500] + "..." if len(description) > 500 else description,
|
| 1150 |
+
"severity": severity,
|
| 1151 |
+
"cvss_score": score,
|
| 1152 |
+
"cwes": cwes,
|
| 1153 |
+
"references": references,
|
| 1154 |
+
"published": cve.get("published", "Unknown"),
|
| 1155 |
+
"last_modified": cve.get("lastModified", "Unknown")
|
| 1156 |
+
})
|
| 1157 |
+
|
| 1158 |
+
return results
|
| 1159 |
+
|
| 1160 |
+
async def get_cve_details(self, cve_id: str) -> Optional[Dict]:
|
| 1161 |
+
"""
|
| 1162 |
+
Get detailed information about a specific CVE.
|
| 1163 |
+
|
| 1164 |
+
Args:
|
| 1165 |
+
cve_id: CVE identifier (e.g., "CVE-2021-44228")
|
| 1166 |
+
|
| 1167 |
+
Returns:
|
| 1168 |
+
Detailed CVE information or None if not found
|
| 1169 |
+
"""
|
| 1170 |
+
params = {"cveId": cve_id}
|
| 1171 |
+
|
| 1172 |
+
headers = {}
|
| 1173 |
+
if self.api_key:
|
| 1174 |
+
headers["apiKey"] = self.api_key
|
| 1175 |
+
|
| 1176 |
+
try:
|
| 1177 |
+
async with aiohttp.ClientSession() as session:
|
| 1178 |
+
async with session.get(
|
| 1179 |
+
self.BASE_URL,
|
| 1180 |
+
params=params,
|
| 1181 |
+
headers=headers,
|
| 1182 |
+
timeout=aiohttp.ClientTimeout(total=30)
|
| 1183 |
+
) as response:
|
| 1184 |
+
if response.status == 200:
|
| 1185 |
+
data = await response.json()
|
| 1186 |
+
results = self._parse_results(data)
|
| 1187 |
+
return results[0] if results else None
|
| 1188 |
+
return None
|
| 1189 |
+
|
| 1190 |
+
except Exception:
|
| 1191 |
+
return None
|
| 1192 |
+
|
| 1193 |
+
async def find_related_cves(self, cwe_id: str, limit: int = 10) -> List[Dict]:
|
| 1194 |
+
"""
|
| 1195 |
+
Find CVEs related to a specific CWE.
|
| 1196 |
+
|
| 1197 |
+
This helps answer "What known attacks use this vulnerability type?"
|
| 1198 |
+
|
| 1199 |
+
Args:
|
| 1200 |
+
cwe_id: CWE identifier (e.g., "CWE-89")
|
| 1201 |
+
limit: Maximum results
|
| 1202 |
+
|
| 1203 |
+
Returns:
|
| 1204 |
+
List of related CVEs
|
| 1205 |
+
"""
|
| 1206 |
+
return await self.search_vulnerabilities(cwe_id=cwe_id, limit=limit)
|
| 1207 |
+
|
| 1208 |
+
|
| 1209 |
+
class WebAppScanner:
|
| 1210 |
+
"""
|
| 1211 |
+
Web Application Scanner
|
| 1212 |
+
|
| 1213 |
+
Analogy: Like a physical security inspector
|
| 1214 |
+
- Checks doors and windows (endpoints)
|
| 1215 |
+
- Tests locks (authentication)
|
| 1216 |
+
- Looks for signs of vulnerability
|
| 1217 |
+
|
| 1218 |
+
This performs basic web security checks without being intrusive.
|
| 1219 |
+
For full web app testing, specialized tools like OWASP ZAP are recommended.
|
| 1220 |
+
"""
|
| 1221 |
+
|
| 1222 |
+
def __init__(self):
|
| 1223 |
+
self.common_paths = [
|
| 1224 |
+
# Admin paths
|
| 1225 |
+
"/admin", "/administrator", "/admin.php", "/admin.html",
|
| 1226 |
+
"/wp-admin", "/cpanel", "/phpmyadmin",
|
| 1227 |
+
# Sensitive files
|
| 1228 |
+
"/.git/config", "/.env", "/config.php", "/wp-config.php",
|
| 1229 |
+
"/.htaccess", "/web.config", "/robots.txt", "/sitemap.xml",
|
| 1230 |
+
# Backup files
|
| 1231 |
+
"/backup.zip", "/backup.sql", "/db.sql", "/database.sql",
|
| 1232 |
+
# API endpoints
|
| 1233 |
+
"/api", "/api/v1", "/graphql", "/swagger.json", "/openapi.json",
|
| 1234 |
+
# Debug/test
|
| 1235 |
+
"/debug", "/test", "/phpinfo.php", "/info.php",
|
| 1236 |
+
]
|
| 1237 |
+
|
| 1238 |
+
self.security_headers = [
|
| 1239 |
+
"Content-Security-Policy",
|
| 1240 |
+
"X-Frame-Options",
|
| 1241 |
+
"X-Content-Type-Options",
|
| 1242 |
+
"X-XSS-Protection",
|
| 1243 |
+
"Strict-Transport-Security",
|
| 1244 |
+
"Referrer-Policy",
|
| 1245 |
+
"Permissions-Policy"
|
| 1246 |
+
]
|
| 1247 |
+
|
| 1248 |
+
async def scan_url(self, url: str) -> List[Vulnerability]:
|
| 1249 |
+
"""
|
| 1250 |
+
Perform security scan on a web application.
|
| 1251 |
+
|
| 1252 |
+
Args:
|
| 1253 |
+
url: Target URL (e.g., "https://example.com")
|
| 1254 |
+
|
| 1255 |
+
Returns:
|
| 1256 |
+
List of discovered vulnerabilities
|
| 1257 |
+
"""
|
| 1258 |
+
vulnerabilities = []
|
| 1259 |
+
|
| 1260 |
+
# Normalize URL
|
| 1261 |
+
if not url.startswith(('http://', 'https://')):
|
| 1262 |
+
url = 'https://' + url
|
| 1263 |
+
|
| 1264 |
+
parsed = urlparse(url)
|
| 1265 |
+
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
| 1266 |
+
|
| 1267 |
+
async with aiohttp.ClientSession() as session:
|
| 1268 |
+
# Check security headers
|
| 1269 |
+
header_vulns = await self._check_security_headers(session, base_url)
|
| 1270 |
+
vulnerabilities.extend(header_vulns)
|
| 1271 |
+
|
| 1272 |
+
# Check for exposed sensitive files
|
| 1273 |
+
exposure_vulns = await self._check_sensitive_paths(session, base_url)
|
| 1274 |
+
vulnerabilities.extend(exposure_vulns)
|
| 1275 |
+
|
| 1276 |
+
# Check HTTPS configuration
|
| 1277 |
+
https_vulns = await self._check_https(session, url)
|
| 1278 |
+
vulnerabilities.extend(https_vulns)
|
| 1279 |
+
|
| 1280 |
+
# Check for common vulnerabilities in responses
|
| 1281 |
+
content_vulns = await self._check_response_content(session, base_url)
|
| 1282 |
+
vulnerabilities.extend(content_vulns)
|
| 1283 |
+
|
| 1284 |
+
return vulnerabilities
|
| 1285 |
+
|
| 1286 |
+
async def _check_security_headers(
|
| 1287 |
+
self, session: aiohttp.ClientSession, url: str
|
| 1288 |
+
) -> List[Vulnerability]:
|
| 1289 |
+
"""Check for missing or misconfigured security headers."""
|
| 1290 |
+
vulnerabilities = []
|
| 1291 |
+
|
| 1292 |
+
try:
|
| 1293 |
+
async with session.head(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
| 1294 |
+
headers = response.headers
|
| 1295 |
+
|
| 1296 |
+
for header in self.security_headers:
|
| 1297 |
+
if header not in headers:
|
| 1298 |
+
vuln = Vulnerability(
|
| 1299 |
+
name=f"Missing Security Header: {header}",
|
| 1300 |
+
description=f"The {header} header is not set. This header helps protect against various attacks.",
|
| 1301 |
+
file_path=url,
|
| 1302 |
+
line_number=0,
|
| 1303 |
+
code_snippet=f"Response headers do not include {header}",
|
| 1304 |
+
risk_level=RiskLevel.LOW if header != "Content-Security-Policy" else RiskLevel.MEDIUM,
|
| 1305 |
+
remediation=self._get_header_remediation(header),
|
| 1306 |
+
cwe_id="CWE-693"
|
| 1307 |
+
)
|
| 1308 |
+
vulnerabilities.append(vuln)
|
| 1309 |
+
|
| 1310 |
+
# Check for server version disclosure
|
| 1311 |
+
if "Server" in headers and any(v in headers["Server"].lower() for v in ["apache/", "nginx/", "iis/"]):
|
| 1312 |
+
vuln = Vulnerability(
|
| 1313 |
+
name="Server Version Disclosure",
|
| 1314 |
+
description=f"Server header reveals version information: {headers['Server']}",
|
| 1315 |
+
file_path=url,
|
| 1316 |
+
line_number=0,
|
| 1317 |
+
code_snippet=f"Server: {headers['Server']}",
|
| 1318 |
+
risk_level=RiskLevel.INFO,
|
| 1319 |
+
remediation="Configure your web server to hide version information. In Apache, use 'ServerTokens Prod'. In Nginx, use 'server_tokens off'.",
|
| 1320 |
+
cwe_id="CWE-200"
|
| 1321 |
+
)
|
| 1322 |
+
vulnerabilities.append(vuln)
|
| 1323 |
+
|
| 1324 |
+
except Exception:
|
| 1325 |
+
pass
|
| 1326 |
+
|
| 1327 |
+
return vulnerabilities
|
| 1328 |
+
|
| 1329 |
+
async def _check_sensitive_paths(
|
| 1330 |
+
self, session: aiohttp.ClientSession, base_url: str
|
| 1331 |
+
) -> List[Vulnerability]:
|
| 1332 |
+
"""Check for exposed sensitive files and directories."""
|
| 1333 |
+
vulnerabilities = []
|
| 1334 |
+
|
| 1335 |
+
async def check_path(path: str):
|
| 1336 |
+
try:
|
| 1337 |
+
url = f"{base_url}{path}"
|
| 1338 |
+
async with session.get(
|
| 1339 |
+
url,
|
| 1340 |
+
timeout=aiohttp.ClientTimeout(total=5),
|
| 1341 |
+
allow_redirects=False
|
| 1342 |
+
) as response:
|
| 1343 |
+
if response.status == 200:
|
| 1344 |
+
return path, response.status
|
| 1345 |
+
return None
|
| 1346 |
+
except Exception:
|
| 1347 |
+
return None
|
| 1348 |
+
|
| 1349 |
+
# Check paths concurrently
|
| 1350 |
+
tasks = [check_path(path) for path in self.common_paths]
|
| 1351 |
+
results = await asyncio.gather(*tasks)
|
| 1352 |
+
|
| 1353 |
+
for result in results:
|
| 1354 |
+
if result:
|
| 1355 |
+
path, status = result
|
| 1356 |
+
risk = RiskLevel.HIGH if any(
|
| 1357 |
+
s in path for s in ['.git', '.env', 'config', 'backup', 'sql']
|
| 1358 |
+
) else RiskLevel.MEDIUM
|
| 1359 |
+
|
| 1360 |
+
vuln = Vulnerability(
|
| 1361 |
+
name=f"Exposed Sensitive Path: {path}",
|
| 1362 |
+
description=f"The path {path} is accessible and may expose sensitive information.",
|
| 1363 |
+
file_path=f"{base_url}{path}",
|
| 1364 |
+
line_number=0,
|
| 1365 |
+
code_snippet=f"HTTP {status} returned for {path}",
|
| 1366 |
+
risk_level=risk,
|
| 1367 |
+
remediation=f"Restrict access to {path} using web server configuration. Add authentication or remove from public access.",
|
| 1368 |
+
cwe_id="CWE-538"
|
| 1369 |
+
)
|
| 1370 |
+
vulnerabilities.append(vuln)
|
| 1371 |
+
|
| 1372 |
+
return vulnerabilities
|
| 1373 |
+
|
| 1374 |
+
async def _check_https(
|
| 1375 |
+
self, session: aiohttp.ClientSession, url: str
|
| 1376 |
+
) -> List[Vulnerability]:
|
| 1377 |
+
"""Check HTTPS configuration."""
|
| 1378 |
+
vulnerabilities = []
|
| 1379 |
+
parsed = urlparse(url)
|
| 1380 |
+
|
| 1381 |
+
if parsed.scheme == "http":
|
| 1382 |
+
vuln = Vulnerability(
|
| 1383 |
+
name="Insecure HTTP Connection",
|
| 1384 |
+
description="The target is using HTTP instead of HTTPS. All data transmitted is unencrypted.",
|
| 1385 |
+
file_path=url,
|
| 1386 |
+
line_number=0,
|
| 1387 |
+
code_snippet=f"URL scheme: {parsed.scheme}",
|
| 1388 |
+
risk_level=RiskLevel.HIGH,
|
| 1389 |
+
remediation="Enable HTTPS with a valid TLS certificate. Consider using Let's Encrypt for free certificates. Configure HSTS to prevent downgrade attacks.",
|
| 1390 |
+
cwe_id="CWE-319"
|
| 1391 |
+
)
|
| 1392 |
+
vulnerabilities.append(vuln)
|
| 1393 |
+
|
| 1394 |
+
return vulnerabilities
|
| 1395 |
+
|
| 1396 |
+
async def _check_response_content(
|
| 1397 |
+
self, session: aiohttp.ClientSession, base_url: str
|
| 1398 |
+
) -> List[Vulnerability]:
|
| 1399 |
+
"""Check response content for potential vulnerabilities."""
|
| 1400 |
+
vulnerabilities = []
|
| 1401 |
+
|
| 1402 |
+
try:
|
| 1403 |
+
async with session.get(
|
| 1404 |
+
base_url,
|
| 1405 |
+
timeout=aiohttp.ClientTimeout(total=10)
|
| 1406 |
+
) as response:
|
| 1407 |
+
if response.status == 200:
|
| 1408 |
+
content = await response.text()
|
| 1409 |
+
|
| 1410 |
+
# Check for error messages that reveal information
|
| 1411 |
+
error_patterns = [
|
| 1412 |
+
(r"mysql_error|mysqli_error|pg_error", "Database Error Disclosure", "CWE-209"),
|
| 1413 |
+
(r"stack\s*trace|traceback|exception.*at\s+line", "Stack Trace Disclosure", "CWE-209"),
|
| 1414 |
+
(r"debug\s*=\s*true|debug_mode|development_mode", "Debug Mode Enabled", "CWE-215"),
|
| 1415 |
+
(r"<!--.*(?:password|api.?key|secret).*-->", "Sensitive Data in Comments", "CWE-615"),
|
| 1416 |
+
]
|
| 1417 |
+
|
| 1418 |
+
for pattern, name, cwe in error_patterns:
|
| 1419 |
+
if re.search(pattern, content, re.IGNORECASE):
|
| 1420 |
+
vuln = Vulnerability(
|
| 1421 |
+
name=name,
|
| 1422 |
+
description=f"The response contains {name.lower()} which may reveal sensitive information.",
|
| 1423 |
+
file_path=base_url,
|
| 1424 |
+
line_number=0,
|
| 1425 |
+
code_snippet=f"Pattern detected: {pattern}",
|
| 1426 |
+
risk_level=RiskLevel.MEDIUM,
|
| 1427 |
+
remediation=f"Remove {name.lower()} from production responses. Configure error handling to show generic messages.",
|
| 1428 |
+
cwe_id=cwe
|
| 1429 |
+
)
|
| 1430 |
+
vulnerabilities.append(vuln)
|
| 1431 |
+
|
| 1432 |
+
except Exception:
|
| 1433 |
+
pass
|
| 1434 |
+
|
| 1435 |
+
return vulnerabilities
|
| 1436 |
+
|
| 1437 |
+
def _get_header_remediation(self, header: str) -> str:
|
| 1438 |
+
"""Get specific remediation advice for missing headers."""
|
| 1439 |
+
remediations = {
|
| 1440 |
+
"Content-Security-Policy": "Add CSP header to control resource loading. Start with: Content-Security-Policy: default-src 'self'",
|
| 1441 |
+
"X-Frame-Options": "Add: X-Frame-Options: DENY (or SAMEORIGIN if you need framing)",
|
| 1442 |
+
"X-Content-Type-Options": "Add: X-Content-Type-Options: nosniff",
|
| 1443 |
+
"X-XSS-Protection": "Add: X-XSS-Protection: 1; mode=block (note: deprecated in favor of CSP)",
|
| 1444 |
+
"Strict-Transport-Security": "Add: Strict-Transport-Security: max-age=31536000; includeSubDomains",
|
| 1445 |
+
"Referrer-Policy": "Add: Referrer-Policy: strict-origin-when-cross-origin",
|
| 1446 |
+
"Permissions-Policy": "Add: Permissions-Policy: geolocation=(), microphone=(), camera=()"
|
| 1447 |
+
}
|
| 1448 |
+
return remediations.get(header, f"Configure the {header} header appropriately.")
|
| 1449 |
+
|
| 1450 |
+
|
| 1451 |
+
class SecurityChecker:
|
| 1452 |
+
"""
|
| 1453 |
+
Main Security Checker - Orchestrates all scanning capabilities.
|
| 1454 |
+
|
| 1455 |
+
Analogy: This is like a complete medical center
|
| 1456 |
+
- Diagnostic imaging (SAST)
|
| 1457 |
+
- Medical database (NVD)
|
| 1458 |
+
- Physical examination (Web Scanner)
|
| 1459 |
+
- Report generation (Results)
|
| 1460 |
+
|
| 1461 |
+
Usage:
|
| 1462 |
+
checker = SecurityChecker()
|
| 1463 |
+
|
| 1464 |
+
# Scan local code
|
| 1465 |
+
result = await checker.scan_local("/path/to/project")
|
| 1466 |
+
|
| 1467 |
+
# Scan web app
|
| 1468 |
+
result = await checker.scan_web("https://example.com")
|
| 1469 |
+
|
| 1470 |
+
# Generate report
|
| 1471 |
+
report = checker.generate_report(result)
|
| 1472 |
+
"""
|
| 1473 |
+
|
| 1474 |
+
def __init__(self, nvd_api_key: Optional[str] = None):
|
| 1475 |
+
self.sast_engine = SASTEngine()
|
| 1476 |
+
self.nvd_client = NVDClient(api_key=nvd_api_key)
|
| 1477 |
+
self.web_scanner = WebAppScanner()
|
| 1478 |
+
|
| 1479 |
+
async def scan_local(self, path: str, include_nvd: bool = True, max_workers: int = 8, use_parallel: bool = True) -> ScanResult:
|
| 1480 |
+
"""
|
| 1481 |
+
Scan a local directory for vulnerabilities.
|
| 1482 |
+
|
| 1483 |
+
Args:
|
| 1484 |
+
path: Path to directory or file
|
| 1485 |
+
include_nvd: Whether to enrich results with NVD data
|
| 1486 |
+
max_workers: Number of parallel workers for file scanning (default: 8)
|
| 1487 |
+
use_parallel: Whether to use parallel processing (default: True)
|
| 1488 |
+
|
| 1489 |
+
Returns:
|
| 1490 |
+
ScanResult with all findings
|
| 1491 |
+
"""
|
| 1492 |
+
result = ScanResult(
|
| 1493 |
+
target=path,
|
| 1494 |
+
scan_type="local",
|
| 1495 |
+
start_time=datetime.now()
|
| 1496 |
+
)
|
| 1497 |
+
|
| 1498 |
+
if not os.path.exists(path):
|
| 1499 |
+
result.errors.append(f"Path does not exist: {path}")
|
| 1500 |
+
result.end_time = datetime.now()
|
| 1501 |
+
return result
|
| 1502 |
+
|
| 1503 |
+
# Run SAST scan
|
| 1504 |
+
if os.path.isfile(path):
|
| 1505 |
+
vulns = self.sast_engine.scan_file(path)
|
| 1506 |
+
result.files_scanned = 1
|
| 1507 |
+
else:
|
| 1508 |
+
vulns, files_scanned = self.sast_engine.scan_directory(
|
| 1509 |
+
path,
|
| 1510 |
+
max_workers=max_workers,
|
| 1511 |
+
use_parallel=use_parallel
|
| 1512 |
+
)
|
| 1513 |
+
result.files_scanned = files_scanned
|
| 1514 |
+
|
| 1515 |
+
# Enrich with NVD data if requested
|
| 1516 |
+
if include_nvd and vulns:
|
| 1517 |
+
vulns = await self._enrich_with_nvd(vulns)
|
| 1518 |
+
|
| 1519 |
+
result.vulnerabilities = vulns
|
| 1520 |
+
result.end_time = datetime.now()
|
| 1521 |
+
|
| 1522 |
+
return result
|
| 1523 |
+
|
| 1524 |
+
async def scan_web(self, url: str, include_nvd: bool = True) -> ScanResult:
|
| 1525 |
+
"""
|
| 1526 |
+
Scan a web application for vulnerabilities.
|
| 1527 |
+
|
| 1528 |
+
Args:
|
| 1529 |
+
url: Target URL
|
| 1530 |
+
include_nvd: Whether to enrich results with NVD data
|
| 1531 |
+
|
| 1532 |
+
Returns:
|
| 1533 |
+
ScanResult with all findings
|
| 1534 |
+
"""
|
| 1535 |
+
result = ScanResult(
|
| 1536 |
+
target=url,
|
| 1537 |
+
scan_type="web",
|
| 1538 |
+
start_time=datetime.now()
|
| 1539 |
+
)
|
| 1540 |
+
|
| 1541 |
+
try:
|
| 1542 |
+
vulns = await self.web_scanner.scan_url(url)
|
| 1543 |
+
|
| 1544 |
+
if include_nvd and vulns:
|
| 1545 |
+
vulns = await self._enrich_with_nvd(vulns)
|
| 1546 |
+
|
| 1547 |
+
result.vulnerabilities = vulns
|
| 1548 |
+
result.files_scanned = 1 # One URL scanned
|
| 1549 |
+
|
| 1550 |
+
except Exception as e:
|
| 1551 |
+
result.errors.append(str(e))
|
| 1552 |
+
|
| 1553 |
+
result.end_time = datetime.now()
|
| 1554 |
+
return result
|
| 1555 |
+
|
| 1556 |
+
async def _enrich_with_nvd(
|
| 1557 |
+
self, vulnerabilities: List[Vulnerability]
|
| 1558 |
+
) -> List[Vulnerability]:
|
| 1559 |
+
"""
|
| 1560 |
+
Enrich vulnerability findings with NVD data.
|
| 1561 |
+
|
| 1562 |
+
This adds related CVEs to each finding, showing real-world
|
| 1563 |
+
examples of the vulnerability being exploited.
|
| 1564 |
+
"""
|
| 1565 |
+
# Group by CWE to reduce API calls
|
| 1566 |
+
cwe_groups = {}
|
| 1567 |
+
for vuln in vulnerabilities:
|
| 1568 |
+
if vuln.cwe_id:
|
| 1569 |
+
if vuln.cwe_id not in cwe_groups:
|
| 1570 |
+
cwe_groups[vuln.cwe_id] = []
|
| 1571 |
+
cwe_groups[vuln.cwe_id].append(vuln)
|
| 1572 |
+
|
| 1573 |
+
# Fetch CVEs for each CWE
|
| 1574 |
+
for cwe_id, vuln_list in cwe_groups.items():
|
| 1575 |
+
try:
|
| 1576 |
+
cves = await self.nvd_client.find_related_cves(cwe_id, limit=5)
|
| 1577 |
+
cve_ids = [cve.get("cve_id") for cve in cves if "error" not in cve]
|
| 1578 |
+
|
| 1579 |
+
for vuln in vuln_list:
|
| 1580 |
+
vuln.cve_ids = cve_ids[:3] # Add top 3 related CVEs
|
| 1581 |
+
|
| 1582 |
+
# Rate limiting
|
| 1583 |
+
await asyncio.sleep(self.nvd_client.rate_limit_delay)
|
| 1584 |
+
|
| 1585 |
+
except Exception:
|
| 1586 |
+
pass
|
| 1587 |
+
|
| 1588 |
+
return vulnerabilities
|
| 1589 |
+
|
| 1590 |
+
async def search_nvd(
|
| 1591 |
+
self,
|
| 1592 |
+
keyword: Optional[str] = None,
|
| 1593 |
+
cwe_id: Optional[str] = None,
|
| 1594 |
+
severity: Optional[str] = None
|
| 1595 |
+
) -> List[Dict]:
|
| 1596 |
+
"""
|
| 1597 |
+
Search the NVD directly.
|
| 1598 |
+
|
| 1599 |
+
Useful for researching specific vulnerabilities.
|
| 1600 |
+
"""
|
| 1601 |
+
return await self.nvd_client.search_vulnerabilities(
|
| 1602 |
+
keyword=keyword,
|
| 1603 |
+
cwe_id=cwe_id,
|
| 1604 |
+
severity=severity
|
| 1605 |
+
)
|
| 1606 |
+
|
| 1607 |
+
def generate_report(
|
| 1608 |
+
self,
|
| 1609 |
+
result: ScanResult,
|
| 1610 |
+
format: str = "text"
|
| 1611 |
+
) -> str:
|
| 1612 |
+
"""
|
| 1613 |
+
Generate a human-readable report.
|
| 1614 |
+
|
| 1615 |
+
Args:
|
| 1616 |
+
result: ScanResult from a scan
|
| 1617 |
+
format: Output format ("text", "json", "html")
|
| 1618 |
+
|
| 1619 |
+
Returns:
|
| 1620 |
+
Formatted report string
|
| 1621 |
+
"""
|
| 1622 |
+
if format == "json":
|
| 1623 |
+
return self._generate_json_report(result)
|
| 1624 |
+
elif format == "markdown":
|
| 1625 |
+
return self._generate_markdown_report(result)
|
| 1626 |
+
else:
|
| 1627 |
+
return self._generate_text_report(result)
|
| 1628 |
+
|
| 1629 |
+
def _generate_text_report(self, result: ScanResult) -> str:
|
| 1630 |
+
"""Generate plain text report."""
|
| 1631 |
+
lines = [
|
| 1632 |
+
"=" * 70,
|
| 1633 |
+
"SECURITY SCAN REPORT",
|
| 1634 |
+
"=" * 70,
|
| 1635 |
+
"",
|
| 1636 |
+
f"Target: {result.target}",
|
| 1637 |
+
f"Scan Type: {result.scan_type.upper()}",
|
| 1638 |
+
f"Start Time: {result.start_time.strftime('%Y-%m-%d %H:%M:%S')}",
|
| 1639 |
+
f"End Time: {result.end_time.strftime('%Y-%m-%d %H:%M:%S') if result.end_time else 'N/A'}",
|
| 1640 |
+
f"Files Scanned: {result.files_scanned}",
|
| 1641 |
+
"",
|
| 1642 |
+
]
|
| 1643 |
+
|
| 1644 |
+
# Summary
|
| 1645 |
+
summary = result.summary()
|
| 1646 |
+
lines.extend([
|
| 1647 |
+
"-" * 70,
|
| 1648 |
+
"SUMMARY",
|
| 1649 |
+
"-" * 70,
|
| 1650 |
+
f"Total Vulnerabilities: {summary['total_vulnerabilities']}",
|
| 1651 |
+
"",
|
| 1652 |
+
"By Severity:",
|
| 1653 |
+
])
|
| 1654 |
+
|
| 1655 |
+
for severity, count in summary["by_severity"].items():
|
| 1656 |
+
if count > 0:
|
| 1657 |
+
lines.append(f" {severity}: {count}")
|
| 1658 |
+
|
| 1659 |
+
lines.append("")
|
| 1660 |
+
|
| 1661 |
+
if result.errors:
|
| 1662 |
+
lines.extend([
|
| 1663 |
+
"-" * 70,
|
| 1664 |
+
"ERRORS",
|
| 1665 |
+
"-" * 70,
|
| 1666 |
+
])
|
| 1667 |
+
for error in result.errors:
|
| 1668 |
+
lines.append(f" • {error}")
|
| 1669 |
+
lines.append("")
|
| 1670 |
+
|
| 1671 |
+
# Vulnerabilities by severity
|
| 1672 |
+
if result.vulnerabilities:
|
| 1673 |
+
lines.extend([
|
| 1674 |
+
"-" * 70,
|
| 1675 |
+
"DETAILED FINDINGS",
|
| 1676 |
+
"-" * 70,
|
| 1677 |
+
"",
|
| 1678 |
+
])
|
| 1679 |
+
|
| 1680 |
+
# Sort by severity
|
| 1681 |
+
severity_order = {
|
| 1682 |
+
RiskLevel.CRITICAL: 0,
|
| 1683 |
+
RiskLevel.HIGH: 1,
|
| 1684 |
+
RiskLevel.MEDIUM: 2,
|
| 1685 |
+
RiskLevel.LOW: 3,
|
| 1686 |
+
RiskLevel.INFO: 4
|
| 1687 |
+
}
|
| 1688 |
+
|
| 1689 |
+
sorted_vulns = sorted(
|
| 1690 |
+
result.vulnerabilities,
|
| 1691 |
+
key=lambda v: severity_order.get(v.risk_level, 5)
|
| 1692 |
+
)
|
| 1693 |
+
|
| 1694 |
+
for i, vuln in enumerate(sorted_vulns, 1):
|
| 1695 |
+
lines.extend([
|
| 1696 |
+
f"[{i}] {vuln.name}",
|
| 1697 |
+
f" Severity: {vuln.risk_level.value}",
|
| 1698 |
+
f" Location: {vuln.file_path}:{vuln.line_number}",
|
| 1699 |
+
f" CWE: {vuln.cwe_id or 'N/A'}",
|
| 1700 |
+
"",
|
| 1701 |
+
f" Description:",
|
| 1702 |
+
f" {vuln.description}",
|
| 1703 |
+
"",
|
| 1704 |
+
f" Code:",
|
| 1705 |
+
" " + "-" * 40,
|
| 1706 |
+
])
|
| 1707 |
+
|
| 1708 |
+
for line in vuln.code_snippet.split('\n'):
|
| 1709 |
+
lines.append(f" {line}")
|
| 1710 |
+
|
| 1711 |
+
lines.extend([
|
| 1712 |
+
" " + "-" * 40,
|
| 1713 |
+
"",
|
| 1714 |
+
f" Remediation:",
|
| 1715 |
+
])
|
| 1716 |
+
|
| 1717 |
+
if vuln.remediation and vuln.remediation != "No known solution":
|
| 1718 |
+
for line in vuln.remediation.split('\n'):
|
| 1719 |
+
lines.append(f" {line}")
|
| 1720 |
+
else:
|
| 1721 |
+
lines.append(" No known solution")
|
| 1722 |
+
|
| 1723 |
+
if vuln.cve_ids:
|
| 1724 |
+
lines.extend([
|
| 1725 |
+
"",
|
| 1726 |
+
f" Related CVEs: {', '.join(vuln.cve_ids)}",
|
| 1727 |
+
])
|
| 1728 |
+
|
| 1729 |
+
lines.extend(["", ""])
|
| 1730 |
+
|
| 1731 |
+
lines.extend([
|
| 1732 |
+
"=" * 70,
|
| 1733 |
+
"END OF REPORT",
|
| 1734 |
+
"=" * 70,
|
| 1735 |
+
])
|
| 1736 |
+
|
| 1737 |
+
return "\n".join(lines)
|
| 1738 |
+
|
| 1739 |
+
def _generate_json_report(self, result: ScanResult) -> str:
|
| 1740 |
+
"""Generate JSON report."""
|
| 1741 |
+
report = {
|
| 1742 |
+
"target": result.target,
|
| 1743 |
+
"scan_type": result.scan_type,
|
| 1744 |
+
"start_time": result.start_time.isoformat(),
|
| 1745 |
+
"end_time": result.end_time.isoformat() if result.end_time else None,
|
| 1746 |
+
"summary": result.summary(),
|
| 1747 |
+
"vulnerabilities": [v.to_dict() for v in result.vulnerabilities],
|
| 1748 |
+
"errors": result.errors
|
| 1749 |
+
}
|
| 1750 |
+
return json.dumps(report, indent=2)
|
| 1751 |
+
|
| 1752 |
+
def _generate_markdown_report(self, result: ScanResult) -> str:
|
| 1753 |
+
"""Generate Markdown report optimized for vibe-coding platforms."""
|
| 1754 |
+
summary = result.summary()
|
| 1755 |
+
severity_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]
|
| 1756 |
+
|
| 1757 |
+
lines = []
|
| 1758 |
+
lines.append("# Security Scan Report\n")
|
| 1759 |
+
lines.append(f"**Target:** `{result.target}` ")
|
| 1760 |
+
lines.append(f"**Scan Type:** {result.scan_type.upper()} ")
|
| 1761 |
+
lines.append(f"**Date:** {result.start_time.strftime('%Y-%m-%d %H:%M:%S')} ")
|
| 1762 |
+
lines.append(f"**Files Scanned:** {result.files_scanned} ")
|
| 1763 |
+
|
| 1764 |
+
# Summary line
|
| 1765 |
+
counts = []
|
| 1766 |
+
for sev in severity_order:
|
| 1767 |
+
count = summary['by_severity'].get(sev, 0)
|
| 1768 |
+
if count > 0:
|
| 1769 |
+
counts.append(f"{count} {sev.capitalize()}")
|
| 1770 |
+
total = summary['total_vulnerabilities']
|
| 1771 |
+
lines.append(f"**Total Vulnerabilities:** {total}" + (f" ({', '.join(counts)})" if counts else ""))
|
| 1772 |
+
lines.append("\n---\n")
|
| 1773 |
+
|
| 1774 |
+
if total == 0:
|
| 1775 |
+
lines.append("No vulnerabilities found.\n")
|
| 1776 |
+
return "\n".join(lines)
|
| 1777 |
+
|
| 1778 |
+
# Group vulnerabilities by severity
|
| 1779 |
+
by_severity = {}
|
| 1780 |
+
for vuln in result.vulnerabilities:
|
| 1781 |
+
sev = vuln.risk_level.value
|
| 1782 |
+
by_severity.setdefault(sev, []).append(vuln)
|
| 1783 |
+
|
| 1784 |
+
finding_num = 0
|
| 1785 |
+
for sev in severity_order:
|
| 1786 |
+
vulns = by_severity.get(sev, [])
|
| 1787 |
+
if not vulns:
|
| 1788 |
+
continue
|
| 1789 |
+
|
| 1790 |
+
lines.append(f"## {sev.capitalize()}\n")
|
| 1791 |
+
|
| 1792 |
+
for vuln in vulns:
|
| 1793 |
+
finding_num += 1
|
| 1794 |
+
cwe = f" ({vuln.cwe_id})" if vuln.cwe_id else ""
|
| 1795 |
+
lines.append(f"### {finding_num}. {vuln.name}{cwe}\n")
|
| 1796 |
+
lines.append(f"- **File:** `{vuln.file_path}:{vuln.line_number}`")
|
| 1797 |
+
lines.append(f"- **Confidence:** {vuln.confidence}")
|
| 1798 |
+
lines.append(f"- **Description:** {vuln.description}")
|
| 1799 |
+
|
| 1800 |
+
if vuln.code_snippet and vuln.code_snippet.strip():
|
| 1801 |
+
ext = os.path.splitext(vuln.file_path)[1].lstrip(".")
|
| 1802 |
+
lang = ext if ext else ""
|
| 1803 |
+
lines.append(f"- **Code:**")
|
| 1804 |
+
lines.append(f" ```{lang}")
|
| 1805 |
+
lines.append(f" {vuln.code_snippet.strip()}")
|
| 1806 |
+
lines.append(f" ```")
|
| 1807 |
+
|
| 1808 |
+
if vuln.remediation:
|
| 1809 |
+
lines.append(f"- **Remediation:** {vuln.remediation.strip()}")
|
| 1810 |
+
|
| 1811 |
+
if vuln.cve_ids:
|
| 1812 |
+
lines.append(f"- **Related CVEs:** {', '.join(vuln.cve_ids)}")
|
| 1813 |
+
|
| 1814 |
+
lines.append("")
|
| 1815 |
+
|
| 1816 |
+
lines.append("---\n")
|
| 1817 |
+
lines.append(f"*Generated by Security Auditor | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n")
|
| 1818 |
+
|
| 1819 |
+
return "\n".join(lines)
|
| 1820 |
+
|
| 1821 |
+
|
| 1822 |
+
# ============================================================
|
| 1823 |
+
# CLI Interface
|
| 1824 |
+
# ============================================================
|
| 1825 |
+
|
| 1826 |
+
async def main():
|
| 1827 |
+
"""Command-line interface for the security checker."""
|
| 1828 |
+
import argparse
|
| 1829 |
+
|
| 1830 |
+
parser = argparse.ArgumentParser(
|
| 1831 |
+
description="Security Checker - SAST and NVD-powered vulnerability scanner",
|
| 1832 |
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
| 1833 |
+
epilog="""
|
| 1834 |
+
Examples:
|
| 1835 |
+
# Scan a local project
|
| 1836 |
+
python security_checker.py /path/to/project
|
| 1837 |
+
|
| 1838 |
+
# Scan a web application
|
| 1839 |
+
python security_checker.py https://example.com --web
|
| 1840 |
+
|
| 1841 |
+
# Generate HTML report
|
| 1842 |
+
python security_checker.py /path/to/project --format html -o report.html
|
| 1843 |
+
|
| 1844 |
+
# Search NVD for SQL injection vulnerabilities
|
| 1845 |
+
python security_checker.py --nvd-search "sql injection" --severity HIGH
|
| 1846 |
+
"""
|
| 1847 |
+
)
|
| 1848 |
+
|
| 1849 |
+
parser.add_argument(
|
| 1850 |
+
"target",
|
| 1851 |
+
nargs="?",
|
| 1852 |
+
help="Target to scan (local path or URL)"
|
| 1853 |
+
)
|
| 1854 |
+
|
| 1855 |
+
parser.add_argument(
|
| 1856 |
+
"--web",
|
| 1857 |
+
action="store_true",
|
| 1858 |
+
help="Treat target as web URL"
|
| 1859 |
+
)
|
| 1860 |
+
|
| 1861 |
+
parser.add_argument(
|
| 1862 |
+
"--format",
|
| 1863 |
+
choices=["text", "json", "html"],
|
| 1864 |
+
default="text",
|
| 1865 |
+
help="Output format (default: text)"
|
| 1866 |
+
)
|
| 1867 |
+
|
| 1868 |
+
parser.add_argument(
|
| 1869 |
+
"-o", "--output",
|
| 1870 |
+
help="Output file (default: stdout)"
|
| 1871 |
+
)
|
| 1872 |
+
|
| 1873 |
+
parser.add_argument(
|
| 1874 |
+
"--nvd-api-key",
|
| 1875 |
+
help="NVD API key for higher rate limits"
|
| 1876 |
+
)
|
| 1877 |
+
|
| 1878 |
+
parser.add_argument(
|
| 1879 |
+
"--no-nvd",
|
| 1880 |
+
action="store_true",
|
| 1881 |
+
help="Skip NVD enrichment"
|
| 1882 |
+
)
|
| 1883 |
+
|
| 1884 |
+
parser.add_argument(
|
| 1885 |
+
"--nvd-search",
|
| 1886 |
+
help="Search NVD for vulnerabilities by keyword"
|
| 1887 |
+
)
|
| 1888 |
+
|
| 1889 |
+
parser.add_argument(
|
| 1890 |
+
"--severity",
|
| 1891 |
+
choices=["LOW", "MEDIUM", "HIGH", "CRITICAL"],
|
| 1892 |
+
help="Filter NVD search by severity"
|
| 1893 |
+
)
|
| 1894 |
+
|
| 1895 |
+
args = parser.parse_args()
|
| 1896 |
+
|
| 1897 |
+
# Initialize checker
|
| 1898 |
+
checker = SecurityChecker(nvd_api_key=args.nvd_api_key)
|
| 1899 |
+
|
| 1900 |
+
# NVD search mode
|
| 1901 |
+
if args.nvd_search:
|
| 1902 |
+
print(f"Searching NVD for: {args.nvd_search}")
|
| 1903 |
+
results = await checker.search_nvd(
|
| 1904 |
+
keyword=args.nvd_search,
|
| 1905 |
+
severity=args.severity
|
| 1906 |
+
)
|
| 1907 |
+
|
| 1908 |
+
if results and "error" not in results[0]:
|
| 1909 |
+
for cve in results:
|
| 1910 |
+
print(f"\n{cve['cve_id']} ({cve['severity']} - {cve['cvss_score']})")
|
| 1911 |
+
print(f" {cve['description'][:200]}...")
|
| 1912 |
+
if cve['cwes']:
|
| 1913 |
+
print(f" CWEs: {', '.join(cve['cwes'])}")
|
| 1914 |
+
else:
|
| 1915 |
+
print(f"Error: {results[0].get('error', 'Unknown error')}")
|
| 1916 |
+
return
|
| 1917 |
+
|
| 1918 |
+
# Require target for scanning
|
| 1919 |
+
if not args.target:
|
| 1920 |
+
parser.print_help()
|
| 1921 |
+
return
|
| 1922 |
+
|
| 1923 |
+
# Perform scan
|
| 1924 |
+
print(f"Scanning: {args.target}")
|
| 1925 |
+
print("This may take a moment...")
|
| 1926 |
+
|
| 1927 |
+
if args.web or args.target.startswith(('http://', 'https://')):
|
| 1928 |
+
result = await checker.scan_web(args.target, include_nvd=not args.no_nvd)
|
| 1929 |
+
else:
|
| 1930 |
+
result = await checker.scan_local(args.target, include_nvd=not args.no_nvd)
|
| 1931 |
+
|
| 1932 |
+
# Generate report
|
| 1933 |
+
report = checker.generate_report(result, format=args.format)
|
| 1934 |
+
|
| 1935 |
+
# Output
|
| 1936 |
+
if args.output:
|
| 1937 |
+
with open(args.output, 'w') as f:
|
| 1938 |
+
f.write(report)
|
| 1939 |
+
print(f"Report saved to: {args.output}")
|
| 1940 |
+
else:
|
| 1941 |
+
print(report)
|
| 1942 |
+
|
| 1943 |
+
|
| 1944 |
+
if __name__ == "__main__":
|
| 1945 |
+
asyncio.run(main())
|
theme.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Custom Gradio Theme for Security Auditor
|
| 3 |
+
Based on design mockups in assets/ folder
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
|
| 8 |
+
def create_security_auditor_theme():
|
| 9 |
+
"""
|
| 10 |
+
Create Anthropic.com-inspired theme for Security Auditor.
|
| 11 |
+
|
| 12 |
+
Color Palette:
|
| 13 |
+
- Background: Light cream #faf9f6 (Anthropic-inspired)
|
| 14 |
+
- Primary Text: Dark slate #131314
|
| 15 |
+
- Accent: Warm terracotta #d97757
|
| 16 |
+
- Cards: Pure white #ffffff
|
| 17 |
+
- Borders: Light gray #e5e7eb
|
| 18 |
+
|
| 19 |
+
Design Philosophy:
|
| 20 |
+
- Minimalist, clean aesthetic with generous whitespace
|
| 21 |
+
- Neutral palette with warm accents
|
| 22 |
+
- Subtle borders, no heavy shadows
|
| 23 |
+
- Accessibility-focused
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
theme = gr.themes.Soft(
|
| 27 |
+
primary_hue=gr.themes.colors.orange,
|
| 28 |
+
secondary_hue=gr.themes.colors.neutral,
|
| 29 |
+
neutral_hue=gr.themes.colors.neutral,
|
| 30 |
+
font=[
|
| 31 |
+
"system-ui",
|
| 32 |
+
"-apple-system",
|
| 33 |
+
"BlinkMacSystemFont",
|
| 34 |
+
"Segoe UI",
|
| 35 |
+
"Roboto",
|
| 36 |
+
"sans-serif"
|
| 37 |
+
],
|
| 38 |
+
font_mono=[
|
| 39 |
+
"ui-monospace",
|
| 40 |
+
"SF Mono",
|
| 41 |
+
"Monaco",
|
| 42 |
+
"Cascadia Code",
|
| 43 |
+
"monospace"
|
| 44 |
+
]
|
| 45 |
+
).set(
|
| 46 |
+
# Background colors
|
| 47 |
+
body_background_fill="#faf9f6", # Light cream
|
| 48 |
+
body_background_fill_dark="#131314", # Dark mode fallback
|
| 49 |
+
|
| 50 |
+
# Block/Card styling
|
| 51 |
+
block_background_fill="#ffffff",
|
| 52 |
+
block_border_width="1px",
|
| 53 |
+
block_border_color="#e5e7eb",
|
| 54 |
+
block_shadow="none", # Minimal shadows
|
| 55 |
+
block_radius="8px", # Smaller radius for cleaner look
|
| 56 |
+
|
| 57 |
+
# Button styling - Anthropic style
|
| 58 |
+
button_primary_background_fill="#d97757", # Warm terracotta
|
| 59 |
+
button_primary_background_fill_hover="#cc6944", # Darker terracotta
|
| 60 |
+
button_primary_text_color="#ffffff",
|
| 61 |
+
button_primary_border_color="#d97757",
|
| 62 |
+
|
| 63 |
+
button_secondary_background_fill="transparent", # Minimal secondary
|
| 64 |
+
button_secondary_background_fill_hover="rgba(217, 119, 87, 0.1)",
|
| 65 |
+
button_secondary_text_color="#131314",
|
| 66 |
+
button_secondary_border_color="#e5e7eb",
|
| 67 |
+
|
| 68 |
+
# Input styling
|
| 69 |
+
input_background_fill="#ffffff",
|
| 70 |
+
input_border_width="1px",
|
| 71 |
+
input_border_color="#e5e7eb",
|
| 72 |
+
input_shadow="none",
|
| 73 |
+
input_radius="6px",
|
| 74 |
+
|
| 75 |
+
# Text colors
|
| 76 |
+
body_text_color="#131314", # Dark slate
|
| 77 |
+
body_text_color_subdued="#6b7280", # Medium gray
|
| 78 |
+
|
| 79 |
+
# Link colors
|
| 80 |
+
link_text_color="#d97757",
|
| 81 |
+
link_text_color_hover="#cc6944"
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
return theme
|
ui_components.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
UI Components for Security Auditor Gradio Interface
|
| 3 |
+
Generates HTML for badges, cards, and sections matching design mockups
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Dict, List
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def create_severity_badge(severity: str, count: int = 0) -> str:
|
| 10 |
+
"""
|
| 11 |
+
Create severity badge as a visual indicator.
|
| 12 |
+
|
| 13 |
+
Args:
|
| 14 |
+
severity: CRITICAL, HIGH, MEDIUM, LOW, INFO
|
| 15 |
+
count: Number of findings
|
| 16 |
+
|
| 17 |
+
Returns:
|
| 18 |
+
HTML string for severity badge
|
| 19 |
+
"""
|
| 20 |
+
colors = {
|
| 21 |
+
"CRITICAL": {"bg": "#fef2f2", "text": "#dc2626", "border": "#fca5a5"},
|
| 22 |
+
"HIGH": {"bg": "#fff7ed", "text": "#ea580c", "border": "#fdba74"},
|
| 23 |
+
"MEDIUM": {"bg": "#fffbeb", "text": "#d97706", "border": "#fcd34d"},
|
| 24 |
+
"LOW": {"bg": "#f0fdfa", "text": "#0d9488", "border": "#5eead4"},
|
| 25 |
+
"INFO": {"bg": "#f9fafb", "text": "#6b7280", "border": "#d1d5db"}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
color = colors.get(severity, colors["INFO"])
|
| 29 |
+
|
| 30 |
+
return f"""
|
| 31 |
+
<div
|
| 32 |
+
class="severity-badge"
|
| 33 |
+
data-severity="{severity}"
|
| 34 |
+
style="
|
| 35 |
+
display: inline-flex;
|
| 36 |
+
flex-direction: column;
|
| 37 |
+
align-items: center;
|
| 38 |
+
padding: 16px 24px;
|
| 39 |
+
background: {color['bg']};
|
| 40 |
+
border: 2px solid {color['border']};
|
| 41 |
+
border-radius: 6px;
|
| 42 |
+
min-width: 100px;
|
| 43 |
+
"
|
| 44 |
+
>
|
| 45 |
+
<div style="
|
| 46 |
+
font-size: 11px;
|
| 47 |
+
font-weight: 600;
|
| 48 |
+
color: {color['text']};
|
| 49 |
+
text-transform: capitalize;
|
| 50 |
+
letter-spacing: 0.05em;
|
| 51 |
+
margin-bottom: 8px;
|
| 52 |
+
">{severity.capitalize()}</div>
|
| 53 |
+
<div style="
|
| 54 |
+
font-size: 32px;
|
| 55 |
+
font-weight: 700;
|
| 56 |
+
color: {color['text']};
|
| 57 |
+
">{count}</div>
|
| 58 |
+
</div>
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def create_finding_card(vulnerability: Dict) -> str:
|
| 63 |
+
"""
|
| 64 |
+
Create HTML for vulnerability finding card.
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
vulnerability: Dict with name, severity, file_path, line_number,
|
| 68 |
+
description, cwe_id, cve_ids, remediation
|
| 69 |
+
|
| 70 |
+
Returns:
|
| 71 |
+
HTML string for finding card
|
| 72 |
+
"""
|
| 73 |
+
severity_colors = {
|
| 74 |
+
"CRITICAL": "#dc2626",
|
| 75 |
+
"HIGH": "#ea580c",
|
| 76 |
+
"MEDIUM": "#d97706",
|
| 77 |
+
"LOW": "#0d9488",
|
| 78 |
+
"INFO": "#6b7280"
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
severity_bg = {
|
| 82 |
+
"CRITICAL": "#fef2f2",
|
| 83 |
+
"HIGH": "#fff7ed",
|
| 84 |
+
"MEDIUM": "#fffbeb",
|
| 85 |
+
"LOW": "#f0fdfa",
|
| 86 |
+
"INFO": "#f9fafb"
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
severity = vulnerability.get('risk_level', 'INFO')
|
| 90 |
+
color = severity_colors.get(severity, severity_colors['INFO'])
|
| 91 |
+
bg = severity_bg.get(severity, severity_bg['INFO'])
|
| 92 |
+
|
| 93 |
+
# Build CWE/CVE tags
|
| 94 |
+
tags_html = ""
|
| 95 |
+
if vulnerability.get('cwe_id'):
|
| 96 |
+
tags_html += f"""
|
| 97 |
+
<span style="
|
| 98 |
+
display: inline-block;
|
| 99 |
+
padding: 4px 12px;
|
| 100 |
+
background: #f3f4f6;
|
| 101 |
+
color: #4b5563;
|
| 102 |
+
border-radius: 6px;
|
| 103 |
+
font-size: 12px;
|
| 104 |
+
font-weight: 500;
|
| 105 |
+
margin-right: 8px;
|
| 106 |
+
">{vulnerability['cwe_id']}</span>
|
| 107 |
+
"""
|
| 108 |
+
|
| 109 |
+
for cve in vulnerability.get('cve_ids', [])[:3]: # Show max 3 CVEs
|
| 110 |
+
tags_html += f"""
|
| 111 |
+
<span style="
|
| 112 |
+
display: inline-block;
|
| 113 |
+
padding: 4px 12px;
|
| 114 |
+
background: #fef2f2;
|
| 115 |
+
color: #dc2626;
|
| 116 |
+
border-radius: 6px;
|
| 117 |
+
font-size: 12px;
|
| 118 |
+
font-weight: 500;
|
| 119 |
+
margin-right: 8px;
|
| 120 |
+
">{cve}</span>
|
| 121 |
+
"""
|
| 122 |
+
|
| 123 |
+
# Build remediation section
|
| 124 |
+
remediation_html = ""
|
| 125 |
+
if vulnerability.get('remediation'):
|
| 126 |
+
# Truncate very long remediation text
|
| 127 |
+
remediation_text = vulnerability['remediation']
|
| 128 |
+
if len(remediation_text) > 500:
|
| 129 |
+
remediation_text = remediation_text[:500] + "..."
|
| 130 |
+
|
| 131 |
+
remediation_html = f"""
|
| 132 |
+
<details style="margin-top: 16px;" class="remediation-details">
|
| 133 |
+
<summary style="
|
| 134 |
+
cursor: pointer;
|
| 135 |
+
padding: 12px 16px;
|
| 136 |
+
background: #f9fafb;
|
| 137 |
+
border-radius: 8px;
|
| 138 |
+
font-weight: 600;
|
| 139 |
+
color: #374151;
|
| 140 |
+
user-select: none;
|
| 141 |
+
display: flex;
|
| 142 |
+
align-items: center;
|
| 143 |
+
justify-content: space-between;
|
| 144 |
+
gap: 8px;
|
| 145 |
+
transition: background 0.2s ease;
|
| 146 |
+
" onmouseover="this.style.background='#f3f4f6'" onmouseout="this.style.background='#f9fafb'">
|
| 147 |
+
<div style="display: flex; align-items: center; gap: 8px;">
|
| 148 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
| 149 |
+
fill="none" stroke="#d97757" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 150 |
+
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
| 151 |
+
</svg>
|
| 152 |
+
<span>Remediation Guidance</span>
|
| 153 |
+
</div>
|
| 154 |
+
<svg class="chevron-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
|
| 155 |
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 156 |
+
style="transition: transform 0.2s ease;">
|
| 157 |
+
<polyline points="6 9 12 15 18 9"></polyline>
|
| 158 |
+
</svg>
|
| 159 |
+
</summary>
|
| 160 |
+
<style>
|
| 161 |
+
details[open] .chevron-icon {{
|
| 162 |
+
transform: rotate(180deg);
|
| 163 |
+
}}
|
| 164 |
+
</style>
|
| 165 |
+
<div style="
|
| 166 |
+
padding: 16px;
|
| 167 |
+
margin-top: 8px;
|
| 168 |
+
background: #f0fdf4;
|
| 169 |
+
border-left: 4px solid #10b981;
|
| 170 |
+
border-radius: 8px;
|
| 171 |
+
">
|
| 172 |
+
<pre style="
|
| 173 |
+
background: white;
|
| 174 |
+
padding: 12px;
|
| 175 |
+
border-radius: 6px;
|
| 176 |
+
overflow-x: auto;
|
| 177 |
+
font-size: 13px;
|
| 178 |
+
line-height: 1.5;
|
| 179 |
+
color: #1f2937;
|
| 180 |
+
white-space: pre-wrap;
|
| 181 |
+
word-wrap: break-word;
|
| 182 |
+
">{remediation_text}</pre>
|
| 183 |
+
</div>
|
| 184 |
+
</details>
|
| 185 |
+
"""
|
| 186 |
+
|
| 187 |
+
return f"""
|
| 188 |
+
<div class="finding-card" data-severity="{severity}" style="
|
| 189 |
+
background: white;
|
| 190 |
+
border: 1px solid #e5e7eb;
|
| 191 |
+
border-radius: 12px;
|
| 192 |
+
padding: 20px;
|
| 193 |
+
margin-bottom: 16px;
|
| 194 |
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
| 195 |
+
">
|
| 196 |
+
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
|
| 197 |
+
<h3 style="
|
| 198 |
+
margin: 0;
|
| 199 |
+
font-size: 18px;
|
| 200 |
+
font-weight: 600;
|
| 201 |
+
color: #111827;
|
| 202 |
+
">{vulnerability.get('name', 'Unknown Vulnerability')}</h3>
|
| 203 |
+
<span style="
|
| 204 |
+
padding: 6px 12px;
|
| 205 |
+
background: {bg};
|
| 206 |
+
color: {color};
|
| 207 |
+
border-radius: 6px;
|
| 208 |
+
font-size: 12px;
|
| 209 |
+
font-weight: 600;
|
| 210 |
+
text-transform: uppercase;
|
| 211 |
+
">{severity}</span>
|
| 212 |
+
</div>
|
| 213 |
+
|
| 214 |
+
<div style="margin-bottom: 12px;">
|
| 215 |
+
{tags_html}
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<div style="
|
| 219 |
+
display: flex;
|
| 220 |
+
align-items: center;
|
| 221 |
+
gap: 8px;
|
| 222 |
+
margin-bottom: 12px;
|
| 223 |
+
color: #6b7280;
|
| 224 |
+
font-size: 14px;
|
| 225 |
+
">
|
| 226 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
|
| 227 |
+
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 228 |
+
style="flex-shrink: 0;">
|
| 229 |
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
| 230 |
+
<polyline points="14 2 14 8 20 8"></polyline>
|
| 231 |
+
</svg>
|
| 232 |
+
<span style="font-family: 'Fira Code', monospace; color: #374151;">{vulnerability.get('file_path', 'N/A')}:{vulnerability.get('line_number', 'N/A')}</span>
|
| 233 |
+
</div>
|
| 234 |
+
|
| 235 |
+
<p style="
|
| 236 |
+
margin: 0 0 16px 0;
|
| 237 |
+
color: #4b5563;
|
| 238 |
+
line-height: 1.6;
|
| 239 |
+
font-size: 14px;
|
| 240 |
+
">{vulnerability.get('description', '')}</p>
|
| 241 |
+
|
| 242 |
+
{remediation_html}
|
| 243 |
+
</div>
|
| 244 |
+
"""
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def create_summary_section(scan_result: Dict) -> str:
|
| 248 |
+
"""
|
| 249 |
+
Create Analysis Summary section with proper alignment and sentence capitalization.
|
| 250 |
+
|
| 251 |
+
Args:
|
| 252 |
+
scan_result: Dict with target, files_scanned, scan_type, summary data
|
| 253 |
+
|
| 254 |
+
Returns:
|
| 255 |
+
HTML string for summary section
|
| 256 |
+
"""
|
| 257 |
+
summary = scan_result.get('summary', {})
|
| 258 |
+
|
| 259 |
+
# Metadata row - Fixed alignment with grid layout
|
| 260 |
+
metadata_html = f"""
|
| 261 |
+
<div style="
|
| 262 |
+
display: grid;
|
| 263 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 264 |
+
gap: 24px;
|
| 265 |
+
margin-bottom: 24px;
|
| 266 |
+
padding: 16px;
|
| 267 |
+
background: #ffffff;
|
| 268 |
+
border: 1px solid #e5e7eb;
|
| 269 |
+
border-radius: 6px;
|
| 270 |
+
">
|
| 271 |
+
<div style="display: flex; flex-direction: column; justify-content: center;">
|
| 272 |
+
<div style="
|
| 273 |
+
font-size: 12px;
|
| 274 |
+
font-weight: 600;
|
| 275 |
+
color: #6b7280;
|
| 276 |
+
margin-bottom: 8px;
|
| 277 |
+
line-height: 1.2;
|
| 278 |
+
">Target</div>
|
| 279 |
+
<div style="
|
| 280 |
+
font-size: 14px;
|
| 281 |
+
font-weight: 600;
|
| 282 |
+
color: #131314;
|
| 283 |
+
font-family: ui-monospace, monospace;
|
| 284 |
+
white-space: nowrap;
|
| 285 |
+
overflow: hidden;
|
| 286 |
+
text-overflow: ellipsis;
|
| 287 |
+
line-height: 1.4;
|
| 288 |
+
" title="{scan_result.get('target', 'N/A')}">{scan_result.get('target', 'N/A')}</div>
|
| 289 |
+
</div>
|
| 290 |
+
|
| 291 |
+
<div style="display: flex; flex-direction: column; justify-content: center;">
|
| 292 |
+
<div style="
|
| 293 |
+
font-size: 12px;
|
| 294 |
+
font-weight: 600;
|
| 295 |
+
color: #6b7280;
|
| 296 |
+
margin-bottom: 8px;
|
| 297 |
+
line-height: 1.2;
|
| 298 |
+
">Files analyzed</div>
|
| 299 |
+
<div style="
|
| 300 |
+
font-size: 24px;
|
| 301 |
+
font-weight: 700;
|
| 302 |
+
color: #131314;
|
| 303 |
+
line-height: 1.2;
|
| 304 |
+
">{scan_result.get('files_scanned', 0)}</div>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<div style="display: flex; flex-direction: column; justify-content: center;">
|
| 308 |
+
<div style="
|
| 309 |
+
font-size: 12px;
|
| 310 |
+
font-weight: 600;
|
| 311 |
+
color: #6b7280;
|
| 312 |
+
margin-bottom: 8px;
|
| 313 |
+
line-height: 1.2;
|
| 314 |
+
">Total findings</div>
|
| 315 |
+
<div style="
|
| 316 |
+
font-size: 28px;
|
| 317 |
+
font-weight: 700;
|
| 318 |
+
color: #dc2626;
|
| 319 |
+
line-height: 1.2;
|
| 320 |
+
">{summary.get('total_vulnerabilities', 0)}</div>
|
| 321 |
+
</div>
|
| 322 |
+
|
| 323 |
+
<div style="display: flex; flex-direction: column; justify-content: center;">
|
| 324 |
+
<div style="
|
| 325 |
+
font-size: 12px;
|
| 326 |
+
font-weight: 600;
|
| 327 |
+
color: #6b7280;
|
| 328 |
+
margin-bottom: 8px;
|
| 329 |
+
line-height: 1.2;
|
| 330 |
+
">Analysis type</div>
|
| 331 |
+
<div style="
|
| 332 |
+
font-size: 14px;
|
| 333 |
+
font-weight: 700;
|
| 334 |
+
color: #131314;
|
| 335 |
+
text-transform: capitalize;
|
| 336 |
+
line-height: 1.2;
|
| 337 |
+
">{scan_result.get('scan_type', 'local')}</div>
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
+
"""
|
| 341 |
+
|
| 342 |
+
# Severity badges row
|
| 343 |
+
by_severity = summary.get('by_severity', {})
|
| 344 |
+
badges_html = f"""
|
| 345 |
+
<div style="
|
| 346 |
+
display: flex;
|
| 347 |
+
gap: 16px;
|
| 348 |
+
flex-wrap: wrap;
|
| 349 |
+
">
|
| 350 |
+
{create_severity_badge('CRITICAL', by_severity.get('CRITICAL', 0))}
|
| 351 |
+
{create_severity_badge('HIGH', by_severity.get('HIGH', 0))}
|
| 352 |
+
{create_severity_badge('MEDIUM', by_severity.get('MEDIUM', 0))}
|
| 353 |
+
{create_severity_badge('LOW', by_severity.get('LOW', 0))}
|
| 354 |
+
{create_severity_badge('INFO', by_severity.get('INFO', 0))}
|
| 355 |
+
</div>
|
| 356 |
+
"""
|
| 357 |
+
|
| 358 |
+
return f"""
|
| 359 |
+
<div style="
|
| 360 |
+
background: white;
|
| 361 |
+
border: 1px solid #e5e7eb;
|
| 362 |
+
border-radius: 12px;
|
| 363 |
+
padding: 24px;
|
| 364 |
+
margin-bottom: 12px;
|
| 365 |
+
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
|
| 366 |
+
">
|
| 367 |
+
<h2 style="
|
| 368 |
+
margin: 0 0 20px 0;
|
| 369 |
+
font-size: 20px;
|
| 370 |
+
font-weight: 700;
|
| 371 |
+
color: #131314;
|
| 372 |
+
display: flex;
|
| 373 |
+
align-items: center;
|
| 374 |
+
gap: 8px;
|
| 375 |
+
">
|
| 376 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24"
|
| 377 |
+
fill="none" stroke="#d97757" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 378 |
+
<path d="M9 11l3 3L22 4"></path>
|
| 379 |
+
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
| 380 |
+
</svg>
|
| 381 |
+
Analysis Summary
|
| 382 |
+
</h2>
|
| 383 |
+
|
| 384 |
+
{metadata_html}
|
| 385 |
+
{badges_html}
|
| 386 |
+
</div>
|
| 387 |
+
"""
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
def create_empty_state() -> str:
|
| 391 |
+
"""
|
| 392 |
+
Create HTML for empty state (no results yet).
|
| 393 |
+
|
| 394 |
+
Returns:
|
| 395 |
+
HTML string for empty state
|
| 396 |
+
"""
|
| 397 |
+
return """
|
| 398 |
+
<div style="
|
| 399 |
+
background: white;
|
| 400 |
+
border: 2px dashed #e5e7eb;
|
| 401 |
+
border-radius: 12px;
|
| 402 |
+
padding: 60px 40px;
|
| 403 |
+
text-align: center;
|
| 404 |
+
margin: 40px 0;
|
| 405 |
+
">
|
| 406 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"
|
| 407 |
+
fill="none" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
| 408 |
+
style="margin: 0 auto 16px; display: block;">
|
| 409 |
+
<circle cx="11" cy="11" r="8"></circle>
|
| 410 |
+
<path d="m21 21-4.35-4.35"></path>
|
| 411 |
+
</svg>
|
| 412 |
+
<h3 style="
|
| 413 |
+
margin: 0 0 8px 0;
|
| 414 |
+
font-size: 20px;
|
| 415 |
+
font-weight: 600;
|
| 416 |
+
color: #131314;
|
| 417 |
+
">Ready to Scan</h3>
|
| 418 |
+
<p style="
|
| 419 |
+
margin: 0;
|
| 420 |
+
color: #6b7280;
|
| 421 |
+
font-size: 14px;
|
| 422 |
+
">Upload files or enter a URL to begin security analysis</p>
|
| 423 |
+
</div>
|
| 424 |
+
"""
|
| 425 |
+
|
| 426 |
+
|
| 427 |
+
def create_loading_state(message: str = "Scanning...") -> str:
|
| 428 |
+
"""
|
| 429 |
+
Create HTML for loading state.
|
| 430 |
+
|
| 431 |
+
Args:
|
| 432 |
+
message: Loading message to display
|
| 433 |
+
|
| 434 |
+
Returns:
|
| 435 |
+
HTML string for loading state
|
| 436 |
+
"""
|
| 437 |
+
return f"""
|
| 438 |
+
<div style="
|
| 439 |
+
background: white;
|
| 440 |
+
border: 1px solid #e5e7eb;
|
| 441 |
+
border-radius: 12px;
|
| 442 |
+
padding: 40px;
|
| 443 |
+
text-align: center;
|
| 444 |
+
margin: 40px 0;
|
| 445 |
+
">
|
| 446 |
+
<div style="
|
| 447 |
+
display: inline-block;
|
| 448 |
+
width: 40px;
|
| 449 |
+
height: 40px;
|
| 450 |
+
border: 4px solid #f3f4f6;
|
| 451 |
+
border-top-color: #f59e0b;
|
| 452 |
+
border-radius: 50%;
|
| 453 |
+
animation: spin 1s linear infinite;
|
| 454 |
+
margin-bottom: 16px;
|
| 455 |
+
"></div>
|
| 456 |
+
<style>
|
| 457 |
+
@keyframes spin {{
|
| 458 |
+
0% {{ transform: rotate(0deg); }}
|
| 459 |
+
100% {{ transform: rotate(360deg); }}
|
| 460 |
+
}}
|
| 461 |
+
</style>
|
| 462 |
+
<h3 style="
|
| 463 |
+
margin: 0 0 8px 0;
|
| 464 |
+
font-size: 18px;
|
| 465 |
+
font-weight: 600;
|
| 466 |
+
color: #111827;
|
| 467 |
+
">{message}</h3>
|
| 468 |
+
<p style="
|
| 469 |
+
margin: 0;
|
| 470 |
+
color: #6b7280;
|
| 471 |
+
font-size: 14px;
|
| 472 |
+
">This may take a few moments...</p>
|
| 473 |
+
</div>
|
| 474 |
+
"""
|