Merge branch 'feat/fix-cron-schedules' into nightly
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -676,29 +676,57 @@ class SneakyScanner:
|
|||||||
|
|
||||||
return services
|
return services
|
||||||
|
|
||||||
def _is_likely_web_service(self, service: Dict) -> bool:
|
def _is_likely_web_service(self, service: Dict, ip: str = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if a service is likely HTTP/HTTPS based on nmap detection or common web ports
|
Check if a service is a web server by actually making an HTTP request
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
service: Service dictionary from nmap results
|
service: Service dictionary from nmap results
|
||||||
|
ip: IP address to test (required for HTTP probe)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if service appears to be web-related
|
True if service responds to HTTP/HTTPS requests
|
||||||
"""
|
"""
|
||||||
# Check service name
|
import requests
|
||||||
|
import urllib3
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
# Quick check for known web service names first
|
||||||
web_services = ['http', 'https', 'ssl', 'http-proxy', 'https-alt',
|
web_services = ['http', 'https', 'ssl', 'http-proxy', 'https-alt',
|
||||||
'http-alt', 'ssl/http', 'ssl/https']
|
'http-alt', 'ssl/http', 'ssl/https']
|
||||||
service_name = service.get('service', '').lower()
|
service_name = service.get('service', '').lower()
|
||||||
|
|
||||||
if service_name in web_services:
|
# If no IP provided, can't do HTTP probe
|
||||||
return True
|
|
||||||
|
|
||||||
# Check common non-standard web ports
|
|
||||||
web_ports = [80, 443, 8000, 8006, 8008, 8080, 8081, 8443, 8888, 9443]
|
|
||||||
port = service.get('port')
|
port = service.get('port')
|
||||||
|
if not ip or not port:
|
||||||
|
# check just the service if no IP - honestly shouldn't get here, but just incase...
|
||||||
|
if service_name in web_services:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
return port in web_ports
|
# Actually try to connect - this is the definitive test
|
||||||
|
# Try HTTPS first, then HTTP
|
||||||
|
for protocol in ['https', 'http']:
|
||||||
|
url = f"{protocol}://{ip}:{port}/"
|
||||||
|
try:
|
||||||
|
response = requests.get(
|
||||||
|
url,
|
||||||
|
timeout=3,
|
||||||
|
verify=False,
|
||||||
|
allow_redirects=False
|
||||||
|
)
|
||||||
|
# Any status code means it's a web server
|
||||||
|
# (including 404, 500, etc. - still a web server)
|
||||||
|
return True
|
||||||
|
except requests.exceptions.SSLError:
|
||||||
|
# SSL error on HTTPS, try HTTP next
|
||||||
|
continue
|
||||||
|
except (requests.exceptions.ConnectionError,
|
||||||
|
requests.exceptions.Timeout,
|
||||||
|
requests.exceptions.RequestException):
|
||||||
|
continue
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def _detect_http_https(self, ip: str, port: int, timeout: int = 5) -> str:
|
def _detect_http_https(self, ip: str, port: int, timeout: int = 5) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -886,7 +914,7 @@ class SneakyScanner:
|
|||||||
ip_results = {}
|
ip_results = {}
|
||||||
|
|
||||||
for service in services:
|
for service in services:
|
||||||
if not self._is_likely_web_service(service):
|
if not self._is_likely_web_service(service, ip):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
port = service['port']
|
port = service['port']
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ scheduled scans with cron expressions.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from croniter import croniter
|
from croniter import croniter
|
||||||
@@ -71,6 +71,7 @@ class ScheduleService:
|
|||||||
next_run = self.calculate_next_run(cron_expression) if enabled else None
|
next_run = self.calculate_next_run(cron_expression) if enabled else None
|
||||||
|
|
||||||
# Create schedule record
|
# Create schedule record
|
||||||
|
now_utc = datetime.now(timezone.utc)
|
||||||
schedule = Schedule(
|
schedule = Schedule(
|
||||||
name=name,
|
name=name,
|
||||||
config_id=config_id,
|
config_id=config_id,
|
||||||
@@ -78,8 +79,8 @@ class ScheduleService:
|
|||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
last_run=None,
|
last_run=None,
|
||||||
next_run=next_run,
|
next_run=next_run,
|
||||||
created_at=datetime.utcnow(),
|
created_at=now_utc,
|
||||||
updated_at=datetime.utcnow()
|
updated_at=now_utc
|
||||||
)
|
)
|
||||||
|
|
||||||
self.db.add(schedule)
|
self.db.add(schedule)
|
||||||
@@ -103,7 +104,14 @@ class ScheduleService:
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError: If schedule not found
|
ValueError: If schedule not found
|
||||||
"""
|
"""
|
||||||
schedule = self.db.query(Schedule).filter(Schedule.id == schedule_id).first()
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
|
schedule = (
|
||||||
|
self.db.query(Schedule)
|
||||||
|
.options(joinedload(Schedule.config))
|
||||||
|
.filter(Schedule.id == schedule_id)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
if not schedule:
|
if not schedule:
|
||||||
raise ValueError(f"Schedule {schedule_id} not found")
|
raise ValueError(f"Schedule {schedule_id} not found")
|
||||||
@@ -138,8 +146,10 @@ class ScheduleService:
|
|||||||
'pages': int
|
'pages': int
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
# Build query
|
from sqlalchemy.orm import joinedload
|
||||||
query = self.db.query(Schedule)
|
|
||||||
|
# Build query and eagerly load config relationship
|
||||||
|
query = self.db.query(Schedule).options(joinedload(Schedule.config))
|
||||||
|
|
||||||
# Apply filter
|
# Apply filter
|
||||||
if enabled_filter is not None:
|
if enabled_filter is not None:
|
||||||
@@ -215,7 +225,7 @@ class ScheduleService:
|
|||||||
if hasattr(schedule, key):
|
if hasattr(schedule, key):
|
||||||
setattr(schedule, key, value)
|
setattr(schedule, key, value)
|
||||||
|
|
||||||
schedule.updated_at = datetime.utcnow()
|
schedule.updated_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
self.db.refresh(schedule)
|
self.db.refresh(schedule)
|
||||||
@@ -298,7 +308,7 @@ class ScheduleService:
|
|||||||
|
|
||||||
schedule.last_run = last_run
|
schedule.last_run = last_run
|
||||||
schedule.next_run = next_run
|
schedule.next_run = next_run
|
||||||
schedule.updated_at = datetime.utcnow()
|
schedule.updated_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
@@ -311,23 +321,43 @@ class ScheduleService:
|
|||||||
Validate a cron expression.
|
Validate a cron expression.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cron_expr: Cron expression to validate
|
cron_expr: Cron expression to validate in standard crontab format
|
||||||
|
Format: minute hour day month day_of_week
|
||||||
|
Day of week: 0=Sunday, 1=Monday, ..., 6=Saturday
|
||||||
|
(APScheduler will convert this to its internal format automatically)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (is_valid, error_message)
|
Tuple of (is_valid, error_message)
|
||||||
- (True, None) if valid
|
- (True, None) if valid
|
||||||
- (False, error_message) if invalid
|
- (False, error_message) if invalid
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This validates using croniter which uses standard crontab format.
|
||||||
|
APScheduler's from_crontab() will handle the conversion when the
|
||||||
|
schedule is registered with the scheduler.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Try to create a croniter instance
|
# Try to create a croniter instance
|
||||||
base_time = datetime.utcnow()
|
# croniter uses standard crontab format (Sunday=0)
|
||||||
|
from datetime import timezone
|
||||||
|
base_time = datetime.now(timezone.utc)
|
||||||
cron = croniter(cron_expr, base_time)
|
cron = croniter(cron_expr, base_time)
|
||||||
|
|
||||||
# Try to get the next run time (validates the expression)
|
# Try to get the next run time (validates the expression)
|
||||||
cron.get_next(datetime)
|
cron.get_next(datetime)
|
||||||
|
|
||||||
|
# Validate basic format (5 fields)
|
||||||
|
fields = cron_expr.split()
|
||||||
|
if len(fields) != 5:
|
||||||
|
return (False, f"Cron expression must have 5 fields (minute hour day month day_of_week), got {len(fields)}")
|
||||||
|
|
||||||
return (True, None)
|
return (True, None)
|
||||||
except (ValueError, KeyError) as e:
|
except (ValueError, KeyError) as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
# Add helpful hint for day_of_week errors
|
||||||
|
if "day" in error_msg.lower() and len(cron_expr.split()) >= 5:
|
||||||
|
hint = "\nNote: Use standard crontab format where 0=Sunday, 1=Monday, ..., 6=Saturday"
|
||||||
|
return (False, f"{error_msg}{hint}")
|
||||||
return (False, str(e))
|
return (False, str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return (False, f"Unexpected error: {str(e)}")
|
return (False, f"Unexpected error: {str(e)}")
|
||||||
@@ -345,17 +375,24 @@ class ScheduleService:
|
|||||||
from_time: Base time (defaults to now UTC)
|
from_time: Base time (defaults to now UTC)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Next run datetime (UTC)
|
Next run datetime (UTC, timezone-aware)
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If cron expression is invalid
|
ValueError: If cron expression is invalid
|
||||||
"""
|
"""
|
||||||
if from_time is None:
|
if from_time is None:
|
||||||
from_time = datetime.utcnow()
|
from_time = datetime.now(timezone.utc)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cron = croniter(cron_expr, from_time)
|
cron = croniter(cron_expr, from_time)
|
||||||
return cron.get_next(datetime)
|
next_run = cron.get_next(datetime)
|
||||||
|
|
||||||
|
# croniter returns naive datetime, so we need to add timezone info
|
||||||
|
# Since we're using UTC for all calculations, add UTC timezone
|
||||||
|
if next_run.tzinfo is None:
|
||||||
|
next_run = next_run.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
return next_run
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError(f"Invalid cron expression '{cron_expr}': {str(e)}")
|
raise ValueError(f"Invalid cron expression '{cron_expr}': {str(e)}")
|
||||||
|
|
||||||
@@ -403,10 +440,16 @@ class ScheduleService:
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary representation
|
Dictionary representation
|
||||||
"""
|
"""
|
||||||
|
# Get config title if relationship is loaded
|
||||||
|
config_name = None
|
||||||
|
if schedule.config:
|
||||||
|
config_name = schedule.config.title
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': schedule.id,
|
'id': schedule.id,
|
||||||
'name': schedule.name,
|
'name': schedule.name,
|
||||||
'config_id': schedule.config_id,
|
'config_id': schedule.config_id,
|
||||||
|
'config_name': config_name,
|
||||||
'cron_expression': schedule.cron_expression,
|
'cron_expression': schedule.cron_expression,
|
||||||
'enabled': schedule.enabled,
|
'enabled': schedule.enabled,
|
||||||
'last_run': schedule.last_run.isoformat() if schedule.last_run else None,
|
'last_run': schedule.last_run.isoformat() if schedule.last_run else None,
|
||||||
@@ -421,7 +464,7 @@ class ScheduleService:
|
|||||||
Format datetime as relative time.
|
Format datetime as relative time.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dt: Datetime to format (UTC)
|
dt: Datetime to format (UTC, can be naive or aware)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Human-readable relative time (e.g., "in 2 hours", "yesterday")
|
Human-readable relative time (e.g., "in 2 hours", "yesterday")
|
||||||
@@ -429,7 +472,13 @@ class ScheduleService:
|
|||||||
if dt is None:
|
if dt is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
now = datetime.utcnow()
|
# Ensure both datetimes are timezone-aware for comparison
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
# If dt is naive, assume it's UTC and add timezone info
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = dt.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
diff = dt - now
|
diff = dt - now
|
||||||
|
|
||||||
# Future times
|
# Future times
|
||||||
|
|||||||
@@ -149,6 +149,51 @@ class SchedulerService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading schedules on startup: {str(e)}", exc_info=True)
|
logger.error(f"Error loading schedules on startup: {str(e)}", exc_info=True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate_cron_expression(cron_expression: str) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Validate a cron expression and provide helpful feedback.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cron_expression: Cron expression to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_valid: bool, message: str)
|
||||||
|
- If valid: (True, "Valid cron expression")
|
||||||
|
- If invalid: (False, "Error message with details")
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Standard crontab format: minute hour day month day_of_week
|
||||||
|
Day of week: 0=Sunday, 1=Monday, ..., 6=Saturday (or 7=Sunday)
|
||||||
|
"""
|
||||||
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to parse the expression
|
||||||
|
trigger = CronTrigger.from_crontab(cron_expression)
|
||||||
|
|
||||||
|
# Validate basic format (5 fields)
|
||||||
|
fields = cron_expression.split()
|
||||||
|
if len(fields) != 5:
|
||||||
|
return False, f"Cron expression must have 5 fields (minute hour day month day_of_week), got {len(fields)}"
|
||||||
|
|
||||||
|
return True, "Valid cron expression"
|
||||||
|
|
||||||
|
except (ValueError, KeyError) as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
|
||||||
|
# Provide helpful hints for common errors
|
||||||
|
if "day_of_week" in error_msg.lower() or (len(cron_expression.split()) >= 5):
|
||||||
|
# Check if day_of_week field might be using APScheduler format by mistake
|
||||||
|
fields = cron_expression.split()
|
||||||
|
if len(fields) == 5:
|
||||||
|
dow_field = fields[4]
|
||||||
|
if dow_field.isdigit() and int(dow_field) >= 0:
|
||||||
|
hint = "\nNote: Use standard crontab format where 0=Sunday, 1=Monday, ..., 6=Saturday"
|
||||||
|
return False, f"Invalid cron expression: {error_msg}{hint}"
|
||||||
|
|
||||||
|
return False, f"Invalid cron expression: {error_msg}"
|
||||||
|
|
||||||
def queue_scan(self, scan_id: int, config_id: int) -> str:
|
def queue_scan(self, scan_id: int, config_id: int) -> str:
|
||||||
"""
|
"""
|
||||||
Queue a scan for immediate background execution.
|
Queue a scan for immediate background execution.
|
||||||
@@ -188,6 +233,10 @@ class SchedulerService:
|
|||||||
schedule_id: Database ID of the schedule
|
schedule_id: Database ID of the schedule
|
||||||
config_id: Database config ID
|
config_id: Database config ID
|
||||||
cron_expression: Cron expression (e.g., "0 2 * * *" for 2am daily)
|
cron_expression: Cron expression (e.g., "0 2 * * *" for 2am daily)
|
||||||
|
IMPORTANT: Use standard crontab format where:
|
||||||
|
- Day of week: 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
||||||
|
- APScheduler automatically converts to its internal format
|
||||||
|
- from_crontab() handles the conversion properly
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Job ID from APScheduler
|
Job ID from APScheduler
|
||||||
@@ -195,18 +244,29 @@ class SchedulerService:
|
|||||||
Raises:
|
Raises:
|
||||||
RuntimeError: If scheduler not initialized
|
RuntimeError: If scheduler not initialized
|
||||||
ValueError: If cron expression is invalid
|
ValueError: If cron expression is invalid
|
||||||
|
|
||||||
|
Note:
|
||||||
|
APScheduler internally uses Monday=0, but from_crontab() accepts
|
||||||
|
standard crontab format (Sunday=0) and converts it automatically.
|
||||||
"""
|
"""
|
||||||
if not self.scheduler:
|
if not self.scheduler:
|
||||||
raise RuntimeError("Scheduler not initialized. Call init_scheduler() first.")
|
raise RuntimeError("Scheduler not initialized. Call init_scheduler() first.")
|
||||||
|
|
||||||
from apscheduler.triggers.cron import CronTrigger
|
from apscheduler.triggers.cron import CronTrigger
|
||||||
|
|
||||||
|
# Validate cron expression first to provide helpful error messages
|
||||||
|
is_valid, message = self.validate_cron_expression(cron_expression)
|
||||||
|
if not is_valid:
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
# Create cron trigger from expression using local timezone
|
# Create cron trigger from expression using local timezone
|
||||||
# This allows users to specify times in their local timezone
|
# from_crontab() parses standard crontab format (Sunday=0)
|
||||||
|
# and converts to APScheduler's internal format (Monday=0) automatically
|
||||||
try:
|
try:
|
||||||
trigger = CronTrigger.from_crontab(cron_expression)
|
trigger = CronTrigger.from_crontab(cron_expression)
|
||||||
# timezone defaults to local system timezone
|
# timezone defaults to local system timezone
|
||||||
except (ValueError, KeyError) as e:
|
except (ValueError, KeyError) as e:
|
||||||
|
# This should not happen due to validation above, but catch anyway
|
||||||
raise ValueError(f"Invalid cron expression '{cron_expression}': {str(e)}")
|
raise ValueError(f"Invalid cron expression '{cron_expression}': {str(e)}")
|
||||||
|
|
||||||
# Add cron job
|
# Add cron job
|
||||||
@@ -294,11 +354,16 @@ class SchedulerService:
|
|||||||
|
|
||||||
# Update schedule's last_run and next_run
|
# Update schedule's last_run and next_run
|
||||||
from croniter import croniter
|
from croniter import croniter
|
||||||
next_run = croniter(schedule['cron_expression'], datetime.utcnow()).get_next(datetime)
|
now_utc = datetime.now(timezone.utc)
|
||||||
|
next_run = croniter(schedule['cron_expression'], now_utc).get_next(datetime)
|
||||||
|
|
||||||
|
# croniter returns naive datetime, add UTC timezone
|
||||||
|
if next_run.tzinfo is None:
|
||||||
|
next_run = next_run.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
schedule_service.update_run_times(
|
schedule_service.update_run_times(
|
||||||
schedule_id=schedule_id,
|
schedule_id=schedule_id,
|
||||||
last_run=datetime.utcnow(),
|
last_run=now_utc,
|
||||||
next_run=next_run
|
next_run=next_run
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -298,7 +298,11 @@ async function loadSchedule() {
|
|||||||
function populateForm(schedule) {
|
function populateForm(schedule) {
|
||||||
document.getElementById('schedule-id').value = schedule.id;
|
document.getElementById('schedule-id').value = schedule.id;
|
||||||
document.getElementById('schedule-name').value = schedule.name;
|
document.getElementById('schedule-name').value = schedule.name;
|
||||||
document.getElementById('config-id').value = schedule.config_id;
|
// Display config name and ID in the readonly config-file field
|
||||||
|
const configDisplay = schedule.config_name
|
||||||
|
? `${schedule.config_name} (ID: ${schedule.config_id})`
|
||||||
|
: `Config ID: ${schedule.config_id}`;
|
||||||
|
document.getElementById('config-file').value = configDisplay;
|
||||||
document.getElementById('cron-expression').value = schedule.cron_expression;
|
document.getElementById('cron-expression').value = schedule.cron_expression;
|
||||||
document.getElementById('schedule-enabled').checked = schedule.enabled;
|
document.getElementById('schedule-enabled').checked = schedule.enabled;
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ SneakyScanner is deployed as a Docker container running a Flask web application
|
|||||||
|
|
||||||
**Architecture:**
|
**Architecture:**
|
||||||
- **Web Application**: Flask app on port 5000 with modern web UI
|
- **Web Application**: Flask app on port 5000 with modern web UI
|
||||||
- **Database**: SQLite (persisted to volume)
|
- **Database**: SQLite (persisted to volume) - stores all configurations, scan results, and settings
|
||||||
- **Background Jobs**: APScheduler for async scan execution
|
- **Background Jobs**: APScheduler for async scan execution
|
||||||
- **Scanner**: masscan, nmap, sslyze, Playwright
|
- **Scanner**: masscan, nmap, sslyze, Playwright
|
||||||
- **Config Creator**: Web-based CIDR-to-YAML configuration builder
|
- **Config Management**: Database-backed configuration system managed entirely via web UI
|
||||||
- **Scheduling**: Cron-based scheduled scans with dashboard management
|
- **Scheduling**: Cron-based scheduled scans with dashboard management
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -143,6 +143,13 @@ docker compose -f docker-compose-standalone.yml up
|
|||||||
|
|
||||||
SneakyScanner is configured via environment variables. The recommended approach is to use a `.env` file.
|
SneakyScanner is configured via environment variables. The recommended approach is to use a `.env` file.
|
||||||
|
|
||||||
|
|
||||||
|
**UDP Port Scanning**
|
||||||
|
|
||||||
|
- UDP Port scanning is disabled by default.
|
||||||
|
- You can turn it on via the .env variable.
|
||||||
|
- By Default, UDP port scanning only scans the top 20 ports, for convenience I have included the NMAP top 100 UDP ports as well.
|
||||||
|
|
||||||
#### Creating Your .env File
|
#### Creating Your .env File
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -160,6 +167,7 @@ python3 -c "from cryptography.fernet import Fernet; print('SNEAKYSCANNER_ENCRYPT
|
|||||||
nano .env
|
nano .env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Key Configuration Options
|
#### Key Configuration Options
|
||||||
|
|
||||||
| Variable | Description | Default | Required |
|
| Variable | Description | Default | Required |
|
||||||
@@ -190,54 +198,30 @@ The application needs these directories (created automatically by Docker):
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Verify directories exist
|
# Verify directories exist
|
||||||
ls -la configs/ data/ output/ logs/
|
ls -la data/ output/ logs/
|
||||||
|
|
||||||
# If missing, create them
|
# If missing, create them
|
||||||
mkdir -p configs data output logs
|
mkdir -p data output logs
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step 2: Configure Scan Targets
|
### Step 2: Configure Scan Targets
|
||||||
|
|
||||||
You can create scan configurations in two ways:
|
After starting the application, create scan configurations using the web UI:
|
||||||
|
|
||||||
**Option A: Using the Web UI (Recommended - Phase 4 Feature)**
|
**Creating Configurations via Web UI**
|
||||||
|
|
||||||
1. Navigate to **Configs** in the web interface
|
1. Navigate to **Configs** in the web interface
|
||||||
2. Click **"Create New Config"**
|
2. Click **"Create New Config"**
|
||||||
3. Use the CIDR-based config creator for quick setup:
|
3. Use the form-based config creator:
|
||||||
- Enter site name
|
- Enter site name
|
||||||
- Enter CIDR range (e.g., `192.168.1.0/24`)
|
- Enter CIDR range (e.g., `192.168.1.0/24`)
|
||||||
- Select expected ports from dropdowns
|
- Select expected TCP/UDP ports from dropdowns
|
||||||
- Click **"Generate Config"**
|
- Optionally enable ping checks
|
||||||
4. Or use the **YAML Editor** for advanced configurations
|
4. Click **"Save Configuration"**
|
||||||
5. Save and use immediately in scans or schedules
|
5. Configuration is saved to database and immediately available for scans and schedules
|
||||||
|
|
||||||
**Option B: Manual YAML Files**
|
**Note**: All configurations are stored in the database, not as files. This provides better reliability, easier backup, and seamless management through the web interface.
|
||||||
|
|
||||||
Create YAML configuration files manually in the `configs/` directory:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Example configuration
|
|
||||||
cat > configs/my-network.yaml <<EOF
|
|
||||||
title: "My Network Infrastructure"
|
|
||||||
sites:
|
|
||||||
- name: "Web Servers"
|
|
||||||
cidr: "192.168.1.0/24" # Scan entire subnet
|
|
||||||
expected_ports:
|
|
||||||
- port: 80
|
|
||||||
protocol: tcp
|
|
||||||
service: "http"
|
|
||||||
- port: 443
|
|
||||||
protocol: tcp
|
|
||||||
service: "https"
|
|
||||||
- port: 22
|
|
||||||
protocol: tcp
|
|
||||||
service: "ssh"
|
|
||||||
ping_expected: true
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note**: Phase 4 introduced a powerful config creator in the web UI that makes it easy to generate configs from CIDR ranges without manually editing YAML.
|
|
||||||
|
|
||||||
### Step 3: Build Docker Image
|
### Step 3: Build Docker Image
|
||||||
|
|
||||||
@@ -389,38 +373,37 @@ The dashboard provides a central view of your scanning activity:
|
|||||||
- **Trend Charts**: Port count trends over time using Chart.js
|
- **Trend Charts**: Port count trends over time using Chart.js
|
||||||
- **Quick Actions**: Buttons to run scans, create configs, manage schedules
|
- **Quick Actions**: Buttons to run scans, create configs, manage schedules
|
||||||
|
|
||||||
### Managing Scan Configurations (Phase 4)
|
### Managing Scan Configurations
|
||||||
|
|
||||||
|
All scan configurations are stored in the database and managed entirely through the web interface.
|
||||||
|
|
||||||
**Creating Configs:**
|
**Creating Configs:**
|
||||||
1. Navigate to **Configs** → **Create New Config**
|
1. Navigate to **Configs** → **Create New Config**
|
||||||
2. **CIDR Creator Mode**:
|
2. Fill in the configuration form:
|
||||||
- Enter site name (e.g., "Production Servers")
|
- Enter site name (e.g., "Production Servers")
|
||||||
- Enter CIDR range (e.g., `192.168.1.0/24`)
|
- Enter CIDR range (e.g., `192.168.1.0/24`)
|
||||||
- Select expected TCP/UDP ports from dropdowns
|
- Select expected TCP/UDP ports from dropdowns
|
||||||
- Click **"Generate Config"** to create YAML
|
- Enable/disable ping checks
|
||||||
3. **YAML Editor Mode**:
|
3. Click **"Save Configuration"**
|
||||||
- Switch to editor tab for advanced configurations
|
4. Configuration is immediately stored in database and available for use
|
||||||
- Syntax highlighting with line numbers
|
|
||||||
- Validate YAML before saving
|
|
||||||
|
|
||||||
**Editing Configs:**
|
**Editing Configs:**
|
||||||
1. Navigate to **Configs** → Select config
|
1. Navigate to **Configs** → Select config from list
|
||||||
2. Click **"Edit"** button
|
2. Click **"Edit"** button
|
||||||
3. Make changes in YAML editor
|
3. Modify any fields in the configuration form
|
||||||
4. Save changes (validates YAML automatically)
|
4. Click **"Save Changes"** to update database
|
||||||
|
|
||||||
**Uploading Configs:**
|
**Viewing Configs:**
|
||||||
1. Navigate to **Configs** → **Upload**
|
- Navigate to **Configs** page to see all saved configurations
|
||||||
2. Select YAML file from your computer
|
- Each config shows site name, CIDR range, and expected ports
|
||||||
3. File is validated and saved to `configs/` directory
|
- Click on any config to view full details
|
||||||
|
|
||||||
**Downloading Configs:**
|
|
||||||
- Click **"Download"** button next to any config
|
|
||||||
- Saves YAML file to your local machine
|
|
||||||
|
|
||||||
**Deleting Configs:**
|
**Deleting Configs:**
|
||||||
- Click **"Delete"** button
|
- Click **"Delete"** button next to any config
|
||||||
- **Warning**: Cannot delete configs used by active schedules
|
- **Warning**: Cannot delete configs used by active schedules
|
||||||
|
- Deletion removes the configuration from the database permanently
|
||||||
|
|
||||||
|
**Note**: All configurations are database-backed, providing automatic backups when you backup the database file.
|
||||||
|
|
||||||
### Running Scans
|
### Running Scans
|
||||||
|
|
||||||
@@ -477,12 +460,11 @@ SneakyScanner uses several mounted volumes for data persistence:
|
|||||||
|
|
||||||
| Volume | Container Path | Purpose | Important? |
|
| Volume | Container Path | Purpose | Important? |
|
||||||
|--------|----------------|---------|------------|
|
|--------|----------------|---------|------------|
|
||||||
| `./configs` | `/app/configs` | Scan configuration files (managed via web UI) | Yes |
|
| `./data` | `/app/data` | SQLite database (contains configurations, scan history, settings) | **Critical** |
|
||||||
| `./data` | `/app/data` | SQLite database (contains all scan history) | **Critical** |
|
|
||||||
| `./output` | `/app/output` | Scan results (JSON, HTML, ZIP, screenshots) | Yes |
|
| `./output` | `/app/output` | Scan results (JSON, HTML, ZIP, screenshots) | Yes |
|
||||||
| `./logs` | `/app/logs` | Application logs (rotating file handler) | No |
|
| `./logs` | `/app/logs` | Application logs (rotating file handler) | No |
|
||||||
|
|
||||||
**Note**: As of Phase 4, the `./configs` volume is read-write to support the web-based config creator and editor. The web UI can now create, edit, and delete configuration files directly.
|
**Note**: All scan configurations are stored in the SQLite database (`./data/sneakyscanner.db`). There is no separate configs directory or YAML files. Backing up the database file ensures all your configurations are preserved.
|
||||||
|
|
||||||
### Backing Up Data
|
### Backing Up Data
|
||||||
|
|
||||||
@@ -490,23 +472,22 @@ SneakyScanner uses several mounted volumes for data persistence:
|
|||||||
# Create backup directory
|
# Create backup directory
|
||||||
mkdir -p backups/$(date +%Y%m%d)
|
mkdir -p backups/$(date +%Y%m%d)
|
||||||
|
|
||||||
# Backup database
|
# Backup database (includes all configurations)
|
||||||
cp data/sneakyscanner.db backups/$(date +%Y%m%d)/
|
cp data/sneakyscanner.db backups/$(date +%Y%m%d)/
|
||||||
|
|
||||||
# Backup scan outputs
|
# Backup scan outputs
|
||||||
tar -czf backups/$(date +%Y%m%d)/output.tar.gz output/
|
tar -czf backups/$(date +%Y%m%d)/output.tar.gz output/
|
||||||
|
|
||||||
# Backup configurations
|
|
||||||
tar -czf backups/$(date +%Y%m%d)/configs.tar.gz configs/
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Important**: The database backup includes all scan configurations, settings, schedules, and scan history. No separate configuration file backup is needed.
|
||||||
|
|
||||||
### Restoring Data
|
### Restoring Data
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stop application
|
# Stop application
|
||||||
docker compose -f docker-compose.yml down
|
docker compose -f docker-compose.yml down
|
||||||
|
|
||||||
# Restore database
|
# Restore database (includes all configurations)
|
||||||
cp backups/YYYYMMDD/sneakyscanner.db data/
|
cp backups/YYYYMMDD/sneakyscanner.db data/
|
||||||
|
|
||||||
# Restore outputs
|
# Restore outputs
|
||||||
@@ -516,6 +497,8 @@ tar -xzf backups/YYYYMMDD/output.tar.gz
|
|||||||
docker compose -f docker-compose.yml up -d
|
docker compose -f docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note**: Restoring the database file restores all configurations, settings, schedules, and scan history.
|
||||||
|
|
||||||
### Cleaning Up Old Scan Results
|
### Cleaning Up Old Scan Results
|
||||||
|
|
||||||
**Option A: Using the Web UI (Recommended)**
|
**Option A: Using the Web UI (Recommended)**
|
||||||
@@ -564,50 +547,52 @@ curl -X POST http://localhost:5000/api/auth/logout \
|
|||||||
-b cookies.txt
|
-b cookies.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config Management (Phase 4)
|
### Config Management
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List all configs
|
# List all configs
|
||||||
curl http://localhost:5000/api/configs \
|
curl http://localhost:5000/api/configs \
|
||||||
-b cookies.txt
|
-b cookies.txt
|
||||||
|
|
||||||
# Get specific config
|
# Get specific config by ID
|
||||||
curl http://localhost:5000/api/configs/prod-network.yaml \
|
curl http://localhost:5000/api/configs/1 \
|
||||||
-b cookies.txt
|
-b cookies.txt
|
||||||
|
|
||||||
# Create new config
|
# Create new config
|
||||||
curl -X POST http://localhost:5000/api/configs \
|
curl -X POST http://localhost:5000/api/configs \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"filename": "test-network.yaml",
|
"name": "Test Network",
|
||||||
"content": "title: Test Network\nsites:\n - name: Test\n cidr: 10.0.0.0/24"
|
"cidr": "10.0.0.0/24",
|
||||||
|
"expected_ports": [
|
||||||
|
{"port": 80, "protocol": "tcp", "service": "http"},
|
||||||
|
{"port": 443, "protocol": "tcp", "service": "https"}
|
||||||
|
],
|
||||||
|
"ping_expected": true
|
||||||
}' \
|
}' \
|
||||||
-b cookies.txt
|
-b cookies.txt
|
||||||
|
|
||||||
# Update config
|
# Update config
|
||||||
curl -X PUT http://localhost:5000/api/configs/test-network.yaml \
|
curl -X PUT http://localhost:5000/api/configs/1 \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"content": "title: Updated Test Network\nsites:\n - name: Test Site\n cidr: 10.0.0.0/24"
|
"name": "Updated Test Network",
|
||||||
|
"cidr": "10.0.1.0/24"
|
||||||
}' \
|
}' \
|
||||||
-b cookies.txt
|
-b cookies.txt
|
||||||
|
|
||||||
# Download config
|
|
||||||
curl http://localhost:5000/api/configs/test-network.yaml/download \
|
|
||||||
-b cookies.txt -o test-network.yaml
|
|
||||||
|
|
||||||
# Delete config
|
# Delete config
|
||||||
curl -X DELETE http://localhost:5000/api/configs/test-network.yaml \
|
curl -X DELETE http://localhost:5000/api/configs/1 \
|
||||||
-b cookies.txt
|
-b cookies.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### Scan Management
|
### Scan Management
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Trigger a scan
|
# Trigger a scan (using config ID from database)
|
||||||
curl -X POST http://localhost:5000/api/scans \
|
curl -X POST http://localhost:5000/api/scans \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"config_id": "/app/configs/prod-network.yaml"}' \
|
-d '{"config_id": 1}' \
|
||||||
-b cookies.txt
|
-b cookies.txt
|
||||||
|
|
||||||
# List all scans
|
# List all scans
|
||||||
@@ -634,12 +619,12 @@ curl -X DELETE http://localhost:5000/api/scans/123 \
|
|||||||
curl http://localhost:5000/api/schedules \
|
curl http://localhost:5000/api/schedules \
|
||||||
-b cookies.txt
|
-b cookies.txt
|
||||||
|
|
||||||
# Create schedule
|
# Create schedule (using config ID from database)
|
||||||
curl -X POST http://localhost:5000/api/schedules \
|
curl -X POST http://localhost:5000/api/schedules \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{
|
-d '{
|
||||||
"name": "Daily Production Scan",
|
"name": "Daily Production Scan",
|
||||||
"config_id": "/app/configs/prod-network.yaml",
|
"config_id": 1,
|
||||||
"cron_expression": "0 2 * * *",
|
"cron_expression": "0 2 * * *",
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}' \
|
}' \
|
||||||
@@ -875,24 +860,25 @@ docker compose -f docker-compose.yml logs web | grep -E "(ERROR|Exception|Traceb
|
|||||||
docker compose -f docker-compose.yml exec web which masscan nmap
|
docker compose -f docker-compose.yml exec web which masscan nmap
|
||||||
```
|
```
|
||||||
|
|
||||||
### Config Files Not Appearing in Web UI
|
### Configs Not Appearing in Web UI
|
||||||
|
|
||||||
**Problem**: Manually created configs don't show up in web interface
|
**Problem**: Created configs don't show up in web interface
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check file permissions (must be readable by web container)
|
# Check database connectivity
|
||||||
ls -la configs/
|
docker compose -f docker-compose.yml logs web | grep -i "database"
|
||||||
|
|
||||||
# Fix permissions if needed
|
# Verify database file exists and is readable
|
||||||
sudo chown -R 1000:1000 configs/
|
ls -lh data/sneakyscanner.db
|
||||||
chmod 644 configs/*.yaml
|
|
||||||
|
|
||||||
# Verify YAML syntax is valid
|
# Check for errors when creating configs
|
||||||
docker compose -f docker-compose.yml exec web python3 -c \
|
|
||||||
"import yaml; yaml.safe_load(open('/app/configs/your-config.yaml'))"
|
|
||||||
|
|
||||||
# Check web logs for parsing errors
|
|
||||||
docker compose -f docker-compose.yml logs web | grep -i "config"
|
docker compose -f docker-compose.yml logs web | grep -i "config"
|
||||||
|
|
||||||
|
# Try accessing configs via API
|
||||||
|
curl http://localhost:5000/api/configs -b cookies.txt
|
||||||
|
|
||||||
|
# If database is corrupted, check integrity
|
||||||
|
docker compose -f docker-compose.yml exec web sqlite3 /app/data/sneakyscanner.db "PRAGMA integrity_check;"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Health Check Failing
|
### Health Check Failing
|
||||||
@@ -979,11 +965,11 @@ server {
|
|||||||
# Ensure proper ownership of data directories
|
# Ensure proper ownership of data directories
|
||||||
sudo chown -R $USER:$USER data/ output/ logs/
|
sudo chown -R $USER:$USER data/ output/ logs/
|
||||||
|
|
||||||
# Restrict database file permissions
|
# Restrict database file permissions (contains configurations and sensitive data)
|
||||||
chmod 600 data/sneakyscanner.db
|
chmod 600 data/sneakyscanner.db
|
||||||
|
|
||||||
# Configs should be read-only
|
# Ensure database directory is writable
|
||||||
chmod 444 configs/*.yaml
|
chmod 700 data/
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -1051,19 +1037,17 @@ mkdir -p "$BACKUP_DIR"
|
|||||||
# Stop application for consistent backup
|
# Stop application for consistent backup
|
||||||
docker compose -f docker-compose.yml stop web
|
docker compose -f docker-compose.yml stop web
|
||||||
|
|
||||||
# Backup database
|
# Backup database (includes all configurations)
|
||||||
cp data/sneakyscanner.db "$BACKUP_DIR/"
|
cp data/sneakyscanner.db "$BACKUP_DIR/"
|
||||||
|
|
||||||
# Backup outputs (last 30 days only)
|
# Backup outputs (last 30 days only)
|
||||||
find output/ -type f -mtime -30 -exec cp --parents {} "$BACKUP_DIR/" \;
|
find output/ -type f -mtime -30 -exec cp --parents {} "$BACKUP_DIR/" \;
|
||||||
|
|
||||||
# Backup configs
|
|
||||||
cp -r configs/ "$BACKUP_DIR/"
|
|
||||||
|
|
||||||
# Restart application
|
# Restart application
|
||||||
docker compose -f docker-compose.yml start web
|
docker compose -f docker-compose.yml start web
|
||||||
|
|
||||||
echo "Backup complete: $BACKUP_DIR"
|
echo "Backup complete: $BACKUP_DIR"
|
||||||
|
echo "Database backup includes all configurations, settings, and scan history"
|
||||||
```
|
```
|
||||||
|
|
||||||
Make executable and schedule with cron:
|
Make executable and schedule with cron:
|
||||||
@@ -1083,15 +1067,18 @@ crontab -e
|
|||||||
# Stop application
|
# Stop application
|
||||||
docker compose -f docker-compose.yml down
|
docker compose -f docker-compose.yml down
|
||||||
|
|
||||||
# Restore files
|
# Restore database (includes all configurations)
|
||||||
cp backups/YYYYMMDD_HHMMSS/sneakyscanner.db data/
|
cp backups/YYYYMMDD_HHMMSS/sneakyscanner.db data/
|
||||||
cp -r backups/YYYYMMDD_HHMMSS/configs/* configs/
|
|
||||||
|
# Restore output files
|
||||||
cp -r backups/YYYYMMDD_HHMMSS/output/* output/
|
cp -r backups/YYYYMMDD_HHMMSS/output/* output/
|
||||||
|
|
||||||
# Start application
|
# Start application
|
||||||
docker compose -f docker-compose.yml up -d
|
docker compose -f docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note**: Restoring the database file will restore all configurations, settings, schedules, and scan history from the backup.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Support and Further Reading
|
## Support and Further Reading
|
||||||
@@ -1105,13 +1092,13 @@ docker compose -f docker-compose.yml up -d
|
|||||||
|
|
||||||
## What's New
|
## What's New
|
||||||
|
|
||||||
### Phase 4 (2025-11-17) - Config Creator ✅
|
### Phase 4+ (2025-11-17) - Database-Backed Configuration System ✅
|
||||||
- **CIDR-based Config Creator**: Web UI for generating scan configs from CIDR ranges
|
- **Database-Backed Configs**: All configurations stored in SQLite database (no YAML files)
|
||||||
- **YAML Editor**: Built-in editor with syntax highlighting (CodeMirror)
|
- **Web-Based Config Creator**: Form-based UI for creating scan configs from CIDR ranges
|
||||||
- **Config Management UI**: List, view, edit, download, and delete configs via web interface
|
- **Config Management UI**: List, view, edit, and delete configs via web interface
|
||||||
- **Config Upload**: Direct YAML file upload for advanced users
|
- **REST API**: Full config management via RESTful API with database storage
|
||||||
- **REST API**: 7 new config management endpoints
|
|
||||||
- **Schedule Protection**: Prevents deleting configs used by active schedules
|
- **Schedule Protection**: Prevents deleting configs used by active schedules
|
||||||
|
- **Automatic Backups**: Configurations included in database backups
|
||||||
|
|
||||||
### Phase 3 (2025-11-14) - Dashboard & Scheduling ✅
|
### Phase 3 (2025-11-14) - Dashboard & Scheduling ✅
|
||||||
- **Dashboard**: Summary stats, recent scans, trend charts
|
- **Dashboard**: Summary stats, recent scans, trend charts
|
||||||
@@ -1133,5 +1120,5 @@ docker compose -f docker-compose.yml up -d
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last Updated**: 2025-11-17
|
**Last Updated**: 2025-11-24
|
||||||
**Version**: Phase 4 - Config Creator Complete
|
**Version**: Phase 4+ - Database-Backed Configuration System
|
||||||
|
|||||||
0
docs/KNOWN_ISSUES.md
Normal file
0
docs/KNOWN_ISSUES.md
Normal file
Reference in New Issue
Block a user