beskar 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +274 -0
- data/README.md +412 -204
- data/app/channels/concerns/beskar/channels/session_security.rb +46 -0
- data/app/controllers/beskar/administrative_actions_controller.rb +16 -0
- data/app/controllers/beskar/application_controller.rb +214 -0
- data/app/controllers/beskar/banned_ips_controller.rb +255 -0
- data/app/controllers/beskar/dashboard_controller.rb +62 -0
- data/app/controllers/beskar/security_events_controller.rb +164 -0
- data/app/controllers/concerns/beskar/controllers/audit_export.rb +54 -0
- data/app/controllers/concerns/beskar/controllers/security_tracking.rb +76 -48
- data/app/controllers/concerns/beskar/controllers/session_security.rb +29 -0
- data/app/jobs/beskar/notification_job.rb +33 -0
- data/app/mailers/beskar/security_mailer.rb +59 -0
- data/app/models/beskar/administrative_action.rb +41 -0
- data/app/models/beskar/banned_ip.rb +105 -105
- data/app/models/beskar/security_event.rb +51 -4
- data/app/models/beskar/security_state.rb +58 -0
- data/app/services/beskar/banned_ip_manager.rb +88 -0
- data/app/views/beskar/administrative_actions/index.html.erb +33 -0
- data/app/views/beskar/administrative_actions/show.html.erb +21 -0
- data/app/views/beskar/banned_ips/edit.html.erb +195 -0
- data/app/views/beskar/banned_ips/index.html.erb +319 -0
- data/app/views/beskar/banned_ips/new.html.erb +190 -0
- data/app/views/beskar/banned_ips/review.html.erb +24 -0
- data/app/views/beskar/banned_ips/show.html.erb +304 -0
- data/app/views/beskar/dashboard/index.html.erb +280 -0
- data/app/views/beskar/security_events/index.html.erb +302 -0
- data/app/views/beskar/security_events/show.html.erb +293 -0
- data/app/views/beskar/shared/_export_form.html.erb +10 -0
- data/app/views/layouts/beskar/_behavior.html.erb +121 -0
- data/app/views/layouts/beskar/application.html.erb +581 -6
- data/config/routes.rb +30 -0
- data/db/migrate/20251016000001_create_beskar_security_events.rb +3 -3
- data/db/migrate/20260910000001_create_beskar_security_states.rb +14 -0
- data/db/migrate/20260911000001_create_beskar_administrative_actions.rb +22 -0
- data/db/migrate/20260911000002_expand_administrative_action_targets.rb +6 -0
- data/docs/README.md +73 -0
- data/docs/archive/project-documentation.md +659 -0
- data/docs/audits/project-review.md +437 -0
- data/docs/audits/repair-status.md +216 -0
- data/docs/guides/audit-and-waf.md +175 -0
- data/docs/guides/audit-lifecycle.md +172 -0
- data/docs/guides/authentication.md +213 -0
- data/docs/guides/configuration.md +182 -0
- data/docs/guides/dashboard-and-search.md +251 -0
- data/docs/guides/notifications-and-recovery.md +157 -0
- data/docs/guides/risk-scoring.md +116 -0
- data/docs/operations/monitor-only-mode.md +85 -0
- data/docs/operations/security-hardening.md +167 -0
- data/docs/operations/state-storage.md +144 -0
- data/docs/research/rust-performance-assessment.md +69 -0
- data/lib/beskar/configuration.rb +105 -20
- data/lib/beskar/configuration_validator.rb +188 -0
- data/lib/beskar/devise_authentication.rb +24 -0
- data/lib/beskar/engine.rb +21 -88
- data/lib/beskar/logger.rb +288 -0
- data/lib/beskar/middleware/request_analyzer.rb +133 -99
- data/lib/beskar/models/security_trackable_authenticable.rb +76 -97
- data/lib/beskar/models/security_trackable_devise.rb +34 -25
- data/lib/beskar/models/security_trackable_generic.rb +171 -214
- data/lib/beskar/risk_level.rb +22 -0
- data/lib/beskar/services/account_locker.rb +90 -81
- data/lib/beskar/services/administrative_audit.rb +36 -0
- data/lib/beskar/services/administrative_bans.rb +104 -0
- data/lib/beskar/services/audit_data.rb +72 -0
- data/lib/beskar/services/authentication.rb +31 -0
- data/lib/beskar/services/authentication_attempt.rb +141 -0
- data/lib/beskar/services/ban_expiry.rb +28 -0
- data/lib/beskar/services/device_detector.rb +32 -41
- data/lib/beskar/services/event_search.rb +58 -0
- data/lib/beskar/services/geolocation_service.rb +83 -114
- data/lib/beskar/services/ip_whitelist.rb +31 -40
- data/lib/beskar/services/location_assessment.rb +109 -0
- data/lib/beskar/services/native_account_lock.rb +82 -0
- data/lib/beskar/services/notifications.rb +46 -0
- data/lib/beskar/services/rate_limiter.rb +99 -125
- data/lib/beskar/services/request_context.rb +64 -0
- data/lib/beskar/services/risk_assessment.rb +58 -0
- data/lib/beskar/services/session_revocation.rb +62 -0
- data/lib/beskar/services/waf.rb +311 -198
- data/lib/beskar/services/waf_request.rb +60 -0
- data/lib/beskar/version.rb +1 -1
- data/lib/beskar/warden_authentication.rb +53 -0
- data/lib/beskar.rb +54 -4
- data/lib/generators/beskar/install/install_generator.rb +158 -0
- data/lib/generators/beskar/install/templates/initializer.rb.tt +261 -0
- data/lib/tasks/beskar_tasks.rake +25 -20
- metadata +93 -12
- data/lib/beskar/templates/beskar_initializer.rb +0 -107
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<% content_for :page_title, "New IP Ban" %>
|
|
2
|
+
|
|
3
|
+
<% content_for :header_actions do %>
|
|
4
|
+
<%= link_to "← Cancel", banned_ips_path, class: "btn btn-secondary" %>
|
|
5
|
+
<% end %>
|
|
6
|
+
|
|
7
|
+
<div class="card">
|
|
8
|
+
<div class="card-header">
|
|
9
|
+
<h3 class="card-title">Ban IP Address</h3>
|
|
10
|
+
<div class="card-subtitle">Add a new IP address to the ban list</div>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="card-body">
|
|
13
|
+
<%= form_with model: @banned_ip, url: banned_ips_path, local: true, data: { beskar_ban_form: true } do |f| %>
|
|
14
|
+
<% if @banned_ip.errors.any? %>
|
|
15
|
+
<div class="alert alert-danger">
|
|
16
|
+
<h4 style="font-size: 14px; font-weight: 600; margin-bottom: 0.5rem;">
|
|
17
|
+
<%= pluralize(@banned_ip.errors.count, "error") %> prohibited this ban from being saved:
|
|
18
|
+
</h4>
|
|
19
|
+
<ul style="margin: 0; padding-left: 1.5rem;">
|
|
20
|
+
<% @banned_ip.errors.full_messages.each do |message| %>
|
|
21
|
+
<li style="font-size: 13px;"><%= message %></li>
|
|
22
|
+
<% end %>
|
|
23
|
+
</ul>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
26
|
+
|
|
27
|
+
<div style="display: grid; grid-template-columns: 1fr; gap: 1.5rem; max-width: 600px;">
|
|
28
|
+
<!-- IP Address -->
|
|
29
|
+
<div class="form-group">
|
|
30
|
+
<%= f.label :ip_address, "IP Address *" %>
|
|
31
|
+
<%= f.text_field :ip_address,
|
|
32
|
+
value: @suggested_ip || @banned_ip.ip_address,
|
|
33
|
+
placeholder: "e.g., 192.168.1.1",
|
|
34
|
+
required: true,
|
|
35
|
+
style: "font-family: monospace;" %>
|
|
36
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
37
|
+
Enter the IP address to ban. IPv4 and IPv6 formats are supported.
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- Reason -->
|
|
42
|
+
<div class="form-group">
|
|
43
|
+
<%= f.label :reason, "Ban Reason *" %>
|
|
44
|
+
<%= f.select :reason,
|
|
45
|
+
options_for_select(ban_reason_options, @suggested_reason || @banned_ip.reason),
|
|
46
|
+
{ prompt: 'Select a reason...' },
|
|
47
|
+
{ required: true } %>
|
|
48
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
49
|
+
Select the primary reason for banning this IP address.
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Ban Duration -->
|
|
54
|
+
<div class="form-group">
|
|
55
|
+
<label>Ban Duration *</label>
|
|
56
|
+
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
57
|
+
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
58
|
+
<%= radio_button_tag :ban_type, 'temporary', !@banned_ip.permanent? %>
|
|
59
|
+
<span style="font-weight: normal;">Temporary Ban</span>
|
|
60
|
+
</label>
|
|
61
|
+
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
62
|
+
<%= radio_button_tag :ban_type, 'permanent', @banned_ip.permanent? %>
|
|
63
|
+
<span style="font-weight: normal; color: #D32F2F;">Permanent Ban</span>
|
|
64
|
+
</label>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Temporary Ban Options -->
|
|
69
|
+
<div id="temporary-options">
|
|
70
|
+
<div class="form-group">
|
|
71
|
+
<label>Duration</label>
|
|
72
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem;">
|
|
73
|
+
<% [
|
|
74
|
+
['1 Hour', 1.hour],
|
|
75
|
+
['6 Hours', 6.hours],
|
|
76
|
+
['24 Hours', 24.hours],
|
|
77
|
+
['7 Days', 7.days],
|
|
78
|
+
['30 Days', 30.days],
|
|
79
|
+
['90 Days', 90.days]
|
|
80
|
+
].each do |label, duration| %>
|
|
81
|
+
<label style="display: flex; align-items: center; padding: 0.75rem; background: #FAFBFC; border: 1px solid #E3E8EE; border-radius: 6px; cursor: pointer;">
|
|
82
|
+
<%= radio_button_tag :duration, duration.to_i, params[:duration].to_s == duration.to_i.to_s %>
|
|
83
|
+
<span style="margin-left: 0.5rem; font-size: 13px; font-weight: normal;"><%= label %></span>
|
|
84
|
+
</label>
|
|
85
|
+
<% end %>
|
|
86
|
+
</div>
|
|
87
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.5rem;">
|
|
88
|
+
Or specify a custom expiry in UTC. Without either, the server applies 24 hours:
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="form-group">
|
|
93
|
+
<%= f.label :expires_at, "Custom Expiry Date/Time (UTC)" %>
|
|
94
|
+
<%= f.datetime_field :expires_at,
|
|
95
|
+
value: (params.dig(:banned_ip, :expires_at).present? ? ban_expiry_input_value(@banned_ip.expires_at) : ""),
|
|
96
|
+
step: "0.001" %>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<!-- Details -->
|
|
101
|
+
<div class="form-group">
|
|
102
|
+
<%= f.label :details, "Additional Details" %>
|
|
103
|
+
<%= f.text_area :details,
|
|
104
|
+
rows: 3,
|
|
105
|
+
placeholder: "Provide any additional context or details about this ban..." %>
|
|
106
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
107
|
+
Optional. Add any relevant information about why this IP was banned.
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<!-- Violation Count -->
|
|
112
|
+
<div class="form-group">
|
|
113
|
+
<%= f.label :violation_count, "Initial Violation Count" %>
|
|
114
|
+
<%= f.number_field :violation_count,
|
|
115
|
+
value: @banned_ip.violation_count || 1,
|
|
116
|
+
min: 1 %>
|
|
117
|
+
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
118
|
+
Set the initial violation count. This affects ban escalation for repeat offenders.
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<hr style="border: none; border-top: 1px solid #E3E8EE; margin: 1.5rem 0;">
|
|
123
|
+
|
|
124
|
+
<!-- Related Security Event (if coming from an event) -->
|
|
125
|
+
<% if params[:event_id].present? %>
|
|
126
|
+
<div class="alert alert-info">
|
|
127
|
+
<strong>Related Event:</strong> This ban is being created in response to security event #<%= params[:event_id] %>.
|
|
128
|
+
<%= link_to "View Event", security_event_path(params[:event_id]), target: "_blank" %>
|
|
129
|
+
</div>
|
|
130
|
+
<% end %>
|
|
131
|
+
|
|
132
|
+
<div class="form-group">
|
|
133
|
+
<%= label_tag :audit_reason, "Administrative reason *" %>
|
|
134
|
+
<%= text_area_tag :audit_reason, params[:audit_reason], required: true, maxlength: 1000, rows: 2 %>
|
|
135
|
+
<p>Use a case reference or brief explanation, without credentials or other secrets.</p>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- Submit Buttons -->
|
|
139
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
140
|
+
<%= f.submit "Ban IP Address", class: "btn btn-danger" %>
|
|
141
|
+
<%= link_to "Cancel", banned_ips_path, class: "btn btn-secondary" %>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
<% end %>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<!-- Preview Card -->
|
|
149
|
+
<div class="card" id="ban-preview" hidden>
|
|
150
|
+
<div class="card-header">
|
|
151
|
+
<h3 class="card-title">Ban Preview</h3>
|
|
152
|
+
<div class="card-subtitle">This is how the ban will appear</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="card-body">
|
|
155
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
156
|
+
<div>
|
|
157
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386;">
|
|
158
|
+
IP Address
|
|
159
|
+
</div>
|
|
160
|
+
<div style="font-family: monospace; font-size: 14px; color: #1A1F36; margin-top: 0.25rem;" id="preview-ip">
|
|
161
|
+
-
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div>
|
|
165
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386;">
|
|
166
|
+
Reason
|
|
167
|
+
</div>
|
|
168
|
+
<div style="font-size: 14px; color: #1A1F36; margin-top: 0.25rem;" id="preview-reason">
|
|
169
|
+
-
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<div>
|
|
173
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386;">
|
|
174
|
+
Duration
|
|
175
|
+
</div>
|
|
176
|
+
<div style="font-size: 14px; color: #1A1F36; margin-top: 0.25rem;" id="preview-duration">
|
|
177
|
+
-
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
<div>
|
|
181
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386;">
|
|
182
|
+
Status
|
|
183
|
+
</div>
|
|
184
|
+
<div style="margin-top: 0.25rem;">
|
|
185
|
+
<span class="badge badge-danger" id="preview-status">Active</span>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<% content_for :page_title, "Review ban action" %>
|
|
2
|
+
<div class="card">
|
|
3
|
+
<div class="card-header"><h3 class="card-title"><%= @operation.titleize %> <%= @banned_ip.ip_address %></h3></div>
|
|
4
|
+
<div class="card-body">
|
|
5
|
+
<p>This change will be recorded with your administrative identity and its previous and resulting state.</p>
|
|
6
|
+
<%= form_with url: (@operation == "unban" ? banned_ip_path(@banned_ip) : extend_banned_ip_path(@banned_ip)),
|
|
7
|
+
method: (@operation == "unban" ? :delete : :post), local: true do %>
|
|
8
|
+
<% if @operation == "extend" %>
|
|
9
|
+
<div class="form-group">
|
|
10
|
+
<%= label_tag :duration, "Extension" %>
|
|
11
|
+
<%= select_tag :duration, options_for_select([['1 hour', '1h'], ['6 hours', '6h'], ['24 hours', '24h'],
|
|
12
|
+
['7 days', '7d'], ['30 days', '30d'], ['Make permanent', 'permanent']], params[:duration] || '24h') %>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
<div class="form-group">
|
|
16
|
+
<%= label_tag :audit_reason, "Reason for this administrative action" %>
|
|
17
|
+
<%= text_area_tag :audit_reason, nil, required: true, maxlength: 1000, rows: 3 %>
|
|
18
|
+
<p>Use a case reference or brief explanation. Do not include credentials or other secrets.</p>
|
|
19
|
+
</div>
|
|
20
|
+
<%= submit_tag "Confirm #{@operation}", class: "btn btn-danger" %>
|
|
21
|
+
<%= link_to "Cancel", banned_ip_path(@banned_ip), class: "btn btn-secondary" %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<% content_for :page_title, "Banned IP: #{@banned_ip.ip_address}" %>
|
|
2
|
+
|
|
3
|
+
<% content_for :header_actions do %>
|
|
4
|
+
<%= link_to "← Back to Banned IPs", banned_ips_path, class: "btn btn-secondary" %>
|
|
5
|
+
<%= link_to "Edit", edit_banned_ip_path(@banned_ip), class: "btn btn-secondary" %>
|
|
6
|
+
<%= link_to "Action History", administrative_actions_path(target_id: @banned_ip.id), class: "btn btn-secondary" %>
|
|
7
|
+
<%= link_to "Unban", review_banned_ip_path(@banned_ip, operation: "unban"), class: "btn btn-danger" %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<!-- Ban Overview -->
|
|
11
|
+
<div class="card">
|
|
12
|
+
<div class="card-header">
|
|
13
|
+
<h3 class="card-title">Ban Details</h3>
|
|
14
|
+
<div class="card-subtitle">IP Address: <%= @banned_ip.ip_address %></div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="card-body">
|
|
17
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem;">
|
|
18
|
+
<!-- IP Address -->
|
|
19
|
+
<div>
|
|
20
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
21
|
+
IP Address
|
|
22
|
+
</div>
|
|
23
|
+
<div style="font-family: monospace; font-size: 18px; font-weight: 500; color: #1A1F36;">
|
|
24
|
+
<%= @banned_ip.ip_address %>
|
|
25
|
+
</div>
|
|
26
|
+
<% if @banned_ip.metadata&.dig("geolocation") %>
|
|
27
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.25rem;">
|
|
28
|
+
<%= [@banned_ip.metadata.dig("geolocation", "city"),
|
|
29
|
+
@banned_ip.metadata.dig("geolocation", "region"),
|
|
30
|
+
@banned_ip.metadata.dig("geolocation", "country")].compact.join(", ") %>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Status -->
|
|
36
|
+
<div>
|
|
37
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
38
|
+
Status
|
|
39
|
+
</div>
|
|
40
|
+
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
|
41
|
+
<% if @banned_ip.permanent? %>
|
|
42
|
+
<span class="badge badge-critical">Permanent Ban</span>
|
|
43
|
+
<% elsif @banned_ip.active? %>
|
|
44
|
+
<span class="badge badge-danger">Active</span>
|
|
45
|
+
<% else %>
|
|
46
|
+
<span class="badge badge-neutral">Expired</span>
|
|
47
|
+
<% end %>
|
|
48
|
+
</div>
|
|
49
|
+
<% if @banned_ip.active? && !@banned_ip.permanent? && @banned_ip.expires_at %>
|
|
50
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.5rem;">
|
|
51
|
+
Expires in <%= distance_of_time_in_words(Time.current, @banned_ip.expires_at) %>
|
|
52
|
+
</div>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- Reason -->
|
|
57
|
+
<div>
|
|
58
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
59
|
+
Ban Reason
|
|
60
|
+
</div>
|
|
61
|
+
<div style="font-size: 14px; color: #1A1F36;">
|
|
62
|
+
<%= @banned_ip.reason.humanize %>
|
|
63
|
+
</div>
|
|
64
|
+
<% if @banned_ip.details.present? %>
|
|
65
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.25rem;">
|
|
66
|
+
<%= @banned_ip.details %>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Banned At -->
|
|
72
|
+
<div>
|
|
73
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
74
|
+
Banned At
|
|
75
|
+
</div>
|
|
76
|
+
<div style="font-size: 14px; color: #1A1F36;">
|
|
77
|
+
<%= @banned_ip.banned_at.strftime("%B %d, %Y") %>
|
|
78
|
+
</div>
|
|
79
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.25rem;">
|
|
80
|
+
<%= @banned_ip.banned_at.strftime("%H:%M:%S %Z") %>
|
|
81
|
+
(<%= time_ago_in_words(@banned_ip.banned_at) %> ago)
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- Expires At -->
|
|
86
|
+
<div>
|
|
87
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
88
|
+
Expires At
|
|
89
|
+
</div>
|
|
90
|
+
<% if @banned_ip.permanent? %>
|
|
91
|
+
<div style="font-size: 14px; color: #D32F2F; font-weight: 500;">
|
|
92
|
+
Never (Permanent Ban)
|
|
93
|
+
</div>
|
|
94
|
+
<% elsif @banned_ip.expires_at %>
|
|
95
|
+
<div style="font-size: 14px; color: #1A1F36;">
|
|
96
|
+
<%= @banned_ip.expires_at.strftime("%B %d, %Y") %>
|
|
97
|
+
</div>
|
|
98
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.25rem;">
|
|
99
|
+
<%= @banned_ip.expires_at.strftime("%H:%M:%S %Z") %>
|
|
100
|
+
</div>
|
|
101
|
+
<% else %>
|
|
102
|
+
<div style="color: #C1C7D0;">Not set</div>
|
|
103
|
+
<% end %>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- Violation Count -->
|
|
107
|
+
<div>
|
|
108
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
109
|
+
Violation Count
|
|
110
|
+
</div>
|
|
111
|
+
<div style="font-size: 24px; font-weight: 600; color: <%= @banned_ip.violation_count > 3 ? '#D32F2F' : '#1A1F36' %>;">
|
|
112
|
+
<%= @banned_ip.violation_count %>
|
|
113
|
+
</div>
|
|
114
|
+
<% if @banned_ip.violation_count > 0 %>
|
|
115
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.25rem;">
|
|
116
|
+
<%= @banned_ip.violation_count == 1 ? 'violation' : 'violations' %> recorded
|
|
117
|
+
</div>
|
|
118
|
+
<% end %>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Statistics -->
|
|
125
|
+
<div class="card">
|
|
126
|
+
<div class="card-header">
|
|
127
|
+
<h3 class="card-title">IP Statistics</h3>
|
|
128
|
+
<div class="card-subtitle">Activity summary for <%= @banned_ip.ip_address %></div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="card-body">
|
|
131
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem;">
|
|
132
|
+
<div style="text-align: center; padding: 1rem; background: #FAFBFC; border-radius: 6px;">
|
|
133
|
+
<div style="font-size: 24px; font-weight: 600; color: #1A1F36;">
|
|
134
|
+
<%= @stats[:total_events] %>
|
|
135
|
+
</div>
|
|
136
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-top: 0.25rem;">
|
|
137
|
+
Total Events
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div style="text-align: center; padding: 1rem; background: #FAFBFC; border-radius: 6px;">
|
|
142
|
+
<div style="font-size: 24px; font-weight: 600; color: <%= risk_level_color(@stats[:avg_risk_score]) %>;">
|
|
143
|
+
<%= @stats[:avg_risk_score] %>
|
|
144
|
+
</div>
|
|
145
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-top: 0.25rem;">
|
|
146
|
+
Avg Risk Score
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div style="text-align: center; padding: 1rem; background: #FAFBFC; border-radius: 6px;">
|
|
151
|
+
<div style="font-size: 24px; font-weight: 600; color: <%= risk_level_color(@stats[:max_risk_score]) %>;">
|
|
152
|
+
<%= @stats[:max_risk_score] %>
|
|
153
|
+
</div>
|
|
154
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-top: 0.25rem;">
|
|
155
|
+
Max Risk Score
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
<% if @stats[:first_seen] %>
|
|
160
|
+
<div style="text-align: center; padding: 1rem; background: #FAFBFC; border-radius: 6px;">
|
|
161
|
+
<div style="font-size: 13px; font-weight: 500; color: #1A1F36;">
|
|
162
|
+
<%= @stats[:first_seen].strftime("%Y-%m-%d") %>
|
|
163
|
+
</div>
|
|
164
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-top: 0.25rem;">
|
|
165
|
+
First Seen
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
<% end %>
|
|
169
|
+
|
|
170
|
+
<% if @stats[:last_seen] %>
|
|
171
|
+
<div style="text-align: center; padding: 1rem; background: #FAFBFC; border-radius: 6px;">
|
|
172
|
+
<div style="font-size: 13px; font-weight: 500; color: #1A1F36;">
|
|
173
|
+
<%= @stats[:last_seen].strftime("%Y-%m-%d") %>
|
|
174
|
+
</div>
|
|
175
|
+
<div style="font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-top: 0.25rem;">
|
|
176
|
+
Last Seen
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<% end %>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<!-- Metadata -->
|
|
185
|
+
<% if @banned_ip.metadata.present? && @banned_ip.metadata.any? %>
|
|
186
|
+
<div class="card">
|
|
187
|
+
<div class="card-header">
|
|
188
|
+
<h3 class="card-title">Additional Metadata</h3>
|
|
189
|
+
</div>
|
|
190
|
+
<div class="card-body">
|
|
191
|
+
<pre style="font-family: monospace; font-size: 12px; background: #F4F5F7; padding: 1rem; border-radius: 6px; overflow-x: auto;"><%= JSON.pretty_generate(@banned_ip.metadata) %></pre>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
<% end %>
|
|
195
|
+
|
|
196
|
+
<!-- Related Security Events -->
|
|
197
|
+
<% if @related_events.any? %>
|
|
198
|
+
<div class="card">
|
|
199
|
+
<div class="card-header">
|
|
200
|
+
<h3 class="card-title">Related Security Events</h3>
|
|
201
|
+
<div class="card-subtitle"><%= @related_events.count %> events from this IP address</div>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="card-body" style="padding: 0;">
|
|
204
|
+
<div class="table-container">
|
|
205
|
+
<table>
|
|
206
|
+
<thead>
|
|
207
|
+
<tr>
|
|
208
|
+
<th>Time</th>
|
|
209
|
+
<th>Event Type</th>
|
|
210
|
+
<th>User</th>
|
|
211
|
+
<th>Risk Score</th>
|
|
212
|
+
<th>Details</th>
|
|
213
|
+
<th>Actions</th>
|
|
214
|
+
</tr>
|
|
215
|
+
</thead>
|
|
216
|
+
<tbody>
|
|
217
|
+
<% @related_events.each do |event| %>
|
|
218
|
+
<tr>
|
|
219
|
+
<td>
|
|
220
|
+
<div style="font-size: 13px;">
|
|
221
|
+
<%= event.created_at.strftime("%Y-%m-%d") %>
|
|
222
|
+
</div>
|
|
223
|
+
<div style="font-size: 11px; color: #697386;">
|
|
224
|
+
<%= event.created_at.strftime("%H:%M:%S") %>
|
|
225
|
+
</div>
|
|
226
|
+
</td>
|
|
227
|
+
<td>
|
|
228
|
+
<div style="font-size: 13px;">
|
|
229
|
+
<%= format_event_type(event.event_type) %>
|
|
230
|
+
</div>
|
|
231
|
+
<div style="font-size: 11px; color: #697386;">
|
|
232
|
+
<%= event.event_type %>
|
|
233
|
+
</div>
|
|
234
|
+
</td>
|
|
235
|
+
<td>
|
|
236
|
+
<% if event.user %>
|
|
237
|
+
<div style="font-size: 13px;">
|
|
238
|
+
<%= audit_user_label(event) %>
|
|
239
|
+
</div>
|
|
240
|
+
<% elsif event.attempted_email %>
|
|
241
|
+
<div style="font-size: 13px; color: #697386;">
|
|
242
|
+
<%= audit_user_label(event) %>
|
|
243
|
+
</div>
|
|
244
|
+
<% else %>
|
|
245
|
+
<span style="color: #C1C7D0;">-</span>
|
|
246
|
+
<% end %>
|
|
247
|
+
</td>
|
|
248
|
+
<td>
|
|
249
|
+
<span class="badge badge-<%= risk_level_class(event.risk_score) %>">
|
|
250
|
+
<%= event.risk_score %>
|
|
251
|
+
</span>
|
|
252
|
+
</td>
|
|
253
|
+
<td>
|
|
254
|
+
<div style="font-size: 12px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
|
255
|
+
<%= event.details || event.metadata&.dig("message") || "-" %>
|
|
256
|
+
</div>
|
|
257
|
+
</td>
|
|
258
|
+
<td>
|
|
259
|
+
<%= link_to "View", security_event_path(event), class: "btn btn-sm btn-secondary" %>
|
|
260
|
+
</td>
|
|
261
|
+
</tr>
|
|
262
|
+
<% end %>
|
|
263
|
+
</tbody>
|
|
264
|
+
</table>
|
|
265
|
+
</div>
|
|
266
|
+
<% if @related_events.count >= 20 %>
|
|
267
|
+
<div style="padding: 1rem; text-align: center; border-top: 1px solid #E3E8EE;">
|
|
268
|
+
<%= link_to "View All Events from this IP →", security_events_path(ip_address: @banned_ip.ip_address),
|
|
269
|
+
style: "color: var(--primary); text-decoration: none; font-size: 13px; font-weight: 500;" %>
|
|
270
|
+
</div>
|
|
271
|
+
<% end %>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
<% end %>
|
|
275
|
+
|
|
276
|
+
<!-- Actions -->
|
|
277
|
+
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
|
|
278
|
+
<%= link_to "← Back to Banned IPs", banned_ips_path, class: "btn btn-secondary" %>
|
|
279
|
+
<%= link_to "Edit Ban", edit_banned_ip_path(@banned_ip), class: "btn btn-secondary" %>
|
|
280
|
+
|
|
281
|
+
<% if @banned_ip.active? && !@banned_ip.permanent? %>
|
|
282
|
+
<div style="display: flex; gap: 0.5rem; margin-left: auto;">
|
|
283
|
+
<%= form_with url: extend_banned_ip_path(@banned_ip), method: :post, local: true, style: "display: flex; gap: 0.5rem;" do |f| %>
|
|
284
|
+
<%= select_tag :duration,
|
|
285
|
+
options_for_select([
|
|
286
|
+
['Extend 1 hour', '1h'],
|
|
287
|
+
['Extend 6 hours', '6h'],
|
|
288
|
+
['Extend 24 hours', '24h'],
|
|
289
|
+
['Extend 7 days', '7d'],
|
|
290
|
+
['Extend 30 days', '30d'],
|
|
291
|
+
['Make Permanent', 'permanent']
|
|
292
|
+
]),
|
|
293
|
+
style: "padding: 0.5rem 0.75rem; font-size: 14px;" %>
|
|
294
|
+
<%= label_tag :audit_reason, "Administrative reason" %>
|
|
295
|
+
<%= text_field_tag :audit_reason, nil, required: true, maxlength: 1000 %>
|
|
296
|
+
<%= submit_tag "Extend Ban", class: "btn btn-warning" %>
|
|
297
|
+
<% end %>
|
|
298
|
+
</div>
|
|
299
|
+
<% end %>
|
|
300
|
+
|
|
301
|
+
<%= link_to "Unban IP", review_banned_ip_path(@banned_ip, operation: "unban"),
|
|
302
|
+
class: "btn btn-danger",
|
|
303
|
+
style: "margin-left: #{ @banned_ip.active? && !@banned_ip.permanent? ? '0' : 'auto' };" %>
|
|
304
|
+
</div>
|