restructure of dirs, huge docs update
This commit is contained in:
201
app/src/screenshot_capture.py
Normal file
201
app/src/screenshot_capture.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
Screenshot capture module for SneakyScanner.
|
||||
|
||||
Uses Playwright with Chromium to capture screenshots of discovered web services.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout
|
||||
|
||||
|
||||
class ScreenshotCapture:
|
||||
"""
|
||||
Handles webpage screenshot capture for web services discovered during scanning.
|
||||
|
||||
Uses Playwright with Chromium in headless mode to capture viewport screenshots
|
||||
of HTTP and HTTPS services. Handles SSL certificate errors gracefully.
|
||||
"""
|
||||
|
||||
def __init__(self, output_dir, scan_timestamp, timeout=15, viewport=None):
|
||||
"""
|
||||
Initialize the screenshot capture handler.
|
||||
|
||||
Args:
|
||||
output_dir (str): Base output directory for scan reports
|
||||
scan_timestamp (str): Timestamp string for this scan (format: YYYYMMDD_HHMMSS)
|
||||
timeout (int): Timeout in seconds for page load and screenshot (default: 15)
|
||||
viewport (dict): Viewport size dict with 'width' and 'height' keys
|
||||
(default: {'width': 1280, 'height': 720})
|
||||
"""
|
||||
self.output_dir = output_dir
|
||||
self.scan_timestamp = scan_timestamp
|
||||
self.timeout = timeout * 1000 # Convert to milliseconds for Playwright
|
||||
self.viewport = viewport or {'width': 1280, 'height': 720}
|
||||
|
||||
self.playwright = None
|
||||
self.browser = None
|
||||
self.screenshot_dir = None
|
||||
|
||||
# Set up logging
|
||||
self.logger = logging.getLogger('SneakyScanner.Screenshot')
|
||||
|
||||
def _get_screenshot_dir(self):
|
||||
"""
|
||||
Create and return the screenshots subdirectory for this scan.
|
||||
|
||||
Returns:
|
||||
Path: Path object for the screenshots directory
|
||||
"""
|
||||
if self.screenshot_dir is None:
|
||||
dir_name = f"scan_report_{self.scan_timestamp}_screenshots"
|
||||
self.screenshot_dir = Path(self.output_dir) / dir_name
|
||||
self.screenshot_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.logger.info(f"Created screenshot directory: {self.screenshot_dir}")
|
||||
|
||||
return self.screenshot_dir
|
||||
|
||||
def _generate_filename(self, ip, port):
|
||||
"""
|
||||
Generate a filename for the screenshot.
|
||||
|
||||
Args:
|
||||
ip (str): IP address of the service
|
||||
port (int): Port number of the service
|
||||
|
||||
Returns:
|
||||
str: Filename in format: {ip}_{port}.png
|
||||
"""
|
||||
# Replace dots in IP with underscores for filesystem compatibility
|
||||
safe_ip = ip.replace('.', '_')
|
||||
return f"{safe_ip}_{port}.png"
|
||||
|
||||
def _launch_browser(self):
|
||||
"""
|
||||
Launch Playwright and Chromium browser in headless mode.
|
||||
|
||||
Returns:
|
||||
bool: True if browser launched successfully, False otherwise
|
||||
"""
|
||||
if self.browser is not None:
|
||||
return True # Already launched
|
||||
|
||||
try:
|
||||
self.logger.info("Launching Chromium browser...")
|
||||
self.playwright = sync_playwright().start()
|
||||
self.browser = self.playwright.chromium.launch(
|
||||
headless=True,
|
||||
args=[
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-gpu',
|
||||
]
|
||||
)
|
||||
self.logger.info("Chromium browser launched successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Failed to launch browser: {e}")
|
||||
return False
|
||||
|
||||
def _close_browser(self):
|
||||
"""
|
||||
Close the browser and cleanup Playwright resources.
|
||||
"""
|
||||
if self.browser:
|
||||
try:
|
||||
self.browser.close()
|
||||
self.logger.info("Browser closed")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error closing browser: {e}")
|
||||
finally:
|
||||
self.browser = None
|
||||
|
||||
if self.playwright:
|
||||
try:
|
||||
self.playwright.stop()
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Error stopping playwright: {e}")
|
||||
finally:
|
||||
self.playwright = None
|
||||
|
||||
def capture(self, ip, port, protocol):
|
||||
"""
|
||||
Capture a screenshot of a web service.
|
||||
|
||||
Args:
|
||||
ip (str): IP address of the service
|
||||
port (int): Port number of the service
|
||||
protocol (str): Protocol to use ('http' or 'https')
|
||||
|
||||
Returns:
|
||||
str: Relative path to the screenshot file, or None if capture failed
|
||||
"""
|
||||
# Validate protocol
|
||||
if protocol not in ['http', 'https']:
|
||||
self.logger.warning(f"Invalid protocol '{protocol}' for {ip}:{port}")
|
||||
return None
|
||||
|
||||
# Launch browser if not already running
|
||||
if not self._launch_browser():
|
||||
return None
|
||||
|
||||
# Build URL
|
||||
url = f"{protocol}://{ip}:{port}"
|
||||
|
||||
# Generate screenshot filename
|
||||
filename = self._generate_filename(ip, port)
|
||||
screenshot_dir = self._get_screenshot_dir()
|
||||
screenshot_path = screenshot_dir / filename
|
||||
|
||||
try:
|
||||
self.logger.info(f"Capturing screenshot: {url}")
|
||||
|
||||
# Create new browser context with viewport and SSL settings
|
||||
context = self.browser.new_context(
|
||||
viewport=self.viewport,
|
||||
ignore_https_errors=True, # Handle self-signed certs
|
||||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
)
|
||||
|
||||
# Create new page
|
||||
page = context.new_page()
|
||||
|
||||
# Set default timeout
|
||||
page.set_default_timeout(self.timeout)
|
||||
|
||||
# Navigate to URL
|
||||
page.goto(url, wait_until='networkidle', timeout=self.timeout)
|
||||
|
||||
# Take screenshot (viewport only)
|
||||
page.screenshot(path=str(screenshot_path), type='png')
|
||||
|
||||
# Close page and context
|
||||
page.close()
|
||||
context.close()
|
||||
|
||||
self.logger.info(f"Screenshot saved: {screenshot_path}")
|
||||
|
||||
# Return relative path (relative to output directory)
|
||||
relative_path = f"{screenshot_dir.name}/{filename}"
|
||||
return relative_path
|
||||
|
||||
except PlaywrightTimeout:
|
||||
self.logger.warning(f"Timeout capturing screenshot for {url}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to capture screenshot for {url}: {e}")
|
||||
return None
|
||||
|
||||
def __enter__(self):
|
||||
"""Context manager entry."""
|
||||
self._launch_browser()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Context manager exit - cleanup browser resources."""
|
||||
self._close_browser()
|
||||
return False # Don't suppress exceptions
|
||||
Reference in New Issue
Block a user