"""Security validation script for the Team Tryouts application. This script performs pre-deployment security checks to validate: - HTTP security headers - Cookie security attributes - Debug mode status - HTTPS configuration - Dependency vulnerabilities - Database connectivity Usage: python security_scan.py [--url http://localhost:5000] """ import os import sys import json import subprocess import urllib.request import ssl from datetime import datetime def check_environment(): """Check required environment variables are set. Returns: bool: True if all critical variables are set. """ print('=' * 60) print('1. ENVIRONMENT VARIABLES CHECK') print('=' * 60) critical_vars = ['SECRET_KEY'] recommended_vars = ['DATABASE_URL', 'CORS_ALLOWED_ORIGINS'] all_ok = True for var in critical_vars: value = os.getenv(var) if value: # Check SECRET_KEY is not a default/weak value if var == 'SECRET_KEY' and len(value) < 32: print(f'[WARN] {var} is set but too short (less than 32 chars)') all_ok = False else: print(f'[OK] {var} is set') else: print(f'[FAIL] {var} is not set!') all_ok = False for var in recommended_vars: value = os.getenv(var) if value: print(f'[OK] {var} is set') else: print(f'[INFO] {var} is not set (using default)') # Check FLASK_DEBUG debug = os.getenv('FLASK_DEBUG', 'false').lower() if debug == 'true': print('[WARN] FLASK_DEBUG is enabled! Should be disabled in production.') else: print('[OK] FLASK_DEBUG is disabled') return all_ok def check_https_headers(url): """Check HTTP security headers from a running application. Args: url: The base URL of the application to check. Returns: bool: True if all critical headers are present. """ print('\n' + '=' * 60) print('2. HTTP SECURITY HEADERS CHECK') print('=' * 60) required_headers = { 'Strict-Transport-Security': 'HSTS enabled', 'X-Content-Type-Options': 'Prevents MIME sniffing', 'X-Frame-Options': 'Prevents clickjacking', 'Content-Security-Policy': 'CSP configured', 'Referrer-Policy': 'Referrer control', 'Permissions-Policy': 'Permissions control', 'Cross-Origin-Opener-Policy': 'Cross-origin isolation', } all_ok = True try: # Create a context that doesn't verify SSL (for local testing) ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE req = urllib.request.Request(url, method='HEAD') try: with urllib.request.urlopen(req, context=ctx, timeout=10) as response: headers = response.headers status = response.status print(f'[INFO] Response status: {status}') for header, description in required_headers.items(): if header in headers: print(f'[OK] {header}: {description}') else: print(f'[FAIL] {header} is missing: {description}') all_ok = False # Check cookie attributes if any set-cookie headers exist if 'Set-Cookie' in headers: cookie = headers['Set-Cookie'] if 'Secure' in cookie: print('[OK] Cookies have Secure flag') else: print('[WARN] Cookies missing Secure flag') all_ok = False if 'HttpOnly' in cookie: print('[OK] Cookies have HttpOnly flag') else: print('[WARN] Cookies missing HttpOnly flag') all_ok = False if 'SameSite' in cookie: print(f'[OK] Cookies have SameSite={cookie.split("SameSite=")[1].split(";")[0] if "SameSite=" in cookie else "?"}') else: print('[WARN] Cookies missing SameSite attribute') all_ok = False else: print('[INFO] No Set-Cookie headers in response') except urllib.error.HTTPError as e: print(f'[INFO] Got HTTP {e.code} (may need authentication)') # Still check headers even on error responses for header, description in required_headers.items(): if header in e.headers: print(f'[OK] {header}: {description}') else: print(f'[FAIL] {header} is missing: {description}') all_ok = False except urllib.error.URLError as e: print(f'[SKIP] Cannot connect to {url}: {e.reason}') print('[SKIP] Run with --url to check headers') return True # Not a failure, just can't check return all_ok def check_dependencies(): """Run pip-audit to check for known vulnerabilities. Returns: bool: True if no critical vulnerabilities found. """ print('\n' + '=' * 60) print('3. DEPENDENCY VULNERABILITY SCAN') print('=' * 60) try: result = subprocess.run( [sys.executable, '-m', 'pip_audit', '--format', 'json'], capture_output=True, text=True, timeout=60 ) if result.returncode == 0: print('[OK] No known vulnerabilities found') return True else: try: data = json.loads(result.stdout) vulns = data.get('dependencies', []) if vulns: for vuln in vulns: print(f'[FAIL] {vuln["name"]}=={vuln["version"]}: {vuln.get("description", "Vulnerability found")}') return False else: print('[OK] No vulnerabilities found') return True except json.JSONDecodeError: if result.stdout: print(f'[INFO] {result.stdout.strip()}') if result.stderr: print(f'[WARN] {result.stderr.strip()}') return True except FileNotFoundError: print('[SKIP] pip-audit not installed. Run: pip install pip-audit') return True except subprocess.TimeoutExpired: print('[WARN] pip-audit timed out') return True def check_file_permissions(): """Check for common security issues in the project structure. Returns: bool: True if no critical issues found. """ print('\n' + '=' * 60) print('4. PROJECT FILES CHECK') print('=' * 60) all_ok = True # Check .gitignore exists and contains important patterns gitignore_path = os.path.join(os.getcwd(), '.gitignore') if os.path.exists(gitignore_path): required_patterns = ['.env', 'instance/', '*.db', '*.log'] with open(gitignore_path, 'r') as f: content = f.read() for pattern in required_patterns: if pattern in content: print(f'[OK] .gitignore contains: {pattern}') else: print(f'[WARN] .gitignore missing: {pattern}') all_ok = False else: print('[FAIL] .gitignore file not found!') all_ok = False # Check for .env in working directory (should NOT be committed) env_path = os.path.join(os.getcwd(), '.env') if os.path.exists(env_path): print('[INFO] .env file exists (ensure it is NOT committed)') else: print('[WARN] No .env file found') # Check for leftover .pyc or __pycache__ pycache_count = 0 for root, dirs, files in os.walk(os.getcwd()): if '__pycache__' in dirs: pycache_count += 1 for f in files: if f.endswith('.pyc'): pycache_count += 1 if pycache_count == 0: print('[OK] No __pycache__ or .pyc files found') else: print(f'[INFO] Found {pycache_count} cache files/dirs (should be in .gitignore)') return all_ok def check_flask_config(): """Check Flask application configuration for security. Returns: bool: True if configuration looks secure. """ print('\n' + '=' * 60) print('5. FLASK CONFIGURATION CHECK') print('=' * 60) all_ok = True try: from app import create_app app = create_app() # Check session cookie settings cookie_checks = [ ('SESSION_COOKIE_SECURE', True, 'Secure cookies'), ('SESSION_COOKIE_HTTPONLY', True, 'HttpOnly cookies'), ('PERMANENT_SESSION_LIFETIME', 3600, 'Session timeout'), ] for config_key, expected, description in cookie_checks: value = app.config.get(config_key) if config_key == 'PERMANENT_SESSION_LIFETIME': if value and value <= 3600: print(f'[OK] {description}: {value}s') else: print(f'[WARN] {description}: {value}s (should be <= 1 hour)') all_ok = False elif value == expected: print(f'[OK] {description}: enabled') else: print(f'[FAIL] {description}: {value}') all_ok = False # Check MAX_CONTENT_LENGTH max_content = app.config.get('MAX_CONTENT_LENGTH') if max_content: mb = max_content / (1024 * 1024) print(f'[OK] MAX_CONTENT_LENGTH: {mb}MB') else: print('[WARN] MAX_CONTENT_LENGTH not set (unlimited uploads)') all_ok = False # Check CSRF csrf_enabled = app.config.get('WTF_CSRF_ENABLED') if csrf_enabled: print('[OK] CSRF protection: enabled') else: print('[FAIL] CSRF protection: disabled') all_ok = False # Check if app is in DEBUG mode if app.debug: print('[FAIL] DEBUG mode is enabled!') all_ok = False else: print('[OK] DEBUG mode: disabled') except Exception as e: print(f'[SKIP] Cannot check Flask config: {e}') return all_ok def main(): """Run all security checks and produce a summary report. Returns: int: 0 if all checks pass, 1 if any fail. """ import argparse parser = argparse.ArgumentParser(description='Security validation scanner') parser.add_argument('--url', default='http://localhost:5000', help='Application URL to check headers (default: http://localhost:5000)') args = parser.parse_args() print('╔══════════════════════════════════════════════════════════╗') print('║ TEAM TRYOUTS - SECURITY VALIDATION SCANNER ║') print('╠══════════════════════════════════════════════════════════╣') print(f'║ Time: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}') print('╚══════════════════════════════════════════════════════════╝') checks = [ check_environment, lambda: check_https_headers(args.url), check_dependencies, check_file_permissions, check_flask_config, ] results = [] for check in checks: results.append(check()) print('\n' + '=' * 60) print('SUMMARY') print('=' * 60) passed = sum(1 for r in results if r) failed = sum(1 for r in results if not r) total = len(results) print(f'Passed: {passed}/{total}') print(f'Failed: {failed}/{total}') if failed == 0: print('\n[OK] All security checks passed!') return 0 else: print(f'\n[WARN] {failed} check(s) failed. Review the output above.') return 1 if __name__ == '__main__': sys.exit(main())