Add 'tags' to event model. Add a dataimport system - currently for MaxMind zip files

This commit is contained in:
Dan Milne
2025-11-11 10:31:36 +11:00
parent 772fae7e8b
commit 26216da9ca
34 changed files with 3580 additions and 14 deletions

View File

@@ -0,0 +1,240 @@
<% content_for :title, "Edit WAF Policy" %>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900">Edit WAF Policy</h1>
<p class="mt-2 text-gray-600">Modify the firewall policy settings</p>
</div>
<div class="flex space-x-3">
<%= link_to "← Back to Policy", waf_policy_path(@waf_policy),
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" %>
</div>
</div>
<%= form_with(model: @waf_policy, local: true, class: "space-y-6") do |form| %>
<!-- Basic Information -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6 space-y-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">📋 Basic Information</h3>
<!-- Name -->
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :name,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "e.g., Block Brazil" %>
</div>
<!-- Description -->
<div>
<%= form.label :description, "Description", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :description, rows: 3,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "Explain why this policy is needed..." %>
</div>
<!-- Action -->
<div>
<%= form.label :action, "Action", class: "block text-sm font-medium text-gray-700" %>
<%= form.select :action,
options_for_select(@actions.map { |action| [action.humanize, action] }, @waf_policy.action),
{ prompt: "Select action" },
{ class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
id: "action-select" } %>
</div>
<!-- Status -->
<div class="flex items-center">
<%= form.check_box :enabled, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" %>
<%= form.label :enabled, "Enable this policy", class: "ml-2 text-sm text-gray-700" %>
</div>
<!-- Expiration -->
<div>
<%= form.label :expires_at, "Expires At (optional)", class: "block text-sm font-medium text-gray-700" %>
<%= form.datetime_local_field :expires_at,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %>
<p class="text-xs text-gray-500 mt-1">Leave blank for permanent policy</p>
</div>
</div>
</div>
<!-- Targets Configuration -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6 space-y-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">🎯 Targets Configuration</h3>
<p class="text-sm text-gray-600">
<strong>Policy Type:</strong> <%= @waf_policy.policy_type.humanize %>
<% unless @waf_policy.new_record? %>
<span class="text-xs text-gray-500">(Cannot change policy type after creation)</span>
<% end %>
</p>
<% if @waf_policy.new_record? %>
<!-- Policy Type (only for new records) -->
<div>
<%= form.label :policy_type, "Policy Type", class: "block text-sm font-medium text-gray-700" %>
<%= form.select :policy_type,
options_for_select(@policy_types.map { |type| [type.humanize, type] }, @waf_policy.policy_type),
{ prompt: "Select policy type" },
{ class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
id: "policy-type-select", disabled: !@waf_policy.new_record? } %>
</div>
<% else %>
<!-- Display policy type for existing records -->
<div>
<%= form.label :policy_type, "Policy Type", class: "block text-sm font-medium text-gray-700" %>
<div class="mt-1">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<%= @waf_policy.policy_type.humanize %>
</span>
</div>
<%= form.hidden_field :policy_type %>
</div>
<% end %>
<!-- Country Policy Targets -->
<% if @waf_policy.country_policy? %>
<div id="country-targets">
<%= form.label :targets, "Countries", class: "block text-sm font-medium text-gray-700 mb-2" %>
<div data-controller="country-selector"
data-country-selector-options-value="<%= CountryHelper.all_for_select.to_json %>"
data-country-selector-placeholder-value="Search and select countries...">
<%= select_tag "waf_policy[targets][]",
options_for_select(@waf_policy.targets.map { |code| [CountryHelper.display_with_flag(code), code] }, @waf_policy.targets),
{
multiple: true,
class: "hidden",
data: { "country-selector-target": "select" }
} %>
</div>
</div>
<% end %>
<!-- ASN Policy Targets -->
<% if @waf_policy.asn_policy? %>
<div id="asn-targets">
<%= form.label :targets, "ASN Numbers", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag "waf_policy[targets][]", @waf_policy.targets.join(', '),
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "e.g., 12345, 67890" %>
<p class="text-xs text-gray-500 mt-1">Enter ASNs separated by commas</p>
</div>
<% end %>
<!-- Company Policy Targets -->
<% if @waf_policy.company_policy? %>
<div id="company-targets">
<%= form.label :targets, "Companies", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag "waf_policy[targets][]", @waf_policy.targets.join(', '),
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "e.g., AWS, Digital Ocean, Google Cloud" %>
<p class="text-xs text-gray-500 mt-1">Enter company names separated by commas</p>
</div>
<% end %>
<!-- Network Type Targets -->
<% if @waf_policy.network_type_policy? %>
<div id="network-type-targets">
<%= form.label :targets, "Network Types", class: "block text-sm font-medium text-gray-700" %>
<div class="space-y-2">
<label class="flex items-center">
<%= check_box_tag "waf_policy[targets][]", "datacenter", @waf_policy.targets.include?("datacenter"), class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2" %>
<span class="text-sm text-gray-700">Datacenter IPs</span>
</label>
<label class="flex items-center">
<%= check_box_tag "waf_policy[targets][]", "proxy", @waf_policy.targets.include?("proxy"), class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2" %>
<span class="text-sm text-gray-700">Proxy/VPN IPs</span>
</label>
<label class="flex items-center">
<%= check_box_tag "waf_policy[targets][]", "standard", @waf_policy.targets.include?("standard"), class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2" %>
<span class="text-sm text-gray-700">Standard ISPs</span>
</label>
</div>
</div>
<% end %>
</div>
</div>
<!-- Additional Configuration -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6 space-y-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">⚙️ Additional Configuration</h3>
<!-- Redirect Settings (for redirect action) -->
<div id="redirect-config" class="space-y-3 <%= 'hidden' unless @waf_policy.redirect_action? %>">
<div>
<%= label_tag "additional_data[redirect_url]", "Redirect URL", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag "additional_data[redirect_url]", @waf_policy.additional_data&.dig('redirect_url'),
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "https://example.com/compliance" %>
</div>
<div>
<%= label_tag "additional_data[redirect_status]", "HTTP Status", class: "block text-sm font-medium text-gray-700" %>
<%= select_tag "additional_data[redirect_status]",
options_for_select([["301 Moved Permanently", 301], ["302 Found", 302], ["307 Temporary Redirect", 307]], @waf_policy.additional_data&.dig('redirect_status')),
{ include_blank: true, class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
</div>
</div>
<!-- Challenge Settings (for challenge action) -->
<div id="challenge-config" class="space-y-3 <%= 'hidden' unless @waf_policy.challenge_action? %>">
<div>
<%= label_tag "additional_data[challenge_type]", "Challenge Type", class: "block text-sm font-medium text-gray-700" %>
<%= select_tag "additional_data[challenge_type]",
options_for_select([["CAPTCHA", "captcha"], ["JavaScript", "javascript"], ["Proof of Work", "proof_of_work"]], @waf_policy.additional_data&.dig('challenge_type')),
{ include_blank: true, class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
</div>
<div>
<%= label_tag "additional_data[challenge_message]", "Challenge Message", class: "block text-sm font-medium text-gray-700" %>
<%= text_area_tag "additional_data[challenge_message]", @waf_policy.additional_data&.dig('challenge_message'), rows: 2,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "Please verify you are human to continue..." %>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-end space-x-3">
<%= link_to "Cancel", waf_policy_path(@waf_policy),
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 "Update Policy",
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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
</div>
<% end %>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const actionSelect = document.getElementById('action-select');
const redirectConfig = document.getElementById('redirect-config');
const challengeConfig = document.getElementById('challenge-config');
function updateActionConfig() {
const selectedAction = actionSelect.value;
// Hide all config sections
redirectConfig.classList.add('hidden');
challengeConfig.classList.add('hidden');
// Show relevant config section
switch(selectedAction) {
case 'redirect':
redirectConfig.classList.remove('hidden');
break;
case 'challenge':
challengeConfig.classList.remove('hidden');
break;
}
}
// Add event listener
actionSelect.addEventListener('change', updateActionConfig);
// Initial update
updateActionConfig();
});
</script>

