Implement complete database schema and Flask application structure for SneakyScan web interface. This establishes the foundation for web-based scan management, scheduling, and visualization. Database & ORM: - Add 11 SQLAlchemy models for comprehensive scan data storage (Scan, ScanSite, ScanIP, ScanPort, ScanService, ScanCertificate, ScanTLSVersion, Schedule, Alert, AlertRule, Setting) - Configure Alembic migrations system with initial schema migration - Add init_db.py script for database initialization and password setup - Support both migration-based and direct table creation Settings System: - Implement SettingsManager with automatic encryption for sensitive values - Add Fernet encryption for SMTP passwords and API tokens - Implement PasswordManager with bcrypt password hashing (work factor 12) - Initialize default settings for SMTP, authentication, and retention Flask Application: - Create Flask app factory pattern with scoped session management - Add 4 API blueprints: scans, schedules, alerts, settings - Implement functional Settings API (GET/PUT/DELETE endpoints) - Add CORS support, error handlers, and request/response logging - Configure development and production logging to file and console Docker & Deployment: - Update Dockerfile to install Flask dependencies - Add docker-compose-web.yml for web application deployment - Configure volume mounts for database, output, and logs persistence - Expose port 5000 for Flask web server Testing & Validation: - Add validate_phase1.py script to verify all deliverables - Validate directory structure, Python syntax, models, and endpoints - All validation checks passing Documentation: - Add PHASE1_COMPLETE.md with comprehensive Phase 1 summary - Update ROADMAP.md with Phase 1 completion status - Update .gitignore to exclude database files and documentation Files changed: 21 files - New: web/ directory with complete Flask app structure - New: migrations/ with Alembic configuration - New: requirements-web.txt with Flask dependencies - Modified: Dockerfile, ROADMAP.md, .gitignore
293 lines
8.1 KiB
Python
293 lines
8.1 KiB
Python
"""
|
|
Flask application factory for SneakyScanner web interface.
|
|
|
|
This module creates and configures the Flask application with all necessary
|
|
extensions, blueprints, and middleware.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from flask import Flask, jsonify
|
|
from flask_cors import CORS
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
|
|
|
from web.models import Base
|
|
|
|
|
|
def create_app(config: dict = None) -> Flask:
|
|
"""
|
|
Create and configure the Flask application.
|
|
|
|
Args:
|
|
config: Optional configuration dictionary to override defaults
|
|
|
|
Returns:
|
|
Configured Flask application instance
|
|
"""
|
|
app = Flask(__name__,
|
|
instance_relative_config=True,
|
|
static_folder='static',
|
|
template_folder='templates')
|
|
|
|
# Load default configuration
|
|
app.config.from_mapping(
|
|
SECRET_KEY=os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production'),
|
|
SQLALCHEMY_DATABASE_URI=os.environ.get('DATABASE_URL', 'sqlite:///./sneakyscanner.db'),
|
|
SQLALCHEMY_TRACK_MODIFICATIONS=False,
|
|
JSON_SORT_KEYS=False, # Preserve order in JSON responses
|
|
MAX_CONTENT_LENGTH=50 * 1024 * 1024, # 50MB max upload size
|
|
)
|
|
|
|
# Override with custom config if provided
|
|
if config:
|
|
app.config.update(config)
|
|
|
|
# Ensure instance folder exists
|
|
try:
|
|
os.makedirs(app.instance_path, exist_ok=True)
|
|
except OSError:
|
|
pass
|
|
|
|
# Configure logging
|
|
configure_logging(app)
|
|
|
|
# Initialize database
|
|
init_database(app)
|
|
|
|
# Initialize extensions
|
|
init_extensions(app)
|
|
|
|
# Register blueprints
|
|
register_blueprints(app)
|
|
|
|
# Register error handlers
|
|
register_error_handlers(app)
|
|
|
|
# Add request/response handlers
|
|
register_request_handlers(app)
|
|
|
|
app.logger.info("SneakyScanner Flask app initialized")
|
|
|
|
return app
|
|
|
|
|
|
def configure_logging(app: Flask) -> None:
|
|
"""
|
|
Configure application logging.
|
|
|
|
Args:
|
|
app: Flask application instance
|
|
"""
|
|
# Set log level from environment or default to INFO
|
|
log_level = os.environ.get('LOG_LEVEL', 'INFO').upper()
|
|
app.logger.setLevel(getattr(logging, log_level, logging.INFO))
|
|
|
|
# Create logs directory if it doesn't exist
|
|
log_dir = Path('logs')
|
|
log_dir.mkdir(exist_ok=True)
|
|
|
|
# File handler for all logs
|
|
file_handler = logging.FileHandler(log_dir / 'sneakyscanner.log')
|
|
file_handler.setLevel(logging.INFO)
|
|
file_formatter = logging.Formatter(
|
|
'%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
file_handler.setFormatter(file_formatter)
|
|
app.logger.addHandler(file_handler)
|
|
|
|
# Console handler for development
|
|
if app.debug:
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setLevel(logging.DEBUG)
|
|
console_formatter = logging.Formatter(
|
|
'[%(levelname)s] %(name)s: %(message)s'
|
|
)
|
|
console_handler.setFormatter(console_formatter)
|
|
app.logger.addHandler(console_handler)
|
|
|
|
|
|
def init_database(app: Flask) -> None:
|
|
"""
|
|
Initialize database connection and session management.
|
|
|
|
Args:
|
|
app: Flask application instance
|
|
"""
|
|
# Create engine
|
|
engine = create_engine(
|
|
app.config['SQLALCHEMY_DATABASE_URI'],
|
|
echo=app.debug, # Log SQL in debug mode
|
|
pool_pre_ping=True, # Verify connections before using
|
|
pool_recycle=3600, # Recycle connections after 1 hour
|
|
)
|
|
|
|
# Create scoped session factory
|
|
db_session = scoped_session(
|
|
sessionmaker(
|
|
autocommit=False,
|
|
autoflush=False,
|
|
bind=engine
|
|
)
|
|
)
|
|
|
|
# Store session in app for use in views
|
|
app.db_session = db_session
|
|
|
|
# Create tables if they don't exist (for development)
|
|
# In production, use Alembic migrations instead
|
|
if app.debug:
|
|
Base.metadata.create_all(bind=engine)
|
|
|
|
@app.teardown_appcontext
|
|
def shutdown_session(exception=None):
|
|
"""Remove database session at end of request."""
|
|
db_session.remove()
|
|
|
|
app.logger.info(f"Database initialized: {app.config['SQLALCHEMY_DATABASE_URI']}")
|
|
|
|
|
|
def init_extensions(app: Flask) -> None:
|
|
"""
|
|
Initialize Flask extensions.
|
|
|
|
Args:
|
|
app: Flask application instance
|
|
"""
|
|
# CORS support for API
|
|
CORS(app, resources={
|
|
r"/api/*": {
|
|
"origins": os.environ.get('CORS_ORIGINS', '*').split(','),
|
|
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
"allow_headers": ["Content-Type", "Authorization"],
|
|
}
|
|
})
|
|
|
|
app.logger.info("Extensions initialized")
|
|
|
|
|
|
def register_blueprints(app: Flask) -> None:
|
|
"""
|
|
Register Flask blueprints for different app sections.
|
|
|
|
Args:
|
|
app: Flask application instance
|
|
"""
|
|
# Import blueprints
|
|
from web.api.scans import bp as scans_bp
|
|
from web.api.schedules import bp as schedules_bp
|
|
from web.api.alerts import bp as alerts_bp
|
|
from web.api.settings import bp as settings_bp
|
|
|
|
# Register API blueprints
|
|
app.register_blueprint(scans_bp, url_prefix='/api/scans')
|
|
app.register_blueprint(schedules_bp, url_prefix='/api/schedules')
|
|
app.register_blueprint(alerts_bp, url_prefix='/api/alerts')
|
|
app.register_blueprint(settings_bp, url_prefix='/api/settings')
|
|
|
|
app.logger.info("Blueprints registered")
|
|
|
|
|
|
def register_error_handlers(app: Flask) -> None:
|
|
"""
|
|
Register error handlers for common HTTP errors.
|
|
|
|
Args:
|
|
app: Flask application instance
|
|
"""
|
|
@app.errorhandler(400)
|
|
def bad_request(error):
|
|
return jsonify({
|
|
'error': 'Bad Request',
|
|
'message': str(error) or 'The request was invalid'
|
|
}), 400
|
|
|
|
@app.errorhandler(401)
|
|
def unauthorized(error):
|
|
return jsonify({
|
|
'error': 'Unauthorized',
|
|
'message': 'Authentication required'
|
|
}), 401
|
|
|
|
@app.errorhandler(403)
|
|
def forbidden(error):
|
|
return jsonify({
|
|
'error': 'Forbidden',
|
|
'message': 'You do not have permission to access this resource'
|
|
}), 403
|
|
|
|
@app.errorhandler(404)
|
|
def not_found(error):
|
|
return jsonify({
|
|
'error': 'Not Found',
|
|
'message': 'The requested resource was not found'
|
|
}), 404
|
|
|
|
@app.errorhandler(405)
|
|
def method_not_allowed(error):
|
|
return jsonify({
|
|
'error': 'Method Not Allowed',
|
|
'message': 'The HTTP method is not allowed for this endpoint'
|
|
}), 405
|
|
|
|
@app.errorhandler(500)
|
|
def internal_server_error(error):
|
|
app.logger.error(f"Internal server error: {error}")
|
|
return jsonify({
|
|
'error': 'Internal Server Error',
|
|
'message': 'An unexpected error occurred'
|
|
}), 500
|
|
|
|
|
|
def register_request_handlers(app: Flask) -> None:
|
|
"""
|
|
Register request and response handlers.
|
|
|
|
Args:
|
|
app: Flask application instance
|
|
"""
|
|
@app.before_request
|
|
def log_request():
|
|
"""Log incoming requests."""
|
|
if app.debug:
|
|
app.logger.debug(f"{request.method} {request.path}")
|
|
|
|
@app.after_request
|
|
def add_security_headers(response):
|
|
"""Add security headers to all responses."""
|
|
# Only add CORS and security headers for API routes
|
|
if request.path.startswith('/api/'):
|
|
response.headers['X-Content-Type-Options'] = 'nosniff'
|
|
response.headers['X-Frame-Options'] = 'DENY'
|
|
response.headers['X-XSS-Protection'] = '1; mode=block'
|
|
|
|
return response
|
|
|
|
# Import request at runtime to avoid circular imports
|
|
from flask import request
|
|
|
|
# Re-apply to ensure request is available
|
|
@app.before_request
|
|
def log_request():
|
|
"""Log incoming requests."""
|
|
if app.debug:
|
|
app.logger.debug(f"{request.method} {request.path}")
|
|
|
|
|
|
# Development server entry point
|
|
def main():
|
|
"""Run development server."""
|
|
app = create_app()
|
|
app.run(
|
|
host=os.environ.get('FLASK_HOST', '0.0.0.0'),
|
|
port=int(os.environ.get('FLASK_PORT', 5000)),
|
|
debug=os.environ.get('FLASK_DEBUG', 'True').lower() == 'true'
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|