268 lines
14 KiB
Plaintext
268 lines
14 KiB
Plaintext
<% content_for :title, "Events - Baffle Hub" %>
|
|
|
|
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">Events</h1>
|
|
<p class="mt-2 text-gray-600">WAF event log and analysis</p>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-lg font-medium text-gray-900">Filters</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<%= form_with url: events_path, method: :get, local: true, class: "space-y-4" do |form| %>
|
|
<!-- Basic Filters Row -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<%= form.label :ip, "IP Address", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_field :ip, value: params[:ip],
|
|
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: "Filter by IP" %>
|
|
</div>
|
|
<div>
|
|
<%= form.label :waf_action, "Action", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.select :waf_action,
|
|
options_for_select([['All', ''], ['Allow', 'allow'], ['Deny', 'deny'], ['Redirect', 'redirect'], ['Challenge', 'challenge']], params[:waf_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" } %>
|
|
</div>
|
|
<div>
|
|
<%= form.label :country, "Country", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_field :country, value: params[:country],
|
|
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: "Country code (e.g. US)" %>
|
|
</div>
|
|
<div class="flex items-end space-x-2">
|
|
<%= form.submit "Apply Filters",
|
|
class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
|
<%= link_to "Clear", events_path,
|
|
class: "inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Network Intelligence Filters Row -->
|
|
<div class="border-t border-gray-200 pt-4">
|
|
<h4 class="text-sm font-medium text-gray-900 mb-3">Network Intelligence Filters</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<%= form.label :company, "Company", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_field :company, value: params[:company],
|
|
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: "e.g., Amazon, Google" %>
|
|
</div>
|
|
<div>
|
|
<%= form.label :network_type, "Network Type", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.select :network_type,
|
|
options_for_select([
|
|
['All', ''],
|
|
['Standard ( Residential/Business )', 'standard'],
|
|
['Datacenter', 'datacenter'],
|
|
['VPN', 'vpn'],
|
|
['Proxy', 'proxy']
|
|
], params[:network_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" } %>
|
|
</div>
|
|
<div>
|
|
<%= form.label :asn, "ASN", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_field :asn, value: params[:asn],
|
|
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: "Autonomous System Number" %>
|
|
</div>
|
|
<div>
|
|
<%= form.label :network_cidr, "Network CIDR", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_field :network_cidr, value: params[:network_cidr],
|
|
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: "e.g., 192.168.1.0/24" %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Events Table -->
|
|
<div class="bg-white shadow rounded-lg" data-controller="timeline" data-timeline-mode-value="events">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-lg font-medium text-gray-900">Events (<%= number_with_delimiter(@events.count) %>)</h3>
|
|
<div class="flex items-center space-x-4">
|
|
<%= link_to "📊 Analytics Dashboard", analytics_path,
|
|
class: "text-sm text-blue-600 hover:text-blue-800 font-medium" %>
|
|
<%= link_to "🌐 Network Analytics", analytics_networks_path,
|
|
class: "text-sm text-blue-600 hover:text-blue-800 font-medium" %>
|
|
<% if @pagy.pages > 1 %>
|
|
<span class="text-sm text-gray-500">
|
|
Page <%= @pagy.page %> of <%= @pagy.pages %>
|
|
</span>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<!-- Top Pagination -->
|
|
<% if @pagy.pages > 1 %>
|
|
<div class="mt-4">
|
|
<%= pagy_nav_tailwind(@pagy, pagy_id: 'events_top') %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<% if @events.any? %>
|
|
<table class="min-w-full divide-y divide-gray-200">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Time</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP Address</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Path</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Method</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Country</th>
|
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User Agent</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<% @events.each do |event| %>
|
|
<tr class="hover:bg-gray-50 cursor-pointer" onclick="window.location='<%= event_path(event) %>'">
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
<div class="text-gray-900" data-timeline-target="timestamp" data-iso="<%= event.timestamp.iso8601 %>">
|
|
<%= event.timestamp.strftime("%H:%M:%S") %>
|
|
</div>
|
|
<div class="text-xs text-gray-500" data-timeline-target="date" data-iso="<%= event.timestamp.iso8601 %>">
|
|
<%= event.timestamp.strftime("%Y-%m-%d") %>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-gray-900">
|
|
<% network_range = event.network_range %>
|
|
<% if network_range %>
|
|
<%= link_to event.ip_address, network_range_path(event.ip_address),
|
|
class: "text-blue-600 hover:text-blue-800 hover:underline font-mono" %>
|
|
<!-- Network Intelligence Summary -->
|
|
<div class="mt-1 space-y-1">
|
|
<% if network_range.company.present? %>
|
|
<div class="text-xs text-gray-600 font-medium">
|
|
<%= network_range.company %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<% if network_range.is_datacenter? || network_range.is_vpn? || network_range.is_proxy? %>
|
|
<div class="flex flex-wrap gap-1">
|
|
<% if network_range.is_datacenter? %>
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-orange-100 text-orange-800" title="Datacenter">DC</span>
|
|
<% end %>
|
|
<% if network_range.is_vpn? %>
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800" title="VPN">VPN</span>
|
|
<% end %>
|
|
<% if network_range.is_proxy? %>
|
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800" title="Proxy">PROXY</span>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="text-xs text-gray-500">
|
|
<%= link_to network_range.cidr, network_range_path(network_range) %>
|
|
<% if network_range.asn.present? %>
|
|
• ASN <%= network_range.asn %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<span class="font-mono"><%= event.ip_address %></span>
|
|
<div class="mt-1 text-xs text-gray-400">Unknown network</div>
|
|
<% end %>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
|
<%= case event.waf_action
|
|
when 'allow' then 'bg-green-100 text-green-800'
|
|
when 'deny' then 'bg-red-100 text-red-800'
|
|
when 'redirect' then 'bg-blue-100 text-blue-800'
|
|
when 'challenge' then 'bg-yellow-100 text-yellow-800'
|
|
else 'bg-gray-100 text-gray-800'
|
|
end %>">
|
|
<%= event.waf_action %>
|
|
</span>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm font-mono text-gray-900">
|
|
<div class="max-w-md break-all">
|
|
<%= event.request_path || '-' %>
|
|
</div>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
<%= event.request_method ? event.request_method.upcase : '-' %>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
<%= event.response_status || '-' %>
|
|
</td>
|
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
<% if event.lookup_country.present? %>
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
|
<%= event.lookup_country %>
|
|
</span>
|
|
<% else %>
|
|
<span class="text-gray-400">-</span>
|
|
<% end %>
|
|
</td>
|
|
<td class="px-6 py-4 text-sm text-gray-900">
|
|
<% if event.user_agent.present? %>
|
|
<% ua = parse_user_agent(event.user_agent) %>
|
|
<div class="space-y-0.5" title="<%= ua[:raw] %>">
|
|
<div class="font-medium text-gray-900">
|
|
<%= ua[:name] if ua[:name].present? %>
|
|
<% if ua[:version].present? && ua[:name].present? %>
|
|
<span class="text-gray-500 font-normal"><%= ua[:version] %></span>
|
|
<% end %>
|
|
</div>
|
|
<% if ua[:os_name].present? %>
|
|
<div class="text-xs text-gray-500">
|
|
<%= ua[:os_name] %>
|
|
<% if ua[:os_version].present? %>
|
|
<%= ua[:os_version] %>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
<% if ua[:bot] %>
|
|
<div class="text-xs">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-orange-100 text-orange-800">
|
|
🤖 <%= ua[:bot_name] || 'Bot' %>
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% else %>
|
|
<span class="text-gray-400">-</span>
|
|
<% end %>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- Bottom Pagination -->
|
|
<% if @pagy.pages > 1 %>
|
|
<%= pagy_nav_tailwind(@pagy, pagy_id: 'events_bottom') %>
|
|
<% end %>
|
|
<% else %>
|
|
<div class="px-6 py-12 text-center">
|
|
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
</svg>
|
|
<h3 class="mt-2 text-sm font-medium text-gray-900">No events</h3>
|
|
<p class="mt-1 text-sm text-gray-500">
|
|
<% if params[:ip].present? || params[:waf_action].present? || params[:country].present? %>
|
|
No events found matching your filters.
|
|
<% else %>
|
|
No events have been received yet.
|
|
<% end %>
|
|
</p>
|
|
<% if params[:ip].present? || params[:waf_action].present? || params[:country].present? %>
|
|
<div class="mt-6">
|
|
<%= link_to "Clear Filters", events_path,
|
|
class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-blue-600 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div> |