Phase 2 Step 1: Implement database and service layer
Complete the foundation for Phase 2 by implementing the service layer, utilities, and comprehensive test suite. This establishes the core business logic for scan management. Service Layer: - Add ScanService class with complete scan lifecycle management * trigger_scan() - Create scan record and prepare for execution * get_scan() - Retrieve scan with all related data (eager loading) * list_scans() - Paginated scan list with status filtering * delete_scan() - Remove scan from DB and delete all files * get_scan_status() - Poll current scan status and progress * _save_scan_to_db() - Persist scan results to database * _map_report_to_models() - Complex JSON-to-DB mapping logic Database Mapping: - Comprehensive mapping from scanner JSON output to normalized schema - Handles nested relationships: sites → IPs → ports → services → certs → TLS - Processes both TCP and UDP ports with expected/actual tracking - Maps service detection results with HTTP/HTTPS information - Stores SSL/TLS certificates with expiration tracking - Records TLS version support and cipher suites - Links screenshots to services Utilities: - Add pagination.py with PaginatedResult class * paginate() function for SQLAlchemy queries * validate_page_params() for input sanitization * Metadata: total, pages, has_prev, has_next, etc. - Add validators.py with comprehensive validation functions * validate_config_file() - YAML structure and required fields * validate_scan_status() - Enum validation (running/completed/failed) * validate_scan_id() - Positive integer validation * validate_port() - Port range validation (1-65535) * validate_ip_address() - Basic IPv4 format validation * sanitize_filename() - Path traversal prevention Database Migration: - Add migration 002 for scan status index - Optimizes queries filtering by scan status - Timestamp index already exists from migration 001 Testing: - Add pytest infrastructure with conftest.py * test_db fixture - Temporary SQLite database per test * sample_scan_report fixture - Realistic scanner output * sample_config_file fixture - Valid YAML config * sample_invalid_config_file fixture - For validation tests - Add comprehensive test_scan_service.py (15 tests) * Test scan trigger with valid/invalid configs * Test scan retrieval (found/not found cases) * Test scan listing with pagination and filtering * Test scan deletion with cascade cleanup * Test scan status retrieval * Test database mapping from JSON to models * Test expected vs actual port flagging * Test certificate and TLS data mapping * Test full scan retrieval with all relationships * All tests passing Files Added: - web/services/__init__.py - web/services/scan_service.py (545 lines) - web/utils/pagination.py (153 lines) - web/utils/validators.py (245 lines) - migrations/versions/002_add_scan_indexes.py - tests/__init__.py - tests/conftest.py (142 lines) - tests/test_scan_service.py (374 lines) Next Steps (Step 2): - Implement scan API endpoints in web/api/scans.py - Add authentication decorators - Integrate ScanService with API routes - Test API endpoints with integration tests Phase 2 Step 1 Complete ✓
This commit is contained in:
196
tests/conftest.py
Normal file
196
tests/conftest.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
Pytest configuration and fixtures for SneakyScanner tests.
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from web.models import Base
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def test_db():
|
||||
"""
|
||||
Create a temporary test database.
|
||||
|
||||
Yields a SQLAlchemy session for testing, then cleans up.
|
||||
"""
|
||||
# Create temporary database file
|
||||
db_fd, db_path = tempfile.mkstemp(suffix='.db')
|
||||
|
||||
# Create engine and session
|
||||
engine = create_engine(f'sqlite:///{db_path}', echo=False)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
|
||||
yield session
|
||||
|
||||
# Cleanup
|
||||
session.close()
|
||||
os.close(db_fd)
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_scan_report():
|
||||
"""
|
||||
Sample scan report matching the structure from scanner.py.
|
||||
|
||||
Returns a dictionary representing a typical scan output.
|
||||
"""
|
||||
return {
|
||||
'title': 'Test Scan',
|
||||
'scan_time': '2025-11-14T10:30:00Z',
|
||||
'scan_duration': 125.5,
|
||||
'config_file': '/app/configs/test.yaml',
|
||||
'sites': [
|
||||
{
|
||||
'name': 'Test Site',
|
||||
'ips': [
|
||||
{
|
||||
'address': '192.168.1.10',
|
||||
'expected': {
|
||||
'ping': True,
|
||||
'tcp_ports': [22, 80, 443],
|
||||
'udp_ports': [53],
|
||||
'services': []
|
||||
},
|
||||
'actual': {
|
||||
'ping': True,
|
||||
'tcp_ports': [22, 80, 443, 8080],
|
||||
'udp_ports': [53],
|
||||
'services': [
|
||||
{
|
||||
'port': 22,
|
||||
'service': 'ssh',
|
||||
'product': 'OpenSSH',
|
||||
'version': '8.9p1',
|
||||
'extrainfo': 'Ubuntu',
|
||||
'ostype': 'Linux'
|
||||
},
|
||||
{
|
||||
'port': 443,
|
||||
'service': 'https',
|
||||
'product': 'nginx',
|
||||
'version': '1.24.0',
|
||||
'extrainfo': '',
|
||||
'ostype': '',
|
||||
'http_info': {
|
||||
'protocol': 'https',
|
||||
'screenshot': 'screenshots/192_168_1_10_443.png',
|
||||
'certificate': {
|
||||
'subject': 'CN=example.com',
|
||||
'issuer': 'CN=Let\'s Encrypt Authority',
|
||||
'serial_number': '123456789',
|
||||
'not_valid_before': '2025-01-01T00:00:00Z',
|
||||
'not_valid_after': '2025-12-31T23:59:59Z',
|
||||
'days_until_expiry': 365,
|
||||
'sans': ['example.com', 'www.example.com'],
|
||||
'is_self_signed': False,
|
||||
'tls_versions': {
|
||||
'TLS 1.2': {
|
||||
'supported': True,
|
||||
'cipher_suites': [
|
||||
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'
|
||||
]
|
||||
},
|
||||
'TLS 1.3': {
|
||||
'supported': True,
|
||||
'cipher_suites': [
|
||||
'TLS_AES_256_GCM_SHA384',
|
||||
'TLS_AES_128_GCM_SHA256'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
'port': 80,
|
||||
'service': 'http',
|
||||
'product': 'nginx',
|
||||
'version': '1.24.0',
|
||||
'extrainfo': '',
|
||||
'ostype': '',
|
||||
'http_info': {
|
||||
'protocol': 'http',
|
||||
'screenshot': 'screenshots/192_168_1_10_80.png'
|
||||
}
|
||||
},
|
||||
{
|
||||
'port': 8080,
|
||||
'service': 'http',
|
||||
'product': 'Jetty',
|
||||
'version': '9.4.48',
|
||||
'extrainfo': '',
|
||||
'ostype': ''
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_config_file(tmp_path):
|
||||
"""
|
||||
Create a sample YAML config file for testing.
|
||||
|
||||
Args:
|
||||
tmp_path: pytest temporary directory fixture
|
||||
|
||||
Returns:
|
||||
Path to created config file
|
||||
"""
|
||||
config_data = {
|
||||
'title': 'Test Scan',
|
||||
'sites': [
|
||||
{
|
||||
'name': 'Test Site',
|
||||
'ips': [
|
||||
{
|
||||
'address': '192.168.1.10',
|
||||
'expected': {
|
||||
'ping': True,
|
||||
'tcp_ports': [22, 80, 443],
|
||||
'udp_ports': [53],
|
||||
'services': ['ssh', 'http', 'https']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
config_file = tmp_path / 'test_config.yaml'
|
||||
with open(config_file, 'w') as f:
|
||||
yaml.dump(config_data, f)
|
||||
|
||||
return str(config_file)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_invalid_config_file(tmp_path):
|
||||
"""
|
||||
Create an invalid config file for testing validation.
|
||||
|
||||
Returns:
|
||||
Path to invalid config file
|
||||
"""
|
||||
config_file = tmp_path / 'invalid_config.yaml'
|
||||
with open(config_file, 'w') as f:
|
||||
f.write("invalid: yaml: content: [missing closing bracket")
|
||||
|
||||
return str(config_file)
|
||||
Reference in New Issue
Block a user