Add certificate details modal and fix SSL/TLS data processing

- Add certificate details modal to scan detail page with subject, issuer,
  validity dates, serial number, self-signed indicator, SANs, and TLS
  version support with expandable cipher suites
- Fix bug where certificate data was not being saved to database due to
  incorrect path lookup (was checking http_info['certificate'] instead of
  http_info['ssl_tls']['certificate'])
- Update requirements: add sslyze 6.0.0 and upgrade cryptography to >=42.0.0
  to fix 'No module named cryptography.x509.verification' error
This commit is contained in:
2025-11-20 10:38:02 -06:00
parent 8d8e53c903
commit 73a3b95834
3 changed files with 170 additions and 7 deletions

View File

@@ -12,7 +12,7 @@ alembic==1.13.0
# Authentication & Security # Authentication & Security
Flask-Login==0.6.3 Flask-Login==0.6.3
bcrypt==4.1.2 bcrypt==4.1.2
cryptography==41.0.7 cryptography>=42.0.0
# API & Serialization # API & Serialization
Flask-CORS==4.0.0 Flask-CORS==4.0.0
@@ -35,3 +35,6 @@ python-dotenv==1.0.0
# Development & Testing # Development & Testing
pytest==7.4.3 pytest==7.4.3
pytest-flask==1.3.0 pytest-flask==1.3.0
# Cert Testing
sslyze==6.0.0

View File

