Merging beta into master #8

Merged
ptarrant merged 8 commits from beta into master 2025-11-21 22:07:07 +00:00
6 changed files with 308 additions and 1 deletions
Showing only changes of commit 4c6b4bf35d - Show all commits

View File

@@ -281,6 +281,45 @@ def get_scan_status(scan_id):
}), 500
@bp.route('/by-ip/<ip_address>', methods=['GET'])
@api_auth_required
def get_scans_by_ip(ip_address):
"""
Get last 10 scans containing a specific IP address.
Args:
ip_address: IP address to search for
Returns:
JSON response with list of scans containing the IP
"""
try:
# Get scans from service
scan_service = ScanService(current_app.db_session)
scans = scan_service.get_scans_by_ip(ip_address)
logger.info(f"Retrieved {len(scans)} scans for IP: {ip_address}")
return jsonify({
'ip_address': ip_address,
'scans': scans,
'count': len(scans)
})
except SQLAlchemyError as e:
logger.error(f"Database error retrieving scans for IP {ip_address}: {str(e)}")
return jsonify({
'error': 'Database error',
'message': 'Failed to retrieve scans'
}), 500
except Exception as e:
logger.error(f"Unexpected error retrieving scans for IP {ip_address}: {str(e)}", exc_info=True)
return jsonify({
'error': 'Internal server error',
'message': 'An unexpected error occurred'
}), 500
@bp.route('/<int:scan_id1>/compare/<int:scan_id2>', methods=['GET'])
@api_auth_required
def compare_scans(scan_id1, scan_id2):

View File

@@ -7,7 +7,7 @@ Provides dashboard and scan viewing pages.
import logging
import os
from flask import Blueprint, current_app, redirect, render_template, send_from_directory, url_for
from flask import Blueprint, current_app, redirect, render_template, request, send_from_directory, url_for
from web.auth.decorators import login_required
@@ -83,6 +83,19 @@ def compare_scans(scan_id1, scan_id2):
return render_template('scan_compare.html', scan_id1=scan_id1, scan_id2=scan_id2)
@bp.route('/search/ip')
@login_required
def search_ip():
"""
IP search results page - shows scans containing a specific IP address.
Returns:
Rendered search results template
"""
ip_address = request.args.get('ip', '').strip()
return render_template('ip_search_results.html', ip_address=ip_address)
@bp.route('/schedules')
@login_required
def schedules():

View File

@@ -260,6 +260,29 @@ class ScanService:
return status_info
def get_scans_by_ip(self, ip_address: str, limit: int = 10) -> List[Dict[str, Any]]:
"""
Get the last N scans containing a specific IP address.
Args:
ip_address: IP address to search for
limit: Maximum number of scans to return (default: 10)
Returns:
List of scan summary dictionaries, most recent first
"""
scans = (
self.db.query(Scan)
.join(ScanIP, Scan.id == ScanIP.scan_id)
.filter(ScanIP.ip_address == ip_address)
.filter(Scan.status == 'completed')
.order_by(Scan.timestamp.desc())
.limit(limit)
.all()
)
return [self._scan_to_summary_dict(scan) for scan in scans]
def cleanup_orphaned_scans(self) -> int:
"""
Clean up orphaned scans that are stuck in 'running' status.

View File

@@ -76,6 +76,13 @@
</ul>
</li>
</ul>
<form class="d-flex me-3" action="{{ url_for('main.search_ip') }}" method="GET">
<input class="form-control form-control-sm me-2" type="search" name="ip"
placeholder="Search IP..." aria-label="Search IP" style="width: 150px;">
<button class="btn btn-outline-primary btn-sm" type="submit">
<i class="bi bi-search"></i>
</button>
</form>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.help' %}active{% endif %}"

View File

@@ -0,0 +1,175 @@
{% 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 %}

View File

@@ -989,6 +989,56 @@ curl -X DELETE http://localhost:5000/api/scans/42 \
-b cookies.txt
```
### Get Scans by IP
Get the last 10 scans containing a specific IP address.
**Endpoint:** `GET /api/scans/by-ip/{ip_address}`
**Authentication:** Required
**Path Parameters:**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `ip_address` | string | Yes | IP address to search for |
**Success Response (200 OK):**
```json
{
"ip_address": "192.168.1.10",
"scans": [
{
"id": 42,
"timestamp": "2025-11-14T10:30:00Z",
"duration": 125.5,
"status": "completed",
"title": "Production Network Scan",
"config_id": 1,
"triggered_by": "manual",
"created_at": "2025-11-14T10:30:00Z"
},
{
"id": 38,
"timestamp": "2025-11-13T10:30:00Z",
"duration": 98.2,
"status": "completed",
"title": "Production Network Scan",
"config_id": 1,
"triggered_by": "scheduled",
"created_at": "2025-11-13T10:30:00Z"
}
],
"count": 2
}
```
**Usage Example:**
```bash
curl -X GET http://localhost:5000/api/scans/by-ip/192.168.1.10 \
-b cookies.txt
```
### Compare Scans
Compare two scans to identify differences in ports, services, and certificates.