more
This commit is contained in:
parent
a9a9ed48f1
commit
f92406356f
|
|
@ -0,0 +1,49 @@
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Django
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
media/
|
||||||
|
staticfiles/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
*.env.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Coverage
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
|
@ -0,0 +1,429 @@
|
||||||
|
# Secure Web Analyzer
|
||||||
|
|
||||||
|
A comprehensive web application security and performance analyzer built with Django, Celery, and modern scanning tools.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Performance Analysis**: Uses Google Lighthouse for Core Web Vitals and performance metrics
|
||||||
|
- **Security Scanning**: Integrates OWASP ZAP for vulnerability detection
|
||||||
|
- **Browser Analysis**: Playwright-based console error and network analysis
|
||||||
|
- **Header Security**: Checks HTTP security headers and TLS configuration
|
||||||
|
- **Async Processing**: Celery workers for background scan processing
|
||||||
|
- **REST API**: Full API access to all scanning functionality
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Frontend (Templates) │
|
||||||
|
│ Tailwind CSS + Alpine.js + Chart.js │
|
||||||
|
└────────────────────────────┬────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌────────────────────────────▼────────────────────────────────────┐
|
||||||
|
│ Django REST Framework │
|
||||||
|
│ /api/scans, /api/websites │
|
||||||
|
└────────────────────────────┬────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────┼──────────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌─────────▼─────────┐ ┌──────▼──────┐ ┌────────▼────────┐
|
||||||
|
│ PostgreSQL DB │ │ Redis │ │ Celery Worker │
|
||||||
|
│ Scans, Issues, │ │Message Queue│ │ Background │
|
||||||
|
│ Metrics │ │ │ │ Processing │
|
||||||
|
└───────────────────┘ └─────────────┘ └────────┬────────┘
|
||||||
|
│
|
||||||
|
┌──────────────────────────────────────┼──────────────────────┐
|
||||||
|
│ │ │
|
||||||
|
┌───────▼───────┐ ┌─────────────────┐ ┌────────▼────────┐ ┌──────────▼──────────┐
|
||||||
|
│ Lighthouse │ │ OWASP ZAP │ │ Playwright │ │ Headers Scanner │
|
||||||
|
│ (Node.js) │ │ (Docker) │ │ (Python) │ │ (requests/ssl) │
|
||||||
|
│ Port 3001 │ │ Port 8081 │ │ │ │ │
|
||||||
|
└───────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker & Docker Compose
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### 1. Clone and Configure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd secure-web
|
||||||
|
|
||||||
|
# Copy environment file
|
||||||
|
cp backend/.env.example backend/.env
|
||||||
|
|
||||||
|
# Edit .env with your settings (optional for development)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start the Stack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start all services
|
||||||
|
docker-compose up --build -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Check service status
|
||||||
|
docker-compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Initialize Database
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run migrations
|
||||||
|
docker-compose exec web python manage.py migrate
|
||||||
|
|
||||||
|
# Create superuser (optional)
|
||||||
|
docker-compose exec web python manage.py createsuperuser
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Access the Application
|
||||||
|
|
||||||
|
- **Web Interface**: http://localhost:8000
|
||||||
|
- **Admin Panel**: http://localhost:8000/admin
|
||||||
|
- **API Documentation**: http://localhost:8000/api/
|
||||||
|
|
||||||
|
## Running a Scan
|
||||||
|
|
||||||
|
### Via Web Interface
|
||||||
|
|
||||||
|
1. Navigate to http://localhost:8000
|
||||||
|
2. Enter a URL in the input field (e.g., `https://example.com`)
|
||||||
|
3. Click "Scan Website"
|
||||||
|
4. Wait for the scan to complete (typically 1-3 minutes)
|
||||||
|
5. View results including scores, metrics, and issues
|
||||||
|
|
||||||
|
### Via API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a new scan
|
||||||
|
curl -X POST http://localhost:8000/api/scans/ \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"url": "https://example.com"}'
|
||||||
|
|
||||||
|
# Response:
|
||||||
|
# {
|
||||||
|
# "id": "uuid-here",
|
||||||
|
# "url": "https://example.com",
|
||||||
|
# "status": "pending",
|
||||||
|
# ...
|
||||||
|
# }
|
||||||
|
|
||||||
|
# Check scan status
|
||||||
|
curl http://localhost:8000/api/scans/{scan-id}/
|
||||||
|
|
||||||
|
# List all scans
|
||||||
|
curl http://localhost:8000/api/scans/
|
||||||
|
|
||||||
|
# Get issues for a scan
|
||||||
|
curl "http://localhost:8000/api/issues/?scan={scan-id}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/scans/` | List all scans |
|
||||||
|
| POST | `/api/scans/` | Create new scan |
|
||||||
|
| GET | `/api/scans/{id}/` | Get scan details |
|
||||||
|
| GET | `/api/websites/` | List all websites |
|
||||||
|
| GET | `/api/issues/` | List all issues |
|
||||||
|
| GET | `/api/issues/?scan={id}` | Issues for specific scan |
|
||||||
|
| GET | `/api/issues/?severity=high` | Filter by severity |
|
||||||
|
|
||||||
|
## Scanner Integration
|
||||||
|
|
||||||
|
### Lighthouse (Performance)
|
||||||
|
|
||||||
|
The Lighthouse scanner runs as a separate Node.js service and provides:
|
||||||
|
- **Performance Score**: Overall performance rating
|
||||||
|
- **Core Web Vitals**: FCP, LCP, CLS, TTI, TBT
|
||||||
|
- **Resource Analysis**: Unused JS, render-blocking resources
|
||||||
|
- **Best Practices**: Modern web development compliance
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Internal service call
|
||||||
|
POST http://lighthouse:3001/scan
|
||||||
|
{
|
||||||
|
"url": "https://example.com",
|
||||||
|
"options": {
|
||||||
|
"preset": "desktop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### OWASP ZAP (Security)
|
||||||
|
|
||||||
|
ZAP performs active security scanning:
|
||||||
|
- **Spider Crawling**: Discovers URLs and entry points
|
||||||
|
- **Passive Scanning**: Analyzes responses for vulnerabilities
|
||||||
|
- **Alert Detection**: XSS, injection, misconfigurations
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ZAP API endpoints used
|
||||||
|
GET http://zap:8081/JSON/spider/action/scan/
|
||||||
|
GET http://zap:8081/JSON/pscan/view/recordsToScan/
|
||||||
|
GET http://zap:8081/JSON/core/view/alerts/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Playwright (Browser Analysis)
|
||||||
|
|
||||||
|
Playwright performs real browser analysis:
|
||||||
|
- **Console Errors**: JavaScript errors and warnings
|
||||||
|
- **Network Metrics**: Response times, failed requests
|
||||||
|
- **Memory Metrics**: JS heap size monitoring
|
||||||
|
- **Resource Loading**: Images, scripts, stylesheets
|
||||||
|
|
||||||
|
### Headers Scanner (HTTP Security)
|
||||||
|
|
||||||
|
Checks security headers and TLS configuration:
|
||||||
|
- **Security Headers**: CSP, HSTS, X-Frame-Options, etc.
|
||||||
|
- **Cookie Security**: Secure, HttpOnly, SameSite flags
|
||||||
|
- **TLS Certificate**: Validity, expiration, issuer
|
||||||
|
- **Information Disclosure**: Server version headers
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Django
|
||||||
|
SECRET_KEY=your-secret-key
|
||||||
|
DEBUG=True
|
||||||
|
ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DATABASE_URL=postgres://user:pass@db:5432/secure_web
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://redis:6379/0
|
||||||
|
CELERY_BROKER_URL=redis://redis:6379/0
|
||||||
|
|
||||||
|
# Scanner Services
|
||||||
|
LIGHTHOUSE_URL=http://lighthouse:3001
|
||||||
|
ZAP_API_URL=http://zap:8081
|
||||||
|
ZAP_API_KEY=changeme
|
||||||
|
|
||||||
|
# Scanner Timeouts
|
||||||
|
LIGHTHOUSE_TIMEOUT=120
|
||||||
|
ZAP_TIMEOUT=300
|
||||||
|
PLAYWRIGHT_TIMEOUT=60
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scanner Configuration
|
||||||
|
|
||||||
|
Modify `backend/core/settings.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
SCANNER_CONFIG = {
|
||||||
|
'lighthouse': {
|
||||||
|
'url': os.getenv('LIGHTHOUSE_URL', 'http://lighthouse:3001'),
|
||||||
|
'timeout': int(os.getenv('LIGHTHOUSE_TIMEOUT', '120')),
|
||||||
|
'preset': 'desktop', # or 'mobile'
|
||||||
|
},
|
||||||
|
'zap': {
|
||||||
|
'url': os.getenv('ZAP_API_URL', 'http://zap:8081'),
|
||||||
|
'api_key': os.getenv('ZAP_API_KEY', 'changeme'),
|
||||||
|
'timeout': int(os.getenv('ZAP_TIMEOUT', '300')),
|
||||||
|
'spider_max_depth': 3,
|
||||||
|
},
|
||||||
|
'playwright': {
|
||||||
|
'timeout': int(os.getenv('PLAYWRIGHT_TIMEOUT', '60')),
|
||||||
|
'viewport': {'width': 1920, 'height': 1080},
|
||||||
|
},
|
||||||
|
'headers': {
|
||||||
|
'timeout': 30,
|
||||||
|
'verify_ssl': True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running Locally (without Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend setup
|
||||||
|
cd backend
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Set environment
|
||||||
|
export DATABASE_URL=postgres://user:pass@localhost:5432/secure_web
|
||||||
|
export REDIS_URL=redis://localhost:6379/0
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
# Start Django
|
||||||
|
python manage.py runserver
|
||||||
|
|
||||||
|
# Start Celery (separate terminal)
|
||||||
|
celery -A core worker -l INFO
|
||||||
|
|
||||||
|
# Start Celery Beat (separate terminal)
|
||||||
|
celery -A core beat -l INFO
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
docker-compose exec web pytest
|
||||||
|
|
||||||
|
# Run specific test file
|
||||||
|
docker-compose exec web pytest tests/test_validators.py -v
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
docker-compose exec web pytest --cov=. --cov-report=html
|
||||||
|
|
||||||
|
# Local testing
|
||||||
|
cd backend
|
||||||
|
pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
secure-web/
|
||||||
|
├── backend/
|
||||||
|
│ ├── core/ # Django project settings
|
||||||
|
│ │ ├── settings.py
|
||||||
|
│ │ ├── urls.py
|
||||||
|
│ │ ├── celery.py
|
||||||
|
│ │ └── wsgi.py
|
||||||
|
│ ├── websites/ # Main app - models
|
||||||
|
│ │ ├── models.py # Website, Scan, Issue, Metric
|
||||||
|
│ │ └── admin.py
|
||||||
|
│ ├── api/ # DRF API
|
||||||
|
│ │ ├── views.py
|
||||||
|
│ │ ├── serializers.py
|
||||||
|
│ │ └── urls.py
|
||||||
|
│ ├── scanner/ # Scanner modules
|
||||||
|
│ │ ├── base.py # BaseScanner ABC
|
||||||
|
│ │ ├── validators.py # URL validation, SSRF protection
|
||||||
|
│ │ ├── headers_scanner.py
|
||||||
|
│ │ ├── lighthouse_scanner.py
|
||||||
|
│ │ ├── playwright_scanner.py
|
||||||
|
│ │ ├── zap_scanner.py
|
||||||
|
│ │ ├── runner.py # Orchestrator
|
||||||
|
│ │ └── tasks.py # Celery tasks
|
||||||
|
│ ├── templates/ # Frontend templates
|
||||||
|
│ │ ├── base.html
|
||||||
|
│ │ ├── index.html
|
||||||
|
│ │ └── scan_detail.html
|
||||||
|
│ └── tests/ # Unit tests
|
||||||
|
│ ├── test_validators.py
|
||||||
|
│ ├── test_scans.py
|
||||||
|
│ └── test_scanner_parsing.py
|
||||||
|
├── lighthouse/ # Lighthouse Node.js service
|
||||||
|
│ ├── server.js
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── Dockerfile
|
||||||
|
└── docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Issue Categories
|
||||||
|
|
||||||
|
| Category | Source | Description |
|
||||||
|
|----------|--------|-------------|
|
||||||
|
| `performance` | Lighthouse | Speed, loading, rendering issues |
|
||||||
|
| `security` | ZAP, Headers | Vulnerabilities, misconfigurations |
|
||||||
|
| `accessibility` | Lighthouse | WCAG compliance issues |
|
||||||
|
| `seo` | Lighthouse | Search optimization issues |
|
||||||
|
| `best_practices` | Lighthouse | Modern web standards |
|
||||||
|
| `console_errors` | Playwright | JavaScript runtime errors |
|
||||||
|
| `network` | Playwright | Failed requests, slow responses |
|
||||||
|
| `headers` | Headers | Missing security headers |
|
||||||
|
| `tls` | Headers | Certificate issues |
|
||||||
|
| `cookies` | Headers | Insecure cookie settings |
|
||||||
|
|
||||||
|
## Issue Severities
|
||||||
|
|
||||||
|
| Level | Color | Description |
|
||||||
|
|-------|-------|-------------|
|
||||||
|
| `critical` | Red | Immediate action required |
|
||||||
|
| `high` | Orange | Significant security/performance risk |
|
||||||
|
| `medium` | Yellow | Should be addressed |
|
||||||
|
| `low` | Blue | Minor improvement |
|
||||||
|
| `info` | Gray | Informational only |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Services not starting:**
|
||||||
|
```bash
|
||||||
|
# Check logs
|
||||||
|
docker-compose logs web
|
||||||
|
docker-compose logs celery_worker
|
||||||
|
docker-compose logs lighthouse
|
||||||
|
docker-compose logs zap
|
||||||
|
|
||||||
|
# Restart services
|
||||||
|
docker-compose restart
|
||||||
|
```
|
||||||
|
|
||||||
|
**Database connection errors:**
|
||||||
|
```bash
|
||||||
|
# Wait for DB to be ready
|
||||||
|
docker-compose exec web python manage.py wait_for_db
|
||||||
|
|
||||||
|
# Check DB status
|
||||||
|
docker-compose exec db psql -U secure_web -c "\l"
|
||||||
|
```
|
||||||
|
|
||||||
|
**ZAP not responding:**
|
||||||
|
```bash
|
||||||
|
# ZAP takes time to start, wait 30-60 seconds
|
||||||
|
docker-compose logs zap
|
||||||
|
|
||||||
|
# Check ZAP status
|
||||||
|
curl http://localhost:8081/JSON/core/view/version/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Scan stuck in pending:**
|
||||||
|
```bash
|
||||||
|
# Check Celery worker
|
||||||
|
docker-compose logs celery_worker
|
||||||
|
|
||||||
|
# Restart worker
|
||||||
|
docker-compose restart celery_worker
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Tips
|
||||||
|
|
||||||
|
- For production, use a dedicated ZAP instance
|
||||||
|
- Consider caching Lighthouse results for repeated scans
|
||||||
|
- Adjust timeouts based on target website complexity
|
||||||
|
- Use Redis persistence for task queue durability
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- URL validation includes SSRF protection (blocks private IPs)
|
||||||
|
- ZAP API key should be changed in production
|
||||||
|
- Consider rate limiting scan endpoints
|
||||||
|
- Validate and sanitize all user inputs
|
||||||
|
- Run containers with minimal privileges
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch
|
||||||
|
3. Write tests for new functionality
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues and feature requests, please use the GitHub issue tracker.
|
||||||
|
|
@ -79,28 +79,37 @@ WSGI_APPLICATION = 'core.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# Parse DATABASE_URL or use default PostgreSQL settings
|
# Parse DATABASE_URL or use SQLite for local development
|
||||||
|
|
||||||
DATABASE_URL = os.getenv('DATABASE_URL', 'postgres://analyzer:analyzer_password@localhost:5432/website_analyzer')
|
DATABASE_URL = os.getenv('DATABASE_URL', '')
|
||||||
|
|
||||||
# Parse the DATABASE_URL
|
if DATABASE_URL:
|
||||||
import re
|
# Parse the DATABASE_URL for PostgreSQL
|
||||||
db_pattern = r'postgres://(?P<user>[^:]+):(?P<password>[^@]+)@(?P<host>[^:]+):(?P<port>\d+)/(?P<name>.+)'
|
import re
|
||||||
db_match = re.match(db_pattern, DATABASE_URL)
|
db_pattern = r'postgres://(?P<user>[^:]+):(?P<password>[^@]+)@(?P<host>[^:]+):(?P<port>\d+)/(?P<name>.+)'
|
||||||
|
db_match = re.match(db_pattern, DATABASE_URL)
|
||||||
if db_match:
|
|
||||||
DATABASES = {
|
if db_match:
|
||||||
'default': {
|
DATABASES = {
|
||||||
'ENGINE': 'django.db.backends.postgresql',
|
'default': {
|
||||||
'NAME': db_match.group('name'),
|
'ENGINE': 'django.db.backends.postgresql',
|
||||||
'USER': db_match.group('user'),
|
'NAME': db_match.group('name'),
|
||||||
'PASSWORD': db_match.group('password'),
|
'USER': db_match.group('user'),
|
||||||
'HOST': db_match.group('host'),
|
'PASSWORD': db_match.group('password'),
|
||||||
'PORT': db_match.group('port'),
|
'HOST': db_match.group('host'),
|
||||||
|
'PORT': db_match.group('port'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Invalid DATABASE_URL format, fallback to SQLite
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
# Fallback for development
|
# No DATABASE_URL set - use SQLite for local development
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.sqlite3',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
|
@ -191,15 +200,25 @@ CELERY_TASK_SOFT_TIME_LIMIT = CELERY_TASK_TIME_LIMIT - 30
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Redis Cache Configuration
|
# Cache Configuration
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
|
REDIS_URL = os.getenv('REDIS_URL', '')
|
||||||
CACHES = {
|
|
||||||
'default': {
|
if REDIS_URL:
|
||||||
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
CACHES = {
|
||||||
'LOCATION': REDIS_URL,
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
|
||||||
|
'LOCATION': REDIS_URL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Use local memory cache for development without Redis
|
||||||
|
CACHES = {
|
||||||
|
'default': {
|
||||||
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||||
|
'LOCATION': 'unique-snowflake',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Pytest configuration
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
DJANGO_SETTINGS_MODULE = core.settings
|
||||||
|
python_files = test_*.py
|
||||||
|
python_classes = Test*
|
||||||
|
python_functions = test_*
|
||||||
|
addopts = -v --tb=short
|
||||||
|
testpaths = tests
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.gitkeep
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""
|
||||||
|
Tests configuration and fixtures.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add backend to Python path
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
# Set Django settings module
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||||
|
|
@ -0,0 +1,347 @@
|
||||||
|
"""
|
||||||
|
Tests for scanner result parsing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# Sample Lighthouse JSON response
|
||||||
|
SAMPLE_LIGHTHOUSE_RESPONSE = {
|
||||||
|
"scanId": "test-123",
|
||||||
|
"url": "https://example.com",
|
||||||
|
"scores": {
|
||||||
|
"performance": 85,
|
||||||
|
"accessibility": 90,
|
||||||
|
"bestPractices": 75,
|
||||||
|
"seo": 80
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"firstContentfulPaint": {"value": 1200, "unit": "ms", "score": 0.9},
|
||||||
|
"largestContentfulPaint": {"value": 2500, "unit": "ms", "score": 0.75},
|
||||||
|
"speedIndex": {"value": 3400, "unit": "ms", "score": 0.7},
|
||||||
|
"timeToInteractive": {"value": 4500, "unit": "ms", "score": 0.65},
|
||||||
|
"totalBlockingTime": {"value": 150, "unit": "ms", "score": 0.85},
|
||||||
|
"cumulativeLayoutShift": {"value": 0.1, "unit": "score", "score": 0.95}
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"totalByteWeight": 2500000,
|
||||||
|
"unusedJavascript": [
|
||||||
|
{"url": "https://example.com/bundle.js", "wastedBytes": 150000}
|
||||||
|
],
|
||||||
|
"renderBlockingResources": [
|
||||||
|
{"url": "https://example.com/styles.css", "wastedMs": 500}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"diagnostics": {
|
||||||
|
"numRequests": 45,
|
||||||
|
"numScripts": 12,
|
||||||
|
"numStylesheets": 3,
|
||||||
|
"numImages": 20
|
||||||
|
},
|
||||||
|
"issues": [
|
||||||
|
{
|
||||||
|
"id": "uses-long-cache-ttl",
|
||||||
|
"category": "performance",
|
||||||
|
"title": "Serve static assets with an efficient cache policy",
|
||||||
|
"description": "A long cache lifetime can speed up repeat visits.",
|
||||||
|
"score": 0.3,
|
||||||
|
"impact": 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestLighthouseResultParsing:
|
||||||
|
"""Tests for parsing Lighthouse scanner results."""
|
||||||
|
|
||||||
|
def test_parse_scores(self):
|
||||||
|
"""Test extracting scores from Lighthouse response."""
|
||||||
|
scores = SAMPLE_LIGHTHOUSE_RESPONSE['scores']
|
||||||
|
|
||||||
|
assert scores['performance'] == 85
|
||||||
|
assert scores['accessibility'] == 90
|
||||||
|
assert scores['bestPractices'] == 75
|
||||||
|
assert scores['seo'] == 80
|
||||||
|
|
||||||
|
def test_parse_core_web_vitals(self):
|
||||||
|
"""Test extracting Core Web Vitals metrics."""
|
||||||
|
metrics = SAMPLE_LIGHTHOUSE_RESPONSE['metrics']
|
||||||
|
|
||||||
|
# FCP
|
||||||
|
assert metrics['firstContentfulPaint']['value'] == 1200
|
||||||
|
assert metrics['firstContentfulPaint']['unit'] == 'ms'
|
||||||
|
|
||||||
|
# LCP
|
||||||
|
assert metrics['largestContentfulPaint']['value'] == 2500
|
||||||
|
|
||||||
|
# CLS
|
||||||
|
assert metrics['cumulativeLayoutShift']['value'] == 0.1
|
||||||
|
assert metrics['cumulativeLayoutShift']['unit'] == 'score'
|
||||||
|
|
||||||
|
def test_parse_resource_metrics(self):
|
||||||
|
"""Test extracting resource metrics."""
|
||||||
|
resources = SAMPLE_LIGHTHOUSE_RESPONSE['resources']
|
||||||
|
diagnostics = SAMPLE_LIGHTHOUSE_RESPONSE['diagnostics']
|
||||||
|
|
||||||
|
assert resources['totalByteWeight'] == 2500000
|
||||||
|
assert diagnostics['numRequests'] == 45
|
||||||
|
assert diagnostics['numScripts'] == 12
|
||||||
|
|
||||||
|
def test_parse_issues(self):
|
||||||
|
"""Test extracting issues from Lighthouse."""
|
||||||
|
issues = SAMPLE_LIGHTHOUSE_RESPONSE['issues']
|
||||||
|
|
||||||
|
assert len(issues) == 1
|
||||||
|
issue = issues[0]
|
||||||
|
assert issue['category'] == 'performance'
|
||||||
|
assert issue['title'] == 'Serve static assets with an efficient cache policy'
|
||||||
|
|
||||||
|
|
||||||
|
# Sample ZAP response
|
||||||
|
SAMPLE_ZAP_ALERTS = [
|
||||||
|
{
|
||||||
|
"alert": "Cross-Site Scripting (Reflected)",
|
||||||
|
"risk": "3", # High
|
||||||
|
"confidence": "2",
|
||||||
|
"cweid": "79",
|
||||||
|
"wascid": "8",
|
||||||
|
"description": "Cross-site Scripting (XSS) is an attack technique...",
|
||||||
|
"url": "https://example.com/search?q=test",
|
||||||
|
"param": "q",
|
||||||
|
"evidence": "<script>alert(1)</script>",
|
||||||
|
"solution": "Phase: Architecture and Design\nUse a vetted library...",
|
||||||
|
"reference": "https://owasp.org/www-community/attacks/xss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alert": "Missing Anti-clickjacking Header",
|
||||||
|
"risk": "2", # Medium
|
||||||
|
"confidence": "3",
|
||||||
|
"cweid": "1021",
|
||||||
|
"wascid": "15",
|
||||||
|
"description": "The response does not include X-Frame-Options...",
|
||||||
|
"url": "https://example.com/",
|
||||||
|
"solution": "Ensure X-Frame-Options HTTP header is included...",
|
||||||
|
"reference": "https://owasp.org/www-community/Security_Headers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alert": "Server Leaks Information",
|
||||||
|
"risk": "1", # Low
|
||||||
|
"confidence": "3",
|
||||||
|
"cweid": "200",
|
||||||
|
"description": "The web/application server is leaking information...",
|
||||||
|
"url": "https://example.com/",
|
||||||
|
"evidence": "nginx/1.18.0",
|
||||||
|
"solution": "Configure the server to hide version information."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"alert": "Information Disclosure",
|
||||||
|
"risk": "0", # Info
|
||||||
|
"confidence": "2",
|
||||||
|
"description": "This is an informational finding.",
|
||||||
|
"url": "https://example.com/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestZAPResultParsing:
|
||||||
|
"""Tests for parsing OWASP ZAP results."""
|
||||||
|
|
||||||
|
def test_parse_alert_severity(self):
|
||||||
|
"""Test mapping ZAP risk levels to severity."""
|
||||||
|
risk_mapping = {
|
||||||
|
'0': 'info',
|
||||||
|
'1': 'low',
|
||||||
|
'2': 'medium',
|
||||||
|
'3': 'high',
|
||||||
|
}
|
||||||
|
|
||||||
|
for alert in SAMPLE_ZAP_ALERTS:
|
||||||
|
risk = alert['risk']
|
||||||
|
expected_severity = risk_mapping[risk]
|
||||||
|
assert expected_severity in ['info', 'low', 'medium', 'high']
|
||||||
|
|
||||||
|
def test_parse_xss_alert(self):
|
||||||
|
"""Test parsing XSS vulnerability alert."""
|
||||||
|
xss_alert = SAMPLE_ZAP_ALERTS[0]
|
||||||
|
|
||||||
|
assert xss_alert['alert'] == 'Cross-Site Scripting (Reflected)'
|
||||||
|
assert xss_alert['risk'] == '3' # High
|
||||||
|
assert xss_alert['cweid'] == '79' # XSS CWE ID
|
||||||
|
assert 'q' in xss_alert['param']
|
||||||
|
|
||||||
|
def test_parse_header_alert(self):
|
||||||
|
"""Test parsing missing header alert."""
|
||||||
|
header_alert = SAMPLE_ZAP_ALERTS[1]
|
||||||
|
|
||||||
|
assert 'X-Frame-Options' in header_alert['alert']
|
||||||
|
assert header_alert['risk'] == '2' # Medium
|
||||||
|
|
||||||
|
def test_categorize_alerts(self):
|
||||||
|
"""Test categorizing ZAP alerts."""
|
||||||
|
def categorize(alert_name):
|
||||||
|
alert_lower = alert_name.lower()
|
||||||
|
if 'xss' in alert_lower or 'cross-site scripting' in alert_lower:
|
||||||
|
return 'security'
|
||||||
|
if 'header' in alert_lower or 'x-frame' in alert_lower:
|
||||||
|
return 'headers'
|
||||||
|
if 'cookie' in alert_lower:
|
||||||
|
return 'security'
|
||||||
|
return 'security'
|
||||||
|
|
||||||
|
assert categorize(SAMPLE_ZAP_ALERTS[0]['alert']) == 'security'
|
||||||
|
assert categorize(SAMPLE_ZAP_ALERTS[1]['alert']) == 'headers'
|
||||||
|
|
||||||
|
|
||||||
|
# Sample HTTP headers response
|
||||||
|
SAMPLE_HEADERS = {
|
||||||
|
'content-type': 'text/html; charset=utf-8',
|
||||||
|
'server': 'nginx/1.18.0',
|
||||||
|
'x-powered-by': 'Express',
|
||||||
|
'strict-transport-security': 'max-age=31536000; includeSubDomains',
|
||||||
|
'x-content-type-options': 'nosniff',
|
||||||
|
'x-frame-options': 'SAMEORIGIN',
|
||||||
|
# Missing: Content-Security-Policy, Referrer-Policy, Permissions-Policy
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestHeadersResultParsing:
|
||||||
|
"""Tests for parsing HTTP headers analysis."""
|
||||||
|
|
||||||
|
REQUIRED_HEADERS = [
|
||||||
|
'strict-transport-security',
|
||||||
|
'content-security-policy',
|
||||||
|
'x-frame-options',
|
||||||
|
'x-content-type-options',
|
||||||
|
'referrer-policy',
|
||||||
|
'permissions-policy',
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_detect_present_headers(self):
|
||||||
|
"""Test detecting which security headers are present."""
|
||||||
|
headers_lower = {k.lower(): v for k, v in SAMPLE_HEADERS.items()}
|
||||||
|
|
||||||
|
present = [h for h in self.REQUIRED_HEADERS if h in headers_lower]
|
||||||
|
|
||||||
|
assert 'strict-transport-security' in present
|
||||||
|
assert 'x-frame-options' in present
|
||||||
|
assert 'x-content-type-options' in present
|
||||||
|
|
||||||
|
def test_detect_missing_headers(self):
|
||||||
|
"""Test detecting which security headers are missing."""
|
||||||
|
headers_lower = {k.lower(): v for k, v in SAMPLE_HEADERS.items()}
|
||||||
|
|
||||||
|
missing = [h for h in self.REQUIRED_HEADERS if h not in headers_lower]
|
||||||
|
|
||||||
|
assert 'content-security-policy' in missing
|
||||||
|
assert 'referrer-policy' in missing
|
||||||
|
assert 'permissions-policy' in missing
|
||||||
|
|
||||||
|
def test_detect_information_disclosure(self):
|
||||||
|
"""Test detecting information disclosure headers."""
|
||||||
|
info_disclosure_headers = ['server', 'x-powered-by', 'x-aspnet-version']
|
||||||
|
|
||||||
|
disclosed = [
|
||||||
|
h for h in info_disclosure_headers
|
||||||
|
if h.lower() in {k.lower() for k in SAMPLE_HEADERS.keys()}
|
||||||
|
]
|
||||||
|
|
||||||
|
assert 'server' in disclosed
|
||||||
|
assert 'x-powered-by' in disclosed
|
||||||
|
|
||||||
|
def test_check_hsts_max_age(self):
|
||||||
|
"""Test checking HSTS max-age value."""
|
||||||
|
hsts = SAMPLE_HEADERS.get('strict-transport-security', '')
|
||||||
|
|
||||||
|
# Extract max-age
|
||||||
|
if 'max-age=' in hsts.lower():
|
||||||
|
max_age_str = hsts.lower().split('max-age=')[1].split(';')[0]
|
||||||
|
max_age = int(max_age_str)
|
||||||
|
|
||||||
|
# Should be at least 1 year (31536000 seconds)
|
||||||
|
assert max_age >= 31536000
|
||||||
|
|
||||||
|
|
||||||
|
class TestScannerResultIntegration:
|
||||||
|
"""Integration tests for combining scanner results."""
|
||||||
|
|
||||||
|
def test_aggregate_scores(self):
|
||||||
|
"""Test aggregating scores from multiple scanners."""
|
||||||
|
lighthouse_scores = SAMPLE_LIGHTHOUSE_RESPONSE['scores']
|
||||||
|
|
||||||
|
# Simulate security score from ZAP findings
|
||||||
|
security_score = 100
|
||||||
|
for alert in SAMPLE_ZAP_ALERTS:
|
||||||
|
risk = alert['risk']
|
||||||
|
if risk == '3':
|
||||||
|
security_score -= 15 # High
|
||||||
|
elif risk == '2':
|
||||||
|
security_score -= 8 # Medium
|
||||||
|
elif risk == '1':
|
||||||
|
security_score -= 3 # Low
|
||||||
|
else:
|
||||||
|
security_score -= 1 # Info
|
||||||
|
|
||||||
|
security_score = max(0, security_score)
|
||||||
|
|
||||||
|
# Calculate overall (simplified)
|
||||||
|
overall = (
|
||||||
|
lighthouse_scores['performance'] * 0.25 +
|
||||||
|
security_score * 0.30 +
|
||||||
|
lighthouse_scores['accessibility'] * 0.15 +
|
||||||
|
lighthouse_scores['seo'] * 0.15 +
|
||||||
|
lighthouse_scores['bestPractices'] * 0.15
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 0 <= overall <= 100
|
||||||
|
|
||||||
|
def test_combine_issues(self):
|
||||||
|
"""Test combining issues from multiple scanners."""
|
||||||
|
# Lighthouse issues
|
||||||
|
lighthouse_issues = [
|
||||||
|
{
|
||||||
|
'category': 'performance',
|
||||||
|
'severity': 'medium',
|
||||||
|
'tool': 'lighthouse',
|
||||||
|
'title': issue['title']
|
||||||
|
}
|
||||||
|
for issue in SAMPLE_LIGHTHOUSE_RESPONSE['issues']
|
||||||
|
]
|
||||||
|
|
||||||
|
# ZAP issues
|
||||||
|
risk_to_severity = {'0': 'info', '1': 'low', '2': 'medium', '3': 'high'}
|
||||||
|
zap_issues = [
|
||||||
|
{
|
||||||
|
'category': 'security',
|
||||||
|
'severity': risk_to_severity[alert['risk']],
|
||||||
|
'tool': 'owasp_zap',
|
||||||
|
'title': alert['alert']
|
||||||
|
}
|
||||||
|
for alert in SAMPLE_ZAP_ALERTS
|
||||||
|
]
|
||||||
|
|
||||||
|
# Header issues
|
||||||
|
headers_lower = {k.lower(): v for k, v in SAMPLE_HEADERS.items()}
|
||||||
|
header_issues = [
|
||||||
|
{
|
||||||
|
'category': 'headers',
|
||||||
|
'severity': 'high' if h == 'content-security-policy' else 'medium',
|
||||||
|
'tool': 'header_check',
|
||||||
|
'title': f'Missing {h} header'
|
||||||
|
}
|
||||||
|
for h in ['content-security-policy', 'referrer-policy', 'permissions-policy']
|
||||||
|
if h not in headers_lower
|
||||||
|
]
|
||||||
|
|
||||||
|
all_issues = lighthouse_issues + zap_issues + header_issues
|
||||||
|
|
||||||
|
assert len(all_issues) > 0
|
||||||
|
|
||||||
|
# Count by severity
|
||||||
|
severity_counts = {}
|
||||||
|
for issue in all_issues:
|
||||||
|
severity = issue['severity']
|
||||||
|
severity_counts[severity] = severity_counts.get(severity, 0) + 1
|
||||||
|
|
||||||
|
assert 'high' in severity_counts or 'medium' in severity_counts
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
# Generated by Django 5.2.9 on 2025-12-08 03:33
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Website',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique identifier for the website', primary_key=True, serialize=False)),
|
||||||
|
('url', models.URLField(help_text='The normalized URL of the website', max_length=2048, unique=True, validators=[django.core.validators.URLValidator(schemes=['http', 'https'])])),
|
||||||
|
('domain', models.CharField(db_index=True, help_text='The domain extracted from the URL', max_length=255)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='When the website was first added')),
|
||||||
|
('last_scanned_at', models.DateTimeField(blank=True, help_text='When the website was last scanned', null=True)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'websites',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
'indexes': [models.Index(fields=['domain'], name='websites_domain_9fabc6_idx'), models.Index(fields=['-last_scanned_at'], name='websites_last_sc_15be22_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Scan',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='Unique identifier for the scan', primary_key=True, serialize=False)),
|
||||||
|
('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('done', 'Completed'), ('failed', 'Failed'), ('partial', 'Partially Completed')], db_index=True, default='pending', help_text='Current status of the scan', max_length=20)),
|
||||||
|
('celery_task_id', models.CharField(blank=True, help_text='Celery task ID for tracking', max_length=255, null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, help_text='When the scan was created')),
|
||||||
|
('started_at', models.DateTimeField(blank=True, help_text='When the scan started running', null=True)),
|
||||||
|
('completed_at', models.DateTimeField(blank=True, help_text='When the scan completed', null=True)),
|
||||||
|
('performance_score', models.IntegerField(blank=True, help_text='Lighthouse performance score (0-100)', null=True)),
|
||||||
|
('accessibility_score', models.IntegerField(blank=True, help_text='Lighthouse accessibility score (0-100)', null=True)),
|
||||||
|
('seo_score', models.IntegerField(blank=True, help_text='Lighthouse SEO score (0-100)', null=True)),
|
||||||
|
('best_practices_score', models.IntegerField(blank=True, help_text='Lighthouse best practices score (0-100)', null=True)),
|
||||||
|
('security_score', models.IntegerField(blank=True, help_text='Computed security score based on issues (0-100)', null=True)),
|
||||||
|
('overall_score', models.IntegerField(blank=True, help_text='Overall health score (0-100)', null=True)),
|
||||||
|
('error_message', models.TextField(blank=True, help_text='Error message if scan failed', null=True)),
|
||||||
|
('raw_lighthouse_data', models.JSONField(blank=True, help_text='Raw Lighthouse report data', null=True)),
|
||||||
|
('raw_zap_data', models.JSONField(blank=True, help_text='Raw OWASP ZAP report data', null=True)),
|
||||||
|
('raw_playwright_data', models.JSONField(blank=True, help_text='Raw Playwright analysis data', null=True)),
|
||||||
|
('raw_headers_data', models.JSONField(blank=True, help_text='Raw HTTP headers analysis data', null=True)),
|
||||||
|
('website', models.ForeignKey(help_text='The website that was scanned', on_delete=django.db.models.deletion.CASCADE, related_name='scans', to='websites.website')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'scans',
|
||||||
|
'ordering': ['-created_at'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Metric',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(db_index=True, help_text="Name of the metric (e.g., 'first_contentful_paint_ms')", max_length=100)),
|
||||||
|
('display_name', models.CharField(help_text='Human-readable name for display', max_length=200)),
|
||||||
|
('value', models.FloatField(help_text='Numeric value of the metric')),
|
||||||
|
('unit', models.CharField(choices=[('ms', 'Milliseconds'), ('s', 'Seconds'), ('bytes', 'Bytes'), ('kb', 'Kilobytes'), ('mb', 'Megabytes'), ('score', 'Score (0-1)'), ('percent', 'Percentage'), ('count', 'Count')], help_text='Unit of measurement', max_length=20)),
|
||||||
|
('source', models.CharField(choices=[('lighthouse', 'Google Lighthouse'), ('owasp_zap', 'OWASP ZAP'), ('playwright', 'Playwright'), ('header_check', 'HTTP Header Check'), ('tls_check', 'TLS/SSL Check')], help_text='Tool that provided this metric', max_length=30)),
|
||||||
|
('score', models.FloatField(blank=True, help_text='Lighthouse score for this metric (0-1)', null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('scan', models.ForeignKey(help_text='The scan that measured this metric', on_delete=django.db.models.deletion.CASCADE, related_name='metrics', to='websites.scan')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'metrics',
|
||||||
|
'ordering': ['name'],
|
||||||
|
'indexes': [models.Index(fields=['scan', 'name'], name='metrics_scan_id_c4cc62_idx'), models.Index(fields=['source'], name='metrics_source_71e403_idx')],
|
||||||
|
'constraints': [models.UniqueConstraint(fields=('scan', 'name'), name='unique_metric_per_scan')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Issue',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('category', models.CharField(choices=[('performance', 'Performance'), ('security', 'Security'), ('headers', 'HTTP Headers'), ('tls', 'TLS/SSL'), ('cors', 'CORS'), ('accessibility', 'Accessibility'), ('seo', 'SEO'), ('best_practices', 'Best Practices'), ('content', 'Content'), ('resources', 'Resources')], db_index=True, help_text='Category of the issue', max_length=30)),
|
||||||
|
('severity', models.CharField(choices=[('critical', 'Critical'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low'), ('info', 'Informational')], db_index=True, help_text='Severity level of the issue', max_length=20)),
|
||||||
|
('tool', models.CharField(choices=[('lighthouse', 'Google Lighthouse'), ('owasp_zap', 'OWASP ZAP'), ('playwright', 'Playwright'), ('header_check', 'HTTP Header Check'), ('tls_check', 'TLS/SSL Check')], help_text='Tool that detected this issue', max_length=30)),
|
||||||
|
('title', models.CharField(help_text='Brief title of the issue', max_length=500)),
|
||||||
|
('description', models.TextField(help_text='Detailed description of the issue')),
|
||||||
|
('affected_url', models.URLField(blank=True, help_text='Specific URL affected by this issue', max_length=2048, null=True)),
|
||||||
|
('remediation', models.TextField(blank=True, help_text='Suggested fix or remediation', null=True)),
|
||||||
|
('raw_data', models.JSONField(blank=True, help_text='Raw data from the scanner for this issue', null=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('scan', models.ForeignKey(help_text='The scan that found this issue', on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='websites.scan')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'issues',
|
||||||
|
'ordering': ['severity', '-created_at'],
|
||||||
|
'indexes': [models.Index(fields=['scan', 'category'], name='issues_scan_id_e7f389_idx'), models.Index(fields=['scan', 'severity'], name='issues_scan_id_c92ffd_idx'), models.Index(fields=['tool'], name='issues_tool_78d942_idx')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='scan',
|
||||||
|
index=models.Index(fields=['status'], name='scans_status_dc5ad7_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='scan',
|
||||||
|
index=models.Index(fields=['-created_at'], name='scans_created_7db2e5_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='scan',
|
||||||
|
index=models.Index(fields=['website', '-created_at'], name='scans_website_6dae4d_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Loading…
Reference in New Issue