MugdhaV commited on
Commit
e1e9580
·
0 Parent(s):

Initial deployment: Gradio frontend with Modal backend - Multi-language security scanner with parallel processing

Browse files
Files changed (10) hide show
  1. .gitattributes +35 -0
  2. .gitignore +33 -0
  3. README.md +75 -0
  4. app.py +10 -0
  5. gradio_app.py +1611 -0
  6. help.html +539 -0
  7. requirements.txt +34 -0
  8. security_checker.py +1945 -0
  9. theme.py +84 -0
  10. ui_components.py +474 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ ENV/
10
+ .venv
11
+
12
+ # Gradio
13
+ flagged/
14
+ gradio_cached_examples/
15
+
16
+ # Testing
17
+ .pytest_cache/
18
+ *.log
19
+
20
+ # IDE
21
+ .vscode/
22
+ .idea/
23
+ *.swp
24
+ *.swo
25
+ *~
26
+
27
+ # OS
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Local environment files (example only, actual secrets in HF Spaces UI)
32
+ .env
33
+ .env.local
README.md ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Security Auditor
3
+ emoji: 🔒
4
+ colorFrom: red
5
+ colorTo: orange
6
+ sdk: gradio
7
+ sdk_version: 4.0.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ short_description: AI-powered security vulnerability scanner with serverless Modal.com backend
12
+ ---
13
+
14
+ # Security Auditor 🔒
15
+
16
+ AI-powered security vulnerability scanner with serverless backend powered by Modal.com.
17
+
18
+ ## Features
19
+
20
+ ✨ **Multi-Language Support**: Python, JavaScript, Java, C/C++, Go, Ruby, PHP, and more
21
+
22
+ ⚡ **Parallel Processing**: Scan hundreds of files in seconds using Modal.com serverless containers
23
+
24
+ 📊 **Comprehensive Reports**: Export findings as JSON or Markdown
25
+
26
+ 🔍 **100+ Vulnerability Patterns**: Detects SQL injection, XSS, hardcoded secrets, insecure crypto, and more
27
+
28
+ ## Architecture
29
+
30
+ - **Frontend**: Gradio (hosted on HuggingFace Spaces)
31
+ - **Backend**: FastAPI + Modal.com (serverless infrastructure)
32
+ - **Parallel Processing**: Up to 100+ concurrent scans via Modal's elastic scaling
33
+
34
+ ## How It Works
35
+
36
+ 1. **Single File Scan**: Paste code directly into the interface
37
+ 2. **Batch Scan**: Upload multiple files or entire project directories
38
+ 3. **Results**: View vulnerabilities organized by severity (Critical, High, Medium, Low)
39
+ 4. **Export**: Download detailed reports in JSON or Markdown format
40
+
41
+ ## Performance
42
+
43
+ - **Single File**: ~2-5 seconds
44
+ - **100 Files**: ~5-15 seconds (10-100x faster than sequential)
45
+ - **500 Files**: ~10-30 seconds
46
+ - **1500+ Files**: ~30-90 seconds
47
+
48
+ ## Security & Privacy
49
+
50
+ - No persistent storage of uploaded code
51
+ - All scans run in isolated, ephemeral containers
52
+ - Containers destroyed immediately after scanning
53
+ - HTTPS encryption for all communications
54
+
55
+ ## Usage
56
+
57
+ 1. **Quick Scan**: Paste code in the "Scan Code" tab
58
+ 2. **Project Scan**: Upload files in the "Scan Directory" tab
59
+ 3. **Review**: Check findings organized by severity
60
+ 4. **Export**: Download reports for documentation
61
+
62
+ ## Technology Stack
63
+
64
+ - **Static Analysis**: Custom SAST engine with pattern matching
65
+ - **Backend**: FastAPI on Modal.com serverless platform
66
+ - **Frontend**: Gradio for interactive web interface
67
+ - **Deployment**: HuggingFace Spaces (frontend) + Modal.com (backend)
68
+
69
+ ## License
70
+
71
+ MIT License - Feel free to use and modify for your projects!
72
+
73
+ ---
74
+
75
+ **Powered by Modal.com serverless infrastructure** ⚡
app.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HuggingFace Spaces Entry Point for Security Auditor
3
+ Launches the Gradio interface with Modal backend integration
4
+ """
5
+
6
+ from gradio_app import SecurityAuditorApp
7
+
8
+ if __name__ == "__main__":
9
+ app = SecurityAuditorApp()
10
+ app.interface.launch()
gradio_app.py ADDED
@@ -0,0 +1,1611 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Modern Gradio Web Interface for Security Auditor
3
+ Based on design mockups in assets/ folder
4
+ """
5
+
6
+ import gradio as gr
7
+ import asyncio
8
+ import tempfile
9
+ import shutil
10
+ from pathlib import Path
11
+ from datetime import datetime
12
+ from typing import List, Dict
13
+ import uuid
14
+ import os
15
+ import sys
16
+ import time
17
+ import threading
18
+ import json
19
+ import base64
20
+ import re
21
+ import httpx
22
+ from concurrent.futures import ThreadPoolExecutor
23
+ import multiprocessing
24
+
25
+ # Import existing security checker (for local mode compatibility)
26
+ from security_checker import SecurityChecker, RiskLevel
27
+
28
+ # Modal backend URL - set via environment variable or use local default
29
+ MODAL_BACKEND_URL = os.getenv("MODAL_BACKEND_URL", "http://localhost:8000")
30
+ USE_MODAL_BACKEND = os.getenv("USE_MODAL_BACKEND", "false").lower() == "true"
31
+
32
+ # Configuration for hybrid parallel processing
33
+ PARALLEL_FILE_READING_WORKERS = min(multiprocessing.cpu_count() * 2, 16)
34
+ PARALLEL_CHUNK_LIMIT = 3 # Max concurrent Modal API calls
35
+ CHUNK_SIZE = 500 # Files per chunk
36
+
37
+
38
+ class ModalScanResult:
39
+ """
40
+ Adapter class to convert Modal backend API responses to ScanResult-like format
41
+ for compatibility with existing UI code
42
+ """
43
+ def __init__(self, modal_response: dict, target: str, files_scanned: int = 1):
44
+ self.target = target
45
+ self.scan_type = "local"
46
+ self.files_scanned = files_scanned
47
+ self.errors = []
48
+
49
+ # Convert Modal response vulnerabilities to Vulnerability objects
50
+ if modal_response.get("success") and "findings" in modal_response:
51
+ vuln_data = modal_response["findings"].get("vulnerabilities", [])
52
+ self.vulnerabilities = []
53
+ for v in vuln_data:
54
+ # Create a simple object with attributes matching Vulnerability
55
+ vuln_obj = type('Vulnerability', (), v)()
56
+ vuln_obj.risk_level = type('RiskLevel', (), {'value': v.get('risk_level', 'INFO'), 'name': v.get('risk_level', 'INFO')})()
57
+ self.vulnerabilities.append(vuln_obj)
58
+ else:
59
+ self.vulnerabilities = []
60
+ if not modal_response.get("success"):
61
+ self.errors.append(modal_response.get("summary", {}).get("error", "Unknown error"))
62
+
63
+ def summary(self) -> dict:
64
+ """Generate a summary compatible with UI expectations"""
65
+ summary = {level.value: 0 for level in RiskLevel}
66
+ for vuln in self.vulnerabilities:
67
+ if hasattr(vuln, 'risk_level'):
68
+ summary[vuln.risk_level.value] += 1
69
+
70
+ return {
71
+ "target": self.target,
72
+ "scan_type": self.scan_type,
73
+ "duration_seconds": 0, # Not tracked in this simple adapter
74
+ "files_scanned": self.files_scanned,
75
+ "total_vulnerabilities": len(self.vulnerabilities),
76
+ **summary
77
+ }
78
+
79
+ # Import custom theme and UI components
80
+ from theme import create_security_auditor_theme
81
+ from ui_components import (
82
+ create_severity_badge,
83
+ create_finding_card,
84
+ create_summary_section,
85
+ create_empty_state,
86
+ create_loading_state
87
+ )
88
+
89
+
90
+ class ModernSecurityAuditorApp:
91
+ """
92
+ Modern Gradio web interface for Security Auditor.
93
+ Based on design mockups in assets/ folder.
94
+ """
95
+
96
+ def __init__(self):
97
+ # Only instantiate SecurityChecker if not using Modal backend
98
+ self.use_modal_backend = USE_MODAL_BACKEND
99
+ self.modal_backend_url = MODAL_BACKEND_URL
100
+ if not self.use_modal_backend:
101
+ self.checker = SecurityChecker()
102
+ else:
103
+ self.checker = None # Not needed when using Modal backend
104
+ self.active_sessions = {}
105
+ self.cleanup_interval = 3600 # 1 hour
106
+ self.max_upload_size_mb = 100
107
+ self._help_html = self._prepare_help_content()
108
+
109
+ @staticmethod
110
+ def read_single_file(file_path: Path, target_path: Path, supported_extensions: set) -> dict:
111
+ """Read a single file - designed for parallel execution"""
112
+ try:
113
+ if file_path.suffix not in supported_extensions:
114
+ return None
115
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
116
+ code = f.read()
117
+ return {
118
+ "code": code,
119
+ "language": file_path.suffix[1:], # Remove the dot
120
+ "filename": str(file_path.relative_to(target_path))
121
+ }
122
+ except Exception as e:
123
+ print(f"Error reading {file_path}: {e}")
124
+ return None
125
+
126
+ async def read_files_parallel(self, file_paths: List[Path], target_path: Path, max_workers: int = 8) -> List[dict]:
127
+ """Read multiple files in parallel using ThreadPoolExecutor"""
128
+ supported_extensions = {'.py', '.js', '.java', '.go', '.php', '.rb', '.rs', '.cpp', '.c', '.ts', '.jsx', '.tsx'}
129
+ loop = asyncio.get_event_loop()
130
+
131
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
132
+ tasks = [
133
+ loop.run_in_executor(
134
+ executor,
135
+ self.read_single_file,
136
+ file_path,
137
+ target_path,
138
+ supported_extensions
139
+ )
140
+ for file_path in file_paths
141
+ ]
142
+ results = await asyncio.gather(*tasks)
143
+
144
+ # Filter out None results (errors or unsupported files)
145
+ return [r for r in results if r is not None]
146
+
147
+ async def process_chunks_parallel(self, chunks: List[List[dict]]) -> List[dict]:
148
+ """Process multiple chunks in parallel via Modal backend with rate limiting"""
149
+ semaphore = asyncio.Semaphore(PARALLEL_CHUNK_LIMIT)
150
+
151
+ async def _scan_with_limit(chunk):
152
+ async with semaphore:
153
+ return await self._scan_batch_via_modal(chunk)
154
+
155
+ tasks = [_scan_with_limit(chunk) for chunk in chunks]
156
+ results = await asyncio.gather(*tasks, return_exceptions=True)
157
+
158
+ # Handle any exceptions
159
+ valid_results = []
160
+ for i, result in enumerate(results):
161
+ if isinstance(result, Exception):
162
+ print(f"Chunk {i+1} failed: {result}")
163
+ else:
164
+ valid_results.append(result)
165
+
166
+ return valid_results
167
+
168
+ async def _scan_code_via_modal(self, code: str, language: str = "python") -> dict:
169
+ """
170
+ Call Modal backend API to scan code
171
+
172
+ Args:
173
+ code: Source code to scan
174
+ language: Programming language (default: python)
175
+
176
+ Returns:
177
+ Dict with scan results from Modal backend
178
+ """
179
+ try:
180
+ async with httpx.AsyncClient(timeout=300.0) as client:
181
+ response = await client.post(
182
+ f"{self.modal_backend_url}/scan",
183
+ json={
184
+ "code": code,
185
+ "language": language,
186
+ "scan_type": "all"
187
+ }
188
+ )
189
+ response.raise_for_status()
190
+ return response.json()
191
+ except httpx.HTTPError as e:
192
+ raise Exception(f"Modal backend error: {str(e)}")
193
+ except Exception as e:
194
+ raise Exception(f"Failed to connect to Modal backend: {str(e)}")
195
+
196
+ async def _scan_batch_via_modal(self, batch_files: List[Dict]) -> dict:
197
+ """
198
+ Call Modal backend API to scan multiple files in parallel using batch endpoint
199
+
200
+ Args:
201
+ batch_files: List of dicts with 'code', 'language', and 'filename' keys
202
+
203
+ Returns:
204
+ Dict with batch scan results from Modal backend
205
+ """
206
+ try:
207
+ # Extended timeout for batch processing (10 minutes)
208
+ async with httpx.AsyncClient(timeout=600.0) as client:
209
+ response = await client.post(
210
+ f"{self.modal_backend_url}/scan/batch",
211
+ json=batch_files
212
+ )
213
+ response.raise_for_status()
214
+ return response.json()
215
+ except httpx.HTTPError as e:
216
+ raise Exception(f"Modal batch backend error: {str(e)}")
217
+ except Exception as e:
218
+ raise Exception(f"Failed to connect to Modal batch backend: {str(e)}")
219
+
220
+ def _prepare_help_content(self):
221
+ """Read help.html and embed images as base64 data URIs for self-contained display."""
222
+ help_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "help.html")
223
+ img_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets", "docimages")
224
+
225
+ try:
226
+ with open(help_path, "r", encoding="utf-8") as f:
227
+ html = f.read()
228
+ except FileNotFoundError:
229
+ return "<html><body><h1>Help file not found</h1></body></html>"
230
+
231
+ def replace_img_src(match):
232
+ filename = match.group(1)
233
+ img_path = os.path.join(img_dir, filename)
234
+ try:
235
+ with open(img_path, "rb") as img_f:
236
+ b64 = base64.b64encode(img_f.read()).decode("ascii")
237
+ return f'src="data:image/png;base64,{b64}"'
238
+ except FileNotFoundError:
239
+ return match.group(0)
240
+
241
+ html = re.sub(r'src="/helpimg/([^"]+)"', replace_img_src, html)
242
+ return html
243
+
244
+ def create_interface(self) -> gr.Blocks:
245
+ """Create modern Gradio interface matching design mockups."""
246
+
247
+ self.theme = create_security_auditor_theme()
248
+
249
+ # Prepare help HTML as JSON-safe string for client-side Blob URL
250
+ self._help_js = json.dumps(self._help_html)
251
+
252
+ # Custom CSS for additional styling
253
+ self.custom_css = """
254
+ /* Tabler Icons CDN */
255
+ @import url('https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/tabler-icons.min.css');
256
+
257
+ /* Anthropic-inspired Theme Overrides */
258
+ :root {
259
+ --anthropic-cream: #faf9f6;
260
+ --anthropic-slate: #131314;
261
+ --anthropic-terracotta: #d97757;
262
+ --anthropic-terracotta-hover: #cc6944;
263
+ --anthropic-gray: #6b7280;
264
+ --anthropic-border: #e5e7eb;
265
+ }
266
+
267
+ /* Logo and branding - Anthropic style */
268
+ .logo-container {
269
+ display: flex;
270
+ align-items: center;
271
+ gap: 12px;
272
+ padding: 20px 24px;
273
+ border-bottom: 1px solid var(--anthropic-border);
274
+ background: var(--anthropic-cream);
275
+ }
276
+
277
+ .logo-icon {
278
+ width: 40px;
279
+ height: 40px;
280
+ background: var(--anthropic-slate);
281
+ border-radius: 6px;
282
+ display: flex;
283
+ align-items: center;
284
+ justify-content: center;
285
+ color: white;
286
+ }
287
+
288
+ .logo-icon svg {
289
+ width: 24px;
290
+ height: 24px;
291
+ stroke: white;
292
+ }
293
+
294
+ /* Section headers - cleaner, less shouty */
295
+ .section-header {
296
+ font-size: 13px;
297
+ font-weight: 600;
298
+ color: var(--anthropic-gray);
299
+ text-transform: none;
300
+ letter-spacing: normal;
301
+ margin: 20px 0 12px 0;
302
+ display: flex;
303
+ align-items: center;
304
+ gap: 8px;
305
+ }
306
+
307
+ .section-header svg {
308
+ width: 18px;
309
+ height: 18px;
310
+ stroke: var(--anthropic-gray);
311
+ }
312
+
313
+ /* Mode selector header */
314
+ .mode-selector-header {
315
+ font-size: 13px;
316
+ font-weight: 600;
317
+ color: var(--anthropic-gray);
318
+ text-transform: none;
319
+ letter-spacing: normal;
320
+ margin-bottom: 12px;
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 8px;
324
+ }
325
+
326
+ .mode-selector-header svg {
327
+ width: 18px;
328
+ height: 18px;
329
+ stroke: var(--anthropic-gray);
330
+ }
331
+
332
+ /* Gradio overrides */
333
+ .gradio-container {
334
+ max-width: 1400px !important;
335
+ }
336
+
337
+ /* Card styling */
338
+ .card-title {
339
+ margin: 0 0 8px 0;
340
+ font-size: 16px;
341
+ font-weight: 600;
342
+ color: var(--anthropic-slate);
343
+ display: flex;
344
+ align-items: center;
345
+ gap: 8px;
346
+ }
347
+
348
+ .card-title svg {
349
+ width: 20px;
350
+ height: 20px;
351
+ stroke: var(--anthropic-terracotta);
352
+ }
353
+
354
+ .card-description {
355
+ margin: 0 0 16px 0;
356
+ color: var(--anthropic-gray);
357
+ font-size: 14px;
358
+ }
359
+
360
+ /* NVD Toggle Switch Styling */
361
+ .nvd-toggle {
362
+ margin: 0 !important;
363
+ padding: 0 !important;
364
+ }
365
+
366
+ .nvd-toggle input[type="checkbox"] {
367
+ appearance: none;
368
+ -webkit-appearance: none;
369
+ width: 48px;
370
+ height: 26px;
371
+ background: #e5e7eb;
372
+ border-radius: 13px;
373
+ position: relative;
374
+ cursor: pointer;
375
+ transition: background 0.3s ease;
376
+ margin: 0;
377
+ }
378
+
379
+ .nvd-toggle input[type="checkbox"]:checked {
380
+ background: var(--anthropic-terracotta);
381
+ }
382
+
383
+ .nvd-toggle input[type="checkbox"]::before {
384
+ content: "";
385
+ position: absolute;
386
+ width: 22px;
387
+ height: 22px;
388
+ border-radius: 50%;
389
+ background: white;
390
+ top: 2px;
391
+ left: 2px;
392
+ transition: left 0.3s ease;
393
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
394
+ }
395
+
396
+ .nvd-toggle input[type="checkbox"]:checked::before {
397
+ left: 24px;
398
+ }
399
+
400
+ .nvd-toggle label {
401
+ display: flex;
402
+ align-items: center;
403
+ cursor: pointer;
404
+ }
405
+
406
+ /* Tooltip positioning */
407
+ .info-icon-tooltip {
408
+ position: relative;
409
+ }
410
+
411
+ /* Show tooltip on hover over the NVD label row */
412
+ .nvd-label-row:hover .info-tooltip-text {
413
+ visibility: visible !important;
414
+ opacity: 1 !important;
415
+ }
416
+
417
+ /* Info badge - subtle, minimal */
418
+ .info-badge {
419
+ display: flex;
420
+ align-items: center;
421
+ gap: 12px;
422
+ padding: 16px;
423
+ background: #ffffff;
424
+ border: 1px solid var(--anthropic-border);
425
+ border-radius: 6px;
426
+ margin-top: 20px;
427
+ }
428
+
429
+ .info-badge-icon svg {
430
+ width: 24px;
431
+ height: 24px;
432
+ stroke: var(--anthropic-terracotta);
433
+ }
434
+
435
+ .info-badge-content {
436
+ flex: 1;
437
+ }
438
+
439
+ .info-badge-title {
440
+ font-size: 14px;
441
+ font-weight: 600;
442
+ color: var(--anthropic-slate);
443
+ margin-bottom: 2px;
444
+ }
445
+
446
+ .info-badge-subtitle {
447
+ font-size: 12px;
448
+ color: var(--anthropic-gray);
449
+ }
450
+
451
+ /* Analyze button - Anthropic terracotta */
452
+ .analyze-button {
453
+ height: 100% !important;
454
+ min-height: 56px !important;
455
+ font-size: 15px !important;
456
+ font-weight: 600 !important;
457
+ display: flex !important;
458
+ align-items: center !important;
459
+ justify-content: center !important;
460
+ gap: 8px !important;
461
+ background: var(--anthropic-terracotta) !important;
462
+ border: none !important;
463
+ border-radius: 6px !important;
464
+ transition: background 0.2s ease !important;
465
+ }
466
+
467
+ .analyze-button:hover {
468
+ background: var(--anthropic-terracotta-hover) !important;
469
+ }
470
+
471
+ .analyze-button svg {
472
+ width: 20px;
473
+ height: 20px;
474
+ stroke: white;
475
+ }
476
+
477
+ /* Reset button - prominent */
478
+ .reset-button {
479
+ min-height: 44px !important;
480
+ font-size: 15px !important;
481
+ font-weight: 600 !important;
482
+ background: var(--anthropic-terracotta) !important;
483
+ border: none !important;
484
+ border-radius: 6px !important;
485
+ color: white !important;
486
+ transition: background 0.2s ease !important;
487
+ }
488
+
489
+ .reset-button:hover {
490
+ background: var(--anthropic-terracotta-hover) !important;
491
+ }
492
+
493
+ /* Help button - outlined, distinct from primary actions */
494
+ .help-button {
495
+ min-height: 44px !important;
496
+ font-size: 15px !important;
497
+ font-weight: 600 !important;
498
+ background: transparent !important;
499
+ border: 2px solid var(--anthropic-border) !important;
500
+ border-radius: 6px !important;
501
+ color: var(--anthropic-gray) !important;
502
+ transition: all 0.2s ease !important;
503
+ margin-top: 8px !important;
504
+ }
505
+
506
+ .help-button:hover {
507
+ border-color: var(--anthropic-terracotta) !important;
508
+ color: var(--anthropic-terracotta) !important;
509
+ background: rgba(217, 119, 87, 0.05) !important;
510
+ }
511
+
512
+ /* Export button styling */
513
+ .export-button {
514
+ display: inline-flex !important;
515
+ align-items: center !important;
516
+ gap: 6px !important;
517
+ }
518
+
519
+ /* Button icons via pseudo-elements */
520
+ .analyze-button::before {
521
+ content: "";
522
+ display: inline-block;
523
+ width: 20px;
524
+ height: 20px;
525
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cpath d='m21 21-4.35-4.35'%3E%3C/path%3E%3C/svg%3E");
526
+ background-size: contain;
527
+ background-repeat: no-repeat;
528
+ }
529
+
530
+ .export-button::before {
531
+ content: "";
532
+ display: inline-block;
533
+ width: 18px;
534
+ height: 18px;
535
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'%3E%3C/path%3E%3Cpolyline points='7 10 12 15 17 10'%3E%3C/polyline%3E%3Cline x1='12' y1='15' x2='12' y2='3'%3E%3C/line%3E%3C/svg%3E");
536
+ background-size: contain;
537
+ background-repeat: no-repeat;
538
+ }
539
+
540
+ /* Severity badges - static visual indicators */
541
+ .severity-badge {
542
+ position: relative;
543
+ }
544
+
545
+ /* JavaScript for moving NVD toggle */
546
+ <script>
547
+ document.addEventListener('DOMContentLoaded', function() {
548
+ // Move NVD toggle into the container
549
+ setTimeout(function() {
550
+ const container = document.getElementById('nvd-toggle-container');
551
+ const toggle = document.querySelector('.nvd-toggle');
552
+ if (container && toggle) {
553
+ container.appendChild(toggle);
554
+ }
555
+ }, 100);
556
+ });
557
+ </script>
558
+ """
559
+
560
+ with gr.Blocks(title="Security Auditor") as app:
561
+
562
+ # Header with logo
563
+ gr.HTML("""
564
+ <div class="logo-container">
565
+ <div class="logo-icon">
566
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
567
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
568
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
569
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
570
+ </svg>
571
+ </div>
572
+ <div>
573
+ <h1 style="margin: 0; font-size: 18px; font-weight: 700; color: #131314;">Security Auditor</h1>
574
+ </div>
575
+ </div>
576
+ """)
577
+
578
+ gr.Markdown("## Scan your application code for security vulnerabilities and get remediation guidance")
579
+
580
+ # Main layout
581
+ with gr.Row():
582
+ # Sidebar
583
+ with gr.Column(scale=1, min_width=250):
584
+ # Analysis Mode with integrated settings
585
+ with gr.Group():
586
+ gr.HTML('''<div class="mode-selector-header">
587
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
588
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
589
+ <line x1="12" y1="20" x2="12" y2="10"></line>
590
+ <line x1="18" y1="20" x2="18" y2="4"></line>
591
+ <line x1="6" y1="20" x2="6" y2="16"></line>
592
+ </svg>
593
+ Analysis Mode
594
+ </div>''')
595
+
596
+ scan_mode = gr.Radio(
597
+ choices=["Local Directory", "Remote URL"],
598
+ value="Local Directory",
599
+ label="",
600
+ container=False,
601
+ interactive=True
602
+ )
603
+
604
+ # NVD Enrichment toggle with info icon - side by side layout
605
+ gr.HTML('''
606
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-top: 16px; gap: 12px;">
607
+ <div class="nvd-label-row" style="display: flex; align-items: center; gap: 8px; flex-shrink: 1; min-width: 0; cursor: default;">
608
+ <span style="
609
+ font-size: 14px;
610
+ font-weight: 600;
611
+ color: #131314;
612
+ white-space: nowrap;
613
+ ">NVD Enriched Scan Results</span>
614
+ <div class="info-icon-tooltip" style="position: relative; display: inline-flex; align-items: center; flex-shrink: 0;">
615
+ <svg class="info-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
616
+ fill="none" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
617
+ style="cursor: pointer; display: block;"
618
+ onclick="toggleTooltip(this)">
619
+ <circle cx="12" cy="12" r="10"></circle>
620
+ <line x1="12" y1="16" x2="12" y2="12"></line>
621
+ <line x1="12" y1="8" x2="12.01" y2="8"></line>
622
+ </svg>
623
+ <div class="info-tooltip-text" style="
624
+ visibility: hidden;
625
+ opacity: 0;
626
+ position: absolute;
627
+ bottom: calc(100% + 8px);
628
+ left: 50%;
629
+ transform: translateX(-50%);
630
+ background: #131314;
631
+ color: white;
632
+ padding: 12px 16px;
633
+ border-radius: 6px;
634
+ font-size: 13px;
635
+ font-weight: 400;
636
+ white-space: normal;
637
+ width: 280px;
638
+ line-height: 1.5;
639
+ z-index: 9999;
640
+ transition: opacity 0.2s, visibility 0.2s;
641
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
642
+ ">
643
+ Enriches scan results with related Common Vulnerabilities and Exposures (CVE) references from the National Vulnerability Database (NVD).
644
+ </div>
645
+ </div>
646
+ </div>
647
+ <div id="nvd-toggle-container" style="flex-shrink: 0;"></div>
648
+ </div>
649
+ <script>
650
+ (function() {
651
+ var outsideClickHandler = null;
652
+
653
+ window.toggleTooltip = function(icon) {
654
+ var tooltip = icon.nextElementSibling;
655
+ var isVisible = tooltip.style.visibility === 'visible';
656
+
657
+ // Hide all tooltips first
658
+ document.querySelectorAll('.info-tooltip-text').forEach(function(t) {
659
+ t.style.visibility = 'hidden';
660
+ t.style.opacity = '0';
661
+ });
662
+
663
+ // Remove any existing outside click handler
664
+ if (outsideClickHandler) {
665
+ document.removeEventListener('click', outsideClickHandler);
666
+ outsideClickHandler = null;
667
+ }
668
+
669
+ if (!isVisible) {
670
+ tooltip.style.visibility = 'visible';
671
+ tooltip.style.opacity = '1';
672
+
673
+ setTimeout(function() {
674
+ outsideClickHandler = function(e) {
675
+ if (!icon.contains(e.target) && !tooltip.contains(e.target)) {
676
+ tooltip.style.visibility = 'hidden';
677
+ tooltip.style.opacity = '0';
678
+ document.removeEventListener('click', outsideClickHandler);
679
+ outsideClickHandler = null;
680
+ }
681
+ };
682
+ document.addEventListener('click', outsideClickHandler);
683
+ }, 100);
684
+ }
685
+ };
686
+ })();
687
+ </script>
688
+ ''')
689
+
690
+ nvd_enrichment = gr.Checkbox(
691
+ label=None,
692
+ value=True,
693
+ elem_classes=["nvd-toggle"],
694
+ container=False,
695
+ show_label=False
696
+ )
697
+
698
+
699
+ # Not necessary to display Actions label with custom lightbolt icon as there is only one action.
700
+ # gr.HTML('''<div class="section-header">
701
+ # <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"
702
+ # fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
703
+ # <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon>
704
+ # </svg>
705
+ # Actions
706
+ # </div>''')
707
+
708
+ reset_btn = gr.Button("Reset", variant="primary", size="lg", elem_classes=["reset-button"])
709
+
710
+ help_btn = gr.Button("Help", variant="secondary", size="lg", elem_classes=["help-button"])
711
+
712
+ # Info badge - Engine information
713
+ gr.HTML('''
714
+ <div class="info-badge">
715
+ <div class="info-badge-icon">
716
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
717
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
718
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
719
+ </svg>
720
+ </div>
721
+ <div class="info-badge-content">
722
+ <div class="info-badge-title">SAST + DAST + NVD</div>
723
+ <div class="info-badge-subtitle">40+ Vulnerability Checks</div>
724
+ </div>
725
+ </div>
726
+ ''')
727
+
728
+ # Main content area
729
+ with gr.Column(scale=3):
730
+
731
+ # Input section (changes based on mode)
732
+ with gr.Group():
733
+ # Local Directory mode
734
+ with gr.Column(visible=True) as local_mode:
735
+ gr.HTML("""
736
+ <div style="background: white; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; margin-bottom: 16px;">
737
+ <h3 class="card-title">
738
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
739
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
740
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
741
+ </svg>
742
+ Application Directory
743
+ </h3>
744
+ <p class="card-description">Scan local directory containing the application code.</p>
745
+ </div>
746
+ """)
747
+
748
+ file_upload = gr.File(
749
+ label="Upload Files - Total Size Maximum 25 MB",
750
+ file_count="multiple",
751
+ file_types=[".py", ".js", ".ts", ".java", ".php", ".go", ".rb", ".c", ".cpp", ".cs", ".swift", ".kt", ".scala", ".rs", ".jsx", ".tsx"],
752
+ height=120
753
+ )
754
+
755
+ gr.HTML("""
756
+ <p style="color: #9ca3af; font-size: 12px; margin: -8px 0 16px 0; line-height: 1.5;">
757
+ Accepted: .py, .js, .ts, .java, .php, .go, .rb, .c, .cpp, .cs, .swift, .kt, .scala, .rs, .jsx, .tsx
758
+ </p>
759
+ """)
760
+
761
+ directory_path = gr.Textbox(
762
+ label="Or Enter Directory Path",
763
+ placeholder="C:/Projects/my-application",
764
+ lines=2,
765
+ max_lines=3
766
+ )
767
+
768
+ analyze_btn_local = gr.Button(
769
+ "Analyze",
770
+ variant="primary",
771
+ size="lg",
772
+ elem_classes=["analyze-button"]
773
+ )
774
+
775
+ # Remote URL mode
776
+ with gr.Column(visible=False) as url_mode:
777
+ gr.HTML("""
778
+ <div style="background: white; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; margin-bottom: 16px;">
779
+ <h3 class="card-title">
780
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
781
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
782
+ <circle cx="12" cy="12" r="10"></circle>
783
+ <line x1="2" y1="12" x2="22" y2="12"></line>
784
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
785
+ </svg>
786
+ Application URL
787
+ </h3>
788
+ <p class="card-description">Scan remote web application or deployment.</p>
789
+ </div>
790
+ """)
791
+
792
+ web_url = gr.Textbox(
793
+ label="Web Application URL",
794
+ placeholder="https://your-app.example.com",
795
+ lines=2,
796
+ max_lines=3
797
+ )
798
+
799
+ analyze_btn_url = gr.Button(
800
+ "Analyze",
801
+ variant="primary",
802
+ size="lg",
803
+ elem_classes=["analyze-button"]
804
+ )
805
+
806
+ # Progress indicator
807
+ progress_box = gr.HTML(value=create_empty_state())
808
+
809
+ # Results section
810
+ results_section = gr.Column(visible=False)
811
+ with results_section:
812
+ # Analysis Summary
813
+ summary_html = gr.HTML()
814
+
815
+ # Security Findings
816
+ gr.HTML("""
817
+ <h2 style="
818
+ margin: 16px 0 13px 0;
819
+ font-size: 20px;
820
+ font-weight: 700;
821
+ color: #131314;
822
+ display: flex;
823
+ align-items: center;
824
+ gap: 8px;
825
+ ">
826
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
827
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
828
+ style="stroke: #d97757;">
829
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
830
+ <line x1="12" y1="9" x2="12" y2="13"></line>
831
+ <line x1="12" y1="17" x2="12.01" y2="17"></line>
832
+ </svg>
833
+ Security Findings
834
+ </h2>
835
+ """)
836
+
837
+ findings_count = gr.HTML()
838
+ findings_html = gr.HTML()
839
+
840
+ # Export button
841
+ with gr.Row():
842
+ export_json_btn = gr.Button(
843
+ "Export JSON Report",
844
+ variant="primary",
845
+ size="lg",
846
+ elem_classes=["export-button"]
847
+ )
848
+ export_md_btn = gr.Button(
849
+ "Export Markdown Report",
850
+ variant="primary",
851
+ size="lg",
852
+ elem_classes=["export-button"]
853
+ )
854
+
855
+ download_file = gr.File(label="Download Report", visible=False)
856
+ download_file_md = gr.File(label="Download Markdown Report", visible=False)
857
+
858
+ # Event handlers
859
+
860
+ def toggle_mode(mode):
861
+ """Toggle visibility based on selected mode."""
862
+ return {
863
+ local_mode: gr.update(visible=(mode == "Local Directory")),
864
+ url_mode: gr.update(visible=(mode == "Remote URL"))
865
+ }
866
+
867
+ scan_mode.change(
868
+ fn=toggle_mode,
869
+ inputs=[scan_mode],
870
+ outputs=[local_mode, url_mode]
871
+ )
872
+
873
+ def reset_interface():
874
+ """Reset the interface to initial state."""
875
+ return (
876
+ create_empty_state(), # progress_box
877
+ gr.update(visible=False), # results_section
878
+ "", # summary_html
879
+ "", # findings_count
880
+ "", # findings_html
881
+ None, # file_upload
882
+ "", # directory_path
883
+ "", # web_url
884
+ )
885
+
886
+ reset_btn.click(
887
+ fn=reset_interface,
888
+ outputs=[
889
+ progress_box,
890
+ results_section,
891
+ summary_html,
892
+ findings_count,
893
+ findings_html,
894
+ file_upload,
895
+ directory_path,
896
+ web_url
897
+ ]
898
+ )
899
+
900
+ help_btn.click(
901
+ fn=None,
902
+ inputs=[],
903
+ outputs=[],
904
+ js=f"() => {{ const html = {self._help_js}; const blob = new Blob([html], {{type:'text/html;charset=utf-8'}}); window.open(URL.createObjectURL(blob), '_blank'); }}"
905
+ )
906
+
907
+ def scan_local_files(files, dir_path, nvd_check):
908
+ """Handle local file/directory scanning."""
909
+ # Show loading state
910
+ yield (
911
+ create_loading_state("Initializing scan..."),
912
+ gr.update(visible=False),
913
+ "", "", "", None, None
914
+ )
915
+
916
+ # Check if we have files or directory path
917
+ if not files and not dir_path:
918
+ yield (
919
+ """<div style="background: #fef2f2; border: 1px solid #fca5a5; border-radius: 12px; padding: 20px; color: #dc2626;">
920
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
921
+ fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
922
+ style="display: inline-block; vertical-align: middle; margin-right: 8px;">
923
+ <circle cx="12" cy="12" r="10"></circle>
924
+ <line x1="15" y1="9" x2="9" y2="15"></line>
925
+ <line x1="9" y1="9" x2="15" y2="15"></line>
926
+ </svg>
927
+ <strong>No input provided</strong><br/>
928
+ Please upload files or enter a directory path.
929
+ </div>""",
930
+ gr.update(visible=False),
931
+ "", "", "", None, None
932
+ )
933
+ return
934
+
935
+ # Create session
936
+ session_id = str(uuid.uuid4())
937
+ session_dir = Path(tempfile.mkdtemp(prefix=f"audit_{session_id}_"))
938
+
939
+ try:
940
+ target_path = session_dir
941
+
942
+ # If files uploaded, save them
943
+ if files:
944
+ yield (
945
+ create_loading_state(f"Uploading {len(files)} files..."),
946
+ gr.update(visible=False),
947
+ "", "", "", None, None
948
+ )
949
+ for file in files:
950
+ dest = session_dir / Path(file.name).name
951
+ shutil.copy(file.name, dest)
952
+
953
+ # If directory path provided, use it directly (if exists)
954
+ elif dir_path and os.path.exists(dir_path):
955
+ target_path = Path(dir_path)
956
+
957
+ yield (
958
+ create_loading_state("Scanning for vulnerabilities..."),
959
+ gr.update(visible=False),
960
+ "", "", "", None, None
961
+ )
962
+
963
+ # Run scan (Modal backend or local)
964
+ if self.use_modal_backend:
965
+ # Use Modal backend API with parallel batch processing
966
+ loop = asyncio.new_event_loop()
967
+ asyncio.set_event_loop(loop)
968
+
969
+ # Collect all file paths first
970
+ file_paths = [
971
+ file_path
972
+ for file_path in target_path.rglob("*")
973
+ if file_path.is_file() and file_path.suffix in ['.py', '.js', '.java', '.go', '.php', '.rb', '.rs', '.cpp', '.c', '.ts', '.jsx', '.tsx']
974
+ ]
975
+
976
+ # Read files in parallel
977
+ yield (
978
+ create_loading_state(f"Reading {len(file_paths)} files in parallel..."),
979
+ gr.update(visible=False),
980
+ "", "", "", None, None
981
+ )
982
+
983
+ batch_files = loop.run_until_complete(
984
+ self.read_files_parallel(file_paths, target_path, max_workers=PARALLEL_FILE_READING_WORKERS)
985
+ )
986
+
987
+ # Process files in parallel using batch endpoint
988
+ # Split into chunks if batch is too large (Modal backend limit)
989
+ all_vulnerabilities = []
990
+
991
+ if len(batch_files) <= CHUNK_SIZE:
992
+ # Single batch - process all files in parallel
993
+ yield (
994
+ create_loading_state(f"Processing {len(batch_files)} files in parallel on Modal..."),
995
+ gr.update(visible=False),
996
+ "", "", "", None, None
997
+ )
998
+
999
+ try:
1000
+ batch_result = loop.run_until_complete(
1001
+ self._scan_batch_via_modal(batch_files)
1002
+ )
1003
+
1004
+ if batch_result.get("success") and "results" in batch_result:
1005
+ for file_result in batch_result["results"]:
1006
+ if file_result.get("status") == "success" and "vulnerabilities" in file_result:
1007
+ all_vulnerabilities.extend(file_result["vulnerabilities"])
1008
+ elif file_result.get("status") == "error":
1009
+ print(f"Error scanning {file_result.get('filename', 'unknown')}: {file_result.get('error', 'Unknown error')}")
1010
+ except Exception as e:
1011
+ print(f"Batch scan error: {e}")
1012
+ # Fall back to sequential processing on error
1013
+ for file_data in batch_files:
1014
+ try:
1015
+ modal_response = loop.run_until_complete(
1016
+ self._scan_code_via_modal(file_data["code"], file_data["language"])
1017
+ )
1018
+ if modal_response.get("success") and "findings" in modal_response:
1019
+ vulns = modal_response["findings"].get("vulnerabilities", [])
1020
+ for v in vulns:
1021
+ v["file_path"] = file_data["filename"]
1022
+ all_vulnerabilities.extend(vulns)
1023
+ except Exception as file_error:
1024
+ print(f"Error scanning {file_data['filename']}: {file_error}")
1025
+ else:
1026
+ # Split into chunks and process all chunks in parallel
1027
+ chunks = [batch_files[i:i + CHUNK_SIZE] for i in range(0, len(batch_files), CHUNK_SIZE)]
1028
+
1029
+ yield (
1030
+ create_loading_state(f"Processing {len(chunks)} chunks in parallel on Modal ({len(batch_files)} files total)..."),
1031
+ gr.update(visible=False),
1032
+ "", "", "", None, None
1033
+ )
1034
+
1035
+ try:
1036
+ # Process all chunks in parallel
1037
+ chunk_results = loop.run_until_complete(
1038
+ self.process_chunks_parallel(chunks)
1039
+ )
1040
+
1041
+ # Aggregate results from all chunks
1042
+ for chunk_result in chunk_results:
1043
+ if chunk_result.get("success") and "results" in chunk_result:
1044
+ for file_result in chunk_result["results"]:
1045
+ if file_result.get("status") == "success" and "vulnerabilities" in file_result:
1046
+ all_vulnerabilities.extend(file_result["vulnerabilities"])
1047
+ elif file_result.get("status") == "error":
1048
+ print(f"Error scanning {file_result.get('filename', 'unknown')}: {file_result.get('error', 'Unknown error')}")
1049
+ except Exception as e:
1050
+ print(f"Parallel chunk processing error: {e}")
1051
+
1052
+ loop.close()
1053
+
1054
+ # Create result object compatible with UI
1055
+ result = ModalScanResult(
1056
+ {"success": True, "findings": {"vulnerabilities": all_vulnerabilities}},
1057
+ str(target_path),
1058
+ len(batch_files)
1059
+ )
1060
+ else:
1061
+ # Use local SecurityChecker with parallel processing
1062
+ loop = asyncio.new_event_loop()
1063
+ asyncio.set_event_loop(loop)
1064
+
1065
+ # Determine number of workers based on CPU count
1066
+ import multiprocessing
1067
+ cpu_count = multiprocessing.cpu_count()
1068
+ max_workers = min(cpu_count * 2, 16) # Use 2x CPU cores, max 16
1069
+
1070
+ yield (
1071
+ create_loading_state(f"Scanning files in parallel (using {max_workers} workers)..."),
1072
+ gr.update(visible=False),
1073
+ "", "", "", None, None
1074
+ )
1075
+
1076
+ result = loop.run_until_complete(
1077
+ self.checker.scan_local(
1078
+ str(target_path),
1079
+ include_nvd=nvd_check,
1080
+ max_workers=max_workers,
1081
+ use_parallel=True
1082
+ )
1083
+ )
1084
+ loop.close()
1085
+
1086
+ # Generate HTML components
1087
+ summary = result.summary()
1088
+ summary_section = create_summary_section({
1089
+ 'target': str(target_path),
1090
+ 'files_scanned': result.files_scanned,
1091
+ 'scan_type': 'local',
1092
+ 'summary': summary
1093
+ })
1094
+
1095
+ # Create filter script for severity badges
1096
+ filter_script = """
1097
+ <script>
1098
+ // Filter function called by severity badges
1099
+ window.filterBySeverityBadge = function(severity) {
1100
+ const allBadges = document.querySelectorAll('.severity-badge');
1101
+ const allCards = document.querySelectorAll('.finding-card');
1102
+
1103
+ // Check if clicking the active badge (toggle off)
1104
+ const clickedBadge = document.querySelector('.severity-badge[data-severity="' + severity + '"]');
1105
+ const isActive = clickedBadge && clickedBadge.classList.contains('active');
1106
+
1107
+ if (isActive) {
1108
+ // Show all findings
1109
+ allBadges.forEach(function(badge) {
1110
+ badge.classList.remove('active');
1111
+ badge.classList.remove('inactive');
1112
+ });
1113
+ allCards.forEach(function(card) {
1114
+ card.style.display = 'block';
1115
+ });
1116
+ updateFindingsCount(allCards.length);
1117
+ } else {
1118
+ // Filter by severity
1119
+ allBadges.forEach(function(badge) {
1120
+ if (badge.getAttribute('data-severity') === severity) {
1121
+ badge.classList.add('active');
1122
+ badge.classList.remove('inactive');
1123
+ } else {
1124
+ badge.classList.remove('active');
1125
+ badge.classList.add('inactive');
1126
+ }
1127
+ });
1128
+
1129
+ var visibleCount = 0;
1130
+ allCards.forEach(function(card) {
1131
+ if (card.getAttribute('data-severity') === severity) {
1132
+ card.style.display = 'block';
1133
+ visibleCount++;
1134
+ } else {
1135
+ card.style.display = 'none';
1136
+ }
1137
+ });
1138
+
1139
+ updateFindingsCount(visibleCount);
1140
+
1141
+ // Scroll to findings
1142
+ const firstCard = document.querySelector('.finding-card[style*="display: block"]');
1143
+ if (firstCard) {
1144
+ firstCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1145
+ }
1146
+ }
1147
+ }
1148
+
1149
+ function updateFindingsCount(count) {
1150
+ const countElement = document.getElementById('findings-count-display');
1151
+ if (countElement) {
1152
+ countElement.innerHTML = 'Showing <strong>' + count + '</strong> finding' + (count !== 1 ? 's' : '');
1153
+ }
1154
+ }
1155
+ </script>
1156
+ """
1157
+
1158
+ findings = ""
1159
+ if result.vulnerabilities:
1160
+ # Deduplicate vulnerabilities based on name, file, and line
1161
+ seen = set()
1162
+ unique_vulns = []
1163
+ for vuln in result.vulnerabilities:
1164
+ # Create unique key
1165
+ key = f"{vuln.name}|{vuln.file_path}|{vuln.line_number}"
1166
+ if key not in seen:
1167
+ seen.add(key)
1168
+ unique_vulns.append(vuln)
1169
+
1170
+ findings_counter = f"""
1171
+ <div id="findings-count-display" style="
1172
+ color: #6b7280;
1173
+ font-size: 14px;
1174
+ margin: 13px 0 16px 0;
1175
+ padding: 12px;
1176
+ background: #f9fafb;
1177
+ border-radius: 8px;
1178
+ ">
1179
+ Showing <strong>{len(unique_vulns)}</strong> findings
1180
+ </div>
1181
+ """
1182
+
1183
+ # Sort by severity
1184
+ severity_order = {
1185
+ RiskLevel.CRITICAL: 0,
1186
+ RiskLevel.HIGH: 1,
1187
+ RiskLevel.MEDIUM: 2,
1188
+ RiskLevel.LOW: 3,
1189
+ RiskLevel.INFO: 4
1190
+ }
1191
+ sorted_vulns = sorted(
1192
+ unique_vulns,
1193
+ key=lambda v: severity_order.get(v.risk_level, 5)
1194
+ )
1195
+
1196
+ for vuln in sorted_vulns:
1197
+ findings += create_finding_card({
1198
+ 'name': vuln.name,
1199
+ 'risk_level': vuln.risk_level.name,
1200
+ 'file_path': vuln.file_path,
1201
+ 'line_number': vuln.line_number,
1202
+ 'description': vuln.description,
1203
+ 'cwe_id': vuln.cwe_id,
1204
+ 'cve_ids': vuln.cve_ids,
1205
+ 'remediation': vuln.remediation
1206
+ })
1207
+ else:
1208
+ findings_counter = ""
1209
+ findings = """
1210
+ <div style="
1211
+ background: #f0fdf4;
1212
+ border: 1px solid #86efac;
1213
+ border-radius: 12px;
1214
+ padding: 40px;
1215
+ text-align: center;
1216
+ color: #166534;
1217
+ ">
1218
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"
1219
+ fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
1220
+ style="margin: 0 auto 16px; display: block;">
1221
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
1222
+ <polyline points="22 4 12 14.01 9 11.01"></polyline>
1223
+ </svg>
1224
+ <h3 style="margin: 0 0 8px 0; font-size: 20px; font-weight: 600;">No Vulnerabilities Found</h3>
1225
+ <p style="margin: 0; font-size: 14px;">Your code looks secure!</p>
1226
+ </div>
1227
+ """
1228
+
1229
+ # Generate reports for download
1230
+ json_report = self.checker.generate_report(result, format="json")
1231
+ json_path = session_dir / "security_report.json"
1232
+ with open(json_path, 'w') as f:
1233
+ f.write(json_report)
1234
+
1235
+ md_report = self.checker.generate_report(result, format="markdown")
1236
+ md_path = session_dir / "security_report.md"
1237
+ with open(md_path, 'w') as f:
1238
+ f.write(md_report)
1239
+
1240
+ # Schedule cleanup
1241
+ self.active_sessions[session_id] = {
1242
+ 'dir': session_dir,
1243
+ 'created': datetime.now(),
1244
+ 'json_path': json_path,
1245
+ 'md_path': md_path
1246
+ }
1247
+
1248
+ progress_msg = f"""
1249
+ <div style="
1250
+ background: #f0fdf4;
1251
+ border: 1px solid #86efac;
1252
+ border-radius: 12px;
1253
+ padding: 20px;
1254
+ color: #166534;
1255
+ ">
1256
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
1257
+ fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
1258
+ style="display: inline-block; vertical-align: middle; margin-right: 8px;">
1259
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
1260
+ <polyline points="22 4 12 14.01 9 11.01"></polyline>
1261
+ </svg>
1262
+ <strong>Scan complete!</strong><br/>
1263
+ Files scanned: {result.files_scanned} | Vulnerabilities found: {summary['total_vulnerabilities']}
1264
+ </div>
1265
+ """
1266
+
1267
+ yield (
1268
+ progress_msg,
1269
+ gr.update(visible=True),
1270
+ summary_section + filter_script,
1271
+ findings_counter,
1272
+ findings,
1273
+ str(json_path),
1274
+ str(md_path)
1275
+ )
1276
+
1277
+ except Exception as e:
1278
+ error_msg = f"""
1279
+ <div style="
1280
+ background: #fef2f2;
1281
+ border: 1px solid #fca5a5;
1282
+ border-radius: 12px;
1283
+ padding: 20px;
1284
+ color: #dc2626;
1285
+ ">
1286
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
1287
+ fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
1288
+ style="display: inline-block; vertical-align: middle; margin-right: 8px;">
1289
+ <circle cx="12" cy="12" r="10"></circle>
1290
+ <line x1="15" y1="9" x2="9" y2="15"></line>
1291
+ <line x1="9" y1="9" x2="15" y2="15"></line>
1292
+ </svg>
1293
+ <strong>Scan failed</strong><br/>
1294
+ {str(e)}
1295
+ </div>
1296
+ """
1297
+ yield (
1298
+ error_msg,
1299
+ gr.update(visible=False),
1300
+ "", "", "", None, None
1301
+ )
1302
+
1303
+ def scan_web_app(url, nvd_check):
1304
+ """Handle web application scanning."""
1305
+ yield (
1306
+ create_loading_state("Scanning web application..."),
1307
+ gr.update(visible=False),
1308
+ "", "", "", None, None
1309
+ )
1310
+
1311
+ if not url:
1312
+ yield (
1313
+ """<div style="background: #fef2f2; border: 1px solid #fca5a5; border-radius: 12px; padding: 20px; color: #dc2626;">
1314
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
1315
+ fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
1316
+ style="display: inline-block; vertical-align: middle; margin-right: 8px;">
1317
+ <circle cx="12" cy="12" r="10"></circle>
1318
+ <line x1="15" y1="9" x2="9" y2="15"></line>
1319
+ <line x1="9" y1="9" x2="15" y2="15"></line>
1320
+ </svg>
1321
+ <strong>No URL provided</strong><br/>
1322
+ Please enter a web application URL.
1323
+ </div>""",
1324
+ gr.update(visible=False),
1325
+ "", "", "", None, None
1326
+ )
1327
+ return
1328
+
1329
+ try:
1330
+ # Ensure URL has protocol
1331
+ if not url.startswith(('http://', 'https://')):
1332
+ url = 'https://' + url
1333
+
1334
+ # Run scan
1335
+ loop = asyncio.new_event_loop()
1336
+ asyncio.set_event_loop(loop)
1337
+ result = loop.run_until_complete(
1338
+ self.checker.scan_web(url, include_nvd=False)
1339
+ )
1340
+ loop.close()
1341
+
1342
+ # Generate HTML components (similar to local scan)
1343
+ summary = result.summary()
1344
+ summary_section = create_summary_section({
1345
+ 'target': url,
1346
+ 'files_scanned': 0,
1347
+ 'scan_type': 'web',
1348
+ 'summary': summary
1349
+ })
1350
+
1351
+ # Create filter script for severity badges
1352
+ filter_script = """
1353
+ <script>
1354
+ // Filter function called by severity badges
1355
+ window.filterBySeverityBadge = function(severity) {
1356
+ const allBadges = document.querySelectorAll('.severity-badge');
1357
+ const allCards = document.querySelectorAll('.finding-card');
1358
+
1359
+ // Check if clicking the active badge (toggle off)
1360
+ const clickedBadge = document.querySelector('.severity-badge[data-severity="' + severity + '"]');
1361
+ const isActive = clickedBadge && clickedBadge.classList.contains('active');
1362
+
1363
+ if (isActive) {
1364
+ // Show all findings
1365
+ allBadges.forEach(function(badge) {
1366
+ badge.classList.remove('active');
1367
+ badge.classList.remove('inactive');
1368
+ });
1369
+ allCards.forEach(function(card) {
1370
+ card.style.display = 'block';
1371
+ });
1372
+ updateFindingsCount(allCards.length);
1373
+ } else {
1374
+ // Filter by severity
1375
+ allBadges.forEach(function(badge) {
1376
+ if (badge.getAttribute('data-severity') === severity) {
1377
+ badge.classList.add('active');
1378
+ badge.classList.remove('inactive');
1379
+ } else {
1380
+ badge.classList.remove('active');
1381
+ badge.classList.add('inactive');
1382
+ }
1383
+ });
1384
+
1385
+ var visibleCount = 0;
1386
+ allCards.forEach(function(card) {
1387
+ if (card.getAttribute('data-severity') === severity) {
1388
+ card.style.display = 'block';
1389
+ visibleCount++;
1390
+ } else {
1391
+ card.style.display = 'none';
1392
+ }
1393
+ });
1394
+
1395
+ updateFindingsCount(visibleCount);
1396
+
1397
+ // Scroll to findings
1398
+ const firstCard = document.querySelector('.finding-card[style*="display: block"]');
1399
+ if (firstCard) {
1400
+ firstCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1401
+ }
1402
+ }
1403
+ }
1404
+
1405
+ function updateFindingsCount(count) {
1406
+ const countElement = document.getElementById('findings-count-display');
1407
+ if (countElement) {
1408
+ countElement.innerHTML = 'Showing <strong>' + count + '</strong> finding' + (count !== 1 ? 's' : '');
1409
+ }
1410
+ }
1411
+ </script>
1412
+ """
1413
+
1414
+ # Deduplicate vulnerabilities
1415
+ seen = set()
1416
+ unique_vulns = []
1417
+ for vuln in result.vulnerabilities:
1418
+ # Create unique key (for URL scans, line_number is usually 0, so use name and description)
1419
+ key = f"{vuln.name}|{vuln.description}"
1420
+ if key not in seen:
1421
+ seen.add(key)
1422
+ unique_vulns.append(vuln)
1423
+
1424
+ findings_counter = f"""
1425
+ <div id="findings-count-display" style="color: #6b7280; font-size: 14px; margin: 13px 0 16px 0; padding: 12px; background: #f9fafb; border-radius: 8px;">
1426
+ Showing <strong>{len(unique_vulns)}</strong> findings
1427
+ </div>
1428
+ """
1429
+
1430
+ findings = ""
1431
+ if unique_vulns:
1432
+ for vuln in unique_vulns:
1433
+ findings += create_finding_card({
1434
+ 'name': vuln.name,
1435
+ 'risk_level': vuln.risk_level.name,
1436
+ 'file_path': url,
1437
+ 'line_number': 0,
1438
+ 'description': vuln.description,
1439
+ 'cwe_id': vuln.cwe_id,
1440
+ 'cve_ids': vuln.cve_ids,
1441
+ 'remediation': vuln.remediation
1442
+ })
1443
+ else:
1444
+ findings_counter = ""
1445
+ findings = """
1446
+ <div style="background: #f0fdf4; border: 1px solid #86efac; border-radius: 12px; padding: 40px; text-align: center; color: #166534;">
1447
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"
1448
+ fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
1449
+ style="margin: 0 auto 16px; display: block;">
1450
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
1451
+ <polyline points="22 4 12 14.01 9 11.01"></polyline>
1452
+ </svg>
1453
+ <h3 style="margin: 0 0 8px 0;">No Issues Found</h3>
1454
+ <p style="margin: 0;">Web application appears to be configured securely.</p>
1455
+ </div>
1456
+ """
1457
+
1458
+ # Generate report
1459
+ session_id = str(uuid.uuid4())
1460
+ session_dir = Path(tempfile.mkdtemp(prefix=f"audit_{session_id}_"))
1461
+ json_report = self.checker.generate_report(result, format="json")
1462
+ json_path = session_dir / "security_report.json"
1463
+ with open(json_path, 'w') as f:
1464
+ f.write(json_report)
1465
+
1466
+ md_report = self.checker.generate_report(result, format="markdown")
1467
+ md_path = session_dir / "security_report.md"
1468
+ with open(md_path, 'w') as f:
1469
+ f.write(md_report)
1470
+
1471
+ progress_msg = f"""
1472
+ <div style="background: #f0fdf4; border: 1px solid #86efac; border-radius: 12px; padding: 20px; color: #166534;">
1473
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
1474
+ fill="none" stroke="#10b981" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
1475
+ style="display: inline-block; vertical-align: middle; margin-right: 8px;">
1476
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
1477
+ <polyline points="22 4 12 14.01 9 11.01"></polyline>
1478
+ </svg>
1479
+ <strong>Scan complete!</strong><br/>
1480
+ Vulnerabilities found: {summary['total_vulnerabilities']}
1481
+ </div>
1482
+ """
1483
+
1484
+ yield (
1485
+ progress_msg,
1486
+ gr.update(visible=True),
1487
+ summary_section + filter_script,
1488
+ findings_counter,
1489
+ findings,
1490
+ str(json_path),
1491
+ str(md_path)
1492
+ )
1493
+
1494
+ except Exception as e:
1495
+ error_msg = f"""
1496
+ <div style="background: #fef2f2; border: 1px solid #fca5a5; border-radius: 12px; padding: 20px; color: #dc2626;">
1497
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
1498
+ fill="none" stroke="#dc2626" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
1499
+ style="display: inline-block; vertical-align: middle; margin-right: 8px;">
1500
+ <circle cx="12" cy="12" r="10"></circle>
1501
+ <line x1="15" y1="9" x2="9" y2="15"></line>
1502
+ <line x1="9" y1="9" x2="15" y2="15"></line>
1503
+ </svg>
1504
+ <strong>Scan failed</strong><br/>
1505
+ {str(e)}
1506
+ </div>
1507
+ """
1508
+ yield (
1509
+ error_msg,
1510
+ gr.update(visible=False),
1511
+ "", "", "", None, None
1512
+ )
1513
+
1514
+ # Connect event handlers
1515
+ analyze_btn_local.click(
1516
+ fn=scan_local_files,
1517
+ inputs=[file_upload, directory_path, nvd_enrichment],
1518
+ outputs=[
1519
+ progress_box,
1520
+ results_section,
1521
+ summary_html,
1522
+ findings_count,
1523
+ findings_html,
1524
+ download_file,
1525
+ download_file_md
1526
+ ]
1527
+ )
1528
+
1529
+ analyze_btn_url.click(
1530
+ fn=scan_web_app,
1531
+ inputs=[web_url, nvd_enrichment],
1532
+ outputs=[
1533
+ progress_box,
1534
+ results_section,
1535
+ summary_html,
1536
+ findings_count,
1537
+ findings_html,
1538
+ download_file,
1539
+ download_file_md
1540
+ ]
1541
+ )
1542
+
1543
+ def export_json():
1544
+ """Export JSON report."""
1545
+ return gr.update(visible=True)
1546
+
1547
+ def export_markdown():
1548
+ """Export Markdown report."""
1549
+ return gr.update(visible=True)
1550
+
1551
+ export_json_btn.click(
1552
+ fn=export_json,
1553
+ outputs=[download_file]
1554
+ )
1555
+
1556
+ export_md_btn.click(
1557
+ fn=export_markdown,
1558
+ outputs=[download_file_md]
1559
+ )
1560
+
1561
+ return app
1562
+
1563
+ def cleanup_old_sessions(self):
1564
+ """Remove sessions older than cleanup_interval."""
1565
+ now = datetime.now()
1566
+ to_remove = []
1567
+
1568
+ for session_id, session in self.active_sessions.items():
1569
+ age = (now - session['created']).total_seconds()
1570
+ if age > self.cleanup_interval:
1571
+ shutil.rmtree(session['dir'], ignore_errors=True)
1572
+ to_remove.append(session_id)
1573
+
1574
+ for session_id in to_remove:
1575
+ del self.active_sessions[session_id]
1576
+
1577
+ def launch(self, **kwargs):
1578
+ """Launch the Gradio application."""
1579
+ # Start cleanup background task
1580
+ def cleanup_loop():
1581
+ while True:
1582
+ time.sleep(600) # Check every 10 minutes
1583
+ self.cleanup_old_sessions()
1584
+
1585
+ cleanup_thread = threading.Thread(target=cleanup_loop, daemon=True)
1586
+ cleanup_thread.start()
1587
+
1588
+ # Create and launch interface
1589
+ interface = self.create_interface()
1590
+ interface.queue()
1591
+
1592
+ # In Gradio 6.0, theme and css are passed to launch() instead of Blocks()
1593
+ interface.launch(
1594
+ theme=self.theme,
1595
+ css=self.custom_css,
1596
+ **kwargs
1597
+ )
1598
+
1599
+
1600
+ def main():
1601
+ """Main entry point."""
1602
+ app = ModernSecurityAuditorApp()
1603
+ app.launch(
1604
+ server_name="0.0.0.0",
1605
+ server_port=7860,
1606
+ share=False
1607
+ )
1608
+
1609
+
1610
+ if __name__ == "__main__":
1611
+ main()
help.html ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Security Auditor - Help Guide</title>
7
+ <style>
8
+ :root {
9
+ --bg: #faf9f6;
10
+ --text: #131314;
11
+ --accent: #d97757;
12
+ --accent-hover: #cc6944;
13
+ --gray: #6b7280;
14
+ --light-gray: #9ca3af;
15
+ --border: #e5e7eb;
16
+ --card-bg: #ffffff;
17
+ --critical: #dc2626;
18
+ --critical-bg: #fef2f2;
19
+ --critical-border: #fca5a5;
20
+ --high: #ea580c;
21
+ --high-bg: #fff7ed;
22
+ --high-border: #fdba74;
23
+ --medium: #d97706;
24
+ --medium-bg: #fffbeb;
25
+ --medium-border: #fcd34d;
26
+ --low: #0d9488;
27
+ --low-bg: #f0fdfa;
28
+ --low-border: #5eead4;
29
+ --info-color: #6b7280;
30
+ --info-bg: #f9fafb;
31
+ --info-border: #d1d5db;
32
+ }
33
+
34
+ * { margin: 0; padding: 0; box-sizing: border-box; }
35
+
36
+ body {
37
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
38
+ background: var(--bg);
39
+ color: var(--text);
40
+ line-height: 1.7;
41
+ max-width: 920px;
42
+ margin: 0 auto;
43
+ padding: 40px 24px 60px;
44
+ }
45
+
46
+ /* Header */
47
+ .header {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 14px;
51
+ padding-bottom: 24px;
52
+ border-bottom: 1px solid var(--border);
53
+ margin-bottom: 32px;
54
+ }
55
+ .header-icon {
56
+ width: 44px;
57
+ height: 44px;
58
+ background: var(--text);
59
+ border-radius: 8px;
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ flex-shrink: 0;
64
+ }
65
+ .header-icon svg { width: 24px; height: 24px; stroke: white; fill: none; }
66
+ .header-title { font-size: 24px; font-weight: 700; }
67
+ .header-subtitle { font-size: 14px; color: var(--gray); margin-top: 2px; }
68
+
69
+ /* Headings */
70
+ h2 {
71
+ font-size: 22px;
72
+ font-weight: 700;
73
+ margin: 48px 0 16px;
74
+ padding-bottom: 8px;
75
+ border-bottom: 2px solid var(--border);
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 10px;
79
+ }
80
+ h2 .h-num {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ width: 30px;
85
+ height: 30px;
86
+ background: var(--accent);
87
+ color: white;
88
+ border-radius: 50%;
89
+ font-size: 14px;
90
+ font-weight: 700;
91
+ flex-shrink: 0;
92
+ }
93
+ h3 {
94
+ font-size: 17px;
95
+ font-weight: 600;
96
+ margin: 28px 0 12px;
97
+ color: var(--text);
98
+ }
99
+
100
+ /* Paragraphs and lists */
101
+ p { margin: 0 0 14px; color: var(--text); }
102
+ ul, ol { margin: 0 0 16px; padding-left: 24px; }
103
+ li { margin: 6px 0; }
104
+
105
+ /* Links */
106
+ a { color: var(--accent); text-decoration: none; }
107
+ a:hover { color: var(--accent-hover); text-decoration: underline; }
108
+
109
+ /* Table of Contents */
110
+ .toc {
111
+ background: var(--card-bg);
112
+ border: 1px solid var(--border);
113
+ border-radius: 10px;
114
+ padding: 24px 28px;
115
+ margin: 0 0 40px;
116
+ }
117
+ .toc-title {
118
+ font-size: 15px;
119
+ font-weight: 700;
120
+ margin-bottom: 12px;
121
+ color: var(--text);
122
+ }
123
+ .toc ol { padding-left: 20px; margin: 0; }
124
+ .toc li { margin: 7px 0; font-size: 15px; }
125
+ .toc a { color: var(--accent); font-weight: 500; }
126
+
127
+ /* Cards */
128
+ .card {
129
+ background: var(--card-bg);
130
+ border: 1px solid var(--border);
131
+ border-radius: 10px;
132
+ padding: 20px 24px;
133
+ margin: 16px 0;
134
+ }
135
+ .card-label {
136
+ font-size: 12px;
137
+ font-weight: 600;
138
+ text-transform: uppercase;
139
+ letter-spacing: 0.05em;
140
+ color: var(--gray);
141
+ margin-bottom: 6px;
142
+ }
143
+
144
+ /* Steps */
145
+ .steps { counter-reset: step; list-style: none; padding-left: 0; }
146
+ .steps li {
147
+ counter-increment: step;
148
+ position: relative;
149
+ padding-left: 40px;
150
+ margin: 14px 0;
151
+ }
152
+ .steps li::before {
153
+ content: counter(step);
154
+ position: absolute;
155
+ left: 0;
156
+ top: 1px;
157
+ width: 26px;
158
+ height: 26px;
159
+ background: var(--accent);
160
+ color: white;
161
+ border-radius: 50%;
162
+ font-size: 13px;
163
+ font-weight: 700;
164
+ display: flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ }
168
+
169
+ /* Severity badges */
170
+ .severity-sample {
171
+ display: inline-flex;
172
+ align-items: center;
173
+ gap: 8px;
174
+ padding: 8px 16px;
175
+ border-radius: 6px;
176
+ font-size: 13px;
177
+ font-weight: 600;
178
+ border: 2px solid;
179
+ margin: 4px 4px 4px 0;
180
+ }
181
+
182
+ /* Severity table */
183
+ .severity-table { width: 100%; border-collapse: collapse; margin: 16px 0; }
184
+ .severity-table th {
185
+ text-align: left;
186
+ padding: 12px 16px;
187
+ background: var(--bg);
188
+ border-bottom: 2px solid var(--border);
189
+ font-size: 13px;
190
+ font-weight: 600;
191
+ color: var(--gray);
192
+ text-transform: uppercase;
193
+ letter-spacing: 0.05em;
194
+ }
195
+ .severity-table td {
196
+ padding: 14px 16px;
197
+ border-bottom: 1px solid var(--border);
198
+ vertical-align: top;
199
+ font-size: 14px;
200
+ }
201
+ .severity-table tr:last-child td { border-bottom: none; }
202
+ .severity-tag {
203
+ display: inline-block;
204
+ padding: 3px 10px;
205
+ border-radius: 4px;
206
+ font-size: 12px;
207
+ font-weight: 700;
208
+ white-space: nowrap;
209
+ }
210
+
211
+ /* Screenshots */
212
+ .screenshot {
213
+ display: block;
214
+ max-width: 100%;
215
+ border: 1px solid var(--border);
216
+ border-radius: 10px;
217
+ margin: 20px 0;
218
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
219
+ }
220
+ .screenshot-caption {
221
+ font-size: 13px;
222
+ color: var(--gray);
223
+ text-align: center;
224
+ margin: -10px 0 20px;
225
+ font-style: italic;
226
+ }
227
+
228
+ /* Tip / Note boxes */
229
+ .tip {
230
+ background: var(--high-bg);
231
+ border-left: 4px solid var(--accent);
232
+ border-radius: 0 8px 8px 0;
233
+ padding: 14px 18px;
234
+ margin: 16px 0;
235
+ font-size: 14px;
236
+ }
237
+ .tip-label {
238
+ font-weight: 700;
239
+ color: var(--accent);
240
+ margin-bottom: 4px;
241
+ }
242
+
243
+ /* Footer */
244
+ .footer {
245
+ margin-top: 60px;
246
+ padding-top: 24px;
247
+ border-top: 1px solid var(--border);
248
+ text-align: center;
249
+ color: var(--gray);
250
+ font-size: 13px;
251
+ }
252
+
253
+ /* Keyboard shortcut styling */
254
+ kbd {
255
+ background: var(--bg);
256
+ border: 1px solid var(--border);
257
+ border-radius: 4px;
258
+ padding: 2px 6px;
259
+ font-size: 13px;
260
+ font-family: ui-monospace, 'SF Mono', Monaco, 'Cascadia Code', monospace;
261
+ }
262
+
263
+ /* Print */
264
+ @media print {
265
+ body { background: white; max-width: 100%; padding: 20px; }
266
+ .toc { break-after: page; }
267
+ .card, .severity-table tr { break-inside: avoid; }
268
+ .screenshot { max-width: 80%; margin: 12px auto; }
269
+ a { color: var(--text); }
270
+ h2 { break-after: avoid; }
271
+ }
272
+
273
+ /* Responsive */
274
+ @media (max-width: 640px) {
275
+ body { padding: 20px 16px 40px; }
276
+ .header-title { font-size: 20px; }
277
+ h2 { font-size: 19px; }
278
+ .severity-table th, .severity-table td { padding: 10px 10px; font-size: 13px; }
279
+ }
280
+ </style>
281
+ </head>
282
+ <body>
283
+
284
+ <!-- Header -->
285
+ <div class="header">
286
+ <div class="header-icon">
287
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
288
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
289
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
290
+ </svg>
291
+ </div>
292
+ <div>
293
+ <div class="header-title">Security Auditor</div>
294
+ <div class="header-subtitle">Help Guide</div>
295
+ </div>
296
+ </div>
297
+
298
+ <!-- Table of Contents -->
299
+ <nav class="toc">
300
+ <div class="toc-title">Contents</div>
301
+ <ol>
302
+ <li><a href="#getting-started">Getting Started</a></li>
303
+ <li><a href="#local-scan">Scanning a Local Directory</a></li>
304
+ <li><a href="#remote-scan">Scanning a Remote URL</a></li>
305
+ <li><a href="#understanding-results">Understanding Your Results</a></li>
306
+ <li><a href="#nvd-enrichment">National Vulnerability Database (NVD) Enrichment</a> <a href="https://nvd.nist.gov/general/cve-process" target="_blank" rel="noopener noreferrer">NVD CVE Process</a></li>
307
+ <li><a href="#severity-guide">Severity Levels Guide</a></li>
308
+ </ol>
309
+ </nav>
310
+
311
+ <!-- Section 1: Getting Started -->
312
+ <h2 id="getting-started"><span class="h-num">1</span> Getting Started</h2>
313
+
314
+ <p>
315
+ Security Auditor is a combined <strong>Static Application Security Testing (SAST)</strong> and
316
+ <strong>Dynamic Application Security Testing (DAST)</strong> platform that identifies security
317
+ vulnerabilities in your application code and web deployments. It performs <strong>40+ security checks</strong>
318
+ across two scanning modes.
319
+ </p>
320
+
321
+ <p>
322
+ The checks this tool performs overlap with the <strong><a href="https://owasp.org/www-project-top-ten/" target="_blank" rel="noopener noreferrer">Open Web Application Security Project (OWASP)</a></strong> Top Ten; many checks map to common OWASP categories such as Injection, Cross-Site Scripting (XSS), Broken Authentication, and Security Misconfiguration.
323
+ </p>
324
+
325
+ <div class="card">
326
+ <div class="card-label">Two Scanning Modes</div>
327
+ <ul>
328
+ <li><strong>Local Directory</strong> &mdash; Upload source code files or specify a directory path. The SAST engine scans your code for 28+ vulnerability patterns including SQL injection, XSS, command injection, hardcoded credentials, and more.</li>
329
+ <li><strong>Remote URL</strong> &mdash; Enter a web application URL. The DAST engine checks HTTP security headers, probes for exposed sensitive paths, verifies HTTPS configuration, and scans response content for information leaks.</li>
330
+ </ul>
331
+ </div>
332
+
333
+ <img src="/helpimg/NewGradioScreenshot_landingPage.png"
334
+ alt="Security Auditor landing page showing Local Directory mode selected"
335
+ class="screenshot" />
336
+ <p class="screenshot-caption">The Security Auditor landing page with Local Directory mode selected.</p>
337
+
338
+ <p>
339
+ <strong>Supported file types for local scanning:</strong><br />
340
+ <code>.py, .js, .ts, .java, .php, .go, .rb, .c, .cpp, .cs, .swift, .kt, .scala, .rs, .jsx, .tsx</code>
341
+ </p>
342
+
343
+ <!-- Section 2: Local Directory Scan -->
344
+ <h2 id="local-scan"><span class="h-num">2</span> Scanning a Local Directory</h2>
345
+
346
+ <p>Use this mode to scan application source code files for security vulnerabilities using static analysis.</p>
347
+
348
+ <ol class="steps">
349
+ <li>Select <strong>Local Directory</strong> in the Analysis Mode panel on the left sidebar.</li>
350
+ <li>Provide your code using one of two methods:
351
+ <ul>
352
+ <li><strong>Upload files</strong> &mdash; Drag and drop or click the upload area (total size maximum 25 MB).</li>
353
+ <li><strong>Enter a directory path</strong> &mdash; Type the full path to a local directory, e.g. <kbd>C:/Projects/my-application</kbd>.</li>
354
+ </ul>
355
+ </li>
356
+ <li>Optionally toggle <strong>NVD Enriched Scan Results</strong> on or off (see <a href="#nvd-enrichment">Section 5</a>).</li>
357
+ <li>Click the <strong>Analyze</strong> button.</li>
358
+ <li>Wait for the scan to complete. A progress indicator shows the current status.</li>
359
+ </ol>
360
+
361
+ <div class="tip">
362
+ <div class="tip-label">Tip</div>
363
+ When uploading files, you can select multiple files at once. The scanner analyses all uploaded files together, detecting cross-file vulnerability patterns.
364
+ </div>
365
+
366
+ <img src="/helpimg/NewGradioScreenshot_LocalAppResult.png"
367
+ alt="Local directory scan results showing Analysis Summary and Security Findings"
368
+ class="screenshot" />
369
+ <p class="screenshot-caption">Results from a local directory scan showing the Analysis Summary, severity badges, and individual finding cards.</p>
370
+
371
+ <!-- Section 3: Remote URL Scan -->
372
+ <h2 id="remote-scan"><span class="h-num">3</span> Scanning a Remote URL</h2>
373
+
374
+ <p>Use this mode to dynamically test a running web application for security misconfigurations and vulnerabilities.</p>
375
+
376
+ <ol class="steps">
377
+ <li>Select <strong>Remote URL</strong> in the Analysis Mode panel.</li>
378
+ <li>Enter the target web application URL in the <strong>Web Application URL</strong> field, e.g. <kbd>https://your-app.example.com</kbd>.</li>
379
+ <li>Click the <strong>Analyze</strong> button.</li>
380
+ </ol>
381
+
382
+ <h3>What Gets Checked</h3>
383
+ <div class="card">
384
+ <ul>
385
+ <li><strong>HTTP Security Headers</strong> &mdash; Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Strict-Transport-Security, Referrer-Policy, Permissions-Policy.</li>
386
+ <li><strong>Sensitive Path Exposure</strong> &mdash; Probes for common exposed paths such as <code>/.env</code>, <code>/.git/config</code>, <code>/admin</code>, <code>/phpinfo.php</code>, <code>/swagger.json</code>, and 15+ other paths.</li>
387
+ <li><strong>HTTPS Configuration</strong> &mdash; Verifies the application uses HTTPS rather than unencrypted HTTP.</li>
388
+ <li><strong>Response Content Analysis</strong> &mdash; Scans HTML responses for database error messages, stack trace disclosures, debug mode indicators, and sensitive data in comments.</li>
389
+ <li><strong>Server Version Disclosure</strong> &mdash; Detects if the server reveals its software version in response headers.</li>
390
+ </ul>
391
+ </div>
392
+
393
+ <img src="/helpimg/NewGradioScreenshot_RemoteURLResult.png"
394
+ alt="Remote URL scan results showing missing security headers and other findings"
395
+ class="screenshot" />
396
+ <p class="screenshot-caption">Results from a remote URL scan showing missing security headers and insecure HTTP connection findings.</p>
397
+
398
+ <!-- Section 4: Understanding Results -->
399
+ <h2 id="understanding-results"><span class="h-num">4</span> Understanding Your Results</h2>
400
+
401
+ <h3>Analysis Summary</h3>
402
+ <p>
403
+ After a scan completes, the <strong>Analysis Summary</strong> section appears at the top of the results. It
404
+ displays metadata about the scan and a count of findings grouped by severity level.
405
+ </p>
406
+ <div class="card">
407
+ <ul>
408
+ <li><strong>Target</strong> &mdash; The directory path or URL that was scanned.</li>
409
+ <li><strong>Files Analyzed</strong> &mdash; Number of source code files processed (local scans only).</li>
410
+ <li><strong>Total Findings</strong> &mdash; Total number of security issues detected.</li>
411
+ <li><strong>Analysis Type</strong> &mdash; Either <em>Local</em> (SAST) or <em>Web</em> (DAST).</li>
412
+ </ul>
413
+ </div>
414
+
415
+ <h3>Severity Badges</h3>
416
+ <p>
417
+ Below the metadata, colour-coded severity badges show how many findings fall into each severity level:
418
+ </p>
419
+ <p>
420
+ <span class="severity-sample" style="background: var(--critical-bg); color: var(--critical); border-color: var(--critical-border);">Critical</span>
421
+ <span class="severity-sample" style="background: var(--high-bg); color: var(--high); border-color: var(--high-border);">High</span>
422
+ <span class="severity-sample" style="background: var(--medium-bg); color: var(--medium); border-color: var(--medium-border);">Medium</span>
423
+ <span class="severity-sample" style="background: var(--low-bg); color: var(--low); border-color: var(--low-border);">Low</span>
424
+ <span class="severity-sample" style="background: var(--info-bg); color: var(--info-color); border-color: var(--info-border);">Info</span>
425
+ </p>
426
+
427
+ <h3>Finding Cards</h3>
428
+ <p>Each detected vulnerability is displayed as a finding card containing the following information:</p>
429
+ <div class="card">
430
+ <ul>
431
+ <li><strong>Vulnerability Name</strong> &mdash; The type of security issue (e.g. "SQL Injection", "Missing Security Header: Content-Security-Policy").</li>
432
+ <li><strong>Severity Tag</strong> &mdash; A colour-coded badge showing CRITICAL, HIGH, MEDIUM, LOW, or INFO.</li>
433
+ <li><strong>Common Weakness Enumeration (CWE) Reference</strong> &mdash; The identifier (e.g. CWE-89).</li>
434
+ <li><strong>CVE References</strong> &mdash; Related Common Vulnerabilities and Exposures entries (when NVD enrichment is enabled).</li>
435
+ <li><strong>File Path &amp; Line Number</strong> &mdash; Exact location in the source code (local scans) or the target URL (remote scans).</li>
436
+ <li><strong>Description</strong> &mdash; Explanation of the vulnerability and its potential impact.</li>
437
+ <li><strong>Remediation Guidance</strong> &mdash; Click the expandable section to view recommended fixes and best practices.</li>
438
+ </ul>
439
+ </div>
440
+
441
+ <h3>Exporting Reports</h3>
442
+ <p>
443
+ At the bottom of the results, two export options are available:
444
+ </p>
445
+ <ul>
446
+ <li><strong>Export JSON Report</strong> &mdash; Downloads a structured JSON file containing all scan data, suitable for integration with CI/CD pipelines or other security tools.</li>
447
+ <li><strong>Export Markdown Report</strong> &mdash; Downloads a Markdown report with findings grouped by severity, including file locations, code snippets, and remediation guidance. Ideal for pasting into vibe-coding platforms (Cursor, Lovable, Bolt, etc.) to fix identified issues.</li>
448
+ </ul>
449
+
450
+ <!-- Section 5: NVD Enrichment -->
451
+ <h2 id="nvd-enrichment"><span class="h-num">5</span> NVD Enrichment</h2>
452
+
453
+ <p>
454
+ The <strong>NVD Enriched Scan Results</strong> toggle in the sidebar controls whether scan findings are
455
+ enriched with data from the <strong><a href="https://nvd.nist.gov/general/cve-process" target="_blank" rel="noopener noreferrer">NVD</a></strong>, maintained by the <strong><a href="https://www.nist.gov/" target="_blank" rel="noopener noreferrer">National Institute of Standards and Technology (NIST)</a></strong>.
456
+ </p>
457
+
458
+ <div class="card">
459
+ <div class="card-label">What NVD Enrichment Adds</div>
460
+ <ul>
461
+ <li>Related <strong><a href="https://nvd.nist.gov/general/cve-process" target="_blank" rel="noopener noreferrer">Common Vulnerabilities and Exposures (CVE)</a></strong> references for each finding.</li>
462
+ </div>
463
+
464
+ <h3>When to Enable</h3>
465
+ <ul>
466
+ <li>Comprehensive security audits where you need full CVE context.</li>
467
+ <li>Compliance reporting that requires specific vulnerability references.</li>
468
+ <li>When you need detailed remediation guidance for each finding.</li>
469
+ </ul>
470
+
471
+ <h3>When to Disable</h3>
472
+ <ul>
473
+ <li>Quick scans where speed is the priority.</li>
474
+ <li>Offline environments without internet access.</li>
475
+ <li>When the NVD API is rate-limited or unavailable.</li>
476
+ </ul>
477
+
478
+ <div class="tip">
479
+ <div class="tip-label">Note</div>
480
+ NVD enrichment adds processing time to the scan. The toggle is enabled by default. You can disable it for faster scans and re-run with enrichment when needed.
481
+ </div>
482
+
483
+ <!-- Section 6: Severity Levels Guide -->
484
+ <h2 id="severity-guide"><span class="h-num">6</span> Severity Levels Guide</h2>
485
+
486
+ <p>
487
+ Findings are classified into five severity levels. Use this guide to prioritise remediation efforts.
488
+ </p>
489
+
490
+ <table class="severity-table">
491
+ <thead>
492
+ <tr>
493
+ <th style="width: 120px;">Severity</th>
494
+ <th>Description</th>
495
+ <th style="width: 200px;">Examples</th>
496
+ </tr>
497
+ </thead>
498
+ <tbody>
499
+ <tr>
500
+ <td><span class="severity-tag" style="background: var(--critical-bg); color: var(--critical);">CRITICAL</span></td>
501
+ <td>Immediate action required. These vulnerabilities can lead to full system compromise, data breaches, or remote code execution.</td>
502
+ <td>SQL Injection, Command Injection, Hardcoded Credentials, Insecure Deserialization</td>
503
+ </tr>
504
+ <tr>
505
+ <td><span class="severity-tag" style="background: var(--high-bg); color: var(--high);">HIGH</span></td>
506
+ <td>Serious vulnerabilities requiring prompt attention. These can lead to significant data exposure or unauthorized access.</td>
507
+ <td>Cross-Site Scripting (XSS), Path Traversal, SSRF, JWT Without Verification</td>
508
+ </tr>
509
+ <tr>
510
+ <td><span class="severity-tag" style="background: var(--medium-bg); color: var(--medium);">MEDIUM</span></td>
511
+ <td>Moderate risk requiring investigation. These may enable attacks under certain conditions or weaken security posture.</td>
512
+ <td>CORS Misconfiguration, Weak Cryptographic Algorithm, Open Redirect, Missing Content-Security-Policy</td>
513
+ </tr>
514
+ <tr>
515
+ <td><span class="severity-tag" style="background: var(--low-bg); color: var(--low);">LOW</span></td>
516
+ <td>Minor issues with lower priority. These represent defence-in-depth concerns or best practice violations.</td>
517
+ <td>Debug Mode Enabled, Missing Non-Critical Headers, Verbose Error Messages, Sensitive Data in Logs</td>
518
+ </tr>
519
+ <tr>
520
+ <td><span class="severity-tag" style="background: var(--info-bg); color: var(--info-color);">INFO</span></td>
521
+ <td>Informational findings with no direct security risk. These highlight areas for awareness or potential improvement.</td>
522
+ <td>Technology Detection, Configuration Notes, Server Version Disclosure</td>
523
+ </tr>
524
+ </tbody>
525
+ </table>
526
+
527
+ <div class="tip">
528
+ <div class="tip-label">Prioritisation Strategy</div>
529
+ Address <strong>Critical</strong> and <strong>High</strong> findings first, as they pose the greatest risk. Medium findings should be reviewed and scheduled for remediation. Low and Info findings can be addressed as part of regular maintenance cycles.
530
+ </div>
531
+
532
+ <!-- Footer -->
533
+ <div class="footer">
534
+ Security Auditor &middot; SAST + DAST Platform<br />
535
+ </div>
536
+
537
+
538
+ </body>
539
+ </html>
requirements.txt ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Security Checker Dependencies
2
+ # =============================
3
+
4
+ # Core Dependencies (Required for running the application)
5
+ # ---------------------------------------------------------
6
+
7
+ # Core async HTTP client for NVD API and web scanning
8
+ aiohttp>=3.8.0
9
+
10
+ # YAML parsing (for config files)
11
+ PyYAML>=6.0
12
+
13
+ # For generating HTML reports
14
+ jinja2>=3.1.0
15
+
16
+ # For advanced pattern matching
17
+ regex>=2023.0.0
18
+
19
+ # For rich console output
20
+ rich>=13.0.0
21
+
22
+ # Gradio web interface
23
+ gradio>=4.0.0
24
+
25
+ # HTTP client for Modal backend API calls
26
+ httpx>=0.27.0
27
+
28
+ # Development Dependencies (Optional - only needed for development/testing)
29
+ # ---------------------------------------------------------------------------
30
+ # Uncomment the lines below if you need testing or type checking:
31
+ #
32
+ # pytest>=7.0.0
33
+ # pytest-asyncio>=0.21.0
34
+ # mypy>=1.0.0
security_checker.py ADDED
@@ -0,0 +1,1945 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Security Checker Application
4
+ ============================
5
+ A comprehensive security analysis tool that combines:
6
+ 1. Static Application Security Testing (SAST)
7
+ 2. NIST National Vulnerability Database (NVD) integration
8
+
9
+ Think of this as a "security doctor" for your applications:
10
+ - SAST = X-ray machine (looks inside without running)
11
+ - NVD = Medical database (known vulnerabilities/diseases)
12
+ - Report = Diagnosis with treatment plan
13
+
14
+ Author: Security Checker Project
15
+ """
16
+
17
+ import os
18
+ import re
19
+ import json
20
+ import hashlib
21
+ import asyncio
22
+ import aiohttp
23
+ from pathlib import Path
24
+ from dataclasses import dataclass, field
25
+ from typing import List, Dict, Optional, Tuple
26
+ from enum import Enum
27
+ from datetime import datetime
28
+ from urllib.parse import urlparse
29
+ import fnmatch
30
+ from concurrent.futures import ThreadPoolExecutor, as_completed
31
+
32
+
33
+ class RiskLevel(Enum):
34
+ """
35
+ Risk levels follow CVSS (Common Vulnerability Scoring System).
36
+ Think of it like triage in an emergency room:
37
+ - CRITICAL: Life-threatening, needs immediate attention
38
+ - HIGH: Serious condition, treat soon
39
+ - MEDIUM: Concerning, schedule treatment
40
+ - LOW: Minor issue, monitor
41
+ - INFO: Just a note for awareness
42
+ """
43
+ CRITICAL = "CRITICAL" # CVSS 9.0-10.0
44
+ HIGH = "HIGH" # CVSS 7.0-8.9
45
+ MEDIUM = "MEDIUM" # CVSS 4.0-6.9
46
+ LOW = "LOW" # CVSS 0.1-3.9
47
+ INFO = "INFO" # Informational
48
+
49
+
50
+ @dataclass
51
+ class Vulnerability:
52
+ """
53
+ Represents a single vulnerability found in the code.
54
+
55
+ Analogy: This is like a medical diagnosis report entry:
56
+ - name: Disease name
57
+ - description: What's wrong
58
+ - file_path: Where in the body (code) the problem is
59
+ - line_number: Exact location
60
+ - code_snippet: The problematic tissue sample
61
+ - risk_level: How serious is it
62
+ - remediation: Treatment plan
63
+ - cve_ids: Reference to known disease database (NVD)
64
+ """
65
+ name: str
66
+ description: str
67
+ file_path: str
68
+ line_number: int
69
+ code_snippet: str
70
+ risk_level: RiskLevel
71
+ remediation: str
72
+ cve_ids: List[str] = field(default_factory=list)
73
+ cwe_id: Optional[str] = None
74
+ confidence: str = "HIGH" # HIGH, MEDIUM, LOW
75
+
76
+ def to_dict(self) -> Dict:
77
+ return {
78
+ "name": self.name,
79
+ "description": self.description,
80
+ "file_path": self.file_path,
81
+ "line_number": self.line_number,
82
+ "code_snippet": self.code_snippet,
83
+ "risk_level": self.risk_level.value,
84
+ "remediation": self.remediation,
85
+ "cve_ids": self.cve_ids,
86
+ "cwe_id": self.cwe_id,
87
+ "confidence": self.confidence
88
+ }
89
+
90
+
91
+ @dataclass
92
+ class ScanResult:
93
+ """
94
+ Complete scan results - like a full medical report.
95
+ """
96
+ target: str
97
+ scan_type: str # "local" or "web"
98
+ start_time: datetime
99
+ end_time: Optional[datetime] = None
100
+ vulnerabilities: List[Vulnerability] = field(default_factory=list)
101
+ files_scanned: int = 0
102
+ errors: List[str] = field(default_factory=list)
103
+
104
+ def summary(self) -> Dict:
105
+ """Generate a summary of findings by risk level."""
106
+ summary = {level.value: 0 for level in RiskLevel}
107
+ for vuln in self.vulnerabilities:
108
+ summary[vuln.risk_level.value] += 1
109
+ return {
110
+ "target": self.target,
111
+ "scan_type": self.scan_type,
112
+ "duration_seconds": (self.end_time - self.start_time).total_seconds() if self.end_time else None,
113
+ "files_scanned": self.files_scanned,
114
+ "total_vulnerabilities": len(self.vulnerabilities),
115
+ "by_severity": summary,
116
+ "errors": len(self.errors)
117
+ }
118
+
119
+
120
+ class SASTRule:
121
+ """
122
+ A single SAST detection rule.
123
+
124
+ Analogy: Like a specific test in a medical lab
125
+ - pattern: What symptom to look for
126
+ - name: Name of the condition
127
+ - languages: Which "body types" this applies to
128
+ """
129
+ def __init__(
130
+ self,
131
+ name: str,
132
+ pattern: str,
133
+ description: str,
134
+ risk_level: RiskLevel,
135
+ remediation: str,
136
+ cwe_id: str,
137
+ languages: List[str],
138
+ false_positive_patterns: List[str] = None
139
+ ):
140
+ self.name = name
141
+ self.pattern = re.compile(pattern, re.IGNORECASE | re.MULTILINE)
142
+ self.description = description
143
+ self.risk_level = risk_level
144
+ self.remediation = remediation
145
+ self.cwe_id = cwe_id
146
+ self.languages = languages # File extensions: ['.py', '.js', etc.]
147
+ self.false_positive_patterns = [
148
+ re.compile(fp, re.IGNORECASE) for fp in (false_positive_patterns or [])
149
+ ]
150
+
151
+ def matches(self, code: str, file_ext: str) -> List[Tuple[int, str]]:
152
+ """
153
+ Find all matches in the code.
154
+ Returns list of (line_number, matched_snippet).
155
+ """
156
+ if file_ext not in self.languages:
157
+ return []
158
+
159
+ matches = []
160
+ lines = code.split('\n')
161
+
162
+ for i, line in enumerate(lines, 1):
163
+ if self.pattern.search(line):
164
+ # Check for false positives
165
+ is_false_positive = any(
166
+ fp.search(line) for fp in self.false_positive_patterns
167
+ )
168
+ if not is_false_positive:
169
+ # Get context (line before and after)
170
+ start = max(0, i - 2)
171
+ end = min(len(lines), i + 1)
172
+ snippet = '\n'.join(lines[start:end])
173
+ matches.append((i, snippet))
174
+
175
+ return matches
176
+
177
+
178
+ class SASTEngine:
179
+ """
180
+ Static Application Security Testing Engine
181
+
182
+ Analogy: This is like a diagnostic imaging department
183
+ - Scans code without executing it (like X-ray/MRI)
184
+ - Looks for known vulnerability patterns
185
+ - Reports findings with locations
186
+
187
+ How it works:
188
+ 1. Load detection rules (what to look for)
189
+ 2. Read source files
190
+ 3. Match patterns against code
191
+ 4. Report findings
192
+ """
193
+
194
+ def __init__(self):
195
+ self.rules = self._load_rules()
196
+ self.file_extensions = {
197
+ '.py': 'python',
198
+ '.js': 'javascript',
199
+ '.ts': 'typescript',
200
+ '.jsx': 'javascript',
201
+ '.tsx': 'typescript',
202
+ '.java': 'java',
203
+ '.php': 'php',
204
+ '.rb': 'ruby',
205
+ '.go': 'go',
206
+ '.cs': 'csharp',
207
+ '.c': 'c',
208
+ '.cpp': 'cpp',
209
+ '.h': 'c',
210
+ '.hpp': 'cpp',
211
+ '.sql': 'sql',
212
+ '.html': 'html',
213
+ '.htm': 'html',
214
+ '.xml': 'xml',
215
+ '.yml': 'yaml',
216
+ '.yaml': 'yaml',
217
+ '.json': 'json',
218
+ '.sh': 'shell',
219
+ '.bash': 'shell',
220
+ }
221
+
222
+ # Directories to skip (like avoiding scanning healthy tissue)
223
+ self.skip_dirs = {
224
+ 'node_modules', 'venv', '.venv', 'env', '.env',
225
+ '__pycache__', '.git', '.svn', '.hg',
226
+ 'dist', 'build', 'target', 'vendor',
227
+ '.idea', '.vscode', 'coverage'
228
+ }
229
+
230
+ def _load_rules(self) -> List[SASTRule]:
231
+ """
232
+ Load vulnerability detection rules.
233
+
234
+ These rules are like a checklist of known security problems.
235
+ Each rule defines:
236
+ - A pattern to match (regex)
237
+ - The type of vulnerability
238
+ - How severe it is
239
+ - How to fix it
240
+ """
241
+ return [
242
+ # ============================================================
243
+ # INJECTION VULNERABILITIES (The "contamination" category)
244
+ # Like checking for contaminants in food/medicine
245
+ # ============================================================
246
+
247
+ SASTRule(
248
+ name="SQL Injection",
249
+ pattern=r"""(?:execute|cursor\.execute|query|raw|rawQuery|executeQuery)\s*\(\s*[f"'].*?%s.*?['"]\s*%|(?:execute|cursor\.execute)\s*\(\s*[f"'].*?\{.*?\}.*?['"]|(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER).*?['"]\s*\+\s*|f['"]\s*(?:SELECT|INSERT|UPDATE|DELETE).*?\{""",
250
+ description="Potential SQL Injection vulnerability. User input may be directly concatenated into SQL queries, allowing attackers to manipulate database operations.",
251
+ risk_level=RiskLevel.CRITICAL,
252
+ remediation="""Use parameterized queries or prepared statements:
253
+
254
+ VULNERABLE:
255
+ cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
256
+
257
+ SECURE:
258
+ cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
259
+
260
+ For ORMs, use built-in query builders instead of raw SQL.""",
261
+ cwe_id="CWE-89",
262
+ languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go', '.cs'],
263
+ false_positive_patterns=[r'#.*SQL', r'//.*SQL', r'/\*.*SQL']
264
+ ),
265
+
266
+ SASTRule(
267
+ name="Command Injection",
268
+ pattern=r"""(?:os\.system|os\.popen|subprocess\.call|subprocess\.run|subprocess\.Popen|exec|eval|Runtime\.getRuntime\(\)\.exec|shell_exec|system|passthru|popen)\s*\([^)]*(?:\+|%|\.format|\{|\$)""",
269
+ description="Potential Command Injection vulnerability. User input may be passed to system commands, allowing attackers to execute arbitrary commands.",
270
+ risk_level=RiskLevel.CRITICAL,
271
+ remediation="""Avoid passing user input to shell commands. If necessary:
272
+
273
+ VULNERABLE:
274
+ os.system(f"ping {user_input}")
275
+
276
+ SECURE:
277
+ import shlex
278
+ subprocess.run(["ping", shlex.quote(user_input)], shell=False)
279
+
280
+ Best practice: Use libraries instead of shell commands when possible.""",
281
+ cwe_id="CWE-78",
282
+ languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go', '.sh']
283
+ ),
284
+
285
+ SASTRule(
286
+ name="XSS (Cross-Site Scripting)",
287
+ pattern=r"""(?:innerHTML|outerHTML|document\.write|\.html\(|v-html|dangerouslySetInnerHTML|\[innerHTML\])\s*=?\s*(?:[^;]*(?:\+|`|\$\{))""",
288
+ description="Potential Cross-Site Scripting (XSS) vulnerability. Untrusted data may be inserted into the DOM without proper encoding.",
289
+ risk_level=RiskLevel.HIGH,
290
+ remediation="""Sanitize and encode output before inserting into HTML:
291
+
292
+ VULNERABLE:
293
+ element.innerHTML = userInput;
294
+
295
+ SECURE:
296
+ element.textContent = userInput; // For text
297
+ // Or use a sanitization library like DOMPurify:
298
+ element.innerHTML = DOMPurify.sanitize(userInput);
299
+
300
+ For React, avoid dangerouslySetInnerHTML unless absolutely necessary.""",
301
+ cwe_id="CWE-79",
302
+ languages=['.js', '.ts', '.jsx', '.tsx', '.html', '.php']
303
+ ),
304
+
305
+ SASTRule(
306
+ name="Path Traversal",
307
+ pattern=r"""(?:open|read|write|file_get_contents|file_put_contents|include|require|fopen|readFile|writeFile|createReadStream)\s*\([^)]*(?:\+|`|\$\{|\.\./)""",
308
+ description="Potential Path Traversal vulnerability. User input may be used to construct file paths, allowing attackers to access unauthorized files.",
309
+ risk_level=RiskLevel.HIGH,
310
+ remediation="""Validate and sanitize file paths:
311
+
312
+ VULNERABLE:
313
+ with open(f"/uploads/{filename}") as f:
314
+
315
+ SECURE:
316
+ import os
317
+ safe_path = os.path.normpath(filename)
318
+ if '..' in safe_path or safe_path.startswith('/'):
319
+ raise ValueError("Invalid path")
320
+ full_path = os.path.join(UPLOAD_DIR, safe_path)
321
+ if not full_path.startswith(UPLOAD_DIR):
322
+ raise ValueError("Path traversal detected")""",
323
+ cwe_id="CWE-22",
324
+ languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go']
325
+ ),
326
+
327
+ SASTRule(
328
+ name="LDAP Injection",
329
+ pattern=r"""(?:ldap_search|ldap_bind|search_s|search_ext_s)\s*\([^)]*(?:\+|%|\.format|\{)""",
330
+ description="Potential LDAP Injection vulnerability. User input may be used in LDAP queries without proper escaping.",
331
+ risk_level=RiskLevel.HIGH,
332
+ remediation="""Escape special LDAP characters in user input:
333
+
334
+ VULNERABLE:
335
+ ldap.search_s(base, scope, f"(uid={username})")
336
+
337
+ SECURE:
338
+ from ldap3.utils.conv import escape_filter_chars
339
+ safe_username = escape_filter_chars(username)
340
+ ldap.search_s(base, scope, f"(uid={safe_username})")""",
341
+ cwe_id="CWE-90",
342
+ languages=['.py', '.java', '.php', '.cs']
343
+ ),
344
+
345
+ # ============================================================
346
+ # AUTHENTICATION & SESSION VULNERABILITIES
347
+ # Like checking if the locks and keys are secure
348
+ # ============================================================
349
+
350
+ SASTRule(
351
+ name="Hardcoded Credentials",
352
+ pattern=r"""(?:password|passwd|pwd|secret|api_key|apikey|api_secret|access_token|auth_token|private_key)\s*[=:]\s*['"]\w{8,}['"]""",
353
+ description="Hardcoded credentials detected. Sensitive information should not be stored in source code.",
354
+ risk_level=RiskLevel.HIGH,
355
+ remediation="""Store credentials securely:
356
+
357
+ VULNERABLE:
358
+ password = "MySecretPass123"
359
+ api_key = "sk-1234567890abcdef"
360
+
361
+ SECURE:
362
+ import os
363
+ password = os.environ.get('DB_PASSWORD')
364
+ api_key = os.environ.get('API_KEY')
365
+
366
+ Use environment variables, secrets managers (AWS Secrets Manager,
367
+ HashiCorp Vault), or encrypted configuration files.""",
368
+ cwe_id="CWE-798",
369
+ languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go', '.cs', '.yml', '.yaml', '.json'],
370
+ false_positive_patterns=[r'example', r'placeholder', r'your_', r'<.*>', r'xxx', r'\$\{']
371
+ ),
372
+
373
+ SASTRule(
374
+ name="Weak Password Hashing",
375
+ pattern=r"""(?:md5|sha1)\s*\(|hashlib\.(?:md5|sha1)\(|MessageDigest\.getInstance\s*\(\s*['"](MD5|SHA-?1)['"]|password.*=.*(?:md5|sha1)""",
376
+ description="Weak hashing algorithm used for passwords. MD5 and SHA1 are cryptographically broken for password storage.",
377
+ risk_level=RiskLevel.HIGH,
378
+ remediation="""Use strong password hashing algorithms:
379
+
380
+ VULNERABLE:
381
+ hashed = hashlib.md5(password.encode()).hexdigest()
382
+
383
+ SECURE:
384
+ import bcrypt
385
+ hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
386
+
387
+ # Or use argon2 (recommended):
388
+ from argon2 import PasswordHasher
389
+ ph = PasswordHasher()
390
+ hashed = ph.hash(password)
391
+
392
+ Recommended algorithms: Argon2, bcrypt, scrypt, PBKDF2""",
393
+ cwe_id="CWE-328",
394
+ languages=['.py', '.java', '.php', '.js', '.ts', '.rb', '.go', '.cs']
395
+ ),
396
+
397
+ SASTRule(
398
+ name="JWT Without Verification",
399
+ pattern=r"""jwt\.decode\s*\([^)]*verify\s*=\s*False|algorithms\s*=\s*\[?\s*['"](none|HS256)['"]|\.decode\(\s*token\s*\)|jsonwebtoken\.decode\s*\(""",
400
+ description="JWT token decoded without proper verification or using weak/no algorithm.",
401
+ risk_level=RiskLevel.HIGH,
402
+ remediation="""Always verify JWT signatures:
403
+
404
+ VULNERABLE:
405
+ payload = jwt.decode(token, verify=False)
406
+ payload = jwt.decode(token, algorithms=['none'])
407
+
408
+ SECURE:
409
+ payload = jwt.decode(
410
+ token,
411
+ SECRET_KEY,
412
+ algorithms=['RS256'], # Use asymmetric algorithms
413
+ options={'verify_exp': True}
414
+ )
415
+
416
+ Use RS256 or ES256 instead of HS256 for better security.""",
417
+ cwe_id="CWE-347",
418
+ languages=['.py', '.js', '.ts', '.java', '.go']
419
+ ),
420
+
421
+ SASTRule(
422
+ name="Session Fixation Risk",
423
+ pattern=r"""session\s*\[\s*['"].*['"]\s*\]\s*=.*request\.|req\.session\s*=.*req\.(body|query|params)|session_id\s*=.*(?:GET|POST|request)""",
424
+ description="Potential session fixation vulnerability. Session identifiers should be regenerated after authentication.",
425
+ risk_level=RiskLevel.MEDIUM,
426
+ remediation="""Regenerate session after authentication:
427
+
428
+ VULNERABLE:
429
+ session['user_id'] = user.id # Without regenerating
430
+
431
+ SECURE (Python/Flask):
432
+ from flask import session
433
+ session.regenerate() # Regenerate session ID
434
+ session['user_id'] = user.id
435
+
436
+ SECURE (Node.js/Express):
437
+ req.session.regenerate((err) => {
438
+ req.session.userId = user.id;
439
+ });""",
440
+ cwe_id="CWE-384",
441
+ languages=['.py', '.js', '.ts', '.php', '.java']
442
+ ),
443
+
444
+ # ============================================================
445
+ # CRYPTOGRAPHIC VULNERABILITIES
446
+ # Like checking if the safe is actually secure
447
+ # ============================================================
448
+
449
+ SASTRule(
450
+ name="Weak Cryptographic Algorithm",
451
+ pattern=r"""(?:DES|RC4|RC2|Blowfish|IDEA)(?:\.|\s|Cipher)|Cipher\.getInstance\s*\(\s*['"](DES|RC4|Blowfish)['"]\)|from\s+Crypto\.Cipher\s+import\s+(DES|Blowfish)|cryptography.*(?:DES|RC4|Blowfish)""",
452
+ description="Weak cryptographic algorithm detected. DES, RC4, RC2, and Blowfish are considered insecure.",
453
+ risk_level=RiskLevel.HIGH,
454
+ remediation="""Use modern cryptographic algorithms:
455
+
456
+ VULNERABLE:
457
+ from Crypto.Cipher import DES
458
+ cipher = DES.new(key, DES.MODE_CBC)
459
+
460
+ SECURE:
461
+ from cryptography.fernet import Fernet
462
+ # Or for low-level:
463
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
464
+ cipher = Cipher(algorithms.AES(key), modes.GCM(iv))
465
+
466
+ Recommended: AES-256-GCM, ChaCha20-Poly1305""",
467
+ cwe_id="CWE-327",
468
+ languages=['.py', '.java', '.js', '.ts', '.go', '.cs', '.php']
469
+ ),
470
+
471
+ SASTRule(
472
+ name="Insecure Random Number Generator",
473
+ pattern=r"""(?:random\.random|random\.randint|Math\.random|rand\(\)|srand\(\)|mt_rand)\s*\(""",
474
+ description="Insecure random number generator used. These are not cryptographically secure and shouldn't be used for security purposes.",
475
+ risk_level=RiskLevel.MEDIUM,
476
+ remediation="""Use cryptographically secure random generators:
477
+
478
+ VULNERABLE:
479
+ token = ''.join(random.choices(string.ascii_letters, k=32))
480
+
481
+ SECURE (Python):
482
+ import secrets
483
+ token = secrets.token_urlsafe(32)
484
+
485
+ SECURE (JavaScript):
486
+ const array = new Uint8Array(32);
487
+ crypto.getRandomValues(array);
488
+
489
+ SECURE (Java):
490
+ SecureRandom random = new SecureRandom();""",
491
+ cwe_id="CWE-338",
492
+ languages=['.py', '.js', '.ts', '.java', '.php', '.c', '.cpp'],
493
+ false_positive_patterns=[r'random\.seed', r'shuffle', r'sample']
494
+ ),
495
+
496
+ SASTRule(
497
+ name="Hardcoded Cryptographic Key",
498
+ pattern=r"""(?:key|iv|nonce|salt)\s*[=:]\s*(?:b?['"]\w{16,}['"]|bytes\s*\(\s*['"]\w{16,}['"])""",
499
+ description="Hardcoded cryptographic key detected. Encryption keys should never be stored in source code.",
500
+ risk_level=RiskLevel.CRITICAL,
501
+ remediation="""Store cryptographic keys securely:
502
+
503
+ VULNERABLE:
504
+ key = b'ThisIsASecretKey1234567890123456'
505
+
506
+ SECURE:
507
+ import os
508
+ key = os.environ.get('ENCRYPTION_KEY').encode()
509
+
510
+ # Or use a key management system:
511
+ from aws_encryption_sdk import KMSMasterKeyProvider
512
+ key_provider = KMSMasterKeyProvider(key_ids=[KEY_ARN])
513
+
514
+ Best practice: Use Hardware Security Modules (HSM) or
515
+ Key Management Services (AWS KMS, Azure Key Vault).""",
516
+ cwe_id="CWE-321",
517
+ languages=['.py', '.java', '.js', '.ts', '.go', '.cs', '.php']
518
+ ),
519
+
520
+ # ============================================================
521
+ # INSECURE DESERIALIZATION
522
+ # Like accepting packages without checking what's inside
523
+ # ============================================================
524
+
525
+ SASTRule(
526
+ name="Insecure Deserialization (Python)",
527
+ pattern=r"""pickle\.loads?\s*\(|yaml\.(?:unsafe_)?load\s*\([^)]*(?!Loader\s*=\s*yaml\.SafeLoader)|marshal\.loads?\s*\(|shelve\.open\s*\(""",
528
+ description="Insecure deserialization detected. Deserializing untrusted data can lead to remote code execution.",
529
+ risk_level=RiskLevel.CRITICAL,
530
+ remediation="""Use safe deserialization methods:
531
+
532
+ VULNERABLE:
533
+ data = pickle.loads(user_input)
534
+ config = yaml.load(file)
535
+
536
+ SECURE:
537
+ import json
538
+ data = json.loads(user_input) # JSON is safe
539
+
540
+ # For YAML, always use SafeLoader:
541
+ config = yaml.load(file, Loader=yaml.SafeLoader)
542
+ # Or better:
543
+ config = yaml.safe_load(file)
544
+
545
+ Never deserialize untrusted data with pickle/marshal.""",
546
+ cwe_id="CWE-502",
547
+ languages=['.py']
548
+ ),
549
+
550
+ SASTRule(
551
+ name="Insecure Deserialization (Java)",
552
+ pattern=r"""ObjectInputStream\s*\(|readObject\s*\(\)|XMLDecoder\s*\(|XStream\.fromXML\s*\(|JSON\.parse\s*\(.*\)\.class""",
553
+ description="Insecure deserialization detected in Java. Can lead to remote code execution.",
554
+ risk_level=RiskLevel.CRITICAL,
555
+ remediation="""Validate and filter deserialization:
556
+
557
+ VULNERABLE:
558
+ ObjectInputStream ois = new ObjectInputStream(input);
559
+ Object obj = ois.readObject();
560
+
561
+ SECURE:
562
+ // Use a whitelist filter
563
+ ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
564
+ "com.myapp.SafeClass;!*"
565
+ );
566
+ ois.setObjectInputFilter(filter);
567
+
568
+ // Or use JSON/Protocol Buffers instead of Java serialization
569
+
570
+ Consider: Jackson with @JsonTypeInfo restrictions,
571
+ or Protocol Buffers for type-safe serialization.""",
572
+ cwe_id="CWE-502",
573
+ languages=['.java']
574
+ ),
575
+
576
+ SASTRule(
577
+ name="Insecure Deserialization (JavaScript)",
578
+ pattern=r"""(?:eval|Function)\s*\(\s*(?:JSON\.parse|atob|decodeURIComponent)|node-serialize|serialize-javascript.*(?:eval|Function)|unserialize\s*\(""",
579
+ description="Insecure deserialization in JavaScript. Eval of untrusted data can lead to code execution.",
580
+ risk_level=RiskLevel.CRITICAL,
581
+ remediation="""Never eval deserialized data:
582
+
583
+ VULNERABLE:
584
+ eval(JSON.parse(userInput).code);
585
+ const obj = serialize.unserialize(userInput);
586
+
587
+ SECURE:
588
+ const data = JSON.parse(userInput);
589
+ // Validate structure before use
590
+ if (typeof data.name !== 'string') {
591
+ throw new Error('Invalid data');
592
+ }
593
+
594
+ Avoid node-serialize and similar libraries with
595
+ eval-based deserialization.""",
596
+ cwe_id="CWE-502",
597
+ languages=['.js', '.ts']
598
+ ),
599
+
600
+ # ============================================================
601
+ # INFORMATION DISCLOSURE
602
+ # Like leaving sensitive documents in public view
603
+ # ============================================================
604
+
605
+ SASTRule(
606
+ name="Debug Mode Enabled",
607
+ pattern=r"""(?:DEBUG|debug)\s*[=:]\s*(?:True|true|1|['"](true|on|yes)['"])|app\.run\s*\([^)]*debug\s*=\s*True|FLASK_DEBUG\s*=\s*1""",
608
+ description="Debug mode appears to be enabled. This can expose sensitive information in production.",
609
+ risk_level=RiskLevel.MEDIUM,
610
+ remediation="""Disable debug mode in production:
611
+
612
+ VULNERABLE:
613
+ app.run(debug=True)
614
+ DEBUG = True
615
+
616
+ SECURE:
617
+ import os
618
+ DEBUG = os.environ.get('DEBUG', 'False').lower() == 'true'
619
+
620
+ # In production:
621
+ app.run(debug=False)
622
+
623
+ Use environment variables to control debug settings.""",
624
+ cwe_id="CWE-215",
625
+ languages=['.py', '.js', '.ts', '.java', '.php', '.rb', '.yml', '.yaml', '.json'],
626
+ false_positive_patterns=[r'#.*DEBUG', r'//.*DEBUG', r'debug.*log']
627
+ ),
628
+
629
+ SASTRule(
630
+ name="Sensitive Data in Logs",
631
+ pattern=r"""(?:log(?:ger)?\.(?:info|debug|warn|error|critical)|print|console\.log|System\.out\.print)\s*\([^)]*(?:password|secret|token|key|credit.?card|ssn|api.?key)""",
632
+ description="Sensitive data may be written to logs. This can expose credentials and personal information.",
633
+ risk_level=RiskLevel.MEDIUM,
634
+ remediation="""Never log sensitive information:
635
+
636
+ VULNERABLE:
637
+ logger.info(f"User login: {username}, password: {password}")
638
+
639
+ SECURE:
640
+ logger.info(f"User login: {username}")
641
+ # Or mask sensitive data:
642
+ logger.info(f"API key: {api_key[:4]}****")
643
+
644
+ Use structured logging with sensitive field filtering.""",
645
+ cwe_id="CWE-532",
646
+ languages=['.py', '.java', '.js', '.ts', '.rb', '.go', '.php']
647
+ ),
648
+
649
+ SASTRule(
650
+ name="Stack Trace Exposure",
651
+ pattern=r"""(?:printStackTrace|traceback\.print_exc|console\.trace|e\.stack|err\.stack)\s*\(?\)?|except.*?:?\s*pass|rescue\s*=>\s*nil""",
652
+ description="Stack traces may be exposed to users or exceptions silently ignored.",
653
+ risk_level=RiskLevel.LOW,
654
+ remediation="""Handle exceptions properly without exposing internals:
655
+
656
+ VULNERABLE:
657
+ except Exception as e:
658
+ return str(e) # Exposes internal details
659
+
660
+ SECURE:
661
+ except Exception as e:
662
+ logger.exception("Operation failed") # Log internally
663
+ return {"error": "An error occurred"} # Generic message
664
+
665
+ Never expose full stack traces to end users.""",
666
+ cwe_id="CWE-209",
667
+ languages=['.py', '.java', '.js', '.ts', '.rb', '.php']
668
+ ),
669
+
670
+ # ============================================================
671
+ # SECURITY MISCONFIGURATION
672
+ # Like leaving doors unlocked or windows open
673
+ # ============================================================
674
+
675
+ SASTRule(
676
+ name="CORS Wildcard",
677
+ pattern=r"""(?:Access-Control-Allow-Origin|cors)\s*[=:]\s*['"]\*['"]|\.allowedOrigins\s*\(\s*['"]\*['"]|cors\s*\(\s*\{[^}]*origin\s*:\s*(?:true|['"]\*['"])""",
678
+ description="CORS configured to allow all origins. This can enable cross-site request attacks.",
679
+ risk_level=RiskLevel.MEDIUM,
680
+ remediation="""Restrict CORS to specific trusted origins:
681
+
682
+ VULNERABLE:
683
+ Access-Control-Allow-Origin: *
684
+ cors({ origin: '*' })
685
+
686
+ SECURE:
687
+ cors({
688
+ origin: ['https://trusted-site.com'],
689
+ methods: ['GET', 'POST'],
690
+ credentials: true
691
+ })
692
+
693
+ Never use wildcard CORS with credentials.""",
694
+ cwe_id="CWE-942",
695
+ languages=['.py', '.java', '.js', '.ts', '.php', '.rb', '.go']
696
+ ),
697
+
698
+ SASTRule(
699
+ name="SSL/TLS Verification Disabled",
700
+ pattern=r"""verify\s*[=:]\s*False|VERIFY_SSL\s*=\s*False|ssl\s*[=:]\s*False|rejectUnauthorized\s*[=:]\s*false|InsecureSkipVerify\s*[=:]\s*true|CURLOPT_SSL_VERIFYPEER.*false""",
701
+ description="SSL/TLS certificate verification is disabled. This makes the application vulnerable to man-in-the-middle attacks.",
702
+ risk_level=RiskLevel.HIGH,
703
+ remediation="""Always verify SSL/TLS certificates:
704
+
705
+ VULNERABLE:
706
+ requests.get(url, verify=False)
707
+ https.request({rejectUnauthorized: false})
708
+
709
+ SECURE:
710
+ requests.get(url, verify=True)
711
+ # Or with custom CA:
712
+ requests.get(url, verify='/path/to/ca-bundle.crt')
713
+
714
+ If you need to use self-signed certs in development,
715
+ use environment-based configuration.""",
716
+ cwe_id="CWE-295",
717
+ languages=['.py', '.java', '.js', '.ts', '.go', '.php', '.rb']
718
+ ),
719
+
720
+ SASTRule(
721
+ name="Insecure HTTP",
722
+ pattern=r"""['"](http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0)[^'"]+)['"]""",
723
+ description="Insecure HTTP URL detected. Data transmitted over HTTP can be intercepted.",
724
+ risk_level=RiskLevel.LOW,
725
+ remediation="""Use HTTPS for all external communications:
726
+
727
+ VULNERABLE:
728
+ api_url = "http://api.example.com/data"
729
+
730
+ SECURE:
731
+ api_url = "https://api.example.com/data"
732
+
733
+ Configure HSTS (HTTP Strict Transport Security) on your servers.""",
734
+ cwe_id="CWE-319",
735
+ languages=['.py', '.java', '.js', '.ts', '.go', '.php', '.rb', '.yml', '.yaml', '.json'],
736
+ false_positive_patterns=[r'#.*http://', r'//.*http://', r'example\.com', r'schema.*http://']
737
+ ),
738
+
739
+ SASTRule(
740
+ name="Missing Security Headers",
741
+ pattern=r"""(?:Content-Security-Policy|X-Frame-Options|X-Content-Type-Options|Strict-Transport-Security)\s*[=:]\s*['"]['""]|no_header|disable.*header""",
742
+ description="Security headers may be missing or disabled. This can enable various attacks.",
743
+ risk_level=RiskLevel.LOW,
744
+ remediation="""Configure security headers:
745
+
746
+ Add these headers to your responses:
747
+ Content-Security-Policy: default-src 'self'
748
+ X-Frame-Options: DENY
749
+ X-Content-Type-Options: nosniff
750
+ Strict-Transport-Security: max-age=31536000; includeSubDomains
751
+ X-XSS-Protection: 1; mode=block
752
+
753
+ Use helmet.js (Node), django-csp, or similar libraries.""",
754
+ cwe_id="CWE-693",
755
+ languages=['.py', '.java', '.js', '.ts', '.php', '.rb']
756
+ ),
757
+
758
+ # ============================================================
759
+ # XML VULNERABILITIES
760
+ # XML parsers can be tricked into dangerous behavior
761
+ # ============================================================
762
+
763
+ SASTRule(
764
+ name="XXE (XML External Entity)",
765
+ pattern=r"""(?:xml\.etree|lxml|xml\.dom|xml\.sax|XMLReader|DocumentBuilder|SAXParser|XMLParser).*(?:parse|read|load)|<!ENTITY|SYSTEM\s+['""]|resolve_entities\s*=\s*True""",
766
+ description="Potential XML External Entity (XXE) vulnerability. XML parsers should disable external entity processing.",
767
+ risk_level=RiskLevel.HIGH,
768
+ remediation="""Disable external entity processing:
769
+
770
+ VULNERABLE (Python):
771
+ tree = etree.parse(xml_file)
772
+
773
+ SECURE (Python):
774
+ parser = etree.XMLParser(resolve_entities=False, no_network=True)
775
+ tree = etree.parse(xml_file, parser)
776
+
777
+ SECURE (Java):
778
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
779
+ dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
780
+ dbf.setExpandEntityReferences(false);""",
781
+ cwe_id="CWE-611",
782
+ languages=['.py', '.java', '.php', '.cs', '.rb']
783
+ ),
784
+
785
+ # ============================================================
786
+ # SERVER-SIDE REQUEST FORGERY (SSRF)
787
+ # Like being tricked into making calls you shouldn't
788
+ # ============================================================
789
+
790
+ SASTRule(
791
+ name="Server-Side Request Forgery (SSRF)",
792
+ pattern=r"""(?:requests\.get|urllib\.request\.urlopen|http\.get|fetch|axios\.get|HttpClient)\s*\([^)]*(?:request\.|req\.|params\.|query\.|body\.|input|GET|POST)""",
793
+ description="Potential SSRF vulnerability. User input may control server-side HTTP requests.",
794
+ risk_level=RiskLevel.HIGH,
795
+ remediation="""Validate and whitelist URLs:
796
+
797
+ VULNERABLE:
798
+ url = request.args.get('url')
799
+ response = requests.get(url)
800
+
801
+ SECURE:
802
+ from urllib.parse import urlparse
803
+
804
+ ALLOWED_HOSTS = ['api.trusted.com', 'data.trusted.com']
805
+
806
+ parsed = urlparse(url)
807
+ if parsed.hostname not in ALLOWED_HOSTS:
808
+ raise ValueError("URL not allowed")
809
+ if parsed.scheme not in ['http', 'https']:
810
+ raise ValueError("Invalid scheme")
811
+ # Block internal IPs
812
+ if is_internal_ip(parsed.hostname):
813
+ raise ValueError("Internal URLs not allowed")""",
814
+ cwe_id="CWE-918",
815
+ languages=['.py', '.java', '.js', '.ts', '.go', '.php', '.rb']
816
+ ),
817
+
818
+ # ============================================================
819
+ # ADDITIONAL COMMON VULNERABILITIES
820
+ # ============================================================
821
+
822
+ SASTRule(
823
+ name="Unsafe Regex (ReDoS)",
824
+ pattern=r"""(?:re\.compile|new\s+RegExp|regex|pattern)\s*\([^)]*(?:\+\*|\*\+|\.+\*|\.+\+|\(\.\*\)|\(\.\+\)|(?:\[[^\]]*\]){2,}\*|\{\d+,\}\*|\{\d+,\}\+)""",
825
+ description="Potentially vulnerable regular expression that could cause ReDoS (Regular Expression Denial of Service).",
826
+ risk_level=RiskLevel.MEDIUM,
827
+ remediation="""Avoid nested quantifiers in regex:
828
+
829
+ VULNERABLE:
830
+ pattern = re.compile(r'(a+)+b') # Catastrophic backtracking
831
+
832
+ SECURE:
833
+ pattern = re.compile(r'a+b') # Simple, efficient
834
+
835
+ # Or use atomic groups/possessive quantifiers where supported
836
+ # Set timeouts for regex operations:
837
+ import regex
838
+ regex.match(pattern, text, timeout=1.0)""",
839
+ cwe_id="CWE-1333",
840
+ languages=['.py', '.java', '.js', '.ts', '.go', '.php', '.rb']
841
+ ),
842
+
843
+ SASTRule(
844
+ name="Prototype Pollution",
845
+ pattern=r"""(?:Object\.assign|_\.merge|_\.extend|_\.defaults|jQuery\.extend|angular\.(?:merge|extend))\s*\([^,]*,\s*(?:req\.|request\.|params\.|body\.|input)|\[['"]__proto__['"]\]|\[['"]constructor['"]\]\.prototype""",
846
+ description="Potential prototype pollution vulnerability. Merging user input into objects can modify Object.prototype.",
847
+ risk_level=RiskLevel.HIGH,
848
+ remediation="""Validate and sanitize object keys:
849
+
850
+ VULNERABLE:
851
+ Object.assign(target, req.body);
852
+ _.merge(config, userInput);
853
+
854
+ SECURE:
855
+ // Use Object.create(null) for prototype-less objects
856
+ const safeObj = Object.create(null);
857
+
858
+ // Whitelist allowed properties
859
+ const allowed = ['name', 'email'];
860
+ for (const key of allowed) {
861
+ if (key in userInput) {
862
+ safeObj[key] = userInput[key];
863
+ }
864
+ }
865
+
866
+ // Or use libraries like 'lodash' with safeguards""",
867
+ cwe_id="CWE-1321",
868
+ languages=['.js', '.ts']
869
+ ),
870
+
871
+ SASTRule(
872
+ name="Open Redirect",
873
+ pattern=r"""(?:redirect|res\.redirect|header\s*\(\s*['""]Location|window\.location|document\.location)\s*[=(]\s*(?:req\.|request\.|params\.|query\.|input|GET|POST|\$_)""",
874
+ description="Potential open redirect vulnerability. User input controls redirect destination.",
875
+ risk_level=RiskLevel.MEDIUM,
876
+ remediation="""Validate redirect URLs:
877
+
878
+ VULNERABLE:
879
+ redirect_url = request.args.get('next')
880
+ return redirect(redirect_url)
881
+
882
+ SECURE:
883
+ from urllib.parse import urlparse
884
+
885
+ redirect_url = request.args.get('next', '/')
886
+ parsed = urlparse(redirect_url)
887
+
888
+ # Only allow relative URLs or specific domains
889
+ if parsed.netloc and parsed.netloc != 'mysite.com':
890
+ redirect_url = '/'
891
+
892
+ return redirect(redirect_url)""",
893
+ cwe_id="CWE-601",
894
+ languages=['.py', '.java', '.js', '.ts', '.php', '.rb']
895
+ ),
896
+
897
+ SASTRule(
898
+ name="Mass Assignment",
899
+ pattern=r"""(?:\.update_attributes|\.update\(|\.create\(|\.build\(|Model\.create|\.save\()\s*\(?[^)]*(?:req\.|request\.|params\[|body\[|:permit\s*\(\s*!)""",
900
+ description="Potential mass assignment vulnerability. User input may modify unintended model attributes.",
901
+ risk_level=RiskLevel.MEDIUM,
902
+ remediation="""Whitelist allowed attributes:
903
+
904
+ VULNERABLE (Rails):
905
+ User.create(params[:user])
906
+
907
+ SECURE (Rails):
908
+ User.create(params.require(:user).permit(:name, :email))
909
+
910
+ VULNERABLE (Django):
911
+ User.objects.create(**request.POST)
912
+
913
+ SECURE (Django):
914
+ User.objects.create(
915
+ name=request.POST.get('name'),
916
+ email=request.POST.get('email')
917
+ )
918
+
919
+ Always explicitly specify which fields can be mass-assigned.""",
920
+ cwe_id="CWE-915",
921
+ languages=['.py', '.rb', '.java', '.js', '.ts', '.php']
922
+ ),
923
+ ]
924
+
925
+ def scan_file(self, file_path: str) -> List[Vulnerability]:
926
+ """
927
+ Scan a single file for vulnerabilities.
928
+
929
+ Like running a specific diagnostic test on one tissue sample.
930
+ """
931
+ vulnerabilities = []
932
+
933
+ try:
934
+ file_ext = Path(file_path).suffix.lower()
935
+
936
+ if file_ext not in self.file_extensions:
937
+ return vulnerabilities
938
+
939
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
940
+ code = f.read()
941
+
942
+ for rule in self.rules:
943
+ matches = rule.matches(code, file_ext)
944
+ for line_num, snippet in matches:
945
+ vuln = Vulnerability(
946
+ name=rule.name,
947
+ description=rule.description,
948
+ file_path=file_path,
949
+ line_number=line_num,
950
+ code_snippet=snippet,
951
+ risk_level=rule.risk_level,
952
+ remediation=rule.remediation,
953
+ cwe_id=rule.cwe_id
954
+ )
955
+ vulnerabilities.append(vuln)
956
+
957
+ except Exception as e:
958
+ # Log but don't fail on individual file errors
959
+ pass
960
+
961
+ return vulnerabilities
962
+
963
+ def scan_directory(self, directory: str, max_workers: int = 8, use_parallel: bool = True) -> Tuple[List[Vulnerability], int]:
964
+ """
965
+ Recursively scan a directory with optional parallel processing.
966
+
967
+ Like performing a full-body scan.
968
+
969
+ Args:
970
+ directory: Path to directory to scan
971
+ max_workers: Number of parallel workers (default: 8)
972
+ use_parallel: Whether to use parallel processing (default: True)
973
+
974
+ Returns:
975
+ Tuple of (vulnerabilities, files_scanned)
976
+ """
977
+ vulnerabilities = []
978
+ files_scanned = 0
979
+
980
+ # Collect all files to scan
981
+ files_to_scan = []
982
+ for root, dirs, files in os.walk(directory):
983
+ # Skip unwanted directories
984
+ dirs[:] = [d for d in dirs if d not in self.skip_dirs]
985
+
986
+ for file in files:
987
+ file_path = os.path.join(root, file)
988
+ file_ext = Path(file_path).suffix.lower()
989
+
990
+ if file_ext in self.file_extensions:
991
+ files_to_scan.append(file_path)
992
+
993
+ if not use_parallel or len(files_to_scan) <= 1:
994
+ # Sequential processing (original behavior)
995
+ for file_path in files_to_scan:
996
+ files_scanned += 1
997
+ vulns = self.scan_file(file_path)
998
+ vulnerabilities.extend(vulns)
999
+ else:
1000
+ # Parallel processing using ThreadPoolExecutor
1001
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
1002
+ # Submit all scan jobs
1003
+ future_to_file = {
1004
+ executor.submit(self.scan_file, file_path): file_path
1005
+ for file_path in files_to_scan
1006
+ }
1007
+
1008
+ # Collect results as they complete
1009
+ for future in as_completed(future_to_file):
1010
+ file_path = future_to_file[future]
1011
+ try:
1012
+ vulns = future.result()
1013
+ vulnerabilities.extend(vulns)
1014
+ files_scanned += 1
1015
+ except Exception as e:
1016
+ # Log error but continue with other files
1017
+ print(f"Error scanning {file_path}: {e}")
1018
+ files_scanned += 1 # Count as scanned even if error
1019
+
1020
+ return vulnerabilities, files_scanned
1021
+
1022
+
1023
+ class NVDClient:
1024
+ """
1025
+ NIST National Vulnerability Database Client
1026
+
1027
+ Analogy: This is like searching a medical journal database
1028
+ - Searches for known vulnerabilities (diseases) by keyword
1029
+ - Retrieves detailed information including severity scores
1030
+ - Provides references to official documentation
1031
+
1032
+ The NVD contains over 200,000 known vulnerabilities (CVEs)
1033
+ with detailed descriptions, severity scores, and references.
1034
+ """
1035
+
1036
+ BASE_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
1037
+
1038
+ def __init__(self, api_key: Optional[str] = None):
1039
+ """
1040
+ Initialize NVD client.
1041
+
1042
+ Args:
1043
+ api_key: Optional NVD API key for higher rate limits.
1044
+ Get one free at: https://nvd.nist.gov/developers/request-an-api-key
1045
+ """
1046
+ self.api_key = api_key
1047
+ self.rate_limit_delay = 0.6 if api_key else 6.0 # NVD rate limits
1048
+
1049
+ async def search_vulnerabilities(
1050
+ self,
1051
+ keyword: Optional[str] = None,
1052
+ cwe_id: Optional[str] = None,
1053
+ severity: Optional[str] = None,
1054
+ limit: int = 20
1055
+ ) -> List[Dict]:
1056
+ """
1057
+ Search the NVD for vulnerabilities.
1058
+
1059
+ Args:
1060
+ keyword: Search term (e.g., "sql injection python")
1061
+ cwe_id: CWE ID to filter by (e.g., "CWE-89")
1062
+ severity: Severity level (LOW, MEDIUM, HIGH, CRITICAL)
1063
+ limit: Maximum results to return
1064
+
1065
+ Returns:
1066
+ List of CVE entries with details
1067
+ """
1068
+ params = {"resultsPerPage": min(limit, 100)}
1069
+
1070
+ if keyword:
1071
+ params["keywordSearch"] = keyword
1072
+
1073
+ if cwe_id:
1074
+ # Format: CWE-89 -> CWE-89
1075
+ params["cweId"] = cwe_id
1076
+
1077
+ if severity:
1078
+ params["cvssV3Severity"] = severity.upper()
1079
+
1080
+ headers = {}
1081
+ if self.api_key:
1082
+ headers["apiKey"] = self.api_key
1083
+
1084
+ try:
1085
+ async with aiohttp.ClientSession() as session:
1086
+ async with session.get(
1087
+ self.BASE_URL,
1088
+ params=params,
1089
+ headers=headers,
1090
+ timeout=aiohttp.ClientTimeout(total=30)
1091
+ ) as response:
1092
+ if response.status == 200:
1093
+ data = await response.json()
1094
+ return self._parse_results(data)
1095
+ elif response.status == 403:
1096
+ return [{"error": "NVD API rate limited. Consider using an API key."}]
1097
+ else:
1098
+ return [{"error": f"NVD API error: {response.status}"}]
1099
+
1100
+ except asyncio.TimeoutError:
1101
+ return [{"error": "NVD API request timed out"}]
1102
+ except Exception as e:
1103
+ return [{"error": f"NVD API error: {str(e)}"}]
1104
+
1105
+ def _parse_results(self, data: Dict) -> List[Dict]:
1106
+ """Parse NVD API response into a cleaner format."""
1107
+ results = []
1108
+
1109
+ for vuln in data.get("vulnerabilities", []):
1110
+ cve = vuln.get("cve", {})
1111
+ cve_id = cve.get("id", "Unknown")
1112
+
1113
+ # Get description
1114
+ descriptions = cve.get("descriptions", [])
1115
+ description = next(
1116
+ (d["value"] for d in descriptions if d.get("lang") == "en"),
1117
+ "No description available"
1118
+ )
1119
+
1120
+ # Get CVSS score and severity
1121
+ metrics = cve.get("metrics", {})
1122
+ cvss_data = None
1123
+ severity = "UNKNOWN"
1124
+ score = 0.0
1125
+
1126
+ # Try CVSS v3.1, then v3.0, then v2.0
1127
+ for version in ["cvssMetricV31", "cvssMetricV30", "cvssMetricV2"]:
1128
+ if version in metrics and metrics[version]:
1129
+ cvss_data = metrics[version][0]
1130
+ if "cvssData" in cvss_data:
1131
+ score = cvss_data["cvssData"].get("baseScore", 0)
1132
+ severity = cvss_data["cvssData"].get("baseSeverity", "UNKNOWN")
1133
+ break
1134
+
1135
+ # Get references
1136
+ references = [
1137
+ ref.get("url") for ref in cve.get("references", [])[:5]
1138
+ ]
1139
+
1140
+ # Get CWE IDs
1141
+ cwes = []
1142
+ for weakness in cve.get("weaknesses", []):
1143
+ for desc in weakness.get("description", []):
1144
+ if desc.get("value", "").startswith("CWE-"):
1145
+ cwes.append(desc["value"])
1146
+
1147
+ results.append({
1148
+ "cve_id": cve_id,
1149
+ "description": description[:500] + "..." if len(description) > 500 else description,
1150
+ "severity": severity,
1151
+ "cvss_score": score,
1152
+ "cwes": cwes,
1153
+ "references": references,
1154
+ "published": cve.get("published", "Unknown"),
1155
+ "last_modified": cve.get("lastModified", "Unknown")
1156
+ })
1157
+
1158
+ return results
1159
+
1160
+ async def get_cve_details(self, cve_id: str) -> Optional[Dict]:
1161
+ """
1162
+ Get detailed information about a specific CVE.
1163
+
1164
+ Args:
1165
+ cve_id: CVE identifier (e.g., "CVE-2021-44228")
1166
+
1167
+ Returns:
1168
+ Detailed CVE information or None if not found
1169
+ """
1170
+ params = {"cveId": cve_id}
1171
+
1172
+ headers = {}
1173
+ if self.api_key:
1174
+ headers["apiKey"] = self.api_key
1175
+
1176
+ try:
1177
+ async with aiohttp.ClientSession() as session:
1178
+ async with session.get(
1179
+ self.BASE_URL,
1180
+ params=params,
1181
+ headers=headers,
1182
+ timeout=aiohttp.ClientTimeout(total=30)
1183
+ ) as response:
1184
+ if response.status == 200:
1185
+ data = await response.json()
1186
+ results = self._parse_results(data)
1187
+ return results[0] if results else None
1188
+ return None
1189
+
1190
+ except Exception:
1191
+ return None
1192
+
1193
+ async def find_related_cves(self, cwe_id: str, limit: int = 10) -> List[Dict]:
1194
+ """
1195
+ Find CVEs related to a specific CWE.
1196
+
1197
+ This helps answer "What known attacks use this vulnerability type?"
1198
+
1199
+ Args:
1200
+ cwe_id: CWE identifier (e.g., "CWE-89")
1201
+ limit: Maximum results
1202
+
1203
+ Returns:
1204
+ List of related CVEs
1205
+ """
1206
+ return await self.search_vulnerabilities(cwe_id=cwe_id, limit=limit)
1207
+
1208
+
1209
+ class WebAppScanner:
1210
+ """
1211
+ Web Application Scanner
1212
+
1213
+ Analogy: Like a physical security inspector
1214
+ - Checks doors and windows (endpoints)
1215
+ - Tests locks (authentication)
1216
+ - Looks for signs of vulnerability
1217
+
1218
+ This performs basic web security checks without being intrusive.
1219
+ For full web app testing, specialized tools like OWASP ZAP are recommended.
1220
+ """
1221
+
1222
+ def __init__(self):
1223
+ self.common_paths = [
1224
+ # Admin paths
1225
+ "/admin", "/administrator", "/admin.php", "/admin.html",
1226
+ "/wp-admin", "/cpanel", "/phpmyadmin",
1227
+ # Sensitive files
1228
+ "/.git/config", "/.env", "/config.php", "/wp-config.php",
1229
+ "/.htaccess", "/web.config", "/robots.txt", "/sitemap.xml",
1230
+ # Backup files
1231
+ "/backup.zip", "/backup.sql", "/db.sql", "/database.sql",
1232
+ # API endpoints
1233
+ "/api", "/api/v1", "/graphql", "/swagger.json", "/openapi.json",
1234
+ # Debug/test
1235
+ "/debug", "/test", "/phpinfo.php", "/info.php",
1236
+ ]
1237
+
1238
+ self.security_headers = [
1239
+ "Content-Security-Policy",
1240
+ "X-Frame-Options",
1241
+ "X-Content-Type-Options",
1242
+ "X-XSS-Protection",
1243
+ "Strict-Transport-Security",
1244
+ "Referrer-Policy",
1245
+ "Permissions-Policy"
1246
+ ]
1247
+
1248
+ async def scan_url(self, url: str) -> List[Vulnerability]:
1249
+ """
1250
+ Perform security scan on a web application.
1251
+
1252
+ Args:
1253
+ url: Target URL (e.g., "https://example.com")
1254
+
1255
+ Returns:
1256
+ List of discovered vulnerabilities
1257
+ """
1258
+ vulnerabilities = []
1259
+
1260
+ # Normalize URL
1261
+ if not url.startswith(('http://', 'https://')):
1262
+ url = 'https://' + url
1263
+
1264
+ parsed = urlparse(url)
1265
+ base_url = f"{parsed.scheme}://{parsed.netloc}"
1266
+
1267
+ async with aiohttp.ClientSession() as session:
1268
+ # Check security headers
1269
+ header_vulns = await self._check_security_headers(session, base_url)
1270
+ vulnerabilities.extend(header_vulns)
1271
+
1272
+ # Check for exposed sensitive files
1273
+ exposure_vulns = await self._check_sensitive_paths(session, base_url)
1274
+ vulnerabilities.extend(exposure_vulns)
1275
+
1276
+ # Check HTTPS configuration
1277
+ https_vulns = await self._check_https(session, url)
1278
+ vulnerabilities.extend(https_vulns)
1279
+
1280
+ # Check for common vulnerabilities in responses
1281
+ content_vulns = await self._check_response_content(session, base_url)
1282
+ vulnerabilities.extend(content_vulns)
1283
+
1284
+ return vulnerabilities
1285
+
1286
+ async def _check_security_headers(
1287
+ self, session: aiohttp.ClientSession, url: str
1288
+ ) -> List[Vulnerability]:
1289
+ """Check for missing or misconfigured security headers."""
1290
+ vulnerabilities = []
1291
+
1292
+ try:
1293
+ async with session.head(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
1294
+ headers = response.headers
1295
+
1296
+ for header in self.security_headers:
1297
+ if header not in headers:
1298
+ vuln = Vulnerability(
1299
+ name=f"Missing Security Header: {header}",
1300
+ description=f"The {header} header is not set. This header helps protect against various attacks.",
1301
+ file_path=url,
1302
+ line_number=0,
1303
+ code_snippet=f"Response headers do not include {header}",
1304
+ risk_level=RiskLevel.LOW if header != "Content-Security-Policy" else RiskLevel.MEDIUM,
1305
+ remediation=self._get_header_remediation(header),
1306
+ cwe_id="CWE-693"
1307
+ )
1308
+ vulnerabilities.append(vuln)
1309
+
1310
+ # Check for server version disclosure
1311
+ if "Server" in headers and any(v in headers["Server"].lower() for v in ["apache/", "nginx/", "iis/"]):
1312
+ vuln = Vulnerability(
1313
+ name="Server Version Disclosure",
1314
+ description=f"Server header reveals version information: {headers['Server']}",
1315
+ file_path=url,
1316
+ line_number=0,
1317
+ code_snippet=f"Server: {headers['Server']}",
1318
+ risk_level=RiskLevel.INFO,
1319
+ remediation="Configure your web server to hide version information. In Apache, use 'ServerTokens Prod'. In Nginx, use 'server_tokens off'.",
1320
+ cwe_id="CWE-200"
1321
+ )
1322
+ vulnerabilities.append(vuln)
1323
+
1324
+ except Exception:
1325
+ pass
1326
+
1327
+ return vulnerabilities
1328
+
1329
+ async def _check_sensitive_paths(
1330
+ self, session: aiohttp.ClientSession, base_url: str
1331
+ ) -> List[Vulnerability]:
1332
+ """Check for exposed sensitive files and directories."""
1333
+ vulnerabilities = []
1334
+
1335
+ async def check_path(path: str):
1336
+ try:
1337
+ url = f"{base_url}{path}"
1338
+ async with session.get(
1339
+ url,
1340
+ timeout=aiohttp.ClientTimeout(total=5),
1341
+ allow_redirects=False
1342
+ ) as response:
1343
+ if response.status == 200:
1344
+ return path, response.status
1345
+ return None
1346
+ except Exception:
1347
+ return None
1348
+
1349
+ # Check paths concurrently
1350
+ tasks = [check_path(path) for path in self.common_paths]
1351
+ results = await asyncio.gather(*tasks)
1352
+
1353
+ for result in results:
1354
+ if result:
1355
+ path, status = result
1356
+ risk = RiskLevel.HIGH if any(
1357
+ s in path for s in ['.git', '.env', 'config', 'backup', 'sql']
1358
+ ) else RiskLevel.MEDIUM
1359
+
1360
+ vuln = Vulnerability(
1361
+ name=f"Exposed Sensitive Path: {path}",
1362
+ description=f"The path {path} is accessible and may expose sensitive information.",
1363
+ file_path=f"{base_url}{path}",
1364
+ line_number=0,
1365
+ code_snippet=f"HTTP {status} returned for {path}",
1366
+ risk_level=risk,
1367
+ remediation=f"Restrict access to {path} using web server configuration. Add authentication or remove from public access.",
1368
+ cwe_id="CWE-538"
1369
+ )
1370
+ vulnerabilities.append(vuln)
1371
+
1372
+ return vulnerabilities
1373
+
1374
+ async def _check_https(
1375
+ self, session: aiohttp.ClientSession, url: str
1376
+ ) -> List[Vulnerability]:
1377
+ """Check HTTPS configuration."""
1378
+ vulnerabilities = []
1379
+ parsed = urlparse(url)
1380
+
1381
+ if parsed.scheme == "http":
1382
+ vuln = Vulnerability(
1383
+ name="Insecure HTTP Connection",
1384
+ description="The target is using HTTP instead of HTTPS. All data transmitted is unencrypted.",
1385
+ file_path=url,
1386
+ line_number=0,
1387
+ code_snippet=f"URL scheme: {parsed.scheme}",
1388
+ risk_level=RiskLevel.HIGH,
1389
+ remediation="Enable HTTPS with a valid TLS certificate. Consider using Let's Encrypt for free certificates. Configure HSTS to prevent downgrade attacks.",
1390
+ cwe_id="CWE-319"
1391
+ )
1392
+ vulnerabilities.append(vuln)
1393
+
1394
+ return vulnerabilities
1395
+
1396
+ async def _check_response_content(
1397
+ self, session: aiohttp.ClientSession, base_url: str
1398
+ ) -> List[Vulnerability]:
1399
+ """Check response content for potential vulnerabilities."""
1400
+ vulnerabilities = []
1401
+
1402
+ try:
1403
+ async with session.get(
1404
+ base_url,
1405
+ timeout=aiohttp.ClientTimeout(total=10)
1406
+ ) as response:
1407
+ if response.status == 200:
1408
+ content = await response.text()
1409
+
1410
+ # Check for error messages that reveal information
1411
+ error_patterns = [
1412
+ (r"mysql_error|mysqli_error|pg_error", "Database Error Disclosure", "CWE-209"),
1413
+ (r"stack\s*trace|traceback|exception.*at\s+line", "Stack Trace Disclosure", "CWE-209"),
1414
+ (r"debug\s*=\s*true|debug_mode|development_mode", "Debug Mode Enabled", "CWE-215"),
1415
+ (r"<!--.*(?:password|api.?key|secret).*-->", "Sensitive Data in Comments", "CWE-615"),
1416
+ ]
1417
+
1418
+ for pattern, name, cwe in error_patterns:
1419
+ if re.search(pattern, content, re.IGNORECASE):
1420
+ vuln = Vulnerability(
1421
+ name=name,
1422
+ description=f"The response contains {name.lower()} which may reveal sensitive information.",
1423
+ file_path=base_url,
1424
+ line_number=0,
1425
+ code_snippet=f"Pattern detected: {pattern}",
1426
+ risk_level=RiskLevel.MEDIUM,
1427
+ remediation=f"Remove {name.lower()} from production responses. Configure error handling to show generic messages.",
1428
+ cwe_id=cwe
1429
+ )
1430
+ vulnerabilities.append(vuln)
1431
+
1432
+ except Exception:
1433
+ pass
1434
+
1435
+ return vulnerabilities
1436
+
1437
+ def _get_header_remediation(self, header: str) -> str:
1438
+ """Get specific remediation advice for missing headers."""
1439
+ remediations = {
1440
+ "Content-Security-Policy": "Add CSP header to control resource loading. Start with: Content-Security-Policy: default-src 'self'",
1441
+ "X-Frame-Options": "Add: X-Frame-Options: DENY (or SAMEORIGIN if you need framing)",
1442
+ "X-Content-Type-Options": "Add: X-Content-Type-Options: nosniff",
1443
+ "X-XSS-Protection": "Add: X-XSS-Protection: 1; mode=block (note: deprecated in favor of CSP)",
1444
+ "Strict-Transport-Security": "Add: Strict-Transport-Security: max-age=31536000; includeSubDomains",
1445
+ "Referrer-Policy": "Add: Referrer-Policy: strict-origin-when-cross-origin",
1446
+ "Permissions-Policy": "Add: Permissions-Policy: geolocation=(), microphone=(), camera=()"
1447
+ }
1448
+ return remediations.get(header, f"Configure the {header} header appropriately.")
1449
+
1450
+
1451
+ class SecurityChecker:
1452
+ """
1453
+ Main Security Checker - Orchestrates all scanning capabilities.
1454
+
1455
+ Analogy: This is like a complete medical center
1456
+ - Diagnostic imaging (SAST)
1457
+ - Medical database (NVD)
1458
+ - Physical examination (Web Scanner)
1459
+ - Report generation (Results)
1460
+
1461
+ Usage:
1462
+ checker = SecurityChecker()
1463
+
1464
+ # Scan local code
1465
+ result = await checker.scan_local("/path/to/project")
1466
+
1467
+ # Scan web app
1468
+ result = await checker.scan_web("https://example.com")
1469
+
1470
+ # Generate report
1471
+ report = checker.generate_report(result)
1472
+ """
1473
+
1474
+ def __init__(self, nvd_api_key: Optional[str] = None):
1475
+ self.sast_engine = SASTEngine()
1476
+ self.nvd_client = NVDClient(api_key=nvd_api_key)
1477
+ self.web_scanner = WebAppScanner()
1478
+
1479
+ async def scan_local(self, path: str, include_nvd: bool = True, max_workers: int = 8, use_parallel: bool = True) -> ScanResult:
1480
+ """
1481
+ Scan a local directory for vulnerabilities.
1482
+
1483
+ Args:
1484
+ path: Path to directory or file
1485
+ include_nvd: Whether to enrich results with NVD data
1486
+ max_workers: Number of parallel workers for file scanning (default: 8)
1487
+ use_parallel: Whether to use parallel processing (default: True)
1488
+
1489
+ Returns:
1490
+ ScanResult with all findings
1491
+ """
1492
+ result = ScanResult(
1493
+ target=path,
1494
+ scan_type="local",
1495
+ start_time=datetime.now()
1496
+ )
1497
+
1498
+ if not os.path.exists(path):
1499
+ result.errors.append(f"Path does not exist: {path}")
1500
+ result.end_time = datetime.now()
1501
+ return result
1502
+
1503
+ # Run SAST scan
1504
+ if os.path.isfile(path):
1505
+ vulns = self.sast_engine.scan_file(path)
1506
+ result.files_scanned = 1
1507
+ else:
1508
+ vulns, files_scanned = self.sast_engine.scan_directory(
1509
+ path,
1510
+ max_workers=max_workers,
1511
+ use_parallel=use_parallel
1512
+ )
1513
+ result.files_scanned = files_scanned
1514
+
1515
+ # Enrich with NVD data if requested
1516
+ if include_nvd and vulns:
1517
+ vulns = await self._enrich_with_nvd(vulns)
1518
+
1519
+ result.vulnerabilities = vulns
1520
+ result.end_time = datetime.now()
1521
+
1522
+ return result
1523
+
1524
+ async def scan_web(self, url: str, include_nvd: bool = True) -> ScanResult:
1525
+ """
1526
+ Scan a web application for vulnerabilities.
1527
+
1528
+ Args:
1529
+ url: Target URL
1530
+ include_nvd: Whether to enrich results with NVD data
1531
+
1532
+ Returns:
1533
+ ScanResult with all findings
1534
+ """
1535
+ result = ScanResult(
1536
+ target=url,
1537
+ scan_type="web",
1538
+ start_time=datetime.now()
1539
+ )
1540
+
1541
+ try:
1542
+ vulns = await self.web_scanner.scan_url(url)
1543
+
1544
+ if include_nvd and vulns:
1545
+ vulns = await self._enrich_with_nvd(vulns)
1546
+
1547
+ result.vulnerabilities = vulns
1548
+ result.files_scanned = 1 # One URL scanned
1549
+
1550
+ except Exception as e:
1551
+ result.errors.append(str(e))
1552
+
1553
+ result.end_time = datetime.now()
1554
+ return result
1555
+
1556
+ async def _enrich_with_nvd(
1557
+ self, vulnerabilities: List[Vulnerability]
1558
+ ) -> List[Vulnerability]:
1559
+ """
1560
+ Enrich vulnerability findings with NVD data.
1561
+
1562
+ This adds related CVEs to each finding, showing real-world
1563
+ examples of the vulnerability being exploited.
1564
+ """
1565
+ # Group by CWE to reduce API calls
1566
+ cwe_groups = {}
1567
+ for vuln in vulnerabilities:
1568
+ if vuln.cwe_id:
1569
+ if vuln.cwe_id not in cwe_groups:
1570
+ cwe_groups[vuln.cwe_id] = []
1571
+ cwe_groups[vuln.cwe_id].append(vuln)
1572
+
1573
+ # Fetch CVEs for each CWE
1574
+ for cwe_id, vuln_list in cwe_groups.items():
1575
+ try:
1576
+ cves = await self.nvd_client.find_related_cves(cwe_id, limit=5)
1577
+ cve_ids = [cve.get("cve_id") for cve in cves if "error" not in cve]
1578
+
1579
+ for vuln in vuln_list:
1580
+ vuln.cve_ids = cve_ids[:3] # Add top 3 related CVEs
1581
+
1582
+ # Rate limiting
1583
+ await asyncio.sleep(self.nvd_client.rate_limit_delay)
1584
+
1585
+ except Exception:
1586
+ pass
1587
+
1588
+ return vulnerabilities
1589
+
1590
+ async def search_nvd(
1591
+ self,
1592
+ keyword: Optional[str] = None,
1593
+ cwe_id: Optional[str] = None,
1594
+ severity: Optional[str] = None
1595
+ ) -> List[Dict]:
1596
+ """
1597
+ Search the NVD directly.
1598
+
1599
+ Useful for researching specific vulnerabilities.
1600
+ """
1601
+ return await self.nvd_client.search_vulnerabilities(
1602
+ keyword=keyword,
1603
+ cwe_id=cwe_id,
1604
+ severity=severity
1605
+ )
1606
+
1607
+ def generate_report(
1608
+ self,
1609
+ result: ScanResult,
1610
+ format: str = "text"
1611
+ ) -> str:
1612
+ """
1613
+ Generate a human-readable report.
1614
+
1615
+ Args:
1616
+ result: ScanResult from a scan
1617
+ format: Output format ("text", "json", "html")
1618
+
1619
+ Returns:
1620
+ Formatted report string
1621
+ """
1622
+ if format == "json":
1623
+ return self._generate_json_report(result)
1624
+ elif format == "markdown":
1625
+ return self._generate_markdown_report(result)
1626
+ else:
1627
+ return self._generate_text_report(result)
1628
+
1629
+ def _generate_text_report(self, result: ScanResult) -> str:
1630
+ """Generate plain text report."""
1631
+ lines = [
1632
+ "=" * 70,
1633
+ "SECURITY SCAN REPORT",
1634
+ "=" * 70,
1635
+ "",
1636
+ f"Target: {result.target}",
1637
+ f"Scan Type: {result.scan_type.upper()}",
1638
+ f"Start Time: {result.start_time.strftime('%Y-%m-%d %H:%M:%S')}",
1639
+ f"End Time: {result.end_time.strftime('%Y-%m-%d %H:%M:%S') if result.end_time else 'N/A'}",
1640
+ f"Files Scanned: {result.files_scanned}",
1641
+ "",
1642
+ ]
1643
+
1644
+ # Summary
1645
+ summary = result.summary()
1646
+ lines.extend([
1647
+ "-" * 70,
1648
+ "SUMMARY",
1649
+ "-" * 70,
1650
+ f"Total Vulnerabilities: {summary['total_vulnerabilities']}",
1651
+ "",
1652
+ "By Severity:",
1653
+ ])
1654
+
1655
+ for severity, count in summary["by_severity"].items():
1656
+ if count > 0:
1657
+ lines.append(f" {severity}: {count}")
1658
+
1659
+ lines.append("")
1660
+
1661
+ if result.errors:
1662
+ lines.extend([
1663
+ "-" * 70,
1664
+ "ERRORS",
1665
+ "-" * 70,
1666
+ ])
1667
+ for error in result.errors:
1668
+ lines.append(f" • {error}")
1669
+ lines.append("")
1670
+
1671
+ # Vulnerabilities by severity
1672
+ if result.vulnerabilities:
1673
+ lines.extend([
1674
+ "-" * 70,
1675
+ "DETAILED FINDINGS",
1676
+ "-" * 70,
1677
+ "",
1678
+ ])
1679
+
1680
+ # Sort by severity
1681
+ severity_order = {
1682
+ RiskLevel.CRITICAL: 0,
1683
+ RiskLevel.HIGH: 1,
1684
+ RiskLevel.MEDIUM: 2,
1685
+ RiskLevel.LOW: 3,
1686
+ RiskLevel.INFO: 4
1687
+ }
1688
+
1689
+ sorted_vulns = sorted(
1690
+ result.vulnerabilities,
1691
+ key=lambda v: severity_order.get(v.risk_level, 5)
1692
+ )
1693
+
1694
+ for i, vuln in enumerate(sorted_vulns, 1):
1695
+ lines.extend([
1696
+ f"[{i}] {vuln.name}",
1697
+ f" Severity: {vuln.risk_level.value}",
1698
+ f" Location: {vuln.file_path}:{vuln.line_number}",
1699
+ f" CWE: {vuln.cwe_id or 'N/A'}",
1700
+ "",
1701
+ f" Description:",
1702
+ f" {vuln.description}",
1703
+ "",
1704
+ f" Code:",
1705
+ " " + "-" * 40,
1706
+ ])
1707
+
1708
+ for line in vuln.code_snippet.split('\n'):
1709
+ lines.append(f" {line}")
1710
+
1711
+ lines.extend([
1712
+ " " + "-" * 40,
1713
+ "",
1714
+ f" Remediation:",
1715
+ ])
1716
+
1717
+ if vuln.remediation and vuln.remediation != "No known solution":
1718
+ for line in vuln.remediation.split('\n'):
1719
+ lines.append(f" {line}")
1720
+ else:
1721
+ lines.append(" No known solution")
1722
+
1723
+ if vuln.cve_ids:
1724
+ lines.extend([
1725
+ "",
1726
+ f" Related CVEs: {', '.join(vuln.cve_ids)}",
1727
+ ])
1728
+
1729
+ lines.extend(["", ""])
1730
+
1731
+ lines.extend([
1732
+ "=" * 70,
1733
+ "END OF REPORT",
1734
+ "=" * 70,
1735
+ ])
1736
+
1737
+ return "\n".join(lines)
1738
+
1739
+ def _generate_json_report(self, result: ScanResult) -> str:
1740
+ """Generate JSON report."""
1741
+ report = {
1742
+ "target": result.target,
1743
+ "scan_type": result.scan_type,
1744
+ "start_time": result.start_time.isoformat(),
1745
+ "end_time": result.end_time.isoformat() if result.end_time else None,
1746
+ "summary": result.summary(),
1747
+ "vulnerabilities": [v.to_dict() for v in result.vulnerabilities],
1748
+ "errors": result.errors
1749
+ }
1750
+ return json.dumps(report, indent=2)
1751
+
1752
+ def _generate_markdown_report(self, result: ScanResult) -> str:
1753
+ """Generate Markdown report optimized for vibe-coding platforms."""
1754
+ summary = result.summary()
1755
+ severity_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]
1756
+
1757
+ lines = []
1758
+ lines.append("# Security Scan Report\n")
1759
+ lines.append(f"**Target:** `{result.target}` ")
1760
+ lines.append(f"**Scan Type:** {result.scan_type.upper()} ")
1761
+ lines.append(f"**Date:** {result.start_time.strftime('%Y-%m-%d %H:%M:%S')} ")
1762
+ lines.append(f"**Files Scanned:** {result.files_scanned} ")
1763
+
1764
+ # Summary line
1765
+ counts = []
1766
+ for sev in severity_order:
1767
+ count = summary['by_severity'].get(sev, 0)
1768
+ if count > 0:
1769
+ counts.append(f"{count} {sev.capitalize()}")
1770
+ total = summary['total_vulnerabilities']
1771
+ lines.append(f"**Total Vulnerabilities:** {total}" + (f" ({', '.join(counts)})" if counts else ""))
1772
+ lines.append("\n---\n")
1773
+
1774
+ if total == 0:
1775
+ lines.append("No vulnerabilities found.\n")
1776
+ return "\n".join(lines)
1777
+
1778
+ # Group vulnerabilities by severity
1779
+ by_severity = {}
1780
+ for vuln in result.vulnerabilities:
1781
+ sev = vuln.risk_level.value
1782
+ by_severity.setdefault(sev, []).append(vuln)
1783
+
1784
+ finding_num = 0
1785
+ for sev in severity_order:
1786
+ vulns = by_severity.get(sev, [])
1787
+ if not vulns:
1788
+ continue
1789
+
1790
+ lines.append(f"## {sev.capitalize()}\n")
1791
+
1792
+ for vuln in vulns:
1793
+ finding_num += 1
1794
+ cwe = f" ({vuln.cwe_id})" if vuln.cwe_id else ""
1795
+ lines.append(f"### {finding_num}. {vuln.name}{cwe}\n")
1796
+ lines.append(f"- **File:** `{vuln.file_path}:{vuln.line_number}`")
1797
+ lines.append(f"- **Confidence:** {vuln.confidence}")
1798
+ lines.append(f"- **Description:** {vuln.description}")
1799
+
1800
+ if vuln.code_snippet and vuln.code_snippet.strip():
1801
+ ext = os.path.splitext(vuln.file_path)[1].lstrip(".")
1802
+ lang = ext if ext else ""
1803
+ lines.append(f"- **Code:**")
1804
+ lines.append(f" ```{lang}")
1805
+ lines.append(f" {vuln.code_snippet.strip()}")
1806
+ lines.append(f" ```")
1807
+
1808
+ if vuln.remediation:
1809
+ lines.append(f"- **Remediation:** {vuln.remediation.strip()}")
1810
+
1811
+ if vuln.cve_ids:
1812
+ lines.append(f"- **Related CVEs:** {', '.join(vuln.cve_ids)}")
1813
+
1814
+ lines.append("")
1815
+
1816
+ lines.append("---\n")
1817
+ lines.append(f"*Generated by Security Auditor | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n")
1818
+
1819
+ return "\n".join(lines)
1820
+
1821
+
1822
+ # ============================================================
1823
+ # CLI Interface
1824
+ # ============================================================
1825
+
1826
+ async def main():
1827
+ """Command-line interface for the security checker."""
1828
+ import argparse
1829
+
1830
+ parser = argparse.ArgumentParser(
1831
+ description="Security Checker - SAST and NVD-powered vulnerability scanner",
1832
+ formatter_class=argparse.RawDescriptionHelpFormatter,
1833
+ epilog="""
1834
+ Examples:
1835
+ # Scan a local project
1836
+ python security_checker.py /path/to/project
1837
+
1838
+ # Scan a web application
1839
+ python security_checker.py https://example.com --web
1840
+
1841
+ # Generate HTML report
1842
+ python security_checker.py /path/to/project --format html -o report.html
1843
+
1844
+ # Search NVD for SQL injection vulnerabilities
1845
+ python security_checker.py --nvd-search "sql injection" --severity HIGH
1846
+ """
1847
+ )
1848
+
1849
+ parser.add_argument(
1850
+ "target",
1851
+ nargs="?",
1852
+ help="Target to scan (local path or URL)"
1853
+ )
1854
+
1855
+ parser.add_argument(
1856
+ "--web",
1857
+ action="store_true",
1858
+ help="Treat target as web URL"
1859
+ )
1860
+
1861
+ parser.add_argument(
1862
+ "--format",
1863
+ choices=["text", "json", "html"],
1864
+ default="text",
1865
+ help="Output format (default: text)"
1866
+ )
1867
+
1868
+ parser.add_argument(
1869
+ "-o", "--output",
1870
+ help="Output file (default: stdout)"
1871
+ )
1872
+
1873
+ parser.add_argument(
1874
+ "--nvd-api-key",
1875
+ help="NVD API key for higher rate limits"
1876
+ )
1877
+
1878
+ parser.add_argument(
1879
+ "--no-nvd",
1880
+ action="store_true",
1881
+ help="Skip NVD enrichment"
1882
+ )
1883
+
1884
+ parser.add_argument(
1885
+ "--nvd-search",
1886
+ help="Search NVD for vulnerabilities by keyword"
1887
+ )
1888
+
1889
+ parser.add_argument(
1890
+ "--severity",
1891
+ choices=["LOW", "MEDIUM", "HIGH", "CRITICAL"],
1892
+ help="Filter NVD search by severity"
1893
+ )
1894
+
1895
+ args = parser.parse_args()
1896
+
1897
+ # Initialize checker
1898
+ checker = SecurityChecker(nvd_api_key=args.nvd_api_key)
1899
+
1900
+ # NVD search mode
1901
+ if args.nvd_search:
1902
+ print(f"Searching NVD for: {args.nvd_search}")
1903
+ results = await checker.search_nvd(
1904
+ keyword=args.nvd_search,
1905
+ severity=args.severity
1906
+ )
1907
+
1908
+ if results and "error" not in results[0]:
1909
+ for cve in results:
1910
+ print(f"\n{cve['cve_id']} ({cve['severity']} - {cve['cvss_score']})")
1911
+ print(f" {cve['description'][:200]}...")
1912
+ if cve['cwes']:
1913
+ print(f" CWEs: {', '.join(cve['cwes'])}")
1914
+ else:
1915
+ print(f"Error: {results[0].get('error', 'Unknown error')}")
1916
+ return
1917
+
1918
+ # Require target for scanning
1919
+ if not args.target:
1920
+ parser.print_help()
1921
+ return
1922
+
1923
+ # Perform scan
1924
+ print(f"Scanning: {args.target}")
1925
+ print("This may take a moment...")
1926
+
1927
+ if args.web or args.target.startswith(('http://', 'https://')):
1928
+ result = await checker.scan_web(args.target, include_nvd=not args.no_nvd)
1929
+ else:
1930
+ result = await checker.scan_local(args.target, include_nvd=not args.no_nvd)
1931
+
1932
+ # Generate report
1933
+ report = checker.generate_report(result, format=args.format)
1934
+
1935
+ # Output
1936
+ if args.output:
1937
+ with open(args.output, 'w') as f:
1938
+ f.write(report)
1939
+ print(f"Report saved to: {args.output}")
1940
+ else:
1941
+ print(report)
1942
+
1943
+
1944
+ if __name__ == "__main__":
1945
+ asyncio.run(main())
theme.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom Gradio Theme for Security Auditor
3
+ Based on design mockups in assets/ folder
4
+ """
5
+
6
+ import gradio as gr
7
+
8
+ def create_security_auditor_theme():
9
+ """
10
+ Create Anthropic.com-inspired theme for Security Auditor.
11
+
12
+ Color Palette:
13
+ - Background: Light cream #faf9f6 (Anthropic-inspired)
14
+ - Primary Text: Dark slate #131314
15
+ - Accent: Warm terracotta #d97757
16
+ - Cards: Pure white #ffffff
17
+ - Borders: Light gray #e5e7eb
18
+
19
+ Design Philosophy:
20
+ - Minimalist, clean aesthetic with generous whitespace
21
+ - Neutral palette with warm accents
22
+ - Subtle borders, no heavy shadows
23
+ - Accessibility-focused
24
+ """
25
+
26
+ theme = gr.themes.Soft(
27
+ primary_hue=gr.themes.colors.orange,
28
+ secondary_hue=gr.themes.colors.neutral,
29
+ neutral_hue=gr.themes.colors.neutral,
30
+ font=[
31
+ "system-ui",
32
+ "-apple-system",
33
+ "BlinkMacSystemFont",
34
+ "Segoe UI",
35
+ "Roboto",
36
+ "sans-serif"
37
+ ],
38
+ font_mono=[
39
+ "ui-monospace",
40
+ "SF Mono",
41
+ "Monaco",
42
+ "Cascadia Code",
43
+ "monospace"
44
+ ]
45
+ ).set(
46
+ # Background colors
47
+ body_background_fill="#faf9f6", # Light cream
48
+ body_background_fill_dark="#131314", # Dark mode fallback
49
+
50
+ # Block/Card styling
51
+ block_background_fill="#ffffff",
52
+ block_border_width="1px",
53
+ block_border_color="#e5e7eb",
54
+ block_shadow="none", # Minimal shadows
55
+ block_radius="8px", # Smaller radius for cleaner look
56
+
57
+ # Button styling - Anthropic style
58
+ button_primary_background_fill="#d97757", # Warm terracotta
59
+ button_primary_background_fill_hover="#cc6944", # Darker terracotta
60
+ button_primary_text_color="#ffffff",
61
+ button_primary_border_color="#d97757",
62
+
63
+ button_secondary_background_fill="transparent", # Minimal secondary
64
+ button_secondary_background_fill_hover="rgba(217, 119, 87, 0.1)",
65
+ button_secondary_text_color="#131314",
66
+ button_secondary_border_color="#e5e7eb",
67
+
68
+ # Input styling
69
+ input_background_fill="#ffffff",
70
+ input_border_width="1px",
71
+ input_border_color="#e5e7eb",
72
+ input_shadow="none",
73
+ input_radius="6px",
74
+
75
+ # Text colors
76
+ body_text_color="#131314", # Dark slate
77
+ body_text_color_subdued="#6b7280", # Medium gray
78
+
79
+ # Link colors
80
+ link_text_color="#d97757",
81
+ link_text_color_hover="#cc6944"
82
+ )
83
+
84
+ return theme
ui_components.py ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI Components for Security Auditor Gradio Interface
3
+ Generates HTML for badges, cards, and sections matching design mockups
4
+ """
5
+
6
+ from typing import Dict, List
7
+
8
+
9
+ def create_severity_badge(severity: str, count: int = 0) -> str:
10
+ """
11
+ Create severity badge as a visual indicator.
12
+
13
+ Args:
14
+ severity: CRITICAL, HIGH, MEDIUM, LOW, INFO
15
+ count: Number of findings
16
+
17
+ Returns:
18
+ HTML string for severity badge
19
+ """
20
+ colors = {
21
+ "CRITICAL": {"bg": "#fef2f2", "text": "#dc2626", "border": "#fca5a5"},
22
+ "HIGH": {"bg": "#fff7ed", "text": "#ea580c", "border": "#fdba74"},
23
+ "MEDIUM": {"bg": "#fffbeb", "text": "#d97706", "border": "#fcd34d"},
24
+ "LOW": {"bg": "#f0fdfa", "text": "#0d9488", "border": "#5eead4"},
25
+ "INFO": {"bg": "#f9fafb", "text": "#6b7280", "border": "#d1d5db"}
26
+ }
27
+
28
+ color = colors.get(severity, colors["INFO"])
29
+
30
+ return f"""
31
+ <div
32
+ class="severity-badge"
33
+ data-severity="{severity}"
34
+ style="
35
+ display: inline-flex;
36
+ flex-direction: column;
37
+ align-items: center;
38
+ padding: 16px 24px;
39
+ background: {color['bg']};
40
+ border: 2px solid {color['border']};
41
+ border-radius: 6px;
42
+ min-width: 100px;
43
+ "
44
+ >
45
+ <div style="
46
+ font-size: 11px;
47
+ font-weight: 600;
48
+ color: {color['text']};
49
+ text-transform: capitalize;
50
+ letter-spacing: 0.05em;
51
+ margin-bottom: 8px;
52
+ ">{severity.capitalize()}</div>
53
+ <div style="
54
+ font-size: 32px;
55
+ font-weight: 700;
56
+ color: {color['text']};
57
+ ">{count}</div>
58
+ </div>
59
+ """
60
+
61
+
62
+ def create_finding_card(vulnerability: Dict) -> str:
63
+ """
64
+ Create HTML for vulnerability finding card.
65
+
66
+ Args:
67
+ vulnerability: Dict with name, severity, file_path, line_number,
68
+ description, cwe_id, cve_ids, remediation
69
+
70
+ Returns:
71
+ HTML string for finding card
72
+ """
73
+ severity_colors = {
74
+ "CRITICAL": "#dc2626",
75
+ "HIGH": "#ea580c",
76
+ "MEDIUM": "#d97706",
77
+ "LOW": "#0d9488",
78
+ "INFO": "#6b7280"
79
+ }
80
+
81
+ severity_bg = {
82
+ "CRITICAL": "#fef2f2",
83
+ "HIGH": "#fff7ed",
84
+ "MEDIUM": "#fffbeb",
85
+ "LOW": "#f0fdfa",
86
+ "INFO": "#f9fafb"
87
+ }
88
+
89
+ severity = vulnerability.get('risk_level', 'INFO')
90
+ color = severity_colors.get(severity, severity_colors['INFO'])
91
+ bg = severity_bg.get(severity, severity_bg['INFO'])
92
+
93
+ # Build CWE/CVE tags
94
+ tags_html = ""
95
+ if vulnerability.get('cwe_id'):
96
+ tags_html += f"""
97
+ <span style="
98
+ display: inline-block;
99
+ padding: 4px 12px;
100
+ background: #f3f4f6;
101
+ color: #4b5563;
102
+ border-radius: 6px;
103
+ font-size: 12px;
104
+ font-weight: 500;
105
+ margin-right: 8px;
106
+ ">{vulnerability['cwe_id']}</span>
107
+ """
108
+
109
+ for cve in vulnerability.get('cve_ids', [])[:3]: # Show max 3 CVEs
110
+ tags_html += f"""
111
+ <span style="
112
+ display: inline-block;
113
+ padding: 4px 12px;
114
+ background: #fef2f2;
115
+ color: #dc2626;
116
+ border-radius: 6px;
117
+ font-size: 12px;
118
+ font-weight: 500;
119
+ margin-right: 8px;
120
+ ">{cve}</span>
121
+ """
122
+
123
+ # Build remediation section
124
+ remediation_html = ""
125
+ if vulnerability.get('remediation'):
126
+ # Truncate very long remediation text
127
+ remediation_text = vulnerability['remediation']
128
+ if len(remediation_text) > 500:
129
+ remediation_text = remediation_text[:500] + "..."
130
+
131
+ remediation_html = f"""
132
+ <details style="margin-top: 16px;" class="remediation-details">
133
+ <summary style="
134
+ cursor: pointer;
135
+ padding: 12px 16px;
136
+ background: #f9fafb;
137
+ border-radius: 8px;
138
+ font-weight: 600;
139
+ color: #374151;
140
+ user-select: none;
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: space-between;
144
+ gap: 8px;
145
+ transition: background 0.2s ease;
146
+ " onmouseover="this.style.background='#f3f4f6'" onmouseout="this.style.background='#f9fafb'">
147
+ <div style="display: flex; align-items: center; gap: 8px;">
148
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
149
+ fill="none" stroke="#d97757" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
150
+ <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
151
+ </svg>
152
+ <span>Remediation Guidance</span>
153
+ </div>
154
+ <svg class="chevron-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
155
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
156
+ style="transition: transform 0.2s ease;">
157
+ <polyline points="6 9 12 15 18 9"></polyline>
158
+ </svg>
159
+ </summary>
160
+ <style>
161
+ details[open] .chevron-icon {{
162
+ transform: rotate(180deg);
163
+ }}
164
+ </style>
165
+ <div style="
166
+ padding: 16px;
167
+ margin-top: 8px;
168
+ background: #f0fdf4;
169
+ border-left: 4px solid #10b981;
170
+ border-radius: 8px;
171
+ ">
172
+ <pre style="
173
+ background: white;
174
+ padding: 12px;
175
+ border-radius: 6px;
176
+ overflow-x: auto;
177
+ font-size: 13px;
178
+ line-height: 1.5;
179
+ color: #1f2937;
180
+ white-space: pre-wrap;
181
+ word-wrap: break-word;
182
+ ">{remediation_text}</pre>
183
+ </div>
184
+ </details>
185
+ """
186
+
187
+ return f"""
188
+ <div class="finding-card" data-severity="{severity}" style="
189
+ background: white;
190
+ border: 1px solid #e5e7eb;
191
+ border-radius: 12px;
192
+ padding: 20px;
193
+ margin-bottom: 16px;
194
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
195
+ ">
196
+ <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
197
+ <h3 style="
198
+ margin: 0;
199
+ font-size: 18px;
200
+ font-weight: 600;
201
+ color: #111827;
202
+ ">{vulnerability.get('name', 'Unknown Vulnerability')}</h3>
203
+ <span style="
204
+ padding: 6px 12px;
205
+ background: {bg};
206
+ color: {color};
207
+ border-radius: 6px;
208
+ font-size: 12px;
209
+ font-weight: 600;
210
+ text-transform: uppercase;
211
+ ">{severity}</span>
212
+ </div>
213
+
214
+ <div style="margin-bottom: 12px;">
215
+ {tags_html}
216
+ </div>
217
+
218
+ <div style="
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 8px;
222
+ margin-bottom: 12px;
223
+ color: #6b7280;
224
+ font-size: 14px;
225
+ ">
226
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24"
227
+ fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
228
+ style="flex-shrink: 0;">
229
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
230
+ <polyline points="14 2 14 8 20 8"></polyline>
231
+ </svg>
232
+ <span style="font-family: 'Fira Code', monospace; color: #374151;">{vulnerability.get('file_path', 'N/A')}:{vulnerability.get('line_number', 'N/A')}</span>
233
+ </div>
234
+
235
+ <p style="
236
+ margin: 0 0 16px 0;
237
+ color: #4b5563;
238
+ line-height: 1.6;
239
+ font-size: 14px;
240
+ ">{vulnerability.get('description', '')}</p>
241
+
242
+ {remediation_html}
243
+ </div>
244
+ """
245
+
246
+
247
+ def create_summary_section(scan_result: Dict) -> str:
248
+ """
249
+ Create Analysis Summary section with proper alignment and sentence capitalization.
250
+
251
+ Args:
252
+ scan_result: Dict with target, files_scanned, scan_type, summary data
253
+
254
+ Returns:
255
+ HTML string for summary section
256
+ """
257
+ summary = scan_result.get('summary', {})
258
+
259
+ # Metadata row - Fixed alignment with grid layout
260
+ metadata_html = f"""
261
+ <div style="
262
+ display: grid;
263
+ grid-template-columns: repeat(4, minmax(0, 1fr));
264
+ gap: 24px;
265
+ margin-bottom: 24px;
266
+ padding: 16px;
267
+ background: #ffffff;
268
+ border: 1px solid #e5e7eb;
269
+ border-radius: 6px;
270
+ ">
271
+ <div style="display: flex; flex-direction: column; justify-content: center;">
272
+ <div style="
273
+ font-size: 12px;
274
+ font-weight: 600;
275
+ color: #6b7280;
276
+ margin-bottom: 8px;
277
+ line-height: 1.2;
278
+ ">Target</div>
279
+ <div style="
280
+ font-size: 14px;
281
+ font-weight: 600;
282
+ color: #131314;
283
+ font-family: ui-monospace, monospace;
284
+ white-space: nowrap;
285
+ overflow: hidden;
286
+ text-overflow: ellipsis;
287
+ line-height: 1.4;
288
+ " title="{scan_result.get('target', 'N/A')}">{scan_result.get('target', 'N/A')}</div>
289
+ </div>
290
+
291
+ <div style="display: flex; flex-direction: column; justify-content: center;">
292
+ <div style="
293
+ font-size: 12px;
294
+ font-weight: 600;
295
+ color: #6b7280;
296
+ margin-bottom: 8px;
297
+ line-height: 1.2;
298
+ ">Files analyzed</div>
299
+ <div style="
300
+ font-size: 24px;
301
+ font-weight: 700;
302
+ color: #131314;
303
+ line-height: 1.2;
304
+ ">{scan_result.get('files_scanned', 0)}</div>
305
+ </div>
306
+
307
+ <div style="display: flex; flex-direction: column; justify-content: center;">
308
+ <div style="
309
+ font-size: 12px;
310
+ font-weight: 600;
311
+ color: #6b7280;
312
+ margin-bottom: 8px;
313
+ line-height: 1.2;
314
+ ">Total findings</div>
315
+ <div style="
316
+ font-size: 28px;
317
+ font-weight: 700;
318
+ color: #dc2626;
319
+ line-height: 1.2;
320
+ ">{summary.get('total_vulnerabilities', 0)}</div>
321
+ </div>
322
+
323
+ <div style="display: flex; flex-direction: column; justify-content: center;">
324
+ <div style="
325
+ font-size: 12px;
326
+ font-weight: 600;
327
+ color: #6b7280;
328
+ margin-bottom: 8px;
329
+ line-height: 1.2;
330
+ ">Analysis type</div>
331
+ <div style="
332
+ font-size: 14px;
333
+ font-weight: 700;
334
+ color: #131314;
335
+ text-transform: capitalize;
336
+ line-height: 1.2;
337
+ ">{scan_result.get('scan_type', 'local')}</div>
338
+ </div>
339
+ </div>
340
+ """
341
+
342
+ # Severity badges row
343
+ by_severity = summary.get('by_severity', {})
344
+ badges_html = f"""
345
+ <div style="
346
+ display: flex;
347
+ gap: 16px;
348
+ flex-wrap: wrap;
349
+ ">
350
+ {create_severity_badge('CRITICAL', by_severity.get('CRITICAL', 0))}
351
+ {create_severity_badge('HIGH', by_severity.get('HIGH', 0))}
352
+ {create_severity_badge('MEDIUM', by_severity.get('MEDIUM', 0))}
353
+ {create_severity_badge('LOW', by_severity.get('LOW', 0))}
354
+ {create_severity_badge('INFO', by_severity.get('INFO', 0))}
355
+ </div>
356
+ """
357
+
358
+ return f"""
359
+ <div style="
360
+ background: white;
361
+ border: 1px solid #e5e7eb;
362
+ border-radius: 12px;
363
+ padding: 24px;
364
+ margin-bottom: 12px;
365
+ box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
366
+ ">
367
+ <h2 style="
368
+ margin: 0 0 20px 0;
369
+ font-size: 20px;
370
+ font-weight: 700;
371
+ color: #131314;
372
+ display: flex;
373
+ align-items: center;
374
+ gap: 8px;
375
+ ">
376
+ <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24"
377
+ fill="none" stroke="#d97757" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
378
+ <path d="M9 11l3 3L22 4"></path>
379
+ <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
380
+ </svg>
381
+ Analysis Summary
382
+ </h2>
383
+
384
+ {metadata_html}
385
+ {badges_html}
386
+ </div>
387
+ """
388
+
389
+
390
+ def create_empty_state() -> str:
391
+ """
392
+ Create HTML for empty state (no results yet).
393
+
394
+ Returns:
395
+ HTML string for empty state
396
+ """
397
+ return """
398
+ <div style="
399
+ background: white;
400
+ border: 2px dashed #e5e7eb;
401
+ border-radius: 12px;
402
+ padding: 60px 40px;
403
+ text-align: center;
404
+ margin: 40px 0;
405
+ ">
406
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24"
407
+ fill="none" stroke="#6b7280" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
408
+ style="margin: 0 auto 16px; display: block;">
409
+ <circle cx="11" cy="11" r="8"></circle>
410
+ <path d="m21 21-4.35-4.35"></path>
411
+ </svg>
412
+ <h3 style="
413
+ margin: 0 0 8px 0;
414
+ font-size: 20px;
415
+ font-weight: 600;
416
+ color: #131314;
417
+ ">Ready to Scan</h3>
418
+ <p style="
419
+ margin: 0;
420
+ color: #6b7280;
421
+ font-size: 14px;
422
+ ">Upload files or enter a URL to begin security analysis</p>
423
+ </div>
424
+ """
425
+
426
+
427
+ def create_loading_state(message: str = "Scanning...") -> str:
428
+ """
429
+ Create HTML for loading state.
430
+
431
+ Args:
432
+ message: Loading message to display
433
+
434
+ Returns:
435
+ HTML string for loading state
436
+ """
437
+ return f"""
438
+ <div style="
439
+ background: white;
440
+ border: 1px solid #e5e7eb;
441
+ border-radius: 12px;
442
+ padding: 40px;
443
+ text-align: center;
444
+ margin: 40px 0;
445
+ ">
446
+ <div style="
447
+ display: inline-block;
448
+ width: 40px;
449
+ height: 40px;
450
+ border: 4px solid #f3f4f6;
451
+ border-top-color: #f59e0b;
452
+ border-radius: 50%;
453
+ animation: spin 1s linear infinite;
454
+ margin-bottom: 16px;
455
+ "></div>
456
+ <style>
457
+ @keyframes spin {{
458
+ 0% {{ transform: rotate(0deg); }}
459
+ 100% {{ transform: rotate(360deg); }}
460
+ }}
461
+ </style>
462
+ <h3 style="
463
+ margin: 0 0 8px 0;
464
+ font-size: 18px;
465
+ font-weight: 600;
466
+ color: #111827;
467
+ ">{message}</h3>
468
+ <p style="
469
+ margin: 0;
470
+ color: #6b7280;
471
+ font-size: 14px;
472
+ ">This may take a few moments...</p>
473
+ </div>
474
+ """