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

@@ -96,8 +96,8 @@
{% endif %}
</td>
<td>
{% if rule.config_file %}
<small class="text-muted">{{ rule.config_file }}</small>
{% if rule.config %}
<small class="text-muted">{{ rule.config.title }}</small>
{% else %}
<span class="badge bg-primary">All Configs</span>
{% endif %}
@@ -209,20 +209,9 @@
<label for="rule-config" class="form-label">Apply to Config (optional)</label>
<select class="form-select" id="rule-config">
<option value="">All Configs (Apply to all scans)</option>
{% if config_files %}
{% for config_file in config_files %}
<option value="{{ config_file }}">{{ config_file }}</option>
{% endfor %}
{% else %}
<option value="" disabled>No config files found</option>
{% endif %}
</select>
<small class="form-text text-muted">
{% if config_files %}
Select a specific config file to limit this rule, or leave as "All Configs" to apply to all scans
{% else %}
No config files found. Upload a config in the Configs section to see available options.
{% endif %}
<small class="form-text text-muted" id="config-help-text">
Select a specific config to limit this rule, or leave as "All Configs" to apply to all scans
</small>
</div>
</div>
@@ -272,12 +261,51 @@
<script>
let editingRuleId = null;
// Load available configs for the dropdown
async function loadConfigsForRule() {
const selectEl = document.getElementById('rule-config');
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 || [];
// Preserve the "All Configs" option and current selection
const currentValue = selectEl.value;
selectEl.innerHTML = '<option value="">All Configs (Apply to all scans)</option>';
configs.forEach(config => {
const option = document.createElement('option');
// Store the config ID as the value
option.value = config.id;
const siteText = config.site_count === 1 ? 'site' : 'sites';
option.textContent = `${config.title} (${config.site_count} ${siteText})`;
selectEl.appendChild(option);
});
// Restore selection if it was set
if (currentValue) {
selectEl.value = currentValue;
}
} catch (error) {
console.error('Error loading configs:', error);
}
}
function showCreateRuleModal() {
editingRuleId = null;
document.getElementById('ruleModalTitle').textContent = 'Create Alert Rule';
document.getElementById('save-rule-text').textContent = 'Create Rule';
document.getElementById('ruleForm').reset();
document.getElementById('rule-enabled').checked = true;
// Load configs when modal is shown
loadConfigsForRule();
new bootstrap.Modal(document.getElementById('ruleModal')).show();
}
@@ -286,33 +314,36 @@ function editRule(ruleId) {
document.getElementById('ruleModalTitle').textContent = 'Edit Alert Rule';
document.getElementById('save-rule-text').textContent = 'Update Rule';
// Fetch rule details
fetch(`/api/alerts/rules`, {
headers: {
'X-API-Key': localStorage.getItem('api_key') || ''
}
})
.then(response => response.json())
.then(data => {
const rule = data.rules.find(r => r.id === ruleId);
if (rule) {
document.getElementById('rule-id').value = rule.id;
document.getElementById('rule-name').value = rule.name || '';
document.getElementById('rule-type').value = rule.rule_type;
document.getElementById('rule-severity').value = rule.severity || 'warning';
document.getElementById('rule-threshold').value = rule.threshold || '';
document.getElementById('rule-config').value = rule.config_file || '';
document.getElementById('rule-email').checked = rule.email_enabled;
document.getElementById('rule-webhook').checked = rule.webhook_enabled;
document.getElementById('rule-enabled').checked = rule.enabled;
// Load configs first, then fetch rule details
loadConfigsForRule().then(() => {
// Fetch rule details
fetch(`/api/alerts/rules`, {
headers: {
'X-API-Key': localStorage.getItem('api_key') || ''
}
})
.then(response => response.json())
.then(data => {
const rule = data.rules.find(r => r.id === ruleId);
if (rule) {
document.getElementById('rule-id').value = rule.id;
document.getElementById('rule-name').value = rule.name || '';
document.getElementById('rule-type').value = rule.rule_type;
document.getElementById('rule-severity').value = rule.severity || 'warning';
document.getElementById('rule-threshold').value = rule.threshold || '';
document.getElementById('rule-config').value = rule.config_id || '';
document.getElementById('rule-email').checked = rule.email_enabled;
document.getElementById('rule-webhook').checked = rule.webhook_enabled;
document.getElementById('rule-enabled').checked = rule.enabled;
updateThresholdLabel();
new bootstrap.Modal(document.getElementById('ruleModal')).show();
}
})
.catch(error => {
console.error('Error fetching rule:', error);
alert('Failed to load rule details');
updateThresholdLabel();
new bootstrap.Modal(document.getElementById('ruleModal')).show();
}
})
.catch(error => {
console.error('Error fetching rule:', error);
alert('Failed to load rule details');
});
});
}
@@ -353,7 +384,7 @@ function saveRule() {
const ruleType = document.getElementById('rule-type').value;
const severity = document.getElementById('rule-severity').value;
const threshold = document.getElementById('rule-threshold').value;
const configFile = document.getElementById('rule-config').value;
const configId = document.getElementById('rule-config').value;
const emailEnabled = document.getElementById('rule-email').checked;
const webhookEnabled = document.getElementById('rule-webhook').checked;
const enabled = document.getElementById('rule-enabled').checked;
@@ -368,7 +399,7 @@ function saveRule() {
rule_type: ruleType,
severity: severity,
threshold: threshold ? parseInt(threshold) : null,
config_file: configFile || null,
config_id: configId ? parseInt(configId) : null,
email_enabled: emailEnabled,
webhook_enabled: webhookEnabled,
enabled: enabled