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
This commit is contained in:
@@ -16,7 +16,7 @@ from sqlalchemy.orm import Session, joinedload
|
|||||||
|
|
||||||
from web.models import (
|
from web.models import (
|
||||||
Scan, ScanSite, ScanIP, ScanPort, ScanService as ScanServiceModel,
|
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.pagination import paginate, PaginatedResult
|
||||||
from web.utils.validators import validate_scan_status
|
from web.utils.validators import validate_scan_status
|
||||||
@@ -630,17 +630,47 @@ class ScanService:
|
|||||||
|
|
||||||
def _site_to_dict(self, site: ScanSite) -> Dict[str, Any]:
|
def _site_to_dict(self, site: ScanSite) -> Dict[str, Any]:
|
||||||
"""Convert ScanSite to dictionary."""
|
"""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 {
|
return {
|
||||||
'id': site.id,
|
'id': site.id,
|
||||||
'name': site.site_name,
|
'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."""
|
"""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 {
|
return {
|
||||||
'id': ip.id,
|
'id': ip.id,
|
||||||
'address': ip.ip_address,
|
'address': ip.ip_address,
|
||||||
|
'site_ip_id': site_ip_id, # The actual SiteIP ID for config updates
|
||||||
'ping_expected': ip.ping_expected,
|
'ping_expected': ip.ping_expected,
|
||||||
'ping_actual': ip.ping_actual,
|
'ping_actual': ip.ping_actual,
|
||||||
'ports': [self._port_to_dict(port) for port in ip.ports]
|
'ports': [self._port_to_dict(port) for port in ip.ports]
|
||||||
|
|||||||
@@ -586,6 +586,19 @@
|
|||||||
const screenshotPath = service && service.screenshot_path ? service.screenshot_path : null;
|
const screenshotPath = service && service.screenshot_path ? service.screenshot_path : null;
|
||||||
const certificate = service && service.certificates && service.certificates.length > 0 ? service.certificates[0] : 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 = '<span class="badge badge-good">Expected</span>';
|
||||||
|
} 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 = `<span class="badge badge-warning">Unexpected</span>`;
|
||||||
|
if (canMarkExpected) {
|
||||||
|
statusCell += ` <button class="btn btn-sm btn-outline-success ms-1" onclick="markPortExpected(${site.site_id}, ${ip.site_ip_id}, ${port.port}, '${port.protocol}')" title="Add to expected ports"><i class="bi bi-plus-circle"></i></button>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.classList.add('scan-row'); // Fix white row bug
|
row.classList.add('scan-row'); // Fix white row bug
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
@@ -595,7 +608,7 @@
|
|||||||
<td>${service ? service.service_name : '-'}</td>
|
<td>${service ? service.service_name : '-'}</td>
|
||||||
<td>${service ? service.product || '-' : '-'}</td>
|
<td>${service ? service.product || '-' : '-'}</td>
|
||||||
<td class="mono">${service ? service.version || '-' : '-'}</td>
|
<td class="mono">${service ? service.version || '-' : '-'}</td>
|
||||||
<td>${port.expected ? '<span class="badge badge-good">Expected</span>' : '<span class="badge badge-warning">Unexpected</span>'}</td>
|
<td>${statusCell}</td>
|
||||||
<td>${screenshotPath ? `<a href="/output/${screenshotPath.replace(/^\/?(?:app\/)?output\/?/, '')}" target="_blank" class="btn btn-sm btn-outline-primary" title="View Screenshot"><i class="bi bi-image"></i></a>` : '-'}</td>
|
<td>${screenshotPath ? `<a href="/output/${screenshotPath.replace(/^\/?(?:app\/)?output\/?/, '')}" target="_blank" class="btn btn-sm btn-outline-primary" title="View Screenshot"><i class="bi bi-image"></i></a>` : '-'}</td>
|
||||||
<td>${certificate ? `<button class="btn btn-sm btn-outline-info" onclick='showCertificateModal(${JSON.stringify(certificate).replace(/'/g, "'")})' title="View Certificate"><i class="bi bi-shield-lock"></i></button>` : '-'}</td>
|
<td>${certificate ? `<button class="btn btn-sm btn-outline-info" onclick='showCertificateModal(${JSON.stringify(certificate).replace(/'/g, "'")})' title="View Certificate"><i class="bi bi-shield-lock"></i></button>` : '-'}</td>
|
||||||
`;
|
`;
|
||||||
@@ -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
|
// Find previous scan and show compare button
|
||||||
let previousScanId = null;
|
let previousScanId = null;
|
||||||
let currentConfigId = null;
|
let currentConfigId = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user