View File

@@ -150,7 +150,13 @@
</div>
<div class="mt-1 text-sm text-gray-500">
<%= policy.policy_type.humanize %> policy targeting
<% if policy.targets.length > 3 %>
<% if policy.country_policy? && policy.targets.any? %>
<% if policy.targets.length > 3 %>
<%= policy.targets.length %> countries
<% else %>
<%= policy.targets.map { |code| CountryHelper.display_with_flag(code) }.join(', ') %>
<% end %>
<% elsif policy.targets.length > 3 %>
<%= policy.targets.length %> items
<% else %>
<%= policy.targets.join(', ') %>

View File

@@ -0,0 +1,240 @@
<% content_for :title, "New WAF Policy" %>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900">New WAF Policy</h1>
<p class="mt-2 text-gray-600">Create a new firewall policy to automatically generate rules</p>
</div>
<div class="flex space-x-3">
<%= link_to "← Back to Policies", waf_policies_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" %>
</div>
</div>
<%= form_with(model: @waf_policy, local: true, class: "space-y-6") do |form| %>
<!-- Basic Information -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6 space-y-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">📋 Basic Information</h3>
<!-- Name -->
<div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %>
<%= form.text_field :name,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "e.g., Block Brazil" %>
</div>
<!-- Description -->
<div>
<%= form.label :description, "Description", class: "block text-sm font-medium text-gray-700" %>
<%= form.text_area :description, rows: 3,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "Explain why this policy is needed..." %>
</div>
<!-- Policy Type -->
<div>
<%= form.label :policy_type, "Policy Type", class: "block text-sm font-medium text-gray-700" %>
<%= form.select :policy_type,
options_for_select(@policy_types.map { |type| [type.humanize, type] }, @waf_policy.policy_type),
{ prompt: "Select policy type" },
{ class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
id: "policy-type-select" } %>
</div>
<!-- Action -->
<div>
<%= form.label :action, "Action", class: "block text-sm font-medium text-gray-700" %>
<%= form.select :action,
options_for_select(@actions.map { |action| [action.humanize, action] }, @waf_policy.action),
{ prompt: "Select action" },
{ class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
id: "action-select" } %>
</div>
</div>
</div>
<!-- Targets Configuration -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6 space-y-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">🎯 Targets Configuration</h3>
<!-- Country Policy Targets -->
<div id="country-targets" class="policy-targets hidden">
<%= form.label :targets, "Countries", class: "block text-sm font-medium text-gray-700 mb-2" %>
<div data-controller="country-selector"
data-country-selector-options-value="<%= CountryHelper.all_for_select.to_json %>"
data-country-selector-placeholder-value="Search and select countries...">
<%= select_tag "waf_policy[targets][]",
options_for_select([]),
{
multiple: true,
class: "hidden",
data: { "country-selector-target": "select" }
} %>
</div>
</div>
<!-- ASN Policy Targets -->
<div id="asn-targets" class="policy-targets hidden">
<%= form.label :targets, "ASN Numbers", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag "waf_policy[targets][]", nil,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "e.g., 12345, 67890" %>
<p class="text-xs text-gray-500 mt-1">Enter ASNs separated by commas</p>
</div>
<!-- Company Policy Targets -->
<div id="company-targets" class="policy-targets hidden">
<%= form.label :targets, "Companies", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag "waf_policy[targets][]", nil,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "e.g., AWS, Digital Ocean, Google Cloud" %>
<p class="text-xs text-gray-500 mt-1">Enter company names separated by commas</p>
</div>
<!-- Network Type Targets -->
<div id="network-type-targets" class="policy-targets hidden">
<%= form.label :targets, "Network Types", class: "block text-sm font-medium text-gray-700" %>
<div class="space-y-2">
<label class="flex items-center">
<%= check_box_tag "waf_policy[targets][]", "datacenter", false, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2" %>
<span class="text-sm text-gray-700">Datacenter IPs</span>
</label>
<label class="flex items-center">
<%= check_box_tag "waf_policy[targets][]", "proxy", false, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2" %>
<span class="text-sm text-gray-700">Proxy/VPN IPs</span>
</label>
<label class="flex items-center">
<%= check_box_tag "waf_policy[targets][]", "standard", false, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2" %>
<span class="text-sm text-gray-700">Standard ISPs</span>
</label>
</div>
</div>
</div>
</div>
<!-- Additional Configuration -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6 space-y-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">⚙️ Additional Configuration</h3>
<!-- Redirect Settings (for redirect action) -->
<div id="redirect-config" class="hidden space-y-3">
<div>
<%= label_tag "additional_data[redirect_url]", "Redirect URL", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag "additional_data[redirect_url]", nil,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "https://example.com/compliance" %>
</div>
<div>
<%= label_tag "additional_data[redirect_status]", "HTTP Status", class: "block text-sm font-medium text-gray-700" %>
<%= select_tag "additional_data[redirect_status]",
options_for_select([["301 Moved Permanently", 301], ["302 Found", 302], ["307 Temporary Redirect", 307]]),
{ include_blank: true, class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
</div>
</div>
<!-- Challenge Settings (for challenge action) -->
<div id="challenge-config" class="hidden space-y-3">
<div>
<%= label_tag "additional_data[challenge_type]", "Challenge Type", class: "block text-sm font-medium text-gray-700" %>
<%= select_tag "additional_data[challenge_type]",
options_for_select([["CAPTCHA", "captcha"], ["JavaScript", "javascript"], ["Proof of Work", "proof_of_work"]]),
{ include_blank: true, class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
</div>
<div>
<%= label_tag "additional_data[challenge_message]", "Challenge Message", class: "block text-sm font-medium text-gray-700" %>
<%= text_area_tag "additional_data[challenge_message]", nil, rows: 2,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm",
placeholder: "Please verify you are human to continue..." %>
</div>
</div>
<!-- Expiration -->
<div>
<%= form.label :expires_at, "Expires At (optional)", class: "block text-sm font-medium text-gray-700" %>
<%= form.datetime_local_field :expires_at,
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %>
<p class="text-xs text-gray-500 mt-1">Leave blank for permanent policy</p>
</div>
<!-- Enabled -->
<div class="flex items-center">
<%= form.check_box :enabled, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" %>
<%= form.label :enabled, "Enable this policy immediately", class: "ml-2 text-sm text-gray-700" %>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-end space-x-3">
<%= link_to "Cancel", waf_policies_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 Policy",
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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
</div>
<% end %>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const policyTypeSelect = document.getElementById('policy-type-select');
const actionSelect = document.getElementById('action-select');
const allTargets = document.querySelectorAll('.policy-targets');
const redirectConfig = document.getElementById('redirect-config');
const challengeConfig = document.getElementById('challenge-config');
function updateTargetsVisibility() {
const selectedType = policyTypeSelect.value;
// Hide all target sections
allTargets.forEach(target => target.classList.add('hidden'));
// Show relevant target section
switch(selectedType) {
case 'country':
document.getElementById('country-targets').classList.remove('hidden');
break;
case 'asn':
document.getElementById('asn-targets').classList.remove('hidden');
break;
case 'company':
document.getElementById('company-targets').classList.remove('hidden');
break;
case 'network_type':
document.getElementById('network-type-targets').classList.remove('hidden');
break;
}
}
function updateActionConfig() {
const selectedAction = actionSelect.value;
// Hide all config sections
redirectConfig.classList.add('hidden');
challengeConfig.classList.add('hidden');
// Show relevant config section
switch(selectedAction) {
case 'redirect':
redirectConfig.classList.remove('hidden');
break;
case 'challenge':
challengeConfig.classList.remove('hidden');
break;
}
}
// Add event listeners
policyTypeSelect.addEventListener('change', updateTargetsVisibility);
actionSelect.addEventListener('change', updateActionConfig);
// Initial update
updateTargetsVisibility();
updateActionConfig();
});
</script>

