demander IA de faire tous les modifications pour que le webapp soit pret au déploiement.
Force connection HTTPS, proxy-inversé, WSGI de production, reset cookie de conncection à chaque reconnection, limite sur les mdp, fichiers et One on One par minute, verification d'injection de SQL dans les champs d'entrées. renommage des fichiers lors du téléchargement, fichier de backup quotidien pour la bd et j'ai oublié quelque chose :(
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
"""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 <application_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())
|
||||
Reference in New Issue
Block a user