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;