beskar 0.0.2 → 0.1.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 +143 -0
- data/README.md +298 -110
- data/app/controllers/beskar/application_controller.rb +170 -0
- data/app/controllers/beskar/banned_ips_controller.rb +280 -0
- data/app/controllers/beskar/dashboard_controller.rb +70 -0
- data/app/controllers/beskar/security_events_controller.rb +182 -0
- data/app/controllers/concerns/beskar/controllers/security_tracking.rb +6 -6
- data/app/models/beskar/banned_ip.rb +68 -27
- data/app/models/beskar/security_event.rb +14 -0
- data/app/services/beskar/banned_ip_manager.rb +78 -0
- data/app/views/beskar/banned_ips/edit.html.erb +259 -0
- data/app/views/beskar/banned_ips/index.html.erb +361 -0
- data/app/views/beskar/banned_ips/new.html.erb +310 -0
- data/app/views/beskar/banned_ips/show.html.erb +310 -0
- data/app/views/beskar/dashboard/index.html.erb +280 -0
- data/app/views/beskar/security_events/index.html.erb +309 -0
- data/app/views/beskar/security_events/show.html.erb +307 -0
- data/app/views/layouts/beskar/application.html.erb +647 -5
- data/config/routes.rb +41 -0
- data/lib/beskar/configuration.rb +24 -10
- data/lib/beskar/engine.rb +4 -4
- data/lib/beskar/logger.rb +293 -0
- data/lib/beskar/middleware/request_analyzer.rb +128 -53
- data/lib/beskar/models/security_trackable_authenticable.rb +11 -11
- data/lib/beskar/models/security_trackable_devise.rb +5 -5
- data/lib/beskar/models/security_trackable_generic.rb +12 -12
- data/lib/beskar/services/account_locker.rb +12 -12
- data/lib/beskar/services/geolocation_service.rb +8 -8
- data/lib/beskar/services/ip_whitelist.rb +2 -2
- data/lib/beskar/services/waf.rb +307 -78
- data/lib/beskar/version.rb +1 -1
- data/lib/beskar.rb +1 -0
- data/lib/generators/beskar/install/install_generator.rb +158 -0
- data/lib/generators/beskar/install/templates/initializer.rb.tt +177 -0
- data/lib/tasks/beskar_tasks.rake +11 -2
- metadata +35 -6
- data/lib/beskar/templates/beskar_initializer.rb +0 -107
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
<% content_for :page_title, "Security Events" %>
|
|
2
|
+
|
|
3
|
+
<% content_for :header_actions do %>
|
|
4
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
5
|
+
<%= link_to "Export CSV", export_security_events_path(request.query_parameters.merge(format: :csv)),
|
|
6
|
+
class: "btn btn-secondary" %>
|
|
7
|
+
<%= link_to "Export JSON", export_security_events_path(request.query_parameters.merge(format: :json)),
|
|
8
|
+
class: "btn btn-secondary" %>
|
|
9
|
+
</div>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<!-- Filters Card -->
|
|
13
|
+
<div class="card">
|
|
14
|
+
<div class="card-header">
|
|
15
|
+
<h3 class="card-title">Filters</h3>
|
|
16
|
+
<div class="card-subtitle">Narrow down security events</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="card-body">
|
|
19
|
+
<%= form_with url: security_events_path, method: :get, local: true do |f| %>
|
|
20
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
21
|
+
<!-- Quick Time Range -->
|
|
22
|
+
<div class="form-group">
|
|
23
|
+
<label>Time Range</label>
|
|
24
|
+
<%= select_tag :time_range,
|
|
25
|
+
options_for_select([
|
|
26
|
+
['All Time', ''],
|
|
27
|
+
['Last Hour', 'last_hour'],
|
|
28
|
+
['Last 24 Hours', 'last_24h'],
|
|
29
|
+
['Last 7 Days', 'last_7d'],
|
|
30
|
+
['Last 30 Days', 'last_30d']
|
|
31
|
+
], params[:time_range]),
|
|
32
|
+
prompt: 'Select time range',
|
|
33
|
+
style: "width: 100%;" %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Event Type -->
|
|
37
|
+
<div class="form-group">
|
|
38
|
+
<label>Event Type</label>
|
|
39
|
+
<%= select_tag :event_type,
|
|
40
|
+
options_for_select(@event_types, params[:event_type]),
|
|
41
|
+
prompt: 'All event types',
|
|
42
|
+
style: "width: 100%;" %>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Risk Level -->
|
|
46
|
+
<div class="form-group">
|
|
47
|
+
<label>Risk Level</label>
|
|
48
|
+
<%= select_tag :risk_level,
|
|
49
|
+
options_for_select([
|
|
50
|
+
['Low (0-30)', 'low'],
|
|
51
|
+
['Medium (31-60)', 'medium'],
|
|
52
|
+
['High (61-85)', 'high'],
|
|
53
|
+
['Critical (86-100)', 'critical']
|
|
54
|
+
], params[:risk_level]),
|
|
55
|
+
prompt: 'All risk levels',
|
|
56
|
+
style: "width: 100%;" %>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- IP Address -->
|
|
60
|
+
<div class="form-group">
|
|
61
|
+
<label>IP Address</label>
|
|
62
|
+
<%= text_field_tag :ip_address, params[:ip_address],
|
|
63
|
+
placeholder: "e.g., 192.168.1.1" %>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Email -->
|
|
67
|
+
<div class="form-group">
|
|
68
|
+
<label>User Email</label>
|
|
69
|
+
<%= text_field_tag :email, params[:email],
|
|
70
|
+
placeholder: "user@example.com" %>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- Search -->
|
|
74
|
+
<div class="form-group">
|
|
75
|
+
<label>Search</label>
|
|
76
|
+
<%= text_field_tag :search, params[:search],
|
|
77
|
+
placeholder: "Search in all fields..." %>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<!-- Date Range -->
|
|
81
|
+
<div class="form-group">
|
|
82
|
+
<label>Start Date</label>
|
|
83
|
+
<%= date_field_tag :start_date, params[:start_date] %>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="form-group">
|
|
87
|
+
<label>End Date</label>
|
|
88
|
+
<%= date_field_tag :end_date, params[:end_date] %>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div style="display: flex; gap: 0.5rem; margin-top: 1rem;">
|
|
93
|
+
<%= submit_tag "Apply Filters", class: "btn btn-primary" %>
|
|
94
|
+
<%= link_to "Clear Filters", security_events_path, class: "btn btn-secondary" %>
|
|
95
|
+
|
|
96
|
+
<div style="margin-left: auto; display: flex; align-items: center; gap: 0.5rem;">
|
|
97
|
+
<%= check_box_tag :threats_only, 'true', params[:threats_only] == 'true' %>
|
|
98
|
+
<label for="threats_only" style="margin: 0; font-size: 13px;">High threats only</label>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
<% end %>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Results Summary -->
|
|
106
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
|
107
|
+
<div style="font-size: 14px; color: #697386;">
|
|
108
|
+
Showing <%= @pagination[:records].count %> of <%= number_with_delimiter(@pagination[:total_count]) %> events
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
112
|
+
<label style="margin: 0; font-size: 13px; color: #697386;">Per page:</label>
|
|
113
|
+
<%= form_with url: security_events_path, method: :get, local: true, style: "display: inline;" do |f| %>
|
|
114
|
+
<% request.query_parameters.except(:per_page).each do |key, value| %>
|
|
115
|
+
<%= hidden_field_tag key, value %>
|
|
116
|
+
<% end %>
|
|
117
|
+
<%= select_tag :per_page,
|
|
118
|
+
options_for_select([10, 25, 50, 100], params[:per_page]&.to_i || 25),
|
|
119
|
+
onchange: "this.form.submit();",
|
|
120
|
+
style: "padding: 0.25rem 0.5rem; font-size: 13px;" %>
|
|
121
|
+
<% end %>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<!-- Events Table -->
|
|
126
|
+
<div class="card">
|
|
127
|
+
<div class="card-body" style="padding: 0;">
|
|
128
|
+
<div class="table-container">
|
|
129
|
+
<table>
|
|
130
|
+
<thead>
|
|
131
|
+
<tr>
|
|
132
|
+
<th>ID</th>
|
|
133
|
+
<th>Timestamp</th>
|
|
134
|
+
<th>Event Type</th>
|
|
135
|
+
<th>IP Address</th>
|
|
136
|
+
<th>User</th>
|
|
137
|
+
<th>Risk Score</th>
|
|
138
|
+
<th>User Agent</th>
|
|
139
|
+
<th>Actions</th>
|
|
140
|
+
</tr>
|
|
141
|
+
</thead>
|
|
142
|
+
<tbody>
|
|
143
|
+
<% if @events.any? %>
|
|
144
|
+
<% @events.each do |event| %>
|
|
145
|
+
<tr>
|
|
146
|
+
<td>
|
|
147
|
+
<span style="font-family: monospace; font-size: 12px; color: #697386;">
|
|
148
|
+
#<%= event.id %>
|
|
149
|
+
</span>
|
|
150
|
+
</td>
|
|
151
|
+
<td>
|
|
152
|
+
<div style="font-size: 13px;">
|
|
153
|
+
<%= event.created_at.strftime("%Y-%m-%d") %>
|
|
154
|
+
</div>
|
|
155
|
+
<div style="font-size: 11px; color: #697386;">
|
|
156
|
+
<%= event.created_at.strftime("%H:%M:%S %Z") %>
|
|
157
|
+
</div>
|
|
158
|
+
</td>
|
|
159
|
+
<td>
|
|
160
|
+
<div style="font-size: 13px; font-weight: 500;">
|
|
161
|
+
<%= format_event_type(event.event_type) %>
|
|
162
|
+
</div>
|
|
163
|
+
<div style="font-size: 11px; color: #697386;">
|
|
164
|
+
<%= event.event_type %>
|
|
165
|
+
</div>
|
|
166
|
+
</td>
|
|
167
|
+
<td>
|
|
168
|
+
<div style="font-family: monospace; font-size: 13px;">
|
|
169
|
+
<%= event.ip_address %>
|
|
170
|
+
</div>
|
|
171
|
+
<% if event.metadata&.dig("geolocation", "country") %>
|
|
172
|
+
<div style="font-size: 11px; color: #697386;">
|
|
173
|
+
<%= [event.metadata.dig("geolocation", "city"),
|
|
174
|
+
event.metadata.dig("geolocation", "country")].compact.join(", ") %>
|
|
175
|
+
</div>
|
|
176
|
+
<% end %>
|
|
177
|
+
</td>
|
|
178
|
+
<td>
|
|
179
|
+
<% if event.user %>
|
|
180
|
+
<div style="font-size: 13px;">
|
|
181
|
+
<%= event.user.try(:email) || "User ##{event.user_id}" %>
|
|
182
|
+
</div>
|
|
183
|
+
<div style="font-size: 11px; color: #697386;">
|
|
184
|
+
ID: <%= event.user_id %>
|
|
185
|
+
</div>
|
|
186
|
+
<% elsif event.attempted_email %>
|
|
187
|
+
<div style="font-size: 13px; color: #697386;">
|
|
188
|
+
<%= event.attempted_email %>
|
|
189
|
+
</div>
|
|
190
|
+
<div style="font-size: 11px; color: #C1C7D0;">
|
|
191
|
+
(attempted)
|
|
192
|
+
</div>
|
|
193
|
+
<% else %>
|
|
194
|
+
<span style="color: #C1C7D0;">-</span>
|
|
195
|
+
<% end %>
|
|
196
|
+
</td>
|
|
197
|
+
<td>
|
|
198
|
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
199
|
+
<span class="badge badge-<%= risk_level_class(event.risk_score) %>">
|
|
200
|
+
<%= event.risk_score %>
|
|
201
|
+
</span>
|
|
202
|
+
<% if event.critical_threat? %>
|
|
203
|
+
<svg width="16" height="16" fill="#D32F2F" viewBox="0 0 24 24">
|
|
204
|
+
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
|
|
205
|
+
</svg>
|
|
206
|
+
<% elsif event.high_risk? %>
|
|
207
|
+
<svg width="16" height="16" fill="#E91E63" viewBox="0 0 24 24">
|
|
208
|
+
<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"/>
|
|
209
|
+
</svg>
|
|
210
|
+
<% end %>
|
|
211
|
+
</div>
|
|
212
|
+
</td>
|
|
213
|
+
<td>
|
|
214
|
+
<div style="font-size: 11px; color: #697386; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
|
215
|
+
<%= event.user_agent || "-" %>
|
|
216
|
+
</div>
|
|
217
|
+
</td>
|
|
218
|
+
<td>
|
|
219
|
+
<div style="display: flex; gap: 0.25rem;">
|
|
220
|
+
<%= link_to "View", security_event_path(event),
|
|
221
|
+
class: "btn btn-sm btn-secondary" %>
|
|
222
|
+
|
|
223
|
+
<% unless Beskar::BannedIp.banned?(event.ip_address) %>
|
|
224
|
+
<%= link_to "Ban IP", new_banned_ip_path(ip_address: event.ip_address, reason: "suspicious_activity"),
|
|
225
|
+
class: "btn btn-sm btn-danger" %>
|
|
226
|
+
<% end %>
|
|
227
|
+
</div>
|
|
228
|
+
</td>
|
|
229
|
+
</tr>
|
|
230
|
+
<% end %>
|
|
231
|
+
<% else %>
|
|
232
|
+
<tr>
|
|
233
|
+
<td colspan="8" style="text-align: center; padding: 3rem;">
|
|
234
|
+
<div style="color: #697386;">
|
|
235
|
+
<svg width="48" height="48" fill="currentColor" viewBox="0 0 24 24" style="margin: 0 auto 1rem;">
|
|
236
|
+
<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"/>
|
|
237
|
+
</svg>
|
|
238
|
+
<div style="font-size: 16px; font-weight: 500; margin-bottom: 0.5rem;">No security events found</div>
|
|
239
|
+
<div style="font-size: 14px;">Try adjusting your filters or time range</div>
|
|
240
|
+
</div>
|
|
241
|
+
</td>
|
|
242
|
+
</tr>
|
|
243
|
+
<% end %>
|
|
244
|
+
</tbody>
|
|
245
|
+
</table>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<!-- Pagination -->
|
|
251
|
+
<% if @pagination[:total_pages] > 1 %>
|
|
252
|
+
<div class="pagination">
|
|
253
|
+
<% if @pagination[:has_previous] %>
|
|
254
|
+
<%= link_to "← Previous", security_events_path(params.to_unsafe_h.merge(page: @pagination[:previous_page])),
|
|
255
|
+
class: "pagination-link" %>
|
|
256
|
+
<% else %>
|
|
257
|
+
<span class="pagination-link disabled">← Previous</span>
|
|
258
|
+
<% end %>
|
|
259
|
+
|
|
260
|
+
<% # Show page numbers with ellipsis for large page counts %>
|
|
261
|
+
<% if @pagination[:total_pages] <= 7 %>
|
|
262
|
+
<% (1..@pagination[:total_pages]).each do |page| %>
|
|
263
|
+
<%= link_to page, security_events_path(params.to_unsafe_h.merge(page: page)),
|
|
264
|
+
class: "pagination-link #{'active' if page == @pagination[:current_page]}" %>
|
|
265
|
+
<% end %>
|
|
266
|
+
<% else %>
|
|
267
|
+
<% if @pagination[:current_page] <= 4 %>
|
|
268
|
+
<% (1..5).each do |page| %>
|
|
269
|
+
<%= link_to page, security_events_path(params.to_unsafe_h.merge(page: page)),
|
|
270
|
+
class: "pagination-link #{'active' if page == @pagination[:current_page]}" %>
|
|
271
|
+
<% end %>
|
|
272
|
+
<span class="pagination-link disabled">...</span>
|
|
273
|
+
<%= link_to @pagination[:total_pages], security_events_path(params.to_unsafe_h.merge(page: @pagination[:total_pages])),
|
|
274
|
+
class: "pagination-link" %>
|
|
275
|
+
<% elsif @pagination[:current_page] >= @pagination[:total_pages] - 3 %>
|
|
276
|
+
<%= link_to 1, security_events_path(params.to_unsafe_h.merge(page: 1)),
|
|
277
|
+
class: "pagination-link" %>
|
|
278
|
+
<span class="pagination-link disabled">...</span>
|
|
279
|
+
<% ((@pagination[:total_pages] - 4)..@pagination[:total_pages]).each do |page| %>
|
|
280
|
+
<%= link_to page, security_events_path(params.to_unsafe_h.merge(page: page)),
|
|
281
|
+
class: "pagination-link #{'active' if page == @pagination[:current_page]}" %>
|
|
282
|
+
<% end %>
|
|
283
|
+
<% else %>
|
|
284
|
+
<%= link_to 1, security_events_path(params.to_unsafe_h.merge(page: 1)),
|
|
285
|
+
class: "pagination-link" %>
|
|
286
|
+
<span class="pagination-link disabled">...</span>
|
|
287
|
+
<% ((@pagination[:current_page] - 1)..(@pagination[:current_page] + 1)).each do |page| %>
|
|
288
|
+
<%= link_to page, security_events_path(params.to_unsafe_h.merge(page: page)),
|
|
289
|
+
class: "pagination-link #{'active' if page == @pagination[:current_page]}" %>
|
|
290
|
+
<% end %>
|
|
291
|
+
<span class="pagination-link disabled">...</span>
|
|
292
|
+
<%= link_to @pagination[:total_pages], security_events_path(params.to_unsafe_h.merge(page: @pagination[:total_pages])),
|
|
293
|
+
class: "pagination-link" %>
|
|
294
|
+
<% end %>
|
|
295
|
+
<% end %>
|
|
296
|
+
|
|
297
|
+
<% if @pagination[:has_next] %>
|
|
298
|
+
<%= link_to "Next →", security_events_path(params.to_unsafe_h.merge(page: @pagination[:next_page])),
|
|
299
|
+
class: "pagination-link" %>
|
|
300
|
+
<% else %>
|
|
301
|
+
<span class="pagination-link disabled">Next →</span>
|
|
302
|
+
<% end %>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div style="text-align: center; margin-top: 1rem; font-size: 13px; color: #697386;">
|
|
306
|
+
Page <%= @pagination[:current_page] %> of <%= @pagination[:total_pages] %> •
|
|
307
|
+
Total: <%= number_with_delimiter(@pagination[:total_count]) %> events
|
|
308
|
+
</div>
|
|
309
|
+
<% end %>
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
<% content_for :page_title, "Security Event ##{@event.id}" %>
|
|
2
|
+
|
|
3
|
+
<% content_for :header_actions do %>
|
|
4
|
+
<%= link_to "← Back to Events", security_events_path, class: "btn btn-secondary" %>
|
|
5
|
+
<% unless Beskar::BannedIp.banned?(@event.ip_address) %>
|
|
6
|
+
<%= link_to "Ban This IP", new_banned_ip_path(ip_address: @event.ip_address, reason: "security_event_#{@event.id}"),
|
|
7
|
+
class: "btn btn-danger" %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<!-- Event Overview -->
|
|
12
|
+
<div class="card">
|
|
13
|
+
<div class="card-header">
|
|
14
|
+
<h3 class="card-title">Event Details</h3>
|
|
15
|
+
<div class="card-subtitle">Event ID: <%= @event.id %></div>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="card-body">
|
|
18
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem;">
|
|
19
|
+
<!-- Event Type -->
|
|
20
|
+
<div>
|
|
21
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
22
|
+
Event Type
|
|
23
|
+
</div>
|
|
24
|
+
<div style="font-size: 16px; font-weight: 500; color: #1A1F36;">
|
|
25
|
+
<%= format_event_type(@event.event_type) %>
|
|
26
|
+
</div>
|
|
27
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.25rem;">
|
|
28
|
+
<%= @event.event_type %>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- Risk Score -->
|
|
33
|
+
<div>
|
|
34
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
35
|
+
Risk Score
|
|
36
|
+
</div>
|
|
37
|
+
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
|
38
|
+
<span style="font-size: 24px; font-weight: 600; color: <%=
|
|
39
|
+
case @event.risk_score
|
|
40
|
+
when 0..30 then '#4CAF50'
|
|
41
|
+
when 31..60 then '#FF9800'
|
|
42
|
+
when 61..85 then '#E91E63'
|
|
43
|
+
else '#D32F2F'
|
|
44
|
+
end
|
|
45
|
+
%>;">
|
|
46
|
+
<%= @event.risk_score %>
|
|
47
|
+
</span>
|
|
48
|
+
<span class="badge badge-<%= risk_level_class(@event.risk_score) %>">
|
|
49
|
+
<%=
|
|
50
|
+
case @event.risk_score
|
|
51
|
+
when 0..30 then 'Low Risk'
|
|
52
|
+
when 31..60 then 'Medium Risk'
|
|
53
|
+
when 61..85 then 'High Risk'
|
|
54
|
+
else 'Critical'
|
|
55
|
+
end
|
|
56
|
+
%>
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Timestamp -->
|
|
62
|
+
<div>
|
|
63
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
64
|
+
Occurred At
|
|
65
|
+
</div>
|
|
66
|
+
<div style="font-size: 14px; color: #1A1F36;">
|
|
67
|
+
<%= @event.created_at.strftime("%B %d, %Y") %>
|
|
68
|
+
</div>
|
|
69
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.25rem;">
|
|
70
|
+
<%= @event.created_at.strftime("%H:%M:%S %Z") %>
|
|
71
|
+
(<%= time_ago_in_words(@event.created_at) %> ago)
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- IP Address -->
|
|
76
|
+
<div>
|
|
77
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
78
|
+
IP Address
|
|
79
|
+
</div>
|
|
80
|
+
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
|
81
|
+
<span style="font-family: monospace; font-size: 16px; color: #1A1F36;">
|
|
82
|
+
<%= @event.ip_address %>
|
|
83
|
+
</span>
|
|
84
|
+
<% if @ip_ban %>
|
|
85
|
+
<span class="badge badge-danger">Banned</span>
|
|
86
|
+
<% end %>
|
|
87
|
+
</div>
|
|
88
|
+
<% if @event.metadata&.dig("geolocation") %>
|
|
89
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.25rem;">
|
|
90
|
+
<%= [@event.metadata.dig("geolocation", "city"),
|
|
91
|
+
@event.metadata.dig("geolocation", "region"),
|
|
92
|
+
@event.metadata.dig("geolocation", "country")].compact.join(", ") %>
|
|
93
|
+
</div>
|
|
94
|
+
<% end %>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- User -->
|
|
98
|
+
<div>
|
|
99
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
100
|
+
User
|
|
101
|
+
</div>
|
|
102
|
+
<% if @event.user %>
|
|
103
|
+
<div style="font-size: 14px; color: #1A1F36;">
|
|
104
|
+
<%= @event.user.try(:email) || "User ##{@event.user_id}" %>
|
|
105
|
+
</div>
|
|
106
|
+
<div style="font-size: 13px; color: #697386; margin-top: 0.25rem;">
|
|
107
|
+
ID: <%= @event.user_id %> • Type: <%= @event.user_type %>
|
|
108
|
+
</div>
|
|
109
|
+
<% elsif @event.attempted_email %>
|
|
110
|
+
<div style="font-size: 14px; color: #697386;">
|
|
111
|
+
<%= @event.attempted_email %> <span style="font-size: 12px;">(attempted)</span>
|
|
112
|
+
</div>
|
|
113
|
+
<% else %>
|
|
114
|
+
<div style="color: #C1C7D0;">No user associated</div>
|
|
115
|
+
<% end %>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<!-- Details -->
|
|
119
|
+
<% if @event.details.present? || @event.metadata&.dig("message").present? %>
|
|
120
|
+
<div>
|
|
121
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
122
|
+
Details
|
|
123
|
+
</div>
|
|
124
|
+
<div style="font-size: 14px; color: #1A1F36;">
|
|
125
|
+
<%= @event.details || @event.metadata&.dig("message") || "No additional details" %>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<% end %>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<!-- User Agent & Device Info -->
|
|
134
|
+
<div class="card">
|
|
135
|
+
<div class="card-header">
|
|
136
|
+
<h3 class="card-title">Device & Browser Information</h3>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="card-body">
|
|
139
|
+
<div style="display: grid; grid-template-columns: 1fr; gap: 1rem;">
|
|
140
|
+
<!-- User Agent -->
|
|
141
|
+
<div>
|
|
142
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386; margin-bottom: 0.5rem;">
|
|
143
|
+
User Agent
|
|
144
|
+
</div>
|
|
145
|
+
<div style="font-family: monospace; font-size: 13px; color: #1A1F36; background: #F4F5F7; padding: 0.75rem; border-radius: 6px; word-break: break-all;">
|
|
146
|
+
<%= @event.user_agent || "Not recorded" %>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<% if @event.device_info.present? %>
|
|
151
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;">
|
|
152
|
+
<% @event.device_info.each do |key, value| %>
|
|
153
|
+
<div>
|
|
154
|
+
<div style="font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.025em; color: #697386;">
|
|
155
|
+
<%= key.to_s.humanize %>
|
|
156
|
+
</div>
|
|
157
|
+
<div style="font-size: 14px; color: #1A1F36; margin-top: 0.25rem;">
|
|
158
|
+
<%= value || "-" %>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
<% end %>
|
|
162
|
+
</div>
|
|
163
|
+
<% end %>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- Metadata -->
|
|
169
|
+
<% if @event.metadata.present? && @event.metadata.any? %>
|
|
170
|
+
<div class="card">
|
|
171
|
+
<div class="card-header">
|
|
172
|
+
<h3 class="card-title">Additional Metadata</h3>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="card-body">
|
|
175
|
+
<pre style="font-family: monospace; font-size: 12px; background: #F4F5F7; padding: 1rem; border-radius: 6px; overflow-x: auto;"><%= JSON.pretty_generate(@event.metadata) %></pre>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
<% end %>
|
|
179
|
+
|
|
180
|
+
<!-- Related Events -->
|
|
181
|
+
<% if @related_events.any? %>
|
|
182
|
+
<div class="card">
|
|
183
|
+
<div class="card-header">
|
|
184
|
+
<h3 class="card-title">Related Events from Same IP</h3>
|
|
185
|
+
<div class="card-subtitle"><%= @related_events.count %> other events from <%= @event.ip_address %></div>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="card-body" style="padding: 0;">
|
|
188
|
+
<div class="table-container">
|
|
189
|
+
<table>
|
|
190
|
+
<thead>
|
|
191
|
+
<tr>
|
|
192
|
+
<th>Time</th>
|
|
193
|
+
<th>Event Type</th>
|
|
194
|
+
<th>User</th>
|
|
195
|
+
<th>Risk Score</th>
|
|
196
|
+
<th>Actions</th>
|
|
197
|
+
</tr>
|
|
198
|
+
</thead>
|
|
199
|
+
<tbody>
|
|
200
|
+
<% @related_events.each do |event| %>
|
|
201
|
+
<tr>
|
|
202
|
+
<td>
|
|
203
|
+
<div style="font-size: 13px;">
|
|
204
|
+
<%= event.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
205
|
+
</div>
|
|
206
|
+
<div style="font-size: 11px; color: #697386;">
|
|
207
|
+
<%= time_ago_in_words(event.created_at) %> ago
|
|
208
|
+
</div>
|
|
209
|
+
</td>
|
|
210
|
+
<td>
|
|
211
|
+
<div style="font-size: 13px;">
|
|
212
|
+
<%= format_event_type(event.event_type) %>
|
|
213
|
+
</div>
|
|
214
|
+
</td>
|
|
215
|
+
<td>
|
|
216
|
+
<% if event.user %>
|
|
217
|
+
<%= event.user.try(:email) || "User ##{event.user_id}" %>
|
|
218
|
+
<% elsif event.attempted_email %>
|
|
219
|
+
<span style="color: #697386;"><%= event.attempted_email %></span>
|
|
220
|
+
<% else %>
|
|
221
|
+
<span style="color: #C1C7D0;">-</span>
|
|
222
|
+
<% end %>
|
|
223
|
+
</td>
|
|
224
|
+
<td>
|
|
225
|
+
<span class="badge badge-<%= risk_level_class(event.risk_score) %>">
|
|
226
|
+
<%= event.risk_score %>
|
|
227
|
+
</span>
|
|
228
|
+
</td>
|
|
229
|
+
<td>
|
|
230
|
+
<%= link_to "View", security_event_path(event), class: "btn btn-sm btn-secondary" %>
|
|
231
|
+
</td>
|
|
232
|
+
</tr>
|
|
233
|
+
<% end %>
|
|
234
|
+
</tbody>
|
|
235
|
+
</table>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
<% end %>
|
|
240
|
+
|
|
241
|
+
<!-- User Events -->
|
|
242
|
+
<% if @user_events&.any? %>
|
|
243
|
+
<div class="card">
|
|
244
|
+
<div class="card-header">
|
|
245
|
+
<h3 class="card-title">User's Recent Events</h3>
|
|
246
|
+
<div class="card-subtitle"><%= @user_events.count %> other events from this user</div>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="card-body" style="padding: 0;">
|
|
249
|
+
<div class="table-container">
|
|
250
|
+
<table>
|
|
251
|
+
<thead>
|
|
252
|
+
<tr>
|
|
253
|
+
<th>Time</th>
|
|
254
|
+
<th>Event Type</th>
|
|
255
|
+
<th>IP Address</th>
|
|
256
|
+
<th>Risk Score</th>
|
|
257
|
+
<th>Actions</th>
|
|
258
|
+
</tr>
|
|
259
|
+
</thead>
|
|
260
|
+
<tbody>
|
|
261
|
+
<% @user_events.each do |event| %>
|
|
262
|
+
<tr>
|
|
263
|
+
<td>
|
|
264
|
+
<div style="font-size: 13px;">
|
|
265
|
+
<%= event.created_at.strftime("%Y-%m-%d %H:%M:%S") %>
|
|
266
|
+
</div>
|
|
267
|
+
<div style="font-size: 11px; color: #697386;">
|
|
268
|
+
<%= time_ago_in_words(event.created_at) %> ago
|
|
269
|
+
</div>
|
|
270
|
+
</td>
|
|
271
|
+
<td>
|
|
272
|
+
<div style="font-size: 13px;">
|
|
273
|
+
<%= format_event_type(event.event_type) %>
|
|
274
|
+
</div>
|
|
275
|
+
</td>
|
|
276
|
+
<td>
|
|
277
|
+
<div style="font-family: monospace; font-size: 13px;">
|
|
278
|
+
<%= event.ip_address %>
|
|
279
|
+
</div>
|
|
280
|
+
</td>
|
|
281
|
+
<td>
|
|
282
|
+
<span class="badge badge-<%= risk_level_class(event.risk_score) %>">
|
|
283
|
+
<%= event.risk_score %>
|
|
284
|
+
</span>
|
|
285
|
+
</td>
|
|
286
|
+
<td>
|
|
287
|
+
<%= link_to "View", security_event_path(event), class: "btn btn-sm btn-secondary" %>
|
|
288
|
+
</td>
|
|
289
|
+
</tr>
|
|
290
|
+
<% end %>
|
|
291
|
+
</tbody>
|
|
292
|
+
</table>
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
<% end %>
|
|
297
|
+
|
|
298
|
+
<!-- Actions -->
|
|
299
|
+
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
|
|
300
|
+
<%= link_to "← Back to Events", security_events_path, class: "btn btn-secondary" %>
|
|
301
|
+
<% unless @ip_ban %>
|
|
302
|
+
<%= link_to "Ban This IP", new_banned_ip_path(ip_address: @event.ip_address, reason: "security_event_#{@event.id}"),
|
|
303
|
+
class: "btn btn-danger" %>
|
|
304
|
+
<% else %>
|
|
305
|
+
<%= link_to "View Ban Details", banned_ip_path(@ip_ban), class: "btn btn-primary" %>
|
|
306
|
+
<% end %>
|
|
307
|
+
</div>
|