Migrate from file-based configs to database with per-IP site configuration

Major architectural changes:
   - Replace YAML config files with database-stored ScanConfig model
   - Remove CIDR block support in favor of individual IP addresses per site
   - Each IP now has its own expected_ping, expected_tcp_ports, expected_udp_ports
   - AlertRule now uses config_id FK instead of config_file string

   API changes:
   - POST /api/scans now requires config_id instead of config_file
   - Alert rules API uses config_id with validation
   - All config dropdowns fetch from /api/configs dynamically

   Template updates:
   - scans.html, dashboard.html, alert_rules.html load configs via API
   - Display format: Config Title (X sites) in dropdowns
   - Removed Jinja2 config_files loops

   Migrations:
   - 008: Expand CIDRs to individual IPs with per-IP port configs
   - 009: Remove CIDR-related columns
   - 010: Add config_id to alert_rules, remove config_file
This commit is contained in:
2025-11-19 19:40:34 -06:00
parent 034f146fa1
commit 0ec338e252
21 changed files with 2004 additions and 686 deletions

View File

@@ -113,34 +113,28 @@
<div class="modal-body">
<form id="trigger-scan-form">
<div class="mb-3">
<label for="config-file" class="form-label">Config File</label>
<select class="form-select" id="config-file" name="config_file" required {% if not config_files %}disabled{% endif %}>
<option value="">Select a config file...</option>
{% for config in config_files %}
<option value="{{ config }}">{{ config }}</option>
{% endfor %}
<label for="config-select" class="form-label">Scan Configuration</label>
<select class="form-select" id="config-select" name="config_id" required>
<option value="">Loading configurations...</option>
</select>
{% if config_files %}
<div class="form-text text-muted">
Select a scan configuration file
<div class="form-text text-muted" id="config-help-text">
Select a scan configuration
</div>
{% else %}
<div class="alert alert-warning mt-2 mb-0" role="alert">
<div id="no-configs-warning" class="alert alert-warning mt-2 mb-0" role="alert" style="display: none;">
<i class="bi bi-exclamation-triangle"></i>
<strong>No configurations available</strong>
<p class="mb-2 mt-2">You need to create a configuration file before you can trigger a scan.</p>
<a href="{{ url_for('main.upload_config') }}" class="btn btn-sm btn-primary">
<p class="mb-2 mt-2">You need to create a configuration before you can trigger a scan.</p>
<a href="{{ url_for('main.configs') }}" class="btn btn-sm btn-primary">
<i class="bi bi-plus-circle"></i> Create Configuration
</a>
</div>
{% endif %}
</div>
<div id="trigger-error" class="alert alert-danger" style="display: none;"></div>
</form>
</div>
<div class="modal-footer" style="border-top: 1px solid #334155;">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="triggerScan()" {% if not config_files %}disabled{% endif %}>
<button type="button" class="btn btn-primary" id="trigger-scan-btn" onclick="triggerScan()">
<span id="modal-trigger-text">Trigger Scan</span>
<span id="modal-trigger-spinner" class="spinner-border spinner-border-sm ms-2" style="display: none;"></span>
</button>
@@ -359,23 +353,75 @@
});
}
// Load available configs
async function loadConfigs() {
const selectEl = document.getElementById('config-select');
const helpTextEl = document.getElementById('config-help-text');
const noConfigsWarning = document.getElementById('no-configs-warning');
const triggerBtn = document.getElementById('trigger-scan-btn');
try {
const response = await fetch('/api/configs');
if (!response.ok) {
throw new Error('Failed to load configurations');
}
const data = await response.json();
const configs = data.configs || [];
// Clear existing options
selectEl.innerHTML = '';
if (configs.length === 0) {
selectEl.innerHTML = '<option value="">No configurations available</option>';
selectEl.disabled = true;
triggerBtn.disabled = true;
helpTextEl.style.display = 'none';
noConfigsWarning.style.display = 'block';
} else {
selectEl.innerHTML = '<option value="">Select a configuration...</option>';
configs.forEach(config => {
const option = document.createElement('option');
option.value = config.id;
const siteText = config.site_count === 1 ? 'site' : 'sites';
option.textContent = `${config.title} (${config.site_count} ${siteText})`;
selectEl.appendChild(option);
});
selectEl.disabled = false;
triggerBtn.disabled = false;
helpTextEl.style.display = 'block';
noConfigsWarning.style.display = 'none';
}
} catch (error) {
console.error('Error loading configs:', error);
selectEl.innerHTML = '<option value="">Error loading configurations</option>';
selectEl.disabled = true;
triggerBtn.disabled = true;
helpTextEl.style.display = 'none';
}
}
// Show trigger scan modal
function showTriggerScanModal() {
const modal = new bootstrap.Modal(document.getElementById('triggerScanModal'));
document.getElementById('trigger-error').style.display = 'none';
document.getElementById('trigger-scan-form').reset();
// Load configs when modal is shown
loadConfigs();
modal.show();
}
// Trigger scan
async function triggerScan() {
const configFile = document.getElementById('config-file').value;
const configId = document.getElementById('config-select').value;
const errorEl = document.getElementById('trigger-error');
const btnText = document.getElementById('modal-trigger-text');
const btnSpinner = document.getElementById('modal-trigger-spinner');
if (!configFile) {
errorEl.textContent = 'Please enter a config file path.';
if (!configId) {
errorEl.textContent = 'Please select a configuration.';
errorEl.style.display = 'block';
return;
}
@@ -392,13 +438,13 @@
'Content-Type': 'application/json',
},
body: JSON.stringify({
config_file: configFile
config_id: parseInt(configId)
})
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Failed to trigger scan');
throw new Error(data.message || data.error || 'Failed to trigger scan');
}
const data = await response.json();