- Add API endpoint GET /api/scans/by-ip/{ip_address} to retrieve
last 10 scans containing a specific IP
- Add ScanService.get_scans_by_ip() method with ScanIP join query
- Add search box to global navigation header
- Create dedicated search results page at /search/ip
- Update API documentation with new endpoint
176 lines
6.5 KiB
HTML
176 lines
6.5 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Search Results for {{ ip_address }} - SneakyScanner{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mt-4">
|
|
<div class="col-12 d-flex justify-content-between align-items-center mb-4">
|
|
<h1>
|
|
<i class="bi bi-search"></i>
|
|
Search Results
|
|
{% if ip_address %}
|
|
<small class="text-muted">for {{ ip_address }}</small>
|
|
{% endif %}
|
|
</h1>
|
|
<a href="{{ url_for('main.scans') }}" class="btn btn-secondary">
|
|
<i class="bi bi-arrow-left"></i> Back to Scans
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{% if not ip_address %}
|
|
<!-- No IP provided -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body text-center py-5">
|
|
<i class="bi bi-exclamation-circle text-warning" style="font-size: 3rem;"></i>
|
|
<h4 class="mt-3">No IP Address Provided</h4>
|
|
<p class="text-muted">Please enter an IP address in the search box to find related scans.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<!-- Results Table -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">Last 10 Scans Containing {{ ip_address }}</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="results-loading" class="text-center py-5">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 text-muted">Searching for scans...</p>
|
|
</div>
|
|
<div id="results-error" class="alert alert-danger" style="display: none;"></div>
|
|
<div id="results-empty" class="text-center py-5 text-muted" style="display: none;">
|
|
<i class="bi bi-search" style="font-size: 3rem;"></i>
|
|
<h5 class="mt-3">No Scans Found</h5>
|
|
<p>No completed scans contain the IP address <strong>{{ ip_address }}</strong>.</p>
|
|
</div>
|
|
<div id="results-table-container" style="display: none;">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 80px;">ID</th>
|
|
<th>Title</th>
|
|
<th style="width: 200px;">Timestamp</th>
|
|
<th style="width: 100px;">Duration</th>
|
|
<th style="width: 120px;">Status</th>
|
|
<th style="width: 100px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="results-tbody">
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="text-muted mt-3">
|
|
Found <span id="result-count">0</span> scan(s) containing this IP address.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
const ipAddress = "{{ ip_address | e }}";
|
|
|
|
// Load results when page loads
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (ipAddress) {
|
|
loadResults();
|
|
}
|
|
});
|
|
|
|
// Load search results from API
|
|
async function loadResults() {
|
|
const loadingEl = document.getElementById('results-loading');
|
|
const errorEl = document.getElementById('results-error');
|
|
const emptyEl = document.getElementById('results-empty');
|
|
const tableEl = document.getElementById('results-table-container');
|
|
|
|
// Show loading state
|
|
loadingEl.style.display = 'block';
|
|
errorEl.style.display = 'none';
|
|
emptyEl.style.display = 'none';
|
|
tableEl.style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch(`/api/scans/by-ip/${encodeURIComponent(ipAddress)}`);
|
|
if (!response.ok) {
|
|
throw new Error('Failed to search for scans');
|
|
}
|
|
|
|
const data = await response.json();
|
|
const scans = data.scans || [];
|
|
|
|
loadingEl.style.display = 'none';
|
|
|
|
if (scans.length === 0) {
|
|
emptyEl.style.display = 'block';
|
|
} else {
|
|
tableEl.style.display = 'block';
|
|
renderResultsTable(scans);
|
|
document.getElementById('result-count').textContent = data.count;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error searching for scans:', error);
|
|
loadingEl.style.display = 'none';
|
|
errorEl.textContent = 'Failed to search for scans. Please try again.';
|
|
errorEl.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Render results table
|
|
function renderResultsTable(scans) {
|
|
const tbody = document.getElementById('results-tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
scans.forEach(scan => {
|
|
const row = document.createElement('tr');
|
|
row.classList.add('scan-row');
|
|
|
|
// Format timestamp
|
|
const timestamp = new Date(scan.timestamp).toLocaleString();
|
|
|
|
// Format duration
|
|
const duration = scan.duration ? `${scan.duration.toFixed(1)}s` : '-';
|
|
|
|
// Status badge
|
|
let statusBadge = '';
|
|
if (scan.status === 'completed') {
|
|
statusBadge = '<span class="badge badge-success">Completed</span>';
|
|
} else if (scan.status === 'running') {
|
|
statusBadge = '<span class="badge badge-info">Running</span>';
|
|
} else if (scan.status === 'failed') {
|
|
statusBadge = '<span class="badge badge-danger">Failed</span>';
|
|
} else {
|
|
statusBadge = `<span class="badge badge-info">${scan.status}</span>`;
|
|
}
|
|
|
|
row.innerHTML = `
|
|
<td class="mono">${scan.id}</td>
|
|
<td>${scan.title || 'Untitled Scan'}</td>
|
|
<td class="text-muted">${timestamp}</td>
|
|
<td class="mono">${duration}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>
|
|
<a href="/scans/${scan.id}" class="btn btn-sm btn-secondary">View</a>
|
|
</td>
|
|
`;
|
|
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|