nightly merge into beta #7
@@ -7,9 +7,11 @@ import argparse
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
import zipfile
|
import zipfile
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -30,6 +32,11 @@ sys.stdout.reconfigure(line_buffering=True)
|
|||||||
sys.stderr.reconfigure(line_buffering=True)
|
sys.stderr.reconfigure(line_buffering=True)
|
||||||
|
|
||||||
|
|
||||||
|
class ScanCancelledError(Exception):
|
||||||
|
"""Raised when a scan is cancelled by the user."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SneakyScanner:
|
class SneakyScanner:
|
||||||
"""Wrapper for masscan to perform network scans based on YAML config or database config"""
|
"""Wrapper for masscan to perform network scans based on YAML config or database config"""
|
||||||
|
|
||||||
@@ -63,6 +70,34 @@ class SneakyScanner:
|
|||||||
|
|
||||||
self.screenshot_capture = None
|
self.screenshot_capture = None
|
||||||
|
|
||||||
|
# Cancellation support
|
||||||
|
self._cancelled = False
|
||||||
|
self._cancel_lock = threading.Lock()
|
||||||
|
self._active_process = None
|
||||||
|
self._process_lock = threading.Lock()
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
"""
|
||||||
|
Cancel the running scan.
|
||||||
|
|
||||||
|
Terminates any active subprocess and sets cancellation flag.
|
||||||
|
"""
|
||||||
|
with self._cancel_lock:
|
||||||
|
self._cancelled = True
|
||||||
|
|
||||||
|
with self._process_lock:
|
||||||
|
if self._active_process and self._active_process.poll() is None:
|
||||||
|
try:
|
||||||
|
# Terminate the process group
|
||||||
|
os.killpg(os.getpgid(self._active_process.pid), signal.SIGTERM)
|
||||||
|
except (ProcessLookupError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_cancelled(self) -> bool:
|
||||||
|
"""Check if scan has been cancelled."""
|
||||||
|
with self._cancel_lock:
|
||||||
|
return self._cancelled
|
||||||
|
|
||||||
def _load_config(self) -> Dict[str, Any]:
|
def _load_config(self) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Load and validate configuration from file or database.
|
Load and validate configuration from file or database.
|
||||||
@@ -383,11 +418,31 @@ class SneakyScanner:
|
|||||||
raise ValueError(f"Invalid protocol: {protocol}")
|
raise ValueError(f"Invalid protocol: {protocol}")
|
||||||
|
|
||||||
print(f"Running: {' '.join(cmd)}", flush=True)
|
print(f"Running: {' '.join(cmd)}", flush=True)
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
|
# Use Popen for cancellation support
|
||||||
|
with self._process_lock:
|
||||||
|
self._active_process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
start_new_session=True
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = self._active_process.communicate()
|
||||||
|
returncode = self._active_process.returncode
|
||||||
|
|
||||||
|
with self._process_lock:
|
||||||
|
self._active_process = None
|
||||||
|
|
||||||
|
# Check if cancelled
|
||||||
|
if self.is_cancelled():
|
||||||
|
return []
|
||||||
|
|
||||||
print(f"Masscan {protocol.upper()} scan completed", flush=True)
|
print(f"Masscan {protocol.upper()} scan completed", flush=True)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if returncode != 0:
|
||||||
print(f"Masscan stderr: {result.stderr}", file=sys.stderr)
|
print(f"Masscan stderr: {stderr}", file=sys.stderr)
|
||||||
|
|
||||||
# Parse masscan JSON output
|
# Parse masscan JSON output
|
||||||
results = []
|
results = []
|
||||||
@@ -435,11 +490,31 @@ class SneakyScanner:
|
|||||||
]
|
]
|
||||||
|
|
||||||
print(f"Running: {' '.join(cmd)}", flush=True)
|
print(f"Running: {' '.join(cmd)}", flush=True)
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
||||||
|
# Use Popen for cancellation support
|
||||||
|
with self._process_lock:
|
||||||
|
self._active_process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
start_new_session=True
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = self._active_process.communicate()
|
||||||
|
returncode = self._active_process.returncode
|
||||||
|
|
||||||
|
with self._process_lock:
|
||||||
|
self._active_process = None
|
||||||
|
|
||||||
|
# Check if cancelled
|
||||||
|
if self.is_cancelled():
|
||||||
|
return {}
|
||||||
|
|
||||||
print(f"Masscan PING scan completed", flush=True)
|
print(f"Masscan PING scan completed", flush=True)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if returncode != 0:
|
||||||
print(f"Masscan stderr: {result.stderr}", file=sys.stderr, flush=True)
|
print(f"Masscan stderr: {stderr}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
# Parse results
|
# Parse results
|
||||||
responding_ips = set()
|
responding_ips = set()
|
||||||
@@ -477,6 +552,10 @@ class SneakyScanner:
|
|||||||
all_services = {}
|
all_services = {}
|
||||||
|
|
||||||
for ip, ports in ip_ports.items():
|
for ip, ports in ip_ports.items():
|
||||||
|
# Check if cancelled before each host
|
||||||
|
if self.is_cancelled():
|
||||||
|
break
|
||||||
|
|
||||||
if not ports:
|
if not ports:
|
||||||
all_services[ip] = []
|
all_services[ip] = []
|
||||||
continue
|
continue
|
||||||
@@ -502,10 +581,29 @@ class SneakyScanner:
|
|||||||
ip
|
ip
|
||||||
]
|
]
|
||||||
|
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
# Use Popen for cancellation support
|
||||||
|
with self._process_lock:
|
||||||
|
self._active_process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
start_new_session=True
|
||||||
|
)
|
||||||
|
|
||||||
if result.returncode != 0:
|
stdout, stderr = self._active_process.communicate(timeout=600)
|
||||||
print(f" Nmap warning for {ip}: {result.stderr}", file=sys.stderr, flush=True)
|
returncode = self._active_process.returncode
|
||||||
|
|
||||||
|
with self._process_lock:
|
||||||
|
self._active_process = None
|
||||||
|
|
||||||
|
# Check if cancelled
|
||||||
|
if self.is_cancelled():
|
||||||
|
Path(xml_output).unlink(missing_ok=True)
|
||||||
|
break
|
||||||
|
|
||||||
|
if returncode != 0:
|
||||||
|
print(f" Nmap warning for {ip}: {stderr}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
# Parse XML output
|
# Parse XML output
|
||||||
services = self._parse_nmap_xml(xml_output)
|
services = self._parse_nmap_xml(xml_output)
|
||||||
@@ -894,6 +992,11 @@ class SneakyScanner:
|
|||||||
progress_callback('ping', None, {'status': 'starting'})
|
progress_callback('ping', None, {'status': 'starting'})
|
||||||
ping_results = self._run_ping_scan(all_ips)
|
ping_results = self._run_ping_scan(all_ips)
|
||||||
|
|
||||||
|
# Check for cancellation
|
||||||
|
if self.is_cancelled():
|
||||||
|
print("\nScan cancelled by user", flush=True)
|
||||||
|
raise ScanCancelledError("Scan cancelled by user")
|
||||||
|
|
||||||
# Report ping results
|
# Report ping results
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback('ping', None, {
|
progress_callback('ping', None, {
|
||||||
@@ -907,6 +1010,11 @@ class SneakyScanner:
|
|||||||
progress_callback('tcp_scan', None, {'status': 'starting'})
|
progress_callback('tcp_scan', None, {'status': 'starting'})
|
||||||
tcp_results = self._run_masscan(all_ips, '0-65535', 'tcp')
|
tcp_results = self._run_masscan(all_ips, '0-65535', 'tcp')
|
||||||
|
|
||||||
|
# Check for cancellation
|
||||||
|
if self.is_cancelled():
|
||||||
|
print("\nScan cancelled by user", flush=True)
|
||||||
|
raise ScanCancelledError("Scan cancelled by user")
|
||||||
|
|
||||||
# Perform UDP scan (if enabled)
|
# Perform UDP scan (if enabled)
|
||||||
udp_enabled = os.environ.get('UDP_SCAN_ENABLED', 'false').lower() == 'true'
|
udp_enabled = os.environ.get('UDP_SCAN_ENABLED', 'false').lower() == 'true'
|
||||||
udp_ports = os.environ.get('UDP_PORTS', '53,67,68,69,123,161,500,514,1900')
|
udp_ports = os.environ.get('UDP_PORTS', '53,67,68,69,123,161,500,514,1900')
|
||||||
@@ -916,6 +1024,11 @@ class SneakyScanner:
|
|||||||
if progress_callback:
|
if progress_callback:
|
||||||
progress_callback('udp_scan', None, {'status': 'starting'})
|
progress_callback('udp_scan', None, {'status': 'starting'})
|
||||||
udp_results = self._run_masscan(all_ips, udp_ports, 'udp')
|
udp_results = self._run_masscan(all_ips, udp_ports, 'udp')
|
||||||
|
|
||||||
|
# Check for cancellation
|
||||||
|
if self.is_cancelled():
|
||||||
|
print("\nScan cancelled by user", flush=True)
|
||||||
|
raise ScanCancelledError("Scan cancelled by user")
|
||||||
else:
|
else:
|
||||||
print(f"\n[3/5] Skipping UDP scan (disabled)...", flush=True)
|
print(f"\n[3/5] Skipping UDP scan (disabled)...", flush=True)
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
@@ -975,6 +1088,11 @@ class SneakyScanner:
|
|||||||
ip_ports = {ip: results_by_ip[ip]['actual']['tcp_ports'] for ip in all_ips}
|
ip_ports = {ip: results_by_ip[ip]['actual']['tcp_ports'] for ip in all_ips}
|
||||||
service_results = self._run_nmap_service_detection(ip_ports)
|
service_results = self._run_nmap_service_detection(ip_ports)
|
||||||
|
|
||||||
|
# Check for cancellation
|
||||||
|
if self.is_cancelled():
|
||||||
|
print("\nScan cancelled by user", flush=True)
|
||||||
|
raise ScanCancelledError("Scan cancelled by user")
|
||||||
|
|
||||||
# Add service information to results
|
# Add service information to results
|
||||||
for ip, services in service_results.items():
|
for ip, services in service_results.items():
|
||||||
if ip in results_by_ip:
|
if ip in results_by_ip:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from web.auth.decorators import api_auth_required
|
|||||||
from web.models import Scan, ScanProgress
|
from web.models import Scan, ScanProgress
|
||||||
from web.services.scan_service import ScanService
|
from web.services.scan_service import ScanService
|
||||||
from web.utils.pagination import validate_page_params
|
from web.utils.pagination import validate_page_params
|
||||||
|
from web.jobs.scan_job import stop_scan
|
||||||
|
|
||||||
bp = Blueprint('scans', __name__)
|
bp = Blueprint('scans', __name__)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -242,6 +243,71 @@ def delete_scan(scan_id):
|
|||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<int:scan_id>/stop', methods=['POST'])
|
||||||
|
@api_auth_required
|
||||||
|
def stop_running_scan(scan_id):
|
||||||
|
"""
|
||||||
|
Stop a running scan.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scan_id: Scan ID to stop
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON response with stop status
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
session = current_app.db_session
|
||||||
|
|
||||||
|
# Check if scan exists and is running
|
||||||
|
scan = session.query(Scan).filter_by(id=scan_id).first()
|
||||||
|
if not scan:
|
||||||
|
logger.warning(f"Scan not found for stop request: {scan_id}")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Not found',
|
||||||
|
'message': f'Scan with ID {scan_id} not found'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
if scan.status != 'running':
|
||||||
|
logger.warning(f"Cannot stop scan {scan_id}: status is '{scan.status}'")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Invalid state',
|
||||||
|
'message': f"Cannot stop scan: status is '{scan.status}'"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
# Get database URL from app config
|
||||||
|
db_url = current_app.config['SQLALCHEMY_DATABASE_URI']
|
||||||
|
|
||||||
|
# Attempt to stop the scan
|
||||||
|
stopped = stop_scan(scan_id, db_url)
|
||||||
|
|
||||||
|
if stopped:
|
||||||
|
logger.info(f"Stop signal sent to scan {scan_id}")
|
||||||
|
return jsonify({
|
||||||
|
'scan_id': scan_id,
|
||||||
|
'message': 'Stop signal sent to scan',
|
||||||
|
'status': 'stopping'
|
||||||
|
}), 200
|
||||||
|
else:
|
||||||
|
logger.warning(f"Failed to stop scan {scan_id}: not found in running scanners")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Stop failed',
|
||||||
|
'message': 'Scan not found in running scanners registry'
|
||||||
|
}), 404
|
||||||
|
|
||||||
|
except SQLAlchemyError as e:
|
||||||
|
logger.error(f"Database error stopping scan {scan_id}: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Database error',
|
||||||
|
'message': 'Failed to stop scan'
|
||||||
|
}), 500
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Unexpected error stopping scan {scan_id}: {str(e)}", exc_info=True)
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Internal server error',
|
||||||
|
'message': 'An unexpected error occurred'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<int:scan_id>/status', methods=['GET'])
|
@bp.route('/<int:scan_id>/status', methods=['GET'])
|
||||||
@api_auth_required
|
@api_auth_required
|
||||||
def get_scan_status(scan_id):
|
def get_scan_status(scan_id):
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ updating database status and handling errors.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -14,13 +15,49 @@ from pathlib import Path
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
from src.scanner import SneakyScanner
|
from src.scanner import SneakyScanner, ScanCancelledError
|
||||||
from web.models import Scan, ScanProgress
|
from web.models import Scan, ScanProgress
|
||||||
from web.services.scan_service import ScanService
|
from web.services.scan_service import ScanService
|
||||||
from web.services.alert_service import AlertService
|
from web.services.alert_service import AlertService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Registry for tracking running scanners (scan_id -> SneakyScanner instance)
|
||||||
|
_running_scanners = {}
|
||||||
|
_running_scanners_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def get_running_scanner(scan_id: int):
|
||||||
|
"""Get a running scanner instance by scan ID."""
|
||||||
|
with _running_scanners_lock:
|
||||||
|
return _running_scanners.get(scan_id)
|
||||||
|
|
||||||
|
|
||||||
|
def stop_scan(scan_id: int, db_url: str) -> bool:
|
||||||
|
"""
|
||||||
|
Stop a running scan.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scan_id: ID of the scan to stop
|
||||||
|
db_url: Database connection URL
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if scan was cancelled, False if not found or already stopped
|
||||||
|
"""
|
||||||
|
logger.info(f"Attempting to stop scan {scan_id}")
|
||||||
|
|
||||||
|
# Get the scanner instance
|
||||||
|
scanner = get_running_scanner(scan_id)
|
||||||
|
if not scanner:
|
||||||
|
logger.warning(f"Scanner for scan {scan_id} not found in registry")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Cancel the scanner
|
||||||
|
scanner.cancel()
|
||||||
|
logger.info(f"Cancellation signal sent to scan {scan_id}")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def create_progress_callback(scan_id: int, session):
|
def create_progress_callback(scan_id: int, session):
|
||||||
"""
|
"""
|
||||||
@@ -186,6 +223,11 @@ def execute_scan(scan_id: int, config_id: int, db_url: str = None):
|
|||||||
# Initialize scanner with database config
|
# Initialize scanner with database config
|
||||||
scanner = SneakyScanner(config_id=config_id)
|
scanner = SneakyScanner(config_id=config_id)
|
||||||
|
|
||||||
|
# Register scanner in the running registry
|
||||||
|
with _running_scanners_lock:
|
||||||
|
_running_scanners[scan_id] = scanner
|
||||||
|
logger.debug(f"Scan {scan_id}: Registered in running scanners registry")
|
||||||
|
|
||||||
# Create progress callback
|
# Create progress callback
|
||||||
progress_callback = create_progress_callback(scan_id, session)
|
progress_callback = create_progress_callback(scan_id, session)
|
||||||
|
|
||||||
@@ -220,6 +262,19 @@ def execute_scan(scan_id: int, config_id: int, db_url: str = None):
|
|||||||
|
|
||||||
logger.info(f"Scan {scan_id}: Completed successfully")
|
logger.info(f"Scan {scan_id}: Completed successfully")
|
||||||
|
|
||||||
|
except ScanCancelledError:
|
||||||
|
# Scan was cancelled by user
|
||||||
|
logger.info(f"Scan {scan_id}: Cancelled by user")
|
||||||
|
|
||||||
|
scan = session.query(Scan).filter_by(id=scan_id).first()
|
||||||
|
if scan:
|
||||||
|
scan.status = 'cancelled'
|
||||||
|
scan.error_message = 'Scan cancelled by user'
|
||||||
|
scan.completed_at = datetime.utcnow()
|
||||||
|
if scan.started_at:
|
||||||
|
scan.duration = (datetime.utcnow() - scan.started_at).total_seconds()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
# Config file not found
|
# Config file not found
|
||||||
error_msg = f"Configuration file not found: {str(e)}"
|
error_msg = f"Configuration file not found: {str(e)}"
|
||||||
@@ -249,6 +304,12 @@ def execute_scan(scan_id: int, config_id: int, db_url: str = None):
|
|||||||
logger.error(f"Scan {scan_id}: Failed to update error status in database: {str(db_error)}")
|
logger.error(f"Scan {scan_id}: Failed to update error status in database: {str(db_error)}")
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
# Unregister scanner from registry
|
||||||
|
with _running_scanners_lock:
|
||||||
|
if scan_id in _running_scanners:
|
||||||
|
del _running_scanners[scan_id]
|
||||||
|
logger.debug(f"Scan {scan_id}: Unregistered from running scanners registry")
|
||||||
|
|
||||||
# Always close the session
|
# Always close the session
|
||||||
session.close()
|
session.close()
|
||||||
logger.info(f"Scan {scan_id}: Background job completed, session closed")
|
logger.info(f"Scan {scan_id}: Background job completed, session closed")
|
||||||
|
|||||||
@@ -257,6 +257,9 @@ class ScanService:
|
|||||||
elif scan.status == 'failed':
|
elif scan.status == 'failed':
|
||||||
status_info['progress'] = 'Failed'
|
status_info['progress'] = 'Failed'
|
||||||
status_info['error_message'] = scan.error_message
|
status_info['error_message'] = scan.error_message
|
||||||
|
elif scan.status == 'cancelled':
|
||||||
|
status_info['progress'] = 'Cancelled'
|
||||||
|
status_info['error_message'] = scan.error_message
|
||||||
|
|
||||||
return status_info
|
return status_info
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
<span id="refresh-text">Refresh</span>
|
<span id="refresh-text">Refresh</span>
|
||||||
<span id="refresh-spinner" class="spinner-border spinner-border-sm ms-1" style="display: none;"></span>
|
<span id="refresh-spinner" class="spinner-border spinner-border-sm ms-1" style="display: none;"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-warning ms-2" onclick="stopScan()" id="stop-btn" style="display: none;">
|
||||||
|
<span id="stop-text">Stop Scan</span>
|
||||||
|
<span id="stop-spinner" class="spinner-border spinner-border-sm ms-1" style="display: none;"></span>
|
||||||
|
</button>
|
||||||
<button class="btn btn-danger ms-2" onclick="deleteScan()" id="delete-btn">Delete Scan</button>
|
<button class="btn btn-danger ms-2" onclick="deleteScan()" id="delete-btn">Delete Scan</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -471,8 +475,11 @@
|
|||||||
} else if (scan.status === 'running') {
|
} else if (scan.status === 'running') {
|
||||||
statusBadge = '<span class="badge badge-info">Running</span>';
|
statusBadge = '<span class="badge badge-info">Running</span>';
|
||||||
document.getElementById('delete-btn').disabled = true;
|
document.getElementById('delete-btn').disabled = true;
|
||||||
|
document.getElementById('stop-btn').style.display = 'inline-block';
|
||||||
} else if (scan.status === 'failed') {
|
} else if (scan.status === 'failed') {
|
||||||
statusBadge = '<span class="badge badge-danger">Failed</span>';
|
statusBadge = '<span class="badge badge-danger">Failed</span>';
|
||||||
|
} else if (scan.status === 'cancelled') {
|
||||||
|
statusBadge = '<span class="badge badge-warning">Cancelled</span>';
|
||||||
} else {
|
} else {
|
||||||
statusBadge = `<span class="badge badge-info">${scan.status}</span>`;
|
statusBadge = `<span class="badge badge-info">${scan.status}</span>`;
|
||||||
}
|
}
|
||||||
@@ -697,6 +704,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop scan
|
||||||
|
async function stopScan() {
|
||||||
|
if (!confirm(`Are you sure you want to stop scan ${scanId}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopBtn = document.getElementById('stop-btn');
|
||||||
|
const stopText = document.getElementById('stop-text');
|
||||||
|
const stopSpinner = document.getElementById('stop-spinner');
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
stopText.style.display = 'none';
|
||||||
|
stopSpinner.style.display = 'inline-block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/scans/${scanId}/stop`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = `HTTP ${response.status}: Failed to stop scan`;
|
||||||
|
try {
|
||||||
|
const data = await response.json();
|
||||||
|
errorMessage = data.message || errorMessage;
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore JSON parse errors
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
showAlert('success', `Stop signal sent to scan ${scanId}.`);
|
||||||
|
|
||||||
|
// Refresh scan data after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
loadScan();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping scan:', error);
|
||||||
|
showAlert('danger', `Failed to stop scan: ${error.message}`);
|
||||||
|
|
||||||
|
// Re-enable button on error
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
stopText.style.display = 'inline';
|
||||||
|
stopSpinner.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find previous scan and show compare button
|
// Find previous scan and show compare button
|
||||||
let previousScanId = null;
|
let previousScanId = null;
|
||||||
let currentConfigId = null;
|
let currentConfigId = null;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<option value="running">Running</option>
|
<option value="running">Running</option>
|
||||||
<option value="completed">Completed</option>
|
<option value="completed">Completed</option>
|
||||||
<option value="failed">Failed</option>
|
<option value="failed">Failed</option>
|
||||||
|
<option value="cancelled">Cancelled</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@@ -248,20 +249,27 @@
|
|||||||
statusBadge = '<span class="badge badge-info">Running</span>';
|
statusBadge = '<span class="badge badge-info">Running</span>';
|
||||||
} else if (scan.status === 'failed') {
|
} else if (scan.status === 'failed') {
|
||||||
statusBadge = '<span class="badge badge-danger">Failed</span>';
|
statusBadge = '<span class="badge badge-danger">Failed</span>';
|
||||||
|
} else if (scan.status === 'cancelled') {
|
||||||
|
statusBadge = '<span class="badge badge-warning">Cancelled</span>';
|
||||||
} else {
|
} else {
|
||||||
statusBadge = `<span class="badge badge-info">${scan.status}</span>`;
|
statusBadge = `<span class="badge badge-info">${scan.status}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
let actionButtons = `<a href="/scans/${scan.id}" class="btn btn-sm btn-secondary">View</a>`;
|
||||||
|
if (scan.status === 'running') {
|
||||||
|
actionButtons += `<button class="btn btn-sm btn-warning ms-1" onclick="stopScan(${scan.id})">Stop</button>`;
|
||||||
|
} else {
|
||||||
|
actionButtons += `<button class="btn btn-sm btn-danger ms-1" onclick="deleteScan(${scan.id})">Delete</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td class="mono">${scan.id}</td>
|
<td class="mono">${scan.id}</td>
|
||||||
<td>${scan.title || 'Untitled Scan'}</td>
|
<td>${scan.title || 'Untitled Scan'}</td>
|
||||||
<td class="text-muted">${timestamp}</td>
|
<td class="text-muted">${timestamp}</td>
|
||||||
<td class="mono">${duration}</td>
|
<td class="mono">${duration}</td>
|
||||||
<td>${statusBadge}</td>
|
<td>${statusBadge}</td>
|
||||||
<td>
|
<td>${actionButtons}</td>
|
||||||
<a href="/scans/${scan.id}" class="btn btn-sm btn-secondary">View</a>
|
|
||||||
${scan.status !== 'running' ? `<button class="btn btn-sm btn-danger ms-1" onclick="deleteScan(${scan.id})">Delete</button>` : ''}
|
|
||||||
</td>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
@@ -489,6 +497,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop scan
|
||||||
|
async function stopScan(scanId) {
|
||||||
|
if (!confirm(`Are you sure you want to stop scan ${scanId}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/scans/${scanId}/stop`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
throw new Error(data.message || 'Failed to stop scan');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
showAlert('success', `Stop signal sent to scan ${scanId}.`);
|
||||||
|
|
||||||
|
// Refresh scans after a short delay
|
||||||
|
setTimeout(() => loadScans(), 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping scan:', error);
|
||||||
|
showAlert('danger', `Failed to stop scan: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Delete scan
|
// Delete scan
|
||||||
async function deleteScan(scanId) {
|
async function deleteScan(scanId) {
|
||||||
if (!confirm(`Are you sure you want to delete scan ${scanId}?`)) {
|
if (!confirm(`Are you sure you want to delete scan ${scanId}?`)) {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def validate_scan_status(status: str) -> tuple[bool, Optional[str]]:
|
|||||||
>>> validate_scan_status('invalid')
|
>>> validate_scan_status('invalid')
|
||||||
(False, 'Invalid status: invalid. Must be one of: running, completed, failed')
|
(False, 'Invalid status: invalid. Must be one of: running, completed, failed')
|
||||||
"""
|
"""
|
||||||
valid_statuses = ['running', 'completed', 'failed']
|
valid_statuses = ['running', 'completed', 'failed', 'cancelled']
|
||||||
|
|
||||||
if status not in valid_statuses:
|
if status not in valid_statuses:
|
||||||
return False, f'Invalid status: {status}. Must be one of: {", ".join(valid_statuses)}'
|
return False, f'Invalid status: {status}. Must be one of: {", ".join(valid_statuses)}'
|
||||||
|
|||||||
Reference in New Issue
Block a user