From 9bd2f6715072f8f9865daed23f605d7e95ae7b2f Mon Sep 17 00:00:00 2001 From: Phillip Tarrant Date: Fri, 21 Nov 2025 15:40:37 -0600 Subject: [PATCH] Add quick button to mark unexpected ports as expected Allow users to add ports to expected list directly from scan results page instead of navigating through site config pages. The button appears next to unexpected ports and updates the site IP configuration via the API. - Add site_id and site_ip_id to scan result data for linking to config - Add "Mark Expected" button next to unexpected ports in scan detail view - Implement markPortExpected() JS function to update site IP settings --- app/web/services/scan_service.py | 36 +++++++++++-- app/web/templates/scan_detail.html | 83 +++++++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 4 deletions(-) diff --git a/app/web/services/scan_service.py b/app/web/services/scan_service.py index c87f273..ba518c6 100644 --- a/app/web/services/scan_service.py +++ b/app/web/services/scan_service.py @@ -16,7 +16,7 @@ from sqlalchemy.orm import Session, joinedload from web.models import ( Scan, ScanSite, ScanIP, ScanPort, ScanService as ScanServiceModel, - ScanCertificate, ScanTLSVersion, Site, ScanSiteAssociation + ScanCertificate, ScanTLSVersion, Site, ScanSiteAssociation, SiteIP ) from web.utils.pagination import paginate, PaginatedResult from web.utils.validators import validate_scan_status @@ -630,17 +630,47 @@ class ScanService: def _site_to_dict(self, site: ScanSite) -> Dict[str, Any]: """Convert ScanSite to dictionary.""" + # Look up the master Site ID from ScanSiteAssociation + master_site_id = None + assoc = ( + self.db.query(ScanSiteAssociation) + .filter( + ScanSiteAssociation.scan_id == site.scan_id, + ) + .join(Site) + .filter(Site.name == site.site_name) + .first() + ) + if assoc: + master_site_id = assoc.site_id + return { 'id': site.id, 'name': site.site_name, - 'ips': [self._ip_to_dict(ip) for ip in site.ips] + 'site_id': master_site_id, # The actual Site ID for config updates + 'ips': [self._ip_to_dict(ip, master_site_id) for ip in site.ips] } - def _ip_to_dict(self, ip: ScanIP) -> Dict[str, Any]: + def _ip_to_dict(self, ip: ScanIP, site_id: Optional[int] = None) -> Dict[str, Any]: """Convert ScanIP to dictionary.""" + # Look up the SiteIP ID for this IP address in the master Site + site_ip_id = None + if site_id: + site_ip = ( + self.db.query(SiteIP) + .filter( + SiteIP.site_id == site_id, + SiteIP.ip_address == ip.ip_address + ) + .first() + ) + if site_ip: + site_ip_id = site_ip.id + return { 'id': ip.id, 'address': ip.ip_address, + 'site_ip_id': site_ip_id, # The actual SiteIP ID for config updates 'ping_expected': ip.ping_expected, 'ping_actual': ip.ping_actual, 'ports': [self._port_to_dict(port) for port in ip.ports] diff --git a/app/web/templates/scan_detail.html b/app/web/templates/scan_detail.html index 2e038c4..2d83a75 100644 --- a/app/web/templates/scan_detail.html +++ b/app/web/templates/scan_detail.html @@ -586,6 +586,19 @@ const screenshotPath = service && service.screenshot_path ? service.screenshot_path : null; const certificate = service && service.certificates && service.certificates.length > 0 ? service.certificates[0] : null; + // Build status cell with optional "Mark Expected" button + let statusCell; + if (port.expected) { + statusCell = 'Expected'; + } else { + // Show "Unexpected" badge with "Mark Expected" button if site_id and site_ip_id are available + const canMarkExpected = site.site_id && ip.site_ip_id; + statusCell = `Unexpected`; + if (canMarkExpected) { + statusCell += ` `; + } + } + const row = document.createElement('tr'); row.classList.add('scan-row'); // Fix white row bug row.innerHTML = ` @@ -595,7 +608,7 @@ ${service ? service.service_name : '-'} ${service ? service.product || '-' : '-'} ${service ? service.version || '-' : '-'} - ${port.expected ? 'Expected' : 'Unexpected'} + ${statusCell} ${screenshotPath ? `` : '-'} ${certificate ? `` : '-'} `; @@ -757,6 +770,74 @@ } } + // Mark a port as expected in the site config + async function markPortExpected(siteId, ipId, portNumber, protocol) { + try { + // First, get the current IP settings - fetch all IPs with high per_page to find the one we need + const getResponse = await fetch(`/api/sites/${siteId}/ips?per_page=200`); + if (!getResponse.ok) { + throw new Error('Failed to get site IPs'); + } + const ipsData = await getResponse.json(); + + // Find the IP in the site + const ipData = ipsData.ips.find(ip => ip.id === ipId); + if (!ipData) { + throw new Error('IP not found in site'); + } + + // Get current expected ports + let expectedTcpPorts = ipData.expected_tcp_ports || []; + let expectedUdpPorts = ipData.expected_udp_ports || []; + + // Add the new port to the appropriate list + if (protocol.toLowerCase() === 'tcp') { + if (!expectedTcpPorts.includes(portNumber)) { + expectedTcpPorts.push(portNumber); + expectedTcpPorts.sort((a, b) => a - b); + } + } else if (protocol.toLowerCase() === 'udp') { + if (!expectedUdpPorts.includes(portNumber)) { + expectedUdpPorts.push(portNumber); + expectedUdpPorts.sort((a, b) => a - b); + } + } + + // Update the IP settings + const updateResponse = await fetch(`/api/sites/${siteId}/ips/${ipId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + expected_tcp_ports: expectedTcpPorts, + expected_udp_ports: expectedUdpPorts + }) + }); + + if (!updateResponse.ok) { + let errorMessage = 'Failed to update IP settings'; + try { + const errorData = await updateResponse.json(); + errorMessage = errorData.message || errorMessage; + } catch (e) { + // Ignore JSON parse errors + } + throw new Error(errorMessage); + } + + // Show success message + showAlert('success', `Port ${portNumber}/${protocol.toUpperCase()} added to expected ports for this IP. Refresh the page to see updated status.`); + + // Optionally refresh the scan data to show the change + // Note: The scan data itself won't change, but the user knows it's been updated + + } catch (error) { + console.error('Error marking port as expected:', error); + showAlert('danger', `Failed to mark port as expected: ${error.message}`); + } + } + // Find previous scan and show compare button let previousScanId = null; let currentConfigId = null;