From 3adb51ece2d2e8209a3a5ab2138e72559b4d47fa Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 21 Nov 2025 11:11:37 -0600 Subject: [PATCH 1/7] Add configurable nmap host timeout setting Move nmap host timeout from hardcoded 5m to configurable setting in app/web/config.py with a default of 2m for faster scans. --- app/src/scanner.py | 3 ++- app/web/config.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/scanner.py b/app/src/scanner.py index 3964cb3..80c99d8 100644 --- a/app/src/scanner.py +++ b/app/src/scanner.py @@ -22,6 +22,7 @@ from libnmap.parser import NmapParser from src.screenshot_capture import ScreenshotCapture from src.report_generator import HTMLReportGenerator +from web.config import NMAP_HOST_TIMEOUT # Force unbuffered output for Docker sys.stdout.reconfigure(line_buffering=True) @@ -496,7 +497,7 @@ class SneakyScanner: '--version-intensity', '5', # Balanced speed/accuracy '-p', port_list, '-oX', xml_output, # XML output - '--host-timeout', '5m', # Timeout per host + '--host-timeout', NMAP_HOST_TIMEOUT, # Timeout per host ip ] diff --git a/app/web/config.py b/app/web/config.py index 3d74f10..13d8785 100644 --- a/app/web/config.py +++ b/app/web/config.py @@ -11,3 +11,6 @@ APP_VERSION = '1.0.0-beta' # Repository URL REPO_URL = 'https://git.sneakygeek.net/sneakygeek/SneakyScan' + +# Scanner settings +NMAP_HOST_TIMEOUT = '2m' # Timeout per host for nmap service detection -- 2.49.1 From 4c6b4bf35d1033b38f3a743297ae58a9740031fa Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 21 Nov 2025 11:29:03 -0600 Subject: [PATCH 2/7] Add IP address search feature with global search box - Add API endpoint GET /api/scans/by-ip/{ip_address} to retrieve last 10 scans containing a specific IP - Add ScanService.get_scans_by_ip() method with ScanIP join query - Add search box to global navigation header - Create dedicated search results page at /search/ip - Update API documentation with new endpoint --- app/web/api/scans.py | 39 +++++ app/web/routes/main.py | 15 +- app/web/services/scan_service.py | 23 +++ app/web/templates/base.html | 7 + app/web/templates/ip_search_results.html | 175 +++++++++++++++++++++++ docs/API_REFERENCE.md | 50 +++++++ 6 files changed, 308 insertions(+), 1 deletion(-) create mode 100644 app/web/templates/ip_search_results.html diff --git a/app/web/api/scans.py b/app/web/api/scans.py index f1d0edf..042cb89 100644 --- a/app/web/api/scans.py +++ b/app/web/api/scans.py @@ -281,6 +281,45 @@ def get_scan_status(scan_id): }), 500 +@bp.route('/by-ip/', methods=['GET']) +@api_auth_required +def get_scans_by_ip(ip_address): + """ + Get last 10 scans containing a specific IP address. + + Args: + ip_address: IP address to search for + + Returns: + JSON response with list of scans containing the IP + """ + try: + # Get scans from service + scan_service = ScanService(current_app.db_session) + scans = scan_service.get_scans_by_ip(ip_address) + + logger.info(f"Retrieved {len(scans)} scans for IP: {ip_address}") + + return jsonify({ + 'ip_address': ip_address, + 'scans': scans, + 'count': len(scans) + }) + + except SQLAlchemyError as e: + logger.error(f"Database error retrieving scans for IP {ip_address}: {str(e)}") + return jsonify({ + 'error': 'Database error', + 'message': 'Failed to retrieve scans' + }), 500 + except Exception as e: + logger.error(f"Unexpected error retrieving scans for IP {ip_address}: {str(e)}", exc_info=True) + return jsonify({ + 'error': 'Internal server error', + 'message': 'An unexpected error occurred' + }), 500 + + @bp.route('//compare/', methods=['GET']) @api_auth_required def compare_scans(scan_id1, scan_id2): diff --git a/app/web/routes/main.py b/app/web/routes/main.py index 8c5f366..ee09097 100644 --- a/app/web/routes/main.py +++ b/app/web/routes/main.py @@ -7,7 +7,7 @@ Provides dashboard and scan viewing pages. import logging import os -from flask import Blueprint, current_app, redirect, render_template, send_from_directory, url_for +from flask import Blueprint, current_app, redirect, render_template, request, send_from_directory, url_for from web.auth.decorators import login_required @@ -83,6 +83,19 @@ def compare_scans(scan_id1, scan_id2): return render_template('scan_compare.html', scan_id1=scan_id1, scan_id2=scan_id2) +@bp.route('/search/ip') +@login_required +def search_ip(): + """ + IP search results page - shows scans containing a specific IP address. + + Returns: + Rendered search results template + """ + ip_address = request.args.get('ip', '').strip() + return render_template('ip_search_results.html', ip_address=ip_address) + + @bp.route('/schedules') @login_required def schedules(): diff --git a/app/web/services/scan_service.py b/app/web/services/scan_service.py index 9aea0b8..c8406bf 100644 --- a/app/web/services/scan_service.py +++ b/app/web/services/scan_service.py @@ -260,6 +260,29 @@ class ScanService: return status_info + def get_scans_by_ip(self, ip_address: str, limit: int = 10) -> List[Dict[str, Any]]: + """ + Get the last N scans containing a specific IP address. + + Args: + ip_address: IP address to search for + limit: Maximum number of scans to return (default: 10) + + Returns: + List of scan summary dictionaries, most recent first + """ + scans = ( + self.db.query(Scan) + .join(ScanIP, Scan.id == ScanIP.scan_id) + .filter(ScanIP.ip_address == ip_address) + .filter(Scan.status == 'completed') + .order_by(Scan.timestamp.desc()) + .limit(limit) + .all() + ) + + return [self._scan_to_summary_dict(scan) for scan in scans] + def cleanup_orphaned_scans(self) -> int: """ Clean up orphaned scans that are stuck in 'running' status. diff --git a/app/web/templates/base.html b/app/web/templates/base.html index 8496d07..b5f1c9d 100644 --- a/app/web/templates/base.html +++ b/app/web/templates/base.html @@ -76,6 +76,13 @@ +
+ + +