View File

@@ -0,0 +1,194 @@
<% content_for :title, "Block Countries" %>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900">🌍 Block Countries</h1>
<p class="mt-2 text-gray-600">Create country-based firewall policies to block or redirect traffic from specific countries</p>
</div>
<div class="flex space-x-3">
<%= link_to "← Back to Policies", waf_policies_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" %>
</div>
</div>
<%= form_with(url: create_country_waf_policies_path, method: :post, local: true, class: "space-y-6") do |form| %>
<!-- Popular Countries Quick Selection -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">🚨 Popular Countries for Blocking</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<% CountryHelper.popular_for_blocking.each do |country| %>
<label class="relative flex items-start p-3 border rounded-lg hover:bg-gray-50 cursor-pointer">
<div class="flex items-center h-5">
<%= check_box_tag "countries[]", country[:code], false, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" %>
</div>
<div class="ml-3 text-sm">
<div class="font-medium text-gray-900">
<%= country[:display] %>
</div>
<div class="text-gray-500">
<%= country[:reason] %>
</div>
</div>
</label>
<% end %>
</div>
</div>
</div>
<!-- Regional Selection -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">🗺️ Select by Region</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<% CountryHelper.by_region.each do |region_name, country_codes| %>
<div>
<h4 class="font-medium text-gray-900 mb-2"><%= region_name %></h4>
<div class="grid grid-cols-2 gap-2">
<% CountryHelper.countries_for_region(region_name).each do |country| %>
<label class="flex items-center text-sm">
<%= check_box_tag "countries[]", country[:code], false, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-2" %>
<%= country[:display] %>
</label>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
</div>
<!-- Policy Settings -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:p-6 space-y-4">
<h3 class="text-lg leading-6 font-medium text-gray-900">⚙️ Policy Settings</h3>
<!-- Action Selection -->
<div>
<%= form.label :action, "What should happen to traffic from selected countries?", class: "block text-sm font-medium text-gray-700" %>
<div class="mt-2 space-y-2">
<label class="flex items-center">
<%= radio_button_tag "action", "deny", true, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" %>
<span class="ml-2 text-sm text-gray-700">
<span class="font-medium">🚫 Block (Deny)</span> - Show 403 Forbidden error
</span>
</label>
<label class="flex items-center">
<%= radio_button_tag "action", "challenge", false, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" %>
<span class="ml-2 text-sm text-gray-700">
<span class="font-medium">🛡️ Challenge</span> - Present CAPTCHA challenge
</span>
</label>
<label class="flex items-center">
<%= radio_button_tag "action", "redirect", false, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" %>
<span class="ml-2 text-sm text-gray-700">
<span class="font-medium">🔄 Redirect</span> - Redirect to compliance page
</span>
</label>
<label class="flex items-center">
<%= radio_button_tag "action", "allow", false, class: "h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300" %>
<span class="ml-2 text-sm text-gray-700">
<span class="font-medium">✅ Allow</span> - Explicitly allow traffic
</span>
</label>
</div>
</div>
<!-- Redirect Settings (conditional) -->
<div id="redirect-settings" class="hidden space-y-3">
<%= form.label :redirect_url, "Redirect URL", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag "additional_data[redirect_url]", nil,
placeholder: "https://example.com/compliance",
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %>
<%= form.label :redirect_status, "HTTP Status", class: "block text-sm font-medium text-gray-700 mt-2" %>
<%= select_tag "additional_data[redirect_status]",
options_for_select([["301 Moved Permanently", 301], ["302 Found", 302], ["307 Temporary Redirect", 307]], 302),
{ class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" } %>
</div>
<!-- Description -->
<div>
<%= form.label :description, "Description (optional)", class: "block text-sm font-medium text-gray-700" %>
<%= text_area_tag "description", nil, rows: 3,
placeholder: "e.g., Block countries with high scanner activity",
class: "block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm" %>
</div>
<!-- Preview -->
<div id="policy-preview" class="border-t pt-4">
<h4 class="text-sm font-medium text-gray-900 mb-2">📋 Policy Preview</h4>
<div class="bg-gray-50 rounded-md p-3 text-sm text-gray-600">
<span id="preview-text">Select countries above to see policy preview...</span>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-end space-x-3">
<%= link_to "Cancel", waf_policies_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" %>
<%= submit_tag "Create Country Policy",
class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
</div>
<% end %>
</div>
<script>
// Show/hide redirect settings based on action selection
document.addEventListener('DOMContentLoaded', function() {
const actionRadios = document.querySelectorAll('input[name="action"]');
const redirectSettings = document.getElementById('redirect-settings');
const previewText = document.getElementById('preview-text');
function updateVisibility() {
const selectedAction = document.querySelector('input[name="action"]:checked')?.value;
if (selectedAction === 'redirect') {
redirectSettings.classList.remove('hidden');
} else {
redirectSettings.classList.add('hidden');
}
updatePreview();
}
function updatePreview() {
const selectedCountries = document.querySelectorAll('input[name="countries[]"]:checked');
const selectedAction = document.querySelector('input[name="action"]:checked')?.value || 'deny';
const actionText = {
'deny': '🚫 Block',
'challenge': '🛡️ Challenge',
'redirect': '🔄 Redirect',
'allow': '✅ Allow'
}[selectedAction];
if (selectedCountries.length > 0) {
const countryNames = Array.from(selectedCountries).map(cb => {
const label = cb.closest('label');
const countryName = label.querySelector('.font-medium')?.textContent || cb.value;
return countryName;
}).join(', ');
previewText.textContent = `${actionText} ${selectedCountries.length} countries: ${countryNames}`;
} else {
previewText.textContent = 'Select countries above to see policy preview...';
}
}
// Add event listeners
actionRadios.forEach(radio => {
radio.addEventListener('change', updateVisibility);
});
document.querySelectorAll('input[name="countries[]"]').forEach(checkbox => {
checkbox.addEventListener('change', updatePreview);
});
// Initial update
updateVisibility();
});
</script>

View File

@@ -0,0 +1,270 @@
<% content_for :title, @waf_policy.name %>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900"><%= @waf_policy.name %></h1>
<p class="mt-2 text-gray-600"><%= @waf_policy.description %></p>
</div>
<div class="flex space-x-3">
<%= link_to "← Back to Policies", waf_policies_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" %>
<%= link_to "Edit", edit_waf_policy_path(@waf_policy),
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" %>
</div>
</div>
<!-- Policy Details -->
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">📋 Policy Details</h3>
</div>
<div class="border-t border-gray-200">
<dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Policy Type</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<%= @waf_policy.policy_type.humanize %>
</span>
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Action</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
<%= case @waf_policy.action
when 'deny' then 'bg-red-100 text-red-800'
when 'allow' then 'bg-green-100 text-green-800'
when 'redirect' then 'bg-yellow-100 text-yellow-800'
when 'challenge' then 'bg-purple-100 text-purple-800'
end %>">
<%= @waf_policy.action.upcase %>
</span>
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Targets</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<% if @waf_policy.targets.any? %>
<div class="flex flex-wrap gap-2">
<% @waf_policy.targets.each do |target| %>
<% if @waf_policy.country_policy? %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
<%= CountryHelper.display_with_flag(target) %>
</span>
<% else %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
<%= target %>
</span>
<% end %>
<% end %>
</div>
<% else %>
<span class="text-gray-400">No targets configured</span>
<% end %>
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Status</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<div class="flex items-center space-x-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
<%= @waf_policy.active? ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' %>">
<%= @waf_policy.active? ? 'Active' : 'Inactive' %>
</span>
<% if @waf_policy.active? %>
<%= link_to "Deactivate", deactivate_waf_policy_path(@waf_policy), method: :post,
data: { confirm: "Are you sure you want to deactivate this policy?" },
class: "text-sm text-red-600 hover:text-red-900" %>
<% else %>
<%= link_to "Activate", activate_waf_policy_path(@waf_policy), method: :post,
class: "text-sm text-green-600 hover:text-green-900" %>
<% end %>
</div>
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Expires At</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<% if @waf_policy.expires_at.present? %>
<%= @waf_policy.expires_at.strftime("%B %d, %Y at %I:%M %p") %>
<% if @waf_policy.expired? %>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Expired
</span>
<% end %>
<% else %>
<span class="text-gray-400">Never expires</span>
<% end %>
</dd>
</div>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Created By</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<%= @waf_policy.user.email_address %>
</dd>
</div>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Created</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<%= @waf_policy.created_at.strftime("%B %d, %Y at %I:%M %p") %>
</dd>
</div>
<!-- Additional Configuration -->
<% if @waf_policy.additional_data && @waf_policy.additional_data.any? %>
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">Additional Config</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2">
<% @waf_policy.additional_data.each do |key, value| %>
<div class="mb-2">
<span class="font-medium"><%= key.humanize %>:</span>
<span class="text-gray-600"><%= value %></span>
</div>
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<!-- Statistics -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Generated Rules</dt>
<dd class="text-lg font-medium text-gray-900"><%= @waf_policy.generated_rules_count %></dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Active Rules</dt>
<dd class="text-lg font-medium text-gray-900"><%= @waf_policy.active_rules_count %></dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Rules Last 7 Days</dt>
<dd class="text-lg font-medium text-gray-900"><%= @waf_policy.effectiveness_stats[:rules_last_7_days] %></dd>
</dl>
</div>
</div>
</div>
</div>
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-purple-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Efficiency Rate</dt>
<dd class="text-lg font-medium text-gray-900">
<%= @waf_policy.active_rules_count.to_f / [@waf_policy.generated_rules_count, 1].max * 100 %>%
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<!-- Generated Rules -->
<% if @generated_rules.any? %>
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">🔧 Generated Rules</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">
Specific rules created by this policy that are enforced by baffle-agents.
</p>
</div>
<div class="border-t border-gray-200">
<ul class="divide-y divide-gray-200">
<% @generated_rules.each do |rule| %>
<li class="hover:bg-gray-50">
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
<%= link_to "📋", rule_path(rule), class: "text-blue-600 hover:text-blue-900" %>
</div>
<div class="ml-4">
<div class="text-sm font-medium text-gray-900">
Rule #<%= rule.id %> - <%= rule.network_range&.cidr || "Unknown" %>
</div>
<div class="text-sm text-gray-500">
<%= rule.action.upcase %> • Created <%= time_ago_in_words(rule.created_at) %> ago
<% if rule.redirect_action? %>
• Redirect to <%= rule.redirect_url %>
<% elsif rule.challenge_action? %>
• <%= rule.challenge_type.humanize %> challenge
<% end %>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<% if rule.active? %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Active
</span>
<% else %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
Inactive
</span>
<% end %>
</div>
</div>
</div>
</li>
<% end %>
</ul>
</div>
</div>
<% end %>
</div>