beskar 0.1.0 → 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 +150 -19
- data/README.md +142 -122
- 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 +69 -25
- data/app/controllers/beskar/banned_ips_controller.rb +116 -141
- data/app/controllers/beskar/dashboard_controller.rb +20 -28
- data/app/controllers/beskar/security_events_controller.rb +37 -55
- 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 +91 -132
- data/app/models/beskar/security_event.rb +37 -4
- data/app/models/beskar/security_state.rb +58 -0
- data/app/services/beskar/banned_ip_manager.rb +16 -6
- 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 +25 -89
- data/app/views/beskar/banned_ips/index.html.erb +20 -62
- data/app/views/beskar/banned_ips/new.html.erb +18 -138
- data/app/views/beskar/banned_ips/review.html.erb +24 -0
- data/app/views/beskar/banned_ips/show.html.erb +9 -15
- data/app/views/beskar/dashboard/index.html.erb +4 -4
- data/app/views/beskar/security_events/index.html.erb +8 -15
- data/app/views/beskar/security_events/show.html.erb +6 -20
- 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 +9 -76
- data/config/routes.rb +10 -21
- 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 +84 -13
- 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 +30 -35
- data/lib/beskar/middleware/request_analyzer.rb +41 -82
- data/lib/beskar/models/security_trackable_authenticable.rb +72 -93
- data/lib/beskar/models/security_trackable_devise.rb +32 -23
- data/lib/beskar/models/security_trackable_generic.rb +169 -212
- data/lib/beskar/risk_level.rb +22 -0
- data/lib/beskar/services/account_locker.rb +85 -76
- 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 +164 -280
- 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 +53 -4
- data/lib/generators/beskar/install/install_generator.rb +36 -36
- data/lib/generators/beskar/install/templates/initializer.rb.tt +104 -20
- data/lib/tasks/beskar_tasks.rake +15 -19
- metadata +60 -8
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
<div class="card-subtitle">Modify ban settings for <%= @banned_ip.ip_address %></div>
|
|
11
11
|
</div>
|
|
12
12
|
<div class="card-body">
|
|
13
|
-
<%= form_with model: @banned_ip, url: banned_ip_path(@banned_ip), method: :patch, local: true do |f| %>
|
|
13
|
+
<%= form_with model: @banned_ip, url: banned_ip_path(@banned_ip), method: :patch, local: true, data: { beskar_ban_form: true } do |f| %>
|
|
14
|
+
<%= hidden_field_tag :expiry_precision, "milliseconds" %>
|
|
14
15
|
<% if @banned_ip.errors.any? %>
|
|
15
16
|
<div class="alert alert-danger">
|
|
16
17
|
<h4 style="font-size: 14px; font-weight: 600; margin-bottom: 0.5rem;">
|
|
@@ -47,11 +48,13 @@
|
|
|
47
48
|
<span style="font-size: 13px; color: #697386;">
|
|
48
49
|
Expires in <%= distance_of_time_in_words(Time.current, @banned_ip.expires_at) %>
|
|
49
50
|
</span>
|
|
50
|
-
<%
|
|
51
|
+
<% elsif @banned_ip.expires_at %>
|
|
51
52
|
<span class="badge badge-neutral">Expired</span>
|
|
52
53
|
<span style="font-size: 13px; color: #697386;">
|
|
53
54
|
Expired <%= time_ago_in_words(@banned_ip.expires_at) %> ago
|
|
54
55
|
</span>
|
|
56
|
+
<% else %>
|
|
57
|
+
<span class="badge badge-warning">Expiry required for a temporary ban</span>
|
|
55
58
|
<% end %>
|
|
56
59
|
</div>
|
|
57
60
|
</div>
|
|
@@ -60,15 +63,7 @@
|
|
|
60
63
|
<div class="form-group">
|
|
61
64
|
<%= f.label :reason, "Ban Reason *" %>
|
|
62
65
|
<%= f.select :reason,
|
|
63
|
-
options_for_select(
|
|
64
|
-
['Rate Limit Abuse', 'rate_limit_abuse'],
|
|
65
|
-
['Authentication Abuse', 'authentication_abuse'],
|
|
66
|
-
['WAF Violation', 'waf_violation'],
|
|
67
|
-
['Brute Force Attack', 'brute_force_attack'],
|
|
68
|
-
['Suspicious Activity', 'suspicious_activity'],
|
|
69
|
-
['Manual Ban', 'manual_ban'],
|
|
70
|
-
['Other', 'other']
|
|
71
|
-
], @banned_ip.reason),
|
|
66
|
+
options_for_select(ban_reason_options, @banned_ip.reason),
|
|
72
67
|
{ prompt: 'Select a reason...' },
|
|
73
68
|
{ required: true } %>
|
|
74
69
|
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
@@ -82,13 +77,11 @@
|
|
|
82
77
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
83
78
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
84
79
|
<%= f.radio_button :permanent, false,
|
|
85
|
-
onchange: "toggleDurationFields()",
|
|
86
80
|
checked: !@banned_ip.permanent? %>
|
|
87
81
|
<span style="font-weight: normal;">Temporary Ban</span>
|
|
88
82
|
</label>
|
|
89
83
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
90
84
|
<%= f.radio_button :permanent, true,
|
|
91
|
-
onchange: "toggleDurationFields()",
|
|
92
85
|
checked: @banned_ip.permanent? %>
|
|
93
86
|
<span style="font-weight: normal; color: #D32F2F;">Permanent Ban</span>
|
|
94
87
|
</label>
|
|
@@ -101,27 +94,28 @@
|
|
|
101
94
|
</div>
|
|
102
95
|
|
|
103
96
|
<!-- Temporary Ban Options -->
|
|
104
|
-
<div id="temporary-options"
|
|
97
|
+
<div id="temporary-options">
|
|
105
98
|
<div class="form-group">
|
|
106
|
-
<%= f.label :expires_at, "Expiry Date/Time" %>
|
|
99
|
+
<%= f.label :expires_at, "Expiry Date/Time (UTC)" %>
|
|
107
100
|
<%= f.datetime_field :expires_at,
|
|
108
|
-
value: @banned_ip.expires_at
|
|
109
|
-
|
|
101
|
+
value: ban_expiry_input_value(@banned_ip.expires_at),
|
|
102
|
+
step: "0.001" %>
|
|
110
103
|
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
111
|
-
|
|
104
|
+
Enter the expiry in UTC, not your browser timezone. Temporary bans require an expiry.
|
|
112
105
|
</div>
|
|
113
106
|
</div>
|
|
114
107
|
|
|
115
108
|
<!-- Quick Extension Options -->
|
|
116
109
|
<div class="form-group">
|
|
117
110
|
<label>Quick Extension</label>
|
|
111
|
+
<noscript><p>Quick buttons require JavaScript. You can enter the UTC expiry directly or select Permanent Ban above.</p></noscript>
|
|
118
112
|
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem;">
|
|
119
|
-
<button type="button"
|
|
120
|
-
<button type="button"
|
|
121
|
-
<button type="button"
|
|
122
|
-
<button type="button"
|
|
123
|
-
<button type="button"
|
|
124
|
-
<button type="button"
|
|
113
|
+
<button type="button" data-beskar-extend-hours="1" class="btn btn-sm btn-secondary">+1 Hour</button>
|
|
114
|
+
<button type="button" data-beskar-extend-hours="24" class="btn btn-sm btn-secondary">+1 Day</button>
|
|
115
|
+
<button type="button" data-beskar-extend-hours="168" class="btn btn-sm btn-secondary">+7 Days</button>
|
|
116
|
+
<button type="button" data-beskar-extend-hours="720" class="btn btn-sm btn-secondary">+30 Days</button>
|
|
117
|
+
<button type="button" data-beskar-extend-hours="2160" class="btn btn-sm btn-secondary">+90 Days</button>
|
|
118
|
+
<button type="button" data-beskar-make-permanent class="btn btn-sm btn-danger">Make Permanent</button>
|
|
125
119
|
</div>
|
|
126
120
|
</div>
|
|
127
121
|
</div>
|
|
@@ -140,7 +134,7 @@
|
|
|
140
134
|
<!-- Violation Count -->
|
|
141
135
|
<div class="form-group">
|
|
142
136
|
<%= f.label :violation_count, "Violation Count" %>
|
|
143
|
-
<%= f.number_field :violation_count, min:
|
|
137
|
+
<%= f.number_field :violation_count, min: 1 %>
|
|
144
138
|
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
145
139
|
Current violation count: <%= @banned_ip.violation_count %>. Higher counts may result in longer bans for repeat offenders.
|
|
146
140
|
</div>
|
|
@@ -183,77 +177,19 @@
|
|
|
183
177
|
</div>
|
|
184
178
|
</div>
|
|
185
179
|
|
|
180
|
+
<div class="form-group">
|
|
181
|
+
<%= label_tag :audit_reason, "Administrative reason *" %>
|
|
182
|
+
<%= text_area_tag :audit_reason, params[:audit_reason], required: true, maxlength: 1000, rows: 2 %>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
186
185
|
<!-- Submit Buttons -->
|
|
187
186
|
<div style="display: flex; gap: 0.5rem;">
|
|
188
187
|
<%= f.submit "Update Ban", class: "btn btn-primary" %>
|
|
189
188
|
<%= link_to "Cancel", banned_ip_path(@banned_ip), class: "btn btn-secondary" %>
|
|
190
|
-
<%= link_to "Delete Ban",
|
|
191
|
-
data: {
|
|
192
|
-
"turbo-method": "delete",
|
|
193
|
-
"turbo-confirm": "Are you sure you want to unban #{@banned_ip.ip_address}?"
|
|
194
|
-
},
|
|
189
|
+
<%= link_to "Delete Ban", review_banned_ip_path(@banned_ip, operation: "unban"),
|
|
195
190
|
class: "btn btn-danger" %>
|
|
196
191
|
</div>
|
|
197
192
|
</div>
|
|
198
193
|
<% end %>
|
|
199
194
|
</div>
|
|
200
195
|
</div>
|
|
201
|
-
|
|
202
|
-
<script>
|
|
203
|
-
function toggleDurationFields() {
|
|
204
|
-
const isPermanent = document.querySelector('input[name="banned_ip[permanent]"]:checked').value === 'true';
|
|
205
|
-
const temporaryOptions = document.getElementById('temporary-options');
|
|
206
|
-
|
|
207
|
-
if (isPermanent) {
|
|
208
|
-
temporaryOptions.style.display = 'none';
|
|
209
|
-
document.getElementById('banned_ip_expires_at').value = '';
|
|
210
|
-
} else {
|
|
211
|
-
temporaryOptions.style.display = 'block';
|
|
212
|
-
// If switching from permanent to temporary, set a default expiry
|
|
213
|
-
if (!document.getElementById('banned_ip_expires_at').value) {
|
|
214
|
-
const defaultExpiry = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours from now
|
|
215
|
-
document.getElementById('banned_ip_expires_at').value = defaultExpiry.toISOString().slice(0, 16);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function extendBan(hours) {
|
|
221
|
-
const expiresField = document.getElementById('banned_ip_expires_at');
|
|
222
|
-
let currentExpiry;
|
|
223
|
-
|
|
224
|
-
if (expiresField.value) {
|
|
225
|
-
currentExpiry = new Date(expiresField.value);
|
|
226
|
-
} else {
|
|
227
|
-
// Use current ban expiry or current time
|
|
228
|
-
<% if @banned_ip.expires_at %>
|
|
229
|
-
currentExpiry = new Date('<%= @banned_ip.expires_at.iso8601 %>');
|
|
230
|
-
<% else %>
|
|
231
|
-
currentExpiry = new Date();
|
|
232
|
-
<% end %>
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Add the specified hours
|
|
236
|
-
currentExpiry.setHours(currentExpiry.getHours() + hours);
|
|
237
|
-
|
|
238
|
-
// Update the field
|
|
239
|
-
expiresField.value = currentExpiry.toISOString().slice(0, 16);
|
|
240
|
-
|
|
241
|
-
// Show a visual feedback
|
|
242
|
-
const originalBg = expiresField.style.background;
|
|
243
|
-
expiresField.style.background = '#E8F5E9';
|
|
244
|
-
setTimeout(() => {
|
|
245
|
-
expiresField.style.background = originalBg;
|
|
246
|
-
}, 500);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function makePermanent() {
|
|
250
|
-
document.querySelector('input[name="banned_ip[permanent]"][value="true"]').checked = true;
|
|
251
|
-
toggleDurationFields();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Initialize on page load
|
|
255
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
256
|
-
// Set initial state
|
|
257
|
-
toggleDurationFields();
|
|
258
|
-
});
|
|
259
|
-
</script>
|
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
<% content_for :header_actions do %>
|
|
4
4
|
<div style="display: flex; gap: 0.5rem;">
|
|
5
5
|
<%= link_to "+ New Ban", new_banned_ip_path, class: "btn btn-primary" %>
|
|
6
|
-
<%=
|
|
7
|
-
class: "btn btn-secondary" %>
|
|
8
|
-
<%= link_to "Export JSON", export_banned_ips_path(format: :json, params: request.query_parameters),
|
|
9
|
-
class: "btn btn-secondary" %>
|
|
6
|
+
<%= render "beskar/shared/export_form", export_url: export_banned_ips_path %>
|
|
10
7
|
</div>
|
|
11
8
|
<% end %>
|
|
12
9
|
|
|
@@ -76,15 +73,16 @@
|
|
|
76
73
|
</div>
|
|
77
74
|
|
|
78
75
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
79
|
-
<label style="margin: 0; font-size: 13px; color: #697386;">Per page:</label>
|
|
76
|
+
<label for="per_page" style="margin: 0; font-size: 13px; color: #697386;">Per page:</label>
|
|
80
77
|
<%= form_with url: banned_ips_path, method: :get, local: true, style: "display: inline;" do |f| %>
|
|
81
|
-
<% request.query_parameters.except(
|
|
78
|
+
<% request.query_parameters.except("per_page", "page").each do |key, value| %>
|
|
82
79
|
<%= hidden_field_tag key, value %>
|
|
83
80
|
<% end %>
|
|
84
81
|
<%= select_tag :per_page,
|
|
85
82
|
options_for_select([10, 25, 50, 100], params[:per_page]&.to_i || 25),
|
|
86
|
-
|
|
83
|
+
data: { beskar_auto_submit: true },
|
|
87
84
|
style: "padding: 0.25rem 0.5rem; font-size: 13px;" %>
|
|
85
|
+
<%= submit_tag "Apply page size", class: "btn btn-sm btn-secondary" %>
|
|
88
86
|
<% end %>
|
|
89
87
|
</div>
|
|
90
88
|
</div>
|
|
@@ -92,7 +90,7 @@
|
|
|
92
90
|
<!-- Bulk Actions Form -->
|
|
93
91
|
<%= form_with url: bulk_action_banned_ips_path, method: :post, local: true, id: "bulk-actions-form" do |f| %>
|
|
94
92
|
<!-- Bulk Actions Bar -->
|
|
95
|
-
<div class="card" id="bulk-actions-bar" style="
|
|
93
|
+
<div class="card" id="bulk-actions-bar" style="margin-bottom: 1rem;">
|
|
96
94
|
<div class="card-body" style="padding: 0.75rem 1rem;">
|
|
97
95
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
98
96
|
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
@@ -100,13 +98,17 @@
|
|
|
100
98
|
<span id="selected-count">0</span> items selected
|
|
101
99
|
</span>
|
|
102
100
|
|
|
101
|
+
<div class="form-group">
|
|
102
|
+
<%= label_tag :audit_reason, "Administrative reason (all selected bans)" %>
|
|
103
|
+
<%= text_field_tag :audit_reason, nil, required: true, maxlength: 1000 %>
|
|
104
|
+
</div>
|
|
103
105
|
<div style="display: flex; gap: 0.5rem;">
|
|
104
|
-
<%=
|
|
106
|
+
<%= button_tag "Unban Selected", name: "bulk_action", value: "unban",
|
|
105
107
|
class: "btn btn-sm btn-secondary",
|
|
106
|
-
|
|
107
|
-
<%=
|
|
108
|
+
data: { beskar_confirm: "Unban all selected IPs?" } %>
|
|
109
|
+
<%= button_tag "Make Permanent", name: "bulk_action", value: "make_permanent",
|
|
108
110
|
class: "btn btn-sm btn-danger",
|
|
109
|
-
|
|
111
|
+
data: { beskar_confirm: "Make all selected bans permanent?" } %>
|
|
110
112
|
|
|
111
113
|
<div style="display: flex; align-items: center; gap: 0.25rem;">
|
|
112
114
|
<%= select_tag :duration,
|
|
@@ -116,13 +118,13 @@
|
|
|
116
118
|
['Extend by 30d', '30d']
|
|
117
119
|
]),
|
|
118
120
|
style: "padding: 0.25rem 0.5rem; font-size: 13px;" %>
|
|
119
|
-
<%=
|
|
121
|
+
<%= button_tag "Extend Selected", name: "bulk_action", value: "extend",
|
|
120
122
|
class: "btn btn-sm btn-warning" %>
|
|
121
123
|
</div>
|
|
122
124
|
</div>
|
|
123
125
|
</div>
|
|
124
126
|
|
|
125
|
-
<button type="button"
|
|
127
|
+
<button type="button" data-beskar-clear-selection class="btn btn-sm btn-secondary">
|
|
126
128
|
Clear Selection
|
|
127
129
|
</button>
|
|
128
130
|
</div>
|
|
@@ -137,7 +139,7 @@
|
|
|
137
139
|
<thead>
|
|
138
140
|
<tr>
|
|
139
141
|
<th style="width: 40px;">
|
|
140
|
-
<input type="checkbox" id="select-all"
|
|
142
|
+
<input type="checkbox" id="select-all" aria-label="Select all bans">
|
|
141
143
|
</th>
|
|
142
144
|
<th>IP Address</th>
|
|
143
145
|
<th>Reason</th>
|
|
@@ -154,8 +156,7 @@
|
|
|
154
156
|
<tr>
|
|
155
157
|
<td>
|
|
156
158
|
<%= check_box_tag "ip_ids[]", ban.id, false,
|
|
157
|
-
class: "ban-checkbox",
|
|
158
|
-
onchange: "updateBulkActions()" %>
|
|
159
|
+
class: "ban-checkbox", aria: { label: "Select ban #{ban.ip_address}" } %>
|
|
159
160
|
</td>
|
|
160
161
|
<td>
|
|
161
162
|
<div style="font-family: monospace; font-size: 14px; font-weight: 500;">
|
|
@@ -226,19 +227,11 @@
|
|
|
226
227
|
class: "btn btn-sm btn-secondary" %>
|
|
227
228
|
|
|
228
229
|
<% if ban.active? && !ban.permanent? %>
|
|
229
|
-
<%= link_to "Extend",
|
|
230
|
-
data: {
|
|
231
|
-
"turbo-method": "post",
|
|
232
|
-
"turbo-confirm": "Extend this ban by 24 hours?"
|
|
233
|
-
},
|
|
230
|
+
<%= link_to "Extend", review_banned_ip_path(ban, operation: 'extend', duration: '24h'),
|
|
234
231
|
class: "btn btn-sm btn-warning" %>
|
|
235
232
|
<% end %>
|
|
236
233
|
|
|
237
|
-
<%= link_to "Unban",
|
|
238
|
-
data: {
|
|
239
|
-
"turbo-method": "delete",
|
|
240
|
-
"turbo-confirm": "Are you sure you want to unban #{ban.ip_address}?"
|
|
241
|
-
},
|
|
234
|
+
<%= link_to "Unban", review_banned_ip_path(ban, operation: 'unban'),
|
|
242
235
|
class: "btn btn-sm btn-danger" %>
|
|
243
236
|
</div>
|
|
244
237
|
</td>
|
|
@@ -324,38 +317,3 @@
|
|
|
324
317
|
Total: <%= number_with_delimiter(@pagination[:total_count]) %> banned IPs
|
|
325
318
|
</div>
|
|
326
319
|
<% end %>
|
|
327
|
-
|
|
328
|
-
<script>
|
|
329
|
-
function toggleSelectAll(checkbox) {
|
|
330
|
-
const checkboxes = document.querySelectorAll('.ban-checkbox');
|
|
331
|
-
checkboxes.forEach(cb => {
|
|
332
|
-
cb.checked = checkbox.checked;
|
|
333
|
-
});
|
|
334
|
-
updateBulkActions();
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function updateBulkActions() {
|
|
338
|
-
const checkboxes = document.querySelectorAll('.ban-checkbox:checked');
|
|
339
|
-
const bulkBar = document.getElementById('bulk-actions-bar');
|
|
340
|
-
const selectedCount = document.getElementById('selected-count');
|
|
341
|
-
|
|
342
|
-
if (checkboxes.length > 0) {
|
|
343
|
-
bulkBar.style.display = 'block';
|
|
344
|
-
selectedCount.textContent = checkboxes.length;
|
|
345
|
-
} else {
|
|
346
|
-
bulkBar.style.display = 'none';
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Update select-all checkbox state
|
|
350
|
-
const selectAll = document.getElementById('select-all');
|
|
351
|
-
const allCheckboxes = document.querySelectorAll('.ban-checkbox');
|
|
352
|
-
if (allCheckboxes.length > 0) {
|
|
353
|
-
selectAll.checked = checkboxes.length === allCheckboxes.length;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function clearSelection() {
|
|
358
|
-
document.getElementById('select-all').checked = false;
|
|
359
|
-
toggleSelectAll(document.getElementById('select-all'));
|
|
360
|
-
}
|
|
361
|
-
</script>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<div class="card-subtitle">Add a new IP address to the ban list</div>
|
|
11
11
|
</div>
|
|
12
12
|
<div class="card-body">
|
|
13
|
-
<%= form_with model: @banned_ip, url: banned_ips_path, local: true do |f| %>
|
|
13
|
+
<%= form_with model: @banned_ip, url: banned_ips_path, local: true, data: { beskar_ban_form: true } do |f| %>
|
|
14
14
|
<% if @banned_ip.errors.any? %>
|
|
15
15
|
<div class="alert alert-danger">
|
|
16
16
|
<h4 style="font-size: 14px; font-weight: 600; margin-bottom: 0.5rem;">
|
|
@@ -42,15 +42,7 @@
|
|
|
42
42
|
<div class="form-group">
|
|
43
43
|
<%= f.label :reason, "Ban Reason *" %>
|
|
44
44
|
<%= f.select :reason,
|
|
45
|
-
options_for_select(
|
|
46
|
-
['Rate Limit Abuse', 'rate_limit_abuse'],
|
|
47
|
-
['Authentication Abuse', 'authentication_abuse'],
|
|
48
|
-
['WAF Violation', 'waf_violation'],
|
|
49
|
-
['Brute Force Attack', 'brute_force_attack'],
|
|
50
|
-
['Suspicious Activity', 'suspicious_activity'],
|
|
51
|
-
['Manual Ban', 'manual_ban'],
|
|
52
|
-
['Other', 'other']
|
|
53
|
-
], @suggested_reason || @banned_ip.reason),
|
|
45
|
+
options_for_select(ban_reason_options, @suggested_reason || @banned_ip.reason),
|
|
54
46
|
{ prompt: 'Select a reason...' },
|
|
55
47
|
{ required: true } %>
|
|
56
48
|
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
@@ -63,20 +55,18 @@
|
|
|
63
55
|
<label>Ban Duration *</label>
|
|
64
56
|
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
65
57
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
66
|
-
<%= radio_button_tag :ban_type, 'temporary', !@banned_ip.permanent
|
|
67
|
-
onchange: "toggleDurationFields()" %>
|
|
58
|
+
<%= radio_button_tag :ban_type, 'temporary', !@banned_ip.permanent? %>
|
|
68
59
|
<span style="font-weight: normal;">Temporary Ban</span>
|
|
69
60
|
</label>
|
|
70
61
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
|
71
|
-
<%= radio_button_tag :ban_type, 'permanent', @banned_ip.permanent
|
|
72
|
-
onchange: "toggleDurationFields()" %>
|
|
62
|
+
<%= radio_button_tag :ban_type, 'permanent', @banned_ip.permanent? %>
|
|
73
63
|
<span style="font-weight: normal; color: #D32F2F;">Permanent Ban</span>
|
|
74
64
|
</label>
|
|
75
65
|
</div>
|
|
76
66
|
</div>
|
|
77
67
|
|
|
78
68
|
<!-- Temporary Ban Options -->
|
|
79
|
-
<div id="temporary-options"
|
|
69
|
+
<div id="temporary-options">
|
|
80
70
|
<div class="form-group">
|
|
81
71
|
<label>Duration</label>
|
|
82
72
|
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem;">
|
|
@@ -89,21 +79,21 @@
|
|
|
89
79
|
['90 Days', 90.days]
|
|
90
80
|
].each do |label, duration| %>
|
|
91
81
|
<label style="display: flex; align-items: center; padding: 0.75rem; background: #FAFBFC; border: 1px solid #E3E8EE; border-radius: 6px; cursor: pointer;">
|
|
92
|
-
<%= radio_button_tag :duration, duration.to_i,
|
|
82
|
+
<%= radio_button_tag :duration, duration.to_i, params[:duration].to_s == duration.to_i.to_s %>
|
|
93
83
|
<span style="margin-left: 0.5rem; font-size: 13px; font-weight: normal;"><%= label %></span>
|
|
94
84
|
</label>
|
|
95
85
|
<% end %>
|
|
96
86
|
</div>
|
|
97
87
|
<div style="font-size: 12px; color: #697386; margin-top: 0.5rem;">
|
|
98
|
-
Or specify a custom expiry
|
|
88
|
+
Or specify a custom expiry in UTC. Without either, the server applies 24 hours:
|
|
99
89
|
</div>
|
|
100
90
|
</div>
|
|
101
91
|
|
|
102
92
|
<div class="form-group">
|
|
103
|
-
<%= f.label :expires_at, "Custom Expiry Date/Time" %>
|
|
93
|
+
<%= f.label :expires_at, "Custom Expiry Date/Time (UTC)" %>
|
|
104
94
|
<%= f.datetime_field :expires_at,
|
|
105
|
-
value: @banned_ip.expires_at
|
|
106
|
-
|
|
95
|
+
value: (params.dig(:banned_ip, :expires_at).present? ? ban_expiry_input_value(@banned_ip.expires_at) : ""),
|
|
96
|
+
step: "0.001" %>
|
|
107
97
|
</div>
|
|
108
98
|
</div>
|
|
109
99
|
|
|
@@ -123,7 +113,7 @@
|
|
|
123
113
|
<%= f.label :violation_count, "Initial Violation Count" %>
|
|
124
114
|
<%= f.number_field :violation_count,
|
|
125
115
|
value: @banned_ip.violation_count || 1,
|
|
126
|
-
min:
|
|
116
|
+
min: 1 %>
|
|
127
117
|
<div style="font-size: 12px; color: #697386; margin-top: 0.25rem;">
|
|
128
118
|
Set the initial violation count. This affects ban escalation for repeat offenders.
|
|
129
119
|
</div>
|
|
@@ -139,6 +129,12 @@
|
|
|
139
129
|
</div>
|
|
140
130
|
<% end %>
|
|
141
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
|
+
|
|
142
138
|
<!-- Submit Buttons -->
|
|
143
139
|
<div style="display: flex; gap: 0.5rem;">
|
|
144
140
|
<%= f.submit "Ban IP Address", class: "btn btn-danger" %>
|
|
@@ -150,7 +146,7 @@
|
|
|
150
146
|
</div>
|
|
151
147
|
|
|
152
148
|
<!-- Preview Card -->
|
|
153
|
-
<div class="card" id="ban-preview"
|
|
149
|
+
<div class="card" id="ban-preview" hidden>
|
|
154
150
|
<div class="card-header">
|
|
155
151
|
<h3 class="card-title">Ban Preview</h3>
|
|
156
152
|
<div class="card-subtitle">This is how the ban will appear</div>
|
|
@@ -192,119 +188,3 @@
|
|
|
192
188
|
</div>
|
|
193
189
|
</div>
|
|
194
190
|
</div>
|
|
195
|
-
|
|
196
|
-
<script>
|
|
197
|
-
function toggleDurationFields() {
|
|
198
|
-
const isPermanent = document.querySelector('input[name="ban_type"]:checked').value === 'permanent';
|
|
199
|
-
const temporaryOptions = document.getElementById('temporary-options');
|
|
200
|
-
|
|
201
|
-
if (isPermanent) {
|
|
202
|
-
temporaryOptions.style.display = 'none';
|
|
203
|
-
// Clear duration selections
|
|
204
|
-
document.querySelectorAll('input[name="duration"]').forEach(input => {
|
|
205
|
-
input.checked = false;
|
|
206
|
-
});
|
|
207
|
-
document.getElementById('banned_ip_expires_at').value = '';
|
|
208
|
-
} else {
|
|
209
|
-
temporaryOptions.style.display = 'block';
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
updatePreview();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function updatePreview() {
|
|
216
|
-
const previewCard = document.getElementById('ban-preview');
|
|
217
|
-
const ipInput = document.getElementById('banned_ip_ip_address');
|
|
218
|
-
const reasonSelect = document.getElementById('banned_ip_reason');
|
|
219
|
-
|
|
220
|
-
if (ipInput.value || reasonSelect.value) {
|
|
221
|
-
previewCard.style.display = 'block';
|
|
222
|
-
|
|
223
|
-
// Update IP
|
|
224
|
-
document.getElementById('preview-ip').textContent = ipInput.value || '-';
|
|
225
|
-
|
|
226
|
-
// Update reason
|
|
227
|
-
const reasonText = reasonSelect.options[reasonSelect.selectedIndex]?.text || '-';
|
|
228
|
-
document.getElementById('preview-reason').textContent = reasonText;
|
|
229
|
-
|
|
230
|
-
// Update duration
|
|
231
|
-
const isPermanent = document.querySelector('input[name="ban_type"]:checked')?.value === 'permanent';
|
|
232
|
-
if (isPermanent) {
|
|
233
|
-
document.getElementById('preview-duration').textContent = 'Permanent';
|
|
234
|
-
document.getElementById('preview-duration').style.color = '#D32F2F';
|
|
235
|
-
document.getElementById('preview-status').className = 'badge badge-critical';
|
|
236
|
-
document.getElementById('preview-status').textContent = 'Permanent';
|
|
237
|
-
} else {
|
|
238
|
-
const durationRadio = document.querySelector('input[name="duration"]:checked');
|
|
239
|
-
const customExpiry = document.getElementById('banned_ip_expires_at').value;
|
|
240
|
-
|
|
241
|
-
if (customExpiry) {
|
|
242
|
-
const expiryDate = new Date(customExpiry);
|
|
243
|
-
document.getElementById('preview-duration').textContent = expiryDate.toLocaleString();
|
|
244
|
-
} else if (durationRadio) {
|
|
245
|
-
const label = durationRadio.parentElement.textContent.trim();
|
|
246
|
-
document.getElementById('preview-duration').textContent = label;
|
|
247
|
-
} else {
|
|
248
|
-
document.getElementById('preview-duration').textContent = 'Not specified';
|
|
249
|
-
}
|
|
250
|
-
document.getElementById('preview-duration').style.color = '#1A1F36';
|
|
251
|
-
document.getElementById('preview-status').className = 'badge badge-danger';
|
|
252
|
-
document.getElementById('preview-status').textContent = 'Active';
|
|
253
|
-
}
|
|
254
|
-
} else {
|
|
255
|
-
previewCard.style.display = 'none';
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// Set up event listeners
|
|
260
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
261
|
-
// Update preview on input changes
|
|
262
|
-
document.getElementById('banned_ip_ip_address').addEventListener('input', updatePreview);
|
|
263
|
-
document.getElementById('banned_ip_reason').addEventListener('change', updatePreview);
|
|
264
|
-
document.getElementById('banned_ip_expires_at').addEventListener('change', updatePreview);
|
|
265
|
-
|
|
266
|
-
document.querySelectorAll('input[name="duration"]').forEach(input => {
|
|
267
|
-
input.addEventListener('change', function() {
|
|
268
|
-
// Clear custom expiry when selecting a preset duration
|
|
269
|
-
document.getElementById('banned_ip_expires_at').value = '';
|
|
270
|
-
updatePreview();
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
// Clear preset duration when entering custom expiry
|
|
275
|
-
document.getElementById('banned_ip_expires_at').addEventListener('input', function() {
|
|
276
|
-
if (this.value) {
|
|
277
|
-
document.querySelectorAll('input[name="duration"]').forEach(input => {
|
|
278
|
-
input.checked = false;
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// Initial preview update
|
|
284
|
-
updatePreview();
|
|
285
|
-
|
|
286
|
-
// Handle form submission
|
|
287
|
-
document.querySelector('form').addEventListener('submit', function(e) {
|
|
288
|
-
const isPermanent = document.querySelector('input[name="ban_type"]:checked').value === 'permanent';
|
|
289
|
-
|
|
290
|
-
if (isPermanent) {
|
|
291
|
-
// Set permanent flag
|
|
292
|
-
const permanentField = document.createElement('input');
|
|
293
|
-
permanentField.type = 'hidden';
|
|
294
|
-
permanentField.name = 'banned_ip[permanent]';
|
|
295
|
-
permanentField.value = 'true';
|
|
296
|
-
this.appendChild(permanentField);
|
|
297
|
-
} else {
|
|
298
|
-
// Calculate expires_at based on duration if not custom
|
|
299
|
-
const durationRadio = document.querySelector('input[name="duration"]:checked');
|
|
300
|
-
const customExpiry = document.getElementById('banned_ip_expires_at').value;
|
|
301
|
-
|
|
302
|
-
if (!customExpiry && durationRadio) {
|
|
303
|
-
const durationSeconds = parseInt(durationRadio.value);
|
|
304
|
-
const expiresAt = new Date(Date.now() + durationSeconds * 1000);
|
|
305
|
-
document.getElementById('banned_ip_expires_at').value = expiresAt.toISOString().slice(0, 16);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
</script>
|
|
@@ -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>
|