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,280 @@
|
|
|
1
|
+
<% content_for :page_title, "Security Dashboard" %>
|
|
2
|
+
|
|
3
|
+
<% content_for :header_actions do %>
|
|
4
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
5
|
+
<% ['1h', '6h', '24h', '7d', '30d'].each do |range| %>
|
|
6
|
+
<%= link_to range.upcase, dashboard_path(time_range: range),
|
|
7
|
+
class: "btn btn-sm #{@time_range == range ? 'btn-primary' : 'btn-secondary'}" %>
|
|
8
|
+
<% end %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<!-- Stats Grid -->
|
|
13
|
+
<div class="stats-grid">
|
|
14
|
+
<div class="stat-card">
|
|
15
|
+
<div class="stat-label">Total Events</div>
|
|
16
|
+
<div class="stat-value"><%= number_with_delimiter(@stats[:total_events]) %></div>
|
|
17
|
+
<div class="stat-change">In selected period</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="stat-card">
|
|
21
|
+
<div class="stat-label">Failed Logins</div>
|
|
22
|
+
<div class="stat-value" style="color: var(--warning);">
|
|
23
|
+
<%= number_with_delimiter(@stats[:failed_logins]) %>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="stat-change">Authentication failures</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="stat-card">
|
|
29
|
+
<div class="stat-label">Blocked IPs</div>
|
|
30
|
+
<div class="stat-value" style="color: var(--danger);">
|
|
31
|
+
<%= number_with_delimiter(@stats[:blocked_ips]) %>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="stat-change">Currently active</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="stat-card">
|
|
37
|
+
<div class="stat-label">High Risk Events</div>
|
|
38
|
+
<div class="stat-value" style="color: var(--primary);">
|
|
39
|
+
<%= number_with_delimiter(@stats[:high_risk_events]) %>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="stat-change">Score ≥ <%= Beskar::RiskLevel::RANGES[:high].begin %></div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div class="stat-card">
|
|
45
|
+
<div class="stat-label">Critical Threats</div>
|
|
46
|
+
<div class="stat-value" style="color: #D32F2F;">
|
|
47
|
+
<%= number_with_delimiter(@stats[:critical_threats]) %>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="stat-change">Score ≥ <%= Beskar::RiskLevel::RANGES[:critical].begin %></div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Main Grid -->
|
|
54
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
|
55
|
+
<!-- Risk Distribution -->
|
|
56
|
+
<div class="card">
|
|
57
|
+
<div class="card-header">
|
|
58
|
+
<h3 class="card-title">Risk Distribution</h3>
|
|
59
|
+
<div class="card-subtitle">Events by risk level</div>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="card-body">
|
|
62
|
+
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
|
63
|
+
<% total_risk_events = @risk_distribution.values.sum.to_f %>
|
|
64
|
+
<% @risk_distribution.each do |level, count| %>
|
|
65
|
+
<div style="display: flex; align-items: center; gap: 1rem;">
|
|
66
|
+
<div style="width: 80px; font-size: 12px; font-weight: 600; text-transform: uppercase; color: #697386;">
|
|
67
|
+
<%= level.to_s.capitalize %>
|
|
68
|
+
</div>
|
|
69
|
+
<div style="flex: 1; position: relative;">
|
|
70
|
+
<div style="background: #F4F5F7; height: 24px; border-radius: 4px; overflow: hidden;">
|
|
71
|
+
<% percentage = total_risk_events > 0 ? (count / total_risk_events * 100) : 0 %>
|
|
72
|
+
<div style="
|
|
73
|
+
background: <%= case level
|
|
74
|
+
when :low then '#4CAF50'
|
|
75
|
+
when :medium then '#FF9800'
|
|
76
|
+
when :high then '#E91E63'
|
|
77
|
+
when :critical then '#D32F2F'
|
|
78
|
+
end %>;
|
|
79
|
+
width: <%= percentage %>%;
|
|
80
|
+
height: 100%;
|
|
81
|
+
transition: width 0.3s ease;
|
|
82
|
+
"></div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div style="width: 60px; text-align: right; font-size: 13px; font-weight: 600;">
|
|
86
|
+
<%= number_with_delimiter(count) %>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
<% end %>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<!-- Event Types -->
|
|
95
|
+
<div class="card">
|
|
96
|
+
<div class="card-header">
|
|
97
|
+
<h3 class="card-title">Event Types</h3>
|
|
98
|
+
<div class="card-subtitle">Distribution by type</div>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="card-body">
|
|
101
|
+
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
|
102
|
+
<% @event_distribution.first(5).each do |event_type, count| %>
|
|
103
|
+
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; border-bottom: 1px solid #F4F5F7;">
|
|
104
|
+
<div>
|
|
105
|
+
<div style="font-size: 14px; color: #1A1F36;">
|
|
106
|
+
<%= format_event_type(event_type) %>
|
|
107
|
+
</div>
|
|
108
|
+
<div style="font-size: 12px; color: #697386; margin-top: 2px;">
|
|
109
|
+
<%= event_type %>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
|
113
|
+
<div style="font-size: 16px; font-weight: 600; color: #1A1F36;">
|
|
114
|
+
<%= number_with_delimiter(count) %>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<% end %>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Recent Activity and Top Threats -->
|
|
125
|
+
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;">
|
|
126
|
+
<!-- Recent Security Events -->
|
|
127
|
+
<div class="card">
|
|
128
|
+
<div class="card-header">
|
|
129
|
+
<h3 class="card-title">Recent Security Events</h3>
|
|
130
|
+
<div class="card-subtitle">Latest activity across all monitors</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="card-body" style="padding: 0;">
|
|
133
|
+
<div class="table-container">
|
|
134
|
+
<table>
|
|
135
|
+
<thead>
|
|
136
|
+
<tr>
|
|
137
|
+
<th>Time</th>
|
|
138
|
+
<th>Event Type</th>
|
|
139
|
+
<th>IP Address</th>
|
|
140
|
+
<th>User</th>
|
|
141
|
+
<th>Risk</th>
|
|
142
|
+
<th></th>
|
|
143
|
+
</tr>
|
|
144
|
+
</thead>
|
|
145
|
+
<tbody>
|
|
146
|
+
<% @recent_events.each do |event| %>
|
|
147
|
+
<tr>
|
|
148
|
+
<td>
|
|
149
|
+
<div style="font-size: 13px;">
|
|
150
|
+
<%= event.created_at.strftime("%H:%M:%S") %>
|
|
151
|
+
</div>
|
|
152
|
+
<div style="font-size: 11px; color: #697386;">
|
|
153
|
+
<%= event.created_at.strftime("%Y-%m-%d") %>
|
|
154
|
+
</div>
|
|
155
|
+
</td>
|
|
156
|
+
<td>
|
|
157
|
+
<div style="font-size: 13px;">
|
|
158
|
+
<%= format_event_type(event.event_type) %>
|
|
159
|
+
</div>
|
|
160
|
+
</td>
|
|
161
|
+
<td>
|
|
162
|
+
<div style="font-family: monospace; font-size: 13px;">
|
|
163
|
+
<%= event.ip_address %>
|
|
164
|
+
</div>
|
|
165
|
+
<% if event.metadata&.dig("geolocation", "country") %>
|
|
166
|
+
<div style="font-size: 11px; color: #697386;">
|
|
167
|
+
<%= [event.metadata.dig("geolocation", "city"),
|
|
168
|
+
event.metadata.dig("geolocation", "country")].compact.join(", ") %>
|
|
169
|
+
</div>
|
|
170
|
+
<% end %>
|
|
171
|
+
</td>
|
|
172
|
+
<td>
|
|
173
|
+
<% if event.user %>
|
|
174
|
+
<div style="font-size: 13px;">
|
|
175
|
+
<%= audit_user_label(event) %>
|
|
176
|
+
</div>
|
|
177
|
+
<% elsif event.attempted_email %>
|
|
178
|
+
<div style="font-size: 13px; color: #697386;">
|
|
179
|
+
<%= audit_user_label(event) %>
|
|
180
|
+
</div>
|
|
181
|
+
<% else %>
|
|
182
|
+
<span style="color: #C1C7D0;">-</span>
|
|
183
|
+
<% end %>
|
|
184
|
+
</td>
|
|
185
|
+
<td>
|
|
186
|
+
<span class="badge badge-<%= risk_level_class(event.risk_score) %>">
|
|
187
|
+
<%= event.risk_score %>
|
|
188
|
+
</span>
|
|
189
|
+
</td>
|
|
190
|
+
<td>
|
|
191
|
+
<%= link_to "View", security_event_path(event),
|
|
192
|
+
class: "btn btn-sm btn-secondary" %>
|
|
193
|
+
</td>
|
|
194
|
+
</tr>
|
|
195
|
+
<% end %>
|
|
196
|
+
</tbody>
|
|
197
|
+
</table>
|
|
198
|
+
</div>
|
|
199
|
+
<div style="padding: 1rem; text-align: center; border-top: 1px solid #E3E8EE;">
|
|
200
|
+
<%= link_to "View All Security Events →", security_events_path,
|
|
201
|
+
style: "color: var(--primary); text-decoration: none; font-size: 13px; font-weight: 500;" %>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<!-- Top Threats & Active Bans -->
|
|
207
|
+
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
|
|
208
|
+
<!-- Top Threat IPs -->
|
|
209
|
+
<div class="card">
|
|
210
|
+
<div class="card-header">
|
|
211
|
+
<h3 class="card-title">Top Threat IPs</h3>
|
|
212
|
+
<div class="card-subtitle">Most active suspicious IPs</div>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="card-body">
|
|
215
|
+
<div style="display: flex; flex-direction: column; gap: 0.75rem;">
|
|
216
|
+
<% @top_threat_ips.each do |threat| %>
|
|
217
|
+
<div style="padding: 0.75rem; background: #FAFBFC; border-radius: 6px;">
|
|
218
|
+
<div style="display: flex; justify-content: space-between; align-items: start;">
|
|
219
|
+
<div>
|
|
220
|
+
<div style="font-family: monospace; font-size: 13px; font-weight: 600; color: #1A1F36;">
|
|
221
|
+
<%= threat.ip_address %>
|
|
222
|
+
</div>
|
|
223
|
+
<div style="font-size: 11px; color: #697386; margin-top: 4px;">
|
|
224
|
+
<%= threat.event_count %> events ·
|
|
225
|
+
Avg risk: <%= threat.avg_risk_score.round(1) %>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
<div style="display: flex; gap: 0.25rem;">
|
|
229
|
+
<% if Beskar::BannedIp.banned?(threat.ip_address) %>
|
|
230
|
+
<span class="badge badge-danger">Banned</span>
|
|
231
|
+
<% else %>
|
|
232
|
+
<%= link_to "Ban", new_banned_ip_path(ip_address: threat.ip_address, reason: 'high_threat_activity'),
|
|
233
|
+
class: "btn btn-sm btn-danger", style: "padding: 0.25rem 0.5rem; font-size: 11px;" %>
|
|
234
|
+
<% end %>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
<% end %>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<!-- Active Bans -->
|
|
244
|
+
<div class="card">
|
|
245
|
+
<div class="card-header">
|
|
246
|
+
<h3 class="card-title">Active Bans</h3>
|
|
247
|
+
<div class="card-subtitle">Recently banned IPs</div>
|
|
248
|
+
</div>
|
|
249
|
+
<div class="card-body">
|
|
250
|
+
<div style="display: flex; flex-direction: column; gap: 0.5rem;">
|
|
251
|
+
<% @active_bans.each do |ban| %>
|
|
252
|
+
<div style="padding: 0.5rem 0; border-bottom: 1px solid #F4F5F7;">
|
|
253
|
+
<div style="display: flex; justify-content: space-between; align-items: start;">
|
|
254
|
+
<div>
|
|
255
|
+
<div style="font-family: monospace; font-size: 13px; color: #1A1F36;">
|
|
256
|
+
<%= ban.ip_address %>
|
|
257
|
+
</div>
|
|
258
|
+
<div style="font-size: 11px; color: #697386; margin-top: 2px;">
|
|
259
|
+
<%= ban.reason.humanize %> ·
|
|
260
|
+
<% if ban.permanent? %>
|
|
261
|
+
Permanent
|
|
262
|
+
<% else %>
|
|
263
|
+
Expires <%= distance_of_time_in_words(Time.current, ban.expires_at) %>
|
|
264
|
+
<% end %>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
<%= link_to "→", banned_ip_path(ban),
|
|
268
|
+
style: "color: #697386; text-decoration: none; font-size: 14px;" %>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
<% end %>
|
|
272
|
+
</div>
|
|
273
|
+
<div style="padding-top: 1rem; text-align: center;">
|
|
274
|
+
<%= link_to "Manage All Bans →", banned_ips_path,
|
|
275
|
+
style: "color: var(--primary); text-decoration: none; font-size: 13px; font-weight: 500;" %>
|
|
276
|
+
</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
<% content_for :page_title, "Security Events" %>
|
|
2
|
+
|
|
3
|
+
<% content_for :header_actions do %>
|
|
4
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
5
|
+
<%= render "beskar/shared/export_form", export_url: export_security_events_path %>
|
|
6
|
+
</div>
|
|
7
|
+
<% end %>
|
|
8
|
+
|
|
9
|
+
<!-- Filters Card -->
|
|
10
|
+
<div class="card">
|
|
11
|
+
<div class="card-header">
|
|
12
|
+
<h3 class="card-title">Filters</h3>
|
|
13
|
+
<div class="card-subtitle">Narrow down security events</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="card-body">
|
|
16
|
+
<%= form_with url: security_events_path, method: :get, local: true do |f| %>
|
|
17
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
18
|
+
<!-- Quick Time Range -->
|
|
19
|
+
<div class="form-group">
|
|
20
|
+
<label>Time Range</label>
|
|
21
|
+
<%= select_tag :time_range,
|
|
22
|
+
options_for_select([
|
|
23
|
+
['All Time', ''],
|
|
24
|
+
['Last Hour', 'last_hour'],
|
|
25
|
+
['Last 24 Hours', 'last_24h'],
|
|
26
|
+
['Last 7 Days', 'last_7d'],
|
|
27
|
+
['Last 30 Days', 'last_30d']
|
|
28
|
+
], params[:time_range]),
|
|
29
|
+
prompt: 'Select time range',
|
|
30
|
+
style: "width: 100%;" %>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- Event Type -->
|
|
34
|
+
<div class="form-group">
|
|
35
|
+
<label>Event Type</label>
|
|
36
|
+
<%= select_tag :event_type,
|
|
37
|
+
options_for_select(@event_types, params[:event_type]),
|
|
38
|
+
prompt: 'All event types',
|
|
39
|
+
style: "width: 100%;" %>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- Risk Level -->
|
|
43
|
+
<div class="form-group">
|
|
44
|
+
<label>Risk Level</label>
|
|
45
|
+
<%= select_tag :risk_level,
|
|
46
|
+
options_for_select(@risk_levels, params[:risk_level]),
|
|
47
|
+
prompt: 'All risk levels',
|
|
48
|
+
style: "width: 100%;" %>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<!-- IP Address -->
|
|
52
|
+
<div class="form-group">
|
|
53
|
+
<label>IP Address</label>
|
|
54
|
+
<%= text_field_tag :ip_address, params[:ip_address],
|
|
55
|
+
placeholder: "e.g., 192.168.1.1" %>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Email -->
|
|
59
|
+
<div class="form-group">
|
|
60
|
+
<label>User Email</label>
|
|
61
|
+
<%= text_field_tag :email, params[:email],
|
|
62
|
+
placeholder: "user@example.com" %>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<!-- Search -->
|
|
66
|
+
<div class="form-group">
|
|
67
|
+
<label>Search</label>
|
|
68
|
+
<%= text_field_tag :search, params[:search],
|
|
69
|
+
placeholder: "Search in all fields..." %>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<!-- Date Range -->
|
|
73
|
+
<div class="form-group">
|
|
74
|
+
<label>Start Date</label>
|
|
75
|
+
<%= date_field_tag :start_date, params[:start_date] %>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="form-group">
|
|
79
|
+
<label>End Date</label>
|
|
80
|
+
<%= date_field_tag :end_date, params[:end_date] %>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
|
|
85
|
+
<%= submit_tag "Apply Filters", class: "btn btn-primary" %>
|
|
86
|
+
<%= link_to "Clear Filters", security_events_path, class: "btn btn-secondary" %>
|
|
87
|
+
|
|
88
|
+
<div style="margin-left: auto; display: flex; align-items: center; gap: 0.5rem;">
|
|
89
|
+
<%= check_box_tag :threats_only, 'true', params[:threats_only] == 'true' %>
|
|
90
|
+
<label for="threats_only" style="margin: 0; font-size: 13px;">High threats only</label>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- Results Summary -->
|
|
98
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
|
99
|
+
<div style="font-size: 14px; color: #697386;">
|
|
100
|
+
Showing <%= @pagination[:records].count %> of <%= number_with_delimiter(@pagination[:total_count]) %> events
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
104
|
+
<label for="per_page" style="margin: 0; font-size: 13px; color: #697386;">Per page:</label>
|
|
105
|
+
<%= form_with url: security_events_path, method: :get, local: true, style: "display: inline;" do |f| %>
|
|
106
|
+
<% request.query_parameters.except("per_page", "page").each do |key, value| %>
|
|
107
|
+
<%= hidden_field_tag key, value %>
|
|
108
|
+
<% end %>
|
|
109
|
+
<%= select_tag :per_page,
|
|
110
|
+
options_for_select([10, 25, 50, 100], params[:per_page]&.to_i || 25),
|
|
111
|
+
data: { beskar_auto_submit: true },
|
|
112
|
+
style: "padding: 0.25rem 0.5rem; font-size: 13px;" %>
|
|
113
|
+
<%= submit_tag "Apply page size", class: "btn btn-sm btn-secondary" %>
|
|
114
|
+
<% end %>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<!-- Events Table -->
|
|
119
|
+
<div class="card">
|
|
120
|
+
<div class="card-body" style="padding: 0;">
|
|
121
|
+
<div class="table-container">
|
|
122
|
+
<table>
|
|
123
|
+
<thead>
|
|
124
|
+
<tr>
|
|
125
|
+
<th>ID</th>
|
|
126
|
+
<th>Timestamp</th>
|
|
127
|
+
<th>Event Type</th>
|
|
128
|
+
<th>IP Address</th>
|
|
129
|
+
<th>User</th>
|
|
130
|
+
<th>Risk Score</th>
|
|
131
|
+
<th>User Agent</th>
|
|
132
|
+
<th>Actions</th>
|
|
133
|
+
</tr>
|
|
134
|
+
</thead>
|
|
135
|
+
<tbody>
|
|
136
|
+
<% if @events.any? %>
|
|
137
|
+
<% @events.each do |event| %>
|
|
138
|
+
<tr>
|
|
139
|
+
<td>
|
|
140
|
+
<span style="font-family: monospace; font-size: 12px; color: #697386;">
|
|
141
|
+
#<%= event.id %>
|
|
142
|
+
</span>
|
|
143
|
+
</td>
|
|
144
|
+
<td>
|
|
145
|
+
<div style="font-size: 13px;">
|
|
146
|
+
<%= event.created_at.strftime("%Y-%m-%d") %>
|
|
147
|
+
</div>
|
|
148
|
+
<div style="font-size: 11px; color: #697386;">
|
|
149
|
+
<%= event.created_at.strftime("%H:%M:%S %Z") %>
|
|
150
|
+
</div>
|
|
151
|
+
</td>
|
|
152
|
+
<td>
|
|
153
|
+
<div style="font-size: 13px; font-weight: 500;">
|
|
154
|
+
<%= format_event_type(event.event_type) %>
|
|
155
|
+
</div>
|
|
156
|
+
<div style="font-size: 11px; color: #697386;">
|
|
157
|
+
<%= event.event_type %>
|
|
158
|
+
</div>
|
|
159
|
+
</td>
|
|
160
|
+
<td>
|
|
161
|
+
<div style="font-family: monospace; font-size: 13px;">
|
|
162
|
+
<%= event.ip_address %>
|
|
163
|
+
</div>
|
|
164
|
+
<% if event.metadata&.dig("geolocation", "country") %>
|
|
165
|
+
<div style="font-size: 11px; color: #697386;">
|
|
166
|
+
<%= [event.metadata.dig("geolocation", "city"),
|
|
167
|
+
event.metadata.dig("geolocation", "country")].compact.join(", ") %>
|
|
168
|
+
</div>
|
|
169
|
+
<% end %>
|
|
170
|
+
</td>
|
|
171
|
+
<td>
|
|
172
|
+
<% if event.user %>
|
|
173
|
+
<div style="font-size: 13px;">
|
|
174
|
+
<%= audit_user_label(event) %>
|
|
175
|
+
</div>
|
|
176
|
+
<div style="font-size: 11px; color: #697386;">
|
|
177
|
+
ID: <%= event.user_id %>
|
|
178
|
+
</div>
|
|
179
|
+
<% elsif event.attempted_email %>
|
|
180
|
+
<div style="font-size: 13px; color: #697386;">
|
|
181
|
+
<%= audit_user_label(event) %>
|
|
182
|
+
</div>
|
|
183
|
+
<div style="font-size: 11px; color: #C1C7D0;">
|
|
184
|
+
(attempted)
|
|
185
|
+
</div>
|
|
186
|
+
<% else %>
|
|
187
|
+
<span style="color: #C1C7D0;">-</span>
|
|
188
|
+
<% end %>
|
|
189
|
+
</td>
|
|
190
|
+
<td>
|
|
191
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
192
|
+
<span class="badge badge-<%= risk_level_class(event.risk_score) %>">
|
|
193
|
+
<%= event.risk_score %>
|
|
194
|
+
</span>
|
|
195
|
+
<% if event.critical_threat? %>
|
|
196
|
+
<svg width="16" height="16" fill="#D32F2F" viewBox="0 0 24 24">
|
|
197
|
+
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
|
198
|
+
</svg>
|
|
199
|
+
<% elsif event.high_risk? %>
|
|
200
|
+
<svg width="16" height="16" fill="#E91E63" viewBox="0 0 24 24">
|
|
201
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
|
|
202
|
+
</svg>
|
|
203
|
+
<% end %>
|
|
204
|
+
</div>
|
|
205
|
+
</td>
|
|
206
|
+
<td>
|
|
207
|
+
<div style="font-size: 11px; color: #697386; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
|
208
|
+
<%= event.user_agent || "-" %>
|
|
209
|
+
</div>
|
|
210
|
+
</td>
|
|
211
|
+
<td>
|
|
212
|
+
<div style="display: flex; gap: 0.25rem;">
|
|
213
|
+
<%= link_to "View", security_event_path(event),
|
|
214
|
+
class: "btn btn-sm btn-secondary" %>
|
|
215
|
+
|
|
216
|
+
<% unless Beskar::BannedIp.banned?(event.ip_address) %>
|
|
217
|
+
<%= link_to "Ban IP", new_banned_ip_path(ip_address: event.ip_address, reason: "suspicious_activity"),
|
|
218
|
+
class: "btn btn-sm btn-danger" %>
|
|
219
|
+
<% end %>
|
|
220
|
+
</div>
|
|
221
|
+
</td>
|
|
222
|
+
</tr>
|
|
223
|
+
<% end %>
|
|
224
|
+
<% else %>
|
|
225
|
+
<tr>
|
|
226
|
+
<td colspan="8" style="text-align: center; padding: 3rem;">
|
|
227
|
+
<div style="color: #697386;">
|
|
228
|
+
<svg width="48" height="48" fill="currentColor" viewBox="0 0 24 24" style="margin: 0 auto 1rem;">
|
|
229
|
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
|
230
|
+
</svg>
|
|
231
|
+
<div style="font-size: 16px; font-weight: 500; margin-bottom: 0.5rem;">No security events found</div>
|
|
232
|
+
<div style="font-size: 14px;">Try adjusting your filters or time range</div>
|
|
233
|
+
</div>
|
|
234
|
+
</td>
|
|
235
|
+
</tr>
|
|
236
|
+
<% end %>
|
|
237
|
+
</tbody>
|
|
238
|
+
</table>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<!-- Pagination -->
|
|
244
|
+
<% if @pagination[:total_pages] > 1 %>
|
|
245
|
+
<div class="pagination">
|
|
246
|
+
<% if @pagination[:has_previous] %>
|
|
247
|
+
<%= link_to "← Previous", security_events_path(params.to_unsafe_h.merge(page: @pagination[:previous_page])),
|
|
248
|
+
class: "pagination-link" %>
|
|
249
|
+
<% else %>
|
|
250
|
+
<span class="pagination-link disabled">← Previous</span>
|
|
251
|
+
<% end %>
|
|
252
|
+
|
|
253
|
+
<% # Show page numbers with ellipsis for large page counts %>
|
|
254
|
+
<% if @pagination[:total_pages] <= 7 %>
|
|
255
|
+
<% (1..@pagination[:total_pages]).each do |page| %>
|
|
256
|
+
<%= link_to page, security_events_path(params.to_unsafe_h.merge(page: page)),
|
|
257
|
+
class: "pagination-link #{'active' if page == @pagination[:current_page]}" %>
|
|
258
|
+
<% end %>
|
|
259
|
+
<% else %>
|
|
260
|
+
<% if @pagination[:current_page] <= 4 %>
|
|
261
|
+
<% (1..5).each do |page| %>
|
|
262
|
+
<%= link_to page, security_events_path(params.to_unsafe_h.merge(page: page)),
|
|
263
|
+
class: "pagination-link #{'active' if page == @pagination[:current_page]}" %>
|
|
264
|
+
<% end %>
|
|
265
|
+
<span class="pagination-link disabled">...</span>
|
|
266
|
+
<%= link_to @pagination[:total_pages], security_events_path(params.to_unsafe_h.merge(page: @pagination[:total_pages])),
|
|
267
|
+
class: "pagination-link" %>
|
|
268
|
+
<% elsif @pagination[:current_page] >= @pagination[:total_pages] - 3 %>
|
|
269
|
+
<%= link_to 1, security_events_path(params.to_unsafe_h.merge(page: 1)),
|
|
270
|
+
class: "pagination-link" %>
|
|
271
|
+
<span class="pagination-link disabled">...</span>
|
|
272
|
+
<% ((@pagination[:total_pages] - 4)..@pagination[:total_pages]).each do |page| %>
|
|
273
|
+
<%= link_to page, security_events_path(params.to_unsafe_h.merge(page: page)),
|
|
274
|
+
class: "pagination-link #{'active' if page == @pagination[:current_page]}" %>
|
|
275
|
+
<% end %>
|
|
276
|
+
<% else %>
|
|
277
|
+
<%= link_to 1, security_events_path(params.to_unsafe_h.merge(page: 1)),
|
|
278
|
+
class: "pagination-link" %>
|
|
279
|
+
<span class="pagination-link disabled">...</span>
|
|
280
|
+
<% ((@pagination[:current_page] - 1)..(@pagination[:current_page] + 1)).each do |page| %>
|
|
281
|
+
<%= link_to page, security_events_path(params.to_unsafe_h.merge(page: page)),
|
|
282
|
+
class: "pagination-link #{'active' if page == @pagination[:current_page]}" %>
|
|
283
|
+
<% end %>
|
|
284
|
+
<span class="pagination-link disabled">...</span>
|
|
285
|
+
<%= link_to @pagination[:total_pages], security_events_path(params.to_unsafe_h.merge(page: @pagination[:total_pages])),
|
|
286
|
+
class: "pagination-link" %>
|
|
287
|
+
<% end %>
|
|
288
|
+
<% end %>
|
|
289
|
+
|
|
290
|
+
<% if @pagination[:has_next] %>
|
|
291
|
+
<%= link_to "Next →", security_events_path(params.to_unsafe_h.merge(page: @pagination[:next_page])),
|
|
292
|
+
class: "pagination-link" %>
|
|
293
|
+
<% else %>
|
|
294
|
+
<span class="pagination-link disabled">Next →</span>
|
|
295
|
+
<% end %>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
<div style="text-align: center; margin-top: 1rem; font-size: 13px; color: #697386;">
|
|
299
|
+
Page <%= @pagination[:current_page] %> of <%= @pagination[:total_pages] %> •
|
|
300
|
+
Total: <%= number_with_delimiter(@pagination[:total_count]) %> events
|
|
301
|
+
</div>
|
|
302
|
+
<% end %>
|