@@ -449,9 +449,10 @@ class ScanService:
# Process certificate and TLS info if present # Process certificate and TLS info if present
http_info = service_data.get('http_info', {}) http_info = service_data.get('http_info', {})
if http_info.get('certificate'): ssl_tls = http_info.get('ssl_tls', {})
if ssl_tls.get('certificate'):
self._process_certificate( self._process_certificate(
http_info['certificate'], ssl_tls,
scan_obj.id, scan_obj.id,
service.id service.id
) )
@@ -489,16 +490,19 @@ class ScanService:
return service return service
return None return None
def _process_certificate(self, cert_data: Dict[str, Any], scan_id: int, def _process_certificate(self, ssl_tls_data: Dict[str, Any], scan_id: int,
service_id: int) -> None: service_id: int) -> None:
""" """
Process certificate and TLS version data. Process certificate and TLS version data.
Args: Args:
cert_data: Certificate data dictionary ssl_tls_data: SSL/TLS data dictionary containing 'certificate' and 'tls_versions'
scan_id: Scan ID scan_id: Scan ID
service_id: Service ID service_id: Service ID
""" """
# Extract certificate data from ssl_tls structure
cert_data = ssl_tls_data.get('certificate', {})
# Create ScanCertificate record # Create ScanCertificate record
cert = ScanCertificate( cert = ScanCertificate(
scan_id=scan_id, scan_id=scan_id,
@@ -516,7 +520,7 @@ class ScanService:
self.db.flush() self.db.flush()
# Process TLS versions # Process TLS versions
tls_versions = cert_data.get('tls_versions', {}) tls_versions = ssl_tls_data.get('tls_versions', {})
for version, version_data in tls_versions.items(): for version, version_data in tls_versions.items():
tls = ScanTLSVersion( tls = ScanTLSVersion(
scan_id=scan_id, scan_id=scan_id,

View File

@@ -154,6 +154,67 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Certificate Details Modal -->
<div class="modal fade" id="certificateModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content" style="background-color: #1e293b; border: 1px solid #334155;">
<div class="modal-header" style="border-bottom: 1px solid #334155;">
<h5 class="modal-title" style="color: #60a5fa;">
<i class="bi bi-shield-lock"></i> Certificate Details
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label text-muted">Subject</label>
<div id="cert-subject" class="mono" style="word-break: break-all;">-</div>
</div>
<div class="col-md-6">
<label class="form-label text-muted">Issuer</label>
<div id="cert-issuer" class="mono" style="word-break: break-all;">-</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<label class="form-label text-muted">Valid From</label>
<div id="cert-valid-from" class="mono">-</div>
</div>
<div class="col-md-4">
<label class="form-label text-muted">Valid Until</label>
<div id="cert-valid-until" class="mono">-</div>
</div>
<div class="col-md-4">
<label class="form-label text-muted">Days Until Expiry</label>
<div id="cert-days-expiry">-</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label text-muted">Serial Number</label>
<div id="cert-serial" class="mono" style="word-break: break-all;">-</div>
</div>
<div class="col-md-6">
<label class="form-label text-muted">Self-Signed</label>
<div id="cert-self-signed">-</div>
</div>
</div>
<div class="mb-3">
<label class="form-label text-muted">Subject Alternative Names (SANs)</label>
<div id="cert-sans">-</div>
</div>
<div class="mb-3">
<label class="form-label text-muted">TLS Version Support</label>
<div id="cert-tls-versions">-</div>
</div>
</div>
<div class="modal-footer" style="border-top: 1px solid #334155;">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
@@ -332,6 +393,7 @@
<th>Version</th> <th>Version</th>
<th>Status</th> <th>Status</th>
<th>Screenshot</th> <th>Screenshot</th>
<th>Certificate</th>
</tr> </tr>
</thead> </thead>
<tbody id="site-${siteIdx}-ip-${ipIdx}-ports"></tbody> <tbody id="site-${siteIdx}-ip-${ipIdx}-ports"></tbody>
@@ -345,11 +407,12 @@
const ports = ip.ports || []; const ports = ip.ports || [];
if (ports.length === 0) { if (ports.length === 0) {
portsContainer.innerHTML = '<tr class="scan-row"><td colspan="8" class="text-center text-muted">No ports found</td></tr>'; portsContainer.innerHTML = '<tr class="scan-row"><td colspan="9" class="text-center text-muted">No ports found</td></tr>';
} else { } else {
ports.forEach(port => { ports.forEach(port => {
const service = port.services && port.services.length > 0 ? port.services[0] : null; const service = port.services && port.services.length > 0 ? port.services[0] : null;
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 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
@@ -362,6 +425,7 @@
<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>${port.expected ? '<span class="badge badge-good">Expected</span>' : '<span class="badge badge-warning">Unexpected</span>'}</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, "&#39;")})' title="View Certificate"><i class="bi bi-shield-lock"></i></button>` : '-'}</td>
`; `;
portsContainer.appendChild(row); portsContainer.appendChild(row);
}); });
@@ -614,5 +678,97 @@
console.error('Error loading historical chart:', error); console.error('Error loading historical chart:', error);
} }
} }
// Show certificate details modal
function showCertificateModal(cert) {
// Populate modal fields
document.getElementById('cert-subject').textContent = cert.subject || '-';
document.getElementById('cert-issuer').textContent = cert.issuer || '-';
document.getElementById('cert-serial').textContent = cert.serial_number || '-';
// Format dates
document.getElementById('cert-valid-from').textContent = cert.not_valid_before
? new Date(cert.not_valid_before).toLocaleString()
: '-';
document.getElementById('cert-valid-until').textContent = cert.not_valid_after
? new Date(cert.not_valid_after).toLocaleString()
: '-';
// Days until expiry with color coding
if (cert.days_until_expiry !== null && cert.days_until_expiry !== undefined) {
let badgeClass = 'badge-success';
if (cert.days_until_expiry < 0) {
badgeClass = 'badge-danger';
} else if (cert.days_until_expiry < 30) {
badgeClass = 'badge-warning';
}
document.getElementById('cert-days-expiry').innerHTML =
`<span class="badge ${badgeClass}">${cert.days_until_expiry} days</span>`;
} else {
document.getElementById('cert-days-expiry').textContent = '-';
}
// Self-signed indicator
document.getElementById('cert-self-signed').innerHTML = cert.is_self_signed
? '<span class="badge badge-warning">Yes</span>'
: '<span class="badge badge-success">No</span>';
// SANs
if (cert.sans && cert.sans.length > 0) {
document.getElementById('cert-sans').innerHTML = cert.sans
.map(san => `<span class="badge bg-secondary me-1 mb-1">${san}</span>`)
.join('');
} else {
document.getElementById('cert-sans').textContent = 'None';
}
// TLS versions
if (cert.tls_versions && cert.tls_versions.length > 0) {
let tlsHtml = '<div class="table-responsive"><table class="table table-sm mb-0">';
tlsHtml += '<thead><tr><th>Version</th><th>Status</th><th>Cipher Suites</th></tr></thead><tbody>';
cert.tls_versions.forEach(tls => {
const statusBadge = tls.supported
? '<span class="badge badge-success">Supported</span>'
: '<span class="badge badge-danger">Not Supported</span>';
let ciphers = '-';
if (tls.cipher_suites && tls.cipher_suites.length > 0) {
ciphers = `<small class="text-muted">${tls.cipher_suites.length} cipher(s)</small>
<button class="btn btn-sm btn-link p-0 ms-1" onclick="toggleCiphers(this, '${tls.tls_version}')" data-ciphers='${JSON.stringify(tls.cipher_suites).replace(/'/g, "&#39;")}'>
<i class="bi bi-chevron-down"></i>
</button>
<div class="cipher-list" style="display:none; font-size: 0.75rem; max-height: 100px; overflow-y: auto;"></div>`;
}
tlsHtml += `<tr class="scan-row"><td>${tls.tls_version}</td><td>${statusBadge}</td><td>${ciphers}</td></tr>`;
});
tlsHtml += '</tbody></table></div>';
document.getElementById('cert-tls-versions').innerHTML = tlsHtml;
} else {
document.getElementById('cert-tls-versions').textContent = 'No TLS information available';
}
// Show modal
const modal = new bootstrap.Modal(document.getElementById('certificateModal'));
modal.show();
}
// Toggle cipher suites display
function toggleCiphers(btn, version) {
const cipherList = btn.nextElementSibling;
const icon = btn.querySelector('i');
if (cipherList.style.display === 'none') {
const ciphers = JSON.parse(btn.dataset.ciphers);
cipherList.innerHTML = ciphers.map(c => `<div class="mono">${c}</div>`).join('');
cipherList.style.display = 'block';
icon.className = 'bi bi-chevron-up';
} else {
cipherList.style.display = 'none';
icon.className = 'bi bi-chevron-down';
}
}
</script> </script>
{% endblock %} {% endblock %}