"""Comprehensive Security Scanner - performs multi-vector scanning on assets.""" import asyncio import aiohttp import socket import ssl from datetime import datetime, timedelta from typing import List, Dict, Optional from app.models.models import Finding, FindingSeverity, FindingCategory, Asset from app.db.session import AsyncSessionLocal from sqlalchemy import select import logging import json logger = logging.getLogger(__name__) class ComprehensiveSecurityScanner: """Multi-vector security scanner with AI-driven analysis.""" async def scan_asset(self, asset_id: str, asset_value: str, asset_type: str) -> List[Dict]: """Perform comprehensive scan on an asset.""" findings = [] logger.info(f"Starting comprehensive scan on {asset_type}: {asset_value}") if asset_type == "domain": findings.extend(await self._scan_domain(asset_value)) elif asset_type == "web_application": findings.extend(await self._scan_web_app(asset_value)) elif asset_type == "api_endpoint": findings.extend(await self._scan_api(asset_value)) elif asset_type == "cloud_resource": findings.extend(await self._scan_cloud(asset_value)) logger.info(f"Scan complete: found {len(findings)} potential issues") return findings async def _scan_domain(self, domain: str) -> List[Dict]: """Scan domain for common issues.""" findings = [] try: # DNS resolution check try: ip = socket.gethostbyname(domain) logger.info(f"Domain {domain} resolves to {ip}") except socket.gaierror: findings.append({ "title": f"Domain {domain} does not resolve", "severity": "high", "category": "external_exposure", "description": "Domain DNS resolution failed - may indicate takeover risk or misconfiguration", "technical": f"DNS lookup for {domain} returned NXDOMAIN", }) # SSL/TLS certificate check try: context = ssl.create_default_context() with socket.create_connection((domain, 443), timeout=5) as sock: with context.wrap_socket(sock, server_hostname=domain) as ssock: cert = ssock.getpeercert() not_after = cert.get('notAfter', '') # Check cert expiry import ssl cert_not_after = datetime.strptime(not_after, '%b %d %H:%M:%S %Y %Z') days_until_expiry = (cert_not_after - datetime.now()).days if days_until_expiry < 30: findings.append({ "title": f"SSL certificate expiring in {days_until_expiry} days", "severity": "medium" if days_until_expiry > 7 else "critical", "category": "external_exposure", "description": f"Certificate will expire on {cert_not_after}", "technical": f"Certificate not_after: {not_after}", }) # Check for weak protocols if ssock.version in ['TLSv1', 'TLSv1.1', 'SSLv3']: findings.append({ "title": f"Weak TLS version detected: {ssock.version}", "severity": "high", "category": "external_exposure", "description": f"Domain uses deprecated {ssock.version}. Should use TLS 1.2+", "technical": f"TLS version: {ssock.version}", }) except Exception as e: logger.warning(f"SSL check failed for {domain}: {e}") # HTTP headers check try: async with aiohttp.ClientSession() as session: async with session.get(f"https://{domain}", timeout=aiohttp.ClientTimeout(total=5)) as resp: headers = resp.headers # Missing security headers security_headers = ['Strict-Transport-Security', 'X-Frame-Options', 'Content-Security-Policy'] missing = [h for h in security_headers if h not in headers] if missing: findings.append({ "title": f"Missing security headers: {', '.join(missing)}", "severity": "medium", "category": "web_application", "description": f"Domain is missing {len(missing)} security headers", "technical": f"Missing: {missing}", }) # Check for information disclosure if 'Server' in headers: findings.append({ "title": f"Server information disclosure: {headers['Server']}", "severity": "low", "category": "web_application", "description": "Server header exposes version information", "technical": f"Server: {headers['Server']}", }) except Exception as e: logger.warning(f"HTTP header check failed for {domain}: {e}") except Exception as e: logger.error(f"Domain scan failed for {domain}: {e}") return findings async def _scan_web_app(self, url: str) -> List[Dict]: """Scan web application.""" findings = await self._scan_domain(url.split('/')[2] if '/' in url else url) try: async with aiohttp.ClientSession() as session: async with session.get(f"https://{url}" if not url.startswith('http') else url, timeout=aiohttp.ClientTimeout(total=5)) as resp: content = await resp.text() # Check for common vulnerabilities in content if 'error' in content.lower() and 'stack' in content.lower(): findings.append({ "title": "Error stack traces exposed in HTML", "severity": "medium", "category": "web_application", "description": "Application leaks stack traces which can aid attackers", "technical": "Stack traces found in page source", }) # Check for debug mode if 'debugbar' in content.lower() or 'debug' in content.lower(): findings.append({ "title": "Debug mode appears to be enabled", "severity": "high", "category": "web_application", "description": "Application appears to be running in debug mode", "technical": "Debug indicators found in page", }) except Exception as e: logger.warning(f"Web app scan failed: {e}") return findings async def _scan_api(self, endpoint: str) -> List[Dict]: """Scan API endpoint.""" findings = [] try: async with aiohttp.ClientSession() as session: # Test unauthenticated access try: async with session.get(endpoint, timeout=aiohttp.ClientTimeout(total=5)) as resp: if resp.status in [200, 201]: findings.append({ "title": f"API endpoint accessible without authentication", "severity": "critical", "category": "external_exposure", "description": f"Endpoint {endpoint} returns data without authentication", "technical": f"HTTP {resp.status} without auth headers", }) except: pass # Test CORS try: async with session.options(endpoint, headers={'Origin': 'http://evil.com'}) as resp: if 'Access-Control-Allow-Origin' in resp.headers: findings.append({ "title": "CORS misconfiguration detected", "severity": "medium", "category": "external_exposure", "description": "API allows cross-origin requests", "technical": f"CORS: {resp.headers.get('Access-Control-Allow-Origin')}", }) except: pass except Exception as e: logger.warning(f"API scan failed: {e}") return findings async def _scan_cloud(self, resource: str) -> List[Dict]: """Scan cloud resource.""" findings = [] # S3 bucket checks if 's3://' in resource or '.s3' in resource: bucket_name = resource.split('/')[-1] if '/' in resource else resource findings.append({ "title": f"S3 bucket {bucket_name} requires permission audit", "severity": "high", "category": "cloud_posture", "description": "S3 bucket should be audited for public access", "technical": f"Bucket: {bucket_name} - Requires ACL review", }) return findings async def run_comprehensive_scan(tenant_id: str, asset_id: str, asset_value: str, asset_type: str) -> List[Finding]: """Run comprehensive scan and store findings in database.""" scanner = ComprehensiveSecurityScanner() scan_results = await scanner.scan_asset(asset_id, asset_value, asset_type) async with AsyncSessionLocal() as db: created_findings = [] for result in scan_results: finding = Finding( tenant_id=tenant_id, title=result['title'], severity=FindingSeverity[result['severity'].lower()], category=FindingCategory[result['category'].lower()], technical_description=result.get('technical', ''), affected_component=asset_value, source="automated_scanner", ai_summary=f"Automated scan detected: {result['title']}", ai_business_impact=result.get('description', ''), ai_impact_level=result['severity'].capitalize(), ai_fix_priority="urgent" if result['severity'] == "critical" else "soon", ) db.add(finding) created_findings.append(finding) await db.commit() logger.info(f"Created {len(created_findings)} findings from scan") return created_findings