500 lines
22 KiB
Plaintext
500 lines
22 KiB
Plaintext
<% content_for :title, "Create New Rule" %>
|
|
|
|
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold text-gray-900">Create New Rule</h1>
|
|
<p class="mt-2 text-gray-600">Create a WAF rule to allow, block, or rate limit traffic</p>
|
|
</div>
|
|
|
|
<div class="bg-white shadow rounded-lg" data-controller="rule-form">
|
|
<%= form_with(model: @rule, local: true, class: "space-y-6") do |form| %>
|
|
<% if @rule.errors.any? %>
|
|
<div class="rounded-md bg-red-50 p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-red-800">
|
|
There were <%= pluralize(@rule.errors.count, "error") %> with your submission:
|
|
</h3>
|
|
<div class="mt-2 text-sm text-red-700">
|
|
<ul class="list-disc list-inside space-y-1">
|
|
<% @rule.errors.full_messages.each do |message| %>
|
|
<li><%= message %></li>
|
|
<% end %>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<!-- Rule Type Selection -->
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-medium text-gray-900">Rule Configuration</h3>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 space-y-6">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<%= form.label :waf_rule_type, "Rule Type", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.select :waf_rule_type,
|
|
options_for_select(@waf_rule_types.map { |type, _| [type.humanize, type] }, @rule.waf_rule_type),
|
|
{ prompt: "Select rule type" },
|
|
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
id: "rule_type_select" } %>
|
|
<p class="mt-2 text-sm text-gray-500">Choose the type of rule you want to create</p>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :waf_action, "Action", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.select :waf_action,
|
|
options_for_select(@waf_actions.map { |action, _| [action.humanize, action] }, @rule.waf_action),
|
|
{ prompt: "Select action" },
|
|
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
data: { rule_form_target: "actionSelect", action: "change->rule-form#updateActionSections" } } %>
|
|
<p class="mt-2 text-sm text-gray-500">What action to take when this rule matches</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Range Selection (shown for network rules) -->
|
|
<div id="network_range_section" class="hidden">
|
|
<%= form.label :network_range_id, "Network Range", class: "block text-sm font-medium text-gray-700 mb-2" %>
|
|
|
|
<!-- Selected Network Range Display -->
|
|
<div id="selected_network_display" class="hidden mb-4 p-4 bg-blue-50 border border-blue-200 rounded-md">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h4 class="text-sm font-medium text-blue-800">Selected Network Range</h4>
|
|
<div id="selected_network_info" class="mt-1 text-sm text-blue-700"></div>
|
|
</div>
|
|
<button type="button" onclick="clearSelectedNetwork()" class="text-blue-600 hover:text-blue-800">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Selection Interface -->
|
|
<div id="network_selection_interface" class="space-y-4">
|
|
<!-- Search Input -->
|
|
<div>
|
|
<input type="text"
|
|
id="network_search"
|
|
placeholder="Search by CIDR, IP, company, or ASN..."
|
|
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
|
|
<p class="mt-2 text-sm text-gray-500">Search existing network ranges or enter a CIDR/IP address below</p>
|
|
</div>
|
|
|
|
<!-- Quick Create Input -->
|
|
<div class="flex space-x-2">
|
|
<%= text_field_tag :new_cidr, params[:cidr],
|
|
placeholder: "e.g., 192.168.1.0/24 or 203.0.113.1",
|
|
class: "flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
id: "new_cidr_input" %>
|
|
<button type="button" onclick="quickCreateNetwork()"
|
|
class="px-4 py-2 bg-green-600 text-white text-sm rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500">
|
|
Create & Select
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Search Results -->
|
|
<div id="network_search_results" class="hidden">
|
|
<div class="border rounded-md divide-y max-h-64 overflow-y-auto">
|
|
<!-- Results will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Hidden field to store selected network range ID -->
|
|
<%= form.hidden_field :network_range_id, id: "selected_network_range_id", value: @rule.network_range_id %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Path Pattern (shown for path_pattern rules) -->
|
|
<div id="path_pattern_section" class="hidden space-y-6">
|
|
<div>
|
|
<%= form.label :path_pattern, "Path Pattern", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= text_field_tag :path_pattern, "",
|
|
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
placeholder: "/admin, /wp-login.php, /.env, /phpmyadmin",
|
|
id: "path_pattern_input" %>
|
|
<p class="mt-2 text-sm text-gray-500">Enter the path to match (e.g., /admin, /wp-login.php)</p>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :match_type, "Match Type", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= select_tag :match_type,
|
|
options_for_select([
|
|
["Exact - Matches path exactly", "exact"],
|
|
["Prefix - Matches path and subpaths (e.g., /admin matches /admin/users)", "prefix"],
|
|
["Suffix - Matches paths ending with pattern (e.g., /.env matches /backup/.env)", "suffix"],
|
|
["Contains - Matches paths containing pattern anywhere", "contains"]
|
|
]),
|
|
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
id: "match_type_select" } %>
|
|
<p class="mt-2 text-sm text-gray-500">How the pattern should be matched against request paths</p>
|
|
</div>
|
|
|
|
<!-- Example Matches (dynamically updated) -->
|
|
<div id="match_examples" class="bg-gray-50 border border-gray-200 rounded-md p-4">
|
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Example Matches:</h4>
|
|
<ul class="text-sm text-gray-600 space-y-1" id="example_list">
|
|
<li>Enter a pattern to see examples</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conditions (shown for other non-network rules) -->
|
|
<div id="conditions_section" class="hidden">
|
|
<div>
|
|
<%= form.label :conditions, "Conditions", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_area :conditions, rows: 4,
|
|
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
placeholder: '{"user_agent": "bot*"}' %>
|
|
<p class="mt-2 text-sm text-gray-500">JSON format with matching conditions</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Header Fields (shown for add_header action) -->
|
|
<div id="add_header_section" class="hidden space-y-4" data-rule-form-target="addHeaderSection">
|
|
<div>
|
|
<%= label_tag :header_name, "Header Name", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= text_field_tag :header_name, "",
|
|
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
placeholder: "X-Bot-Agent",
|
|
id: "header_name_input" %>
|
|
<p class="mt-2 text-sm text-gray-500">The HTTP header name to add (e.g., X-Bot-Agent, X-Network-Type)</p>
|
|
</div>
|
|
|
|
<div>
|
|
<%= label_tag :header_value, "Header Value", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= text_field_tag :header_value, "",
|
|
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
placeholder: "BingBot",
|
|
id: "header_value_input" %>
|
|
<p class="mt-2 text-sm text-gray-500">The value for the header (e.g., BingBot, GoogleBot, Unknown)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata -->
|
|
<div data-controller="json-validator" data-json-validator-valid-class="json-valid" data-json-validator-invalid-class="json-invalid" data-json-validator-valid-status-class="json-valid-status" data-json-validator-invalid-status-class="json-invalid-status">
|
|
<%= form.label :metadata, "Metadata", class: "block text-sm font-medium text-gray-700" %>
|
|
<div class="relative">
|
|
<%= form.text_area :metadata, rows: 3,
|
|
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
placeholder: '{"reason": "Suspicious activity detected", "source": "manual"}',
|
|
data: { json_validator_target: "textarea", action: "input->json-validator#validate" } %>
|
|
<div class="mt-1 flex items-center justify-between">
|
|
<div data-json-validator-target="status" class="text-sm"></div>
|
|
<div class="flex space-x-2">
|
|
<button type="button"
|
|
data-action="click->json-validator#format"
|
|
class="text-xs text-gray-500 hover:text-gray-700 underline">
|
|
Format JSON
|
|
</button>
|
|
<button type="button"
|
|
data-action="click->json-validator#insertSample"
|
|
data-json-validator-json-sample='{"reason": "Block malicious ISP", "threat_type": "botnet", "confidence": "high", "source": "manual"}'
|
|
class="text-xs text-gray-500 hover:text-gray-700 underline">
|
|
Insert Sample
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="mt-2 text-sm text-gray-500">JSON format with additional metadata</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div>
|
|
<%= form.label :source, "Source", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.select :source,
|
|
options_for_select(Rule::SOURCES.map { |source| [source.humanize, source] }, @rule.source || "manual"),
|
|
{ },
|
|
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" } %>
|
|
<p class="mt-2 text-sm text-gray-500">How this rule was created</p>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="flex items-center mb-2">
|
|
<%= check_box_tag :set_expiration, "1", false,
|
|
class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500",
|
|
data: { rule_form_target: "expirationCheckbox", action: "change->rule-form#toggleExpiration" } %>
|
|
<%= label_tag :set_expiration, "Set expiration", class: "ml-2 block text-sm font-medium text-gray-700" %>
|
|
</div>
|
|
<div class="hidden" data-rule-form-target="expirationField">
|
|
<%= form.label :expires_at, "Expires At", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.datetime_local_field :expires_at,
|
|
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
|
<p class="mt-2 text-sm text-gray-500">When this rule should automatically expire</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center pt-6">
|
|
<%= form.check_box :enabled, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
|
<%= form.label :enabled, "Enable immediately", class: "ml-2 block text-sm text-gray-900" %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
|
<div class="flex justify-end space-x-3">
|
|
<%= link_to "Cancel", rules_path, class: "inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50" %>
|
|
<%= form.submit "Create Rule", class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700" %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let selectedNetworkData = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const ruleTypeSelect = document.getElementById('rule_type_select');
|
|
const networkSection = document.getElementById('network_range_section');
|
|
const pathPatternSection = document.getElementById('path_pattern_section');
|
|
const conditionsSection = document.getElementById('conditions_section');
|
|
const pathPatternInput = document.getElementById('path_pattern_input');
|
|
const matchTypeSelect = document.getElementById('match_type_select');
|
|
|
|
function toggleSections() {
|
|
const ruleType = ruleTypeSelect.value;
|
|
|
|
// Hide all sections first
|
|
networkSection.classList.add('hidden');
|
|
pathPatternSection.classList.add('hidden');
|
|
conditionsSection.classList.add('hidden');
|
|
|
|
// Show appropriate section
|
|
if (ruleType === 'network') {
|
|
networkSection.classList.remove('hidden');
|
|
} else if (ruleType === 'path_pattern') {
|
|
pathPatternSection.classList.remove('hidden');
|
|
} else {
|
|
conditionsSection.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
function updatePathExamples() {
|
|
const pattern = pathPatternInput.value.trim();
|
|
const matchType = matchTypeSelect.value;
|
|
const exampleList = document.getElementById('example_list');
|
|
|
|
if (!pattern) {
|
|
exampleList.innerHTML = '<li>Enter a pattern to see examples</li>';
|
|
return;
|
|
}
|
|
|
|
let examples = [];
|
|
const cleanPattern = pattern.startsWith('/') ? pattern : '/' + pattern;
|
|
|
|
switch(matchType) {
|
|
case 'exact':
|
|
examples = [
|
|
`✓ ${cleanPattern}`,
|
|
`✗ ${cleanPattern}/users (extra segments)`,
|
|
`✗ /api${cleanPattern} (not at root)`
|
|
];
|
|
break;
|
|
case 'prefix':
|
|
examples = [
|
|
`✓ ${cleanPattern}`,
|
|
`✓ ${cleanPattern}/users`,
|
|
`✓ ${cleanPattern}/dashboard/settings`,
|
|
`✗ /api${cleanPattern} (not at start)`
|
|
];
|
|
break;
|
|
case 'suffix':
|
|
examples = [
|
|
`✓ ${cleanPattern}`,
|
|
`✓ /backup${cleanPattern}`,
|
|
`✓ /config/backup${cleanPattern}`,
|
|
`✗ ${cleanPattern}/test (extra at end)`
|
|
];
|
|
break;
|
|
case 'contains':
|
|
examples = [
|
|
`✓ ${cleanPattern}`,
|
|
`✓ /api${cleanPattern}/users`,
|
|
`✓ /super/secret${cleanPattern}/panel`,
|
|
`✗ ${cleanPattern}tool (different segment)`
|
|
];
|
|
break;
|
|
}
|
|
|
|
exampleList.innerHTML = examples.map(ex => `<li>${ex}</li>`).join('');
|
|
}
|
|
|
|
ruleTypeSelect.addEventListener('change', toggleSections);
|
|
pathPatternInput.addEventListener('input', updatePathExamples);
|
|
matchTypeSelect.addEventListener('change', updatePathExamples);
|
|
|
|
toggleSections(); // Initial state
|
|
|
|
// Pre-select network range if provided
|
|
<% if @rule.network_range.present? %>
|
|
// Show selected network display
|
|
const displayDiv = document.getElementById('selected_network_display');
|
|
const infoDiv = document.getElementById('selected_network_info');
|
|
const selectionInterface = document.getElementById('network_selection_interface');
|
|
|
|
let infoHtml = '<strong><%= @rule.network_range.network %></strong>';
|
|
<% if @rule.network_range.company.present? %>
|
|
infoHtml += ' - <%= @rule.network_range.company %>';
|
|
<% end %>
|
|
<% if @rule.network_range.asn_org.present? %>
|
|
infoHtml += ' (ASN: <%= @rule.network_range.asn_org %>)';
|
|
<% end %>
|
|
|
|
infoDiv.innerHTML = infoHtml;
|
|
displayDiv.classList.remove('hidden');
|
|
selectionInterface.classList.add('hidden');
|
|
<% end %>
|
|
|
|
// Pre-fill CIDR if provided
|
|
<% if params[:cidr].present? %>
|
|
if (ruleTypeSelect.value === 'network') {
|
|
document.getElementById('new_cidr_input').value = '<%= params[:cidr] %>';
|
|
}
|
|
<% end %>
|
|
|
|
// Set up search on Enter key
|
|
document.getElementById('network_search').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
searchNetworkRanges();
|
|
}
|
|
});
|
|
|
|
// Set up quick create on Enter key
|
|
document.getElementById('new_cidr_input').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
quickCreateNetwork();
|
|
}
|
|
});
|
|
});
|
|
|
|
function searchNetworkRanges() {
|
|
const query = document.getElementById('network_search').value.trim();
|
|
if (!query) return;
|
|
|
|
const resultsDiv = document.getElementById('network_search_results');
|
|
resultsDiv.innerHTML = '<div class="p-4 text-center text-gray-500">Searching...</div>';
|
|
resultsDiv.classList.remove('hidden');
|
|
|
|
fetch(`/network_ranges/search?q=${encodeURIComponent(query)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="p-4 text-center text-gray-500">No network ranges found. Try creating a new one below.</div>';
|
|
return;
|
|
}
|
|
|
|
const html = data.map(network => `
|
|
<div class="p-3 hover:bg-gray-50 cursor-pointer flex justify-between items-center"
|
|
onclick="selectNetworkRange(${network.id}, '${network.network}', '${network.company || ''}', '${network.asn_org || ''}')">
|
|
<div>
|
|
<div class="font-medium text-gray-900">${network.network}</div>
|
|
${network.company ? `<div class="text-sm text-gray-600">${network.company}</div>` : ''}
|
|
${network.asn_org ? `<div class="text-sm text-gray-500">ASN: ${network.asn} - ${network.asn_org}</div>` : ''}
|
|
${network.country ? `<div class="text-sm text-gray-400">Country: ${network.country}</div>` : ''}
|
|
</div>
|
|
<div class="text-xs text-gray-400">
|
|
${network.is_datacenter ? '<span class="bg-gray-100 px-2 py-1 rounded">DC</span>' : ''}
|
|
${network.is_vpn ? '<span class="bg-blue-100 px-2 py-1 rounded">VPN</span>' : ''}
|
|
${network.is_proxy ? '<span class="bg-red-100 px-2 py-1 rounded">Proxy</span>' : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
resultsDiv.innerHTML = html;
|
|
})
|
|
.catch(error => {
|
|
console.error('Search error:', error);
|
|
resultsDiv.innerHTML = '<div class="p-4 text-center text-red-500">Search failed. Please try again.</div>';
|
|
});
|
|
}
|
|
|
|
function selectNetworkRange(id, network, company, asnOrg) {
|
|
selectedNetworkData = { id, network, company, asnOrg };
|
|
|
|
// Update hidden field
|
|
document.getElementById('selected_network_range_id').value = id;
|
|
|
|
// Update display
|
|
const displayDiv = document.getElementById('selected_network_display');
|
|
const infoDiv = document.getElementById('selected_network_info');
|
|
|
|
let infoHtml = `<strong>${network}</strong>`;
|
|
if (company) infoHtml += ` - ${company}`;
|
|
if (asnOrg) infoHtml += ` (ASN: ${asnOrg})`;
|
|
|
|
infoDiv.innerHTML = infoHtml;
|
|
displayDiv.classList.remove('hidden');
|
|
|
|
// Hide the entire selection interface
|
|
document.getElementById('network_selection_interface').classList.add('hidden');
|
|
|
|
// Clear search results
|
|
document.getElementById('network_search_results').classList.add('hidden');
|
|
document.getElementById('network_search').value = '';
|
|
}
|
|
|
|
function clearSelectedNetwork() {
|
|
selectedNetworkData = null;
|
|
document.getElementById('selected_network_range_id').value = '';
|
|
document.getElementById('selected_network_display').classList.add('hidden');
|
|
|
|
// Show the selection interface again
|
|
document.getElementById('network_selection_interface').classList.remove('hidden');
|
|
}
|
|
|
|
function quickCreateNetwork() {
|
|
const cidr = document.getElementById('new_cidr_input').value.trim();
|
|
if (!cidr) {
|
|
alert('Please enter a CIDR or IP address');
|
|
return;
|
|
}
|
|
|
|
// Simple CIDR validation
|
|
const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}(\/\d{1,3})?$/;
|
|
if (!cidrRegex.test(cidr)) {
|
|
alert('Invalid CIDR or IP address format');
|
|
return;
|
|
}
|
|
|
|
// Create network range via API
|
|
fetch('/network_ranges', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
|
},
|
|
body: JSON.stringify({
|
|
network_range: {
|
|
network: cidr,
|
|
source: 'manual',
|
|
creation_reason: 'Created from rule form'
|
|
}
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.id) {
|
|
selectNetworkRange(data.id, data.network, data.company, data.asn_org);
|
|
document.getElementById('new_cidr_input').value = '';
|
|
} else {
|
|
alert('Failed to create network range: ' + (data.error || 'Unknown error'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Create error:', error);
|
|
alert('Failed to create network range. Please try again.');
|
|
});
|
|
}
|
|
</script> |