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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +274 -0
  3. data/README.md +412 -204
  4. data/app/channels/concerns/beskar/channels/session_security.rb +46 -0
  5. data/app/controllers/beskar/administrative_actions_controller.rb +16 -0
  6. data/app/controllers/beskar/application_controller.rb +214 -0
  7. data/app/controllers/beskar/banned_ips_controller.rb +255 -0
  8. data/app/controllers/beskar/dashboard_controller.rb +62 -0
  9. data/app/controllers/beskar/security_events_controller.rb +164 -0
  10. data/app/controllers/concerns/beskar/controllers/audit_export.rb +54 -0
  11. data/app/controllers/concerns/beskar/controllers/security_tracking.rb +76 -48
  12. data/app/controllers/concerns/beskar/controllers/session_security.rb +29 -0
  13. data/app/jobs/beskar/notification_job.rb +33 -0
  14. data/app/mailers/beskar/security_mailer.rb +59 -0
  15. data/app/models/beskar/administrative_action.rb +41 -0
  16. data/app/models/beskar/banned_ip.rb +105 -105
  17. data/app/models/beskar/security_event.rb +51 -4
  18. data/app/models/beskar/security_state.rb +58 -0
  19. data/app/services/beskar/banned_ip_manager.rb +88 -0
  20. data/app/views/beskar/administrative_actions/index.html.erb +33 -0
  21. data/app/views/beskar/administrative_actions/show.html.erb +21 -0
  22. data/app/views/beskar/banned_ips/edit.html.erb +195 -0
  23. data/app/views/beskar/banned_ips/index.html.erb +319 -0
  24. data/app/views/beskar/banned_ips/new.html.erb +190 -0
  25. data/app/views/beskar/banned_ips/review.html.erb +24 -0
  26. data/app/views/beskar/banned_ips/show.html.erb +304 -0
  27. data/app/views/beskar/dashboard/index.html.erb +280 -0
  28. data/app/views/beskar/security_events/index.html.erb +302 -0
  29. data/app/views/beskar/security_events/show.html.erb +293 -0
  30. data/app/views/beskar/shared/_export_form.html.erb +10 -0
  31. data/app/views/layouts/beskar/_behavior.html.erb +121 -0
  32. data/app/views/layouts/beskar/application.html.erb +581 -6
  33. data/config/routes.rb +30 -0
  34. data/db/migrate/20251016000001_create_beskar_security_events.rb +3 -3
  35. data/db/migrate/20260910000001_create_beskar_security_states.rb +14 -0
  36. data/db/migrate/20260911000001_create_beskar_administrative_actions.rb +22 -0
  37. data/db/migrate/20260911000002_expand_administrative_action_targets.rb +6 -0
  38. data/docs/README.md +73 -0
  39. data/docs/archive/project-documentation.md +659 -0
  40. data/docs/audits/project-review.md +437 -0
  41. data/docs/audits/repair-status.md +216 -0
  42. data/docs/guides/audit-and-waf.md +175 -0
  43. data/docs/guides/audit-lifecycle.md +172 -0
  44. data/docs/guides/authentication.md +213 -0
  45. data/docs/guides/configuration.md +182 -0
  46. data/docs/guides/dashboard-and-search.md +251 -0
  47. data/docs/guides/notifications-and-recovery.md +157 -0
  48. data/docs/guides/risk-scoring.md +116 -0
  49. data/docs/operations/monitor-only-mode.md +85 -0
  50. data/docs/operations/security-hardening.md +167 -0
  51. data/docs/operations/state-storage.md +144 -0
  52. data/docs/research/rust-performance-assessment.md +69 -0
  53. data/lib/beskar/configuration.rb +105 -20
  54. data/lib/beskar/configuration_validator.rb +188 -0
  55. data/lib/beskar/devise_authentication.rb +24 -0
  56. data/lib/beskar/engine.rb +21 -88
  57. data/lib/beskar/logger.rb +288 -0
  58. data/lib/beskar/middleware/request_analyzer.rb +133 -99
  59. data/lib/beskar/models/security_trackable_authenticable.rb +76 -97
  60. data/lib/beskar/models/security_trackable_devise.rb +34 -25
  61. data/lib/beskar/models/security_trackable_generic.rb +171 -214
  62. data/lib/beskar/risk_level.rb +22 -0
  63. data/lib/beskar/services/account_locker.rb +90 -81
  64. data/lib/beskar/services/administrative_audit.rb +36 -0
  65. data/lib/beskar/services/administrative_bans.rb +104 -0
  66. data/lib/beskar/services/audit_data.rb +72 -0
  67. data/lib/beskar/services/authentication.rb +31 -0
  68. data/lib/beskar/services/authentication_attempt.rb +141 -0
  69. data/lib/beskar/services/ban_expiry.rb +28 -0
  70. data/lib/beskar/services/device_detector.rb +32 -41
  71. data/lib/beskar/services/event_search.rb +58 -0
  72. data/lib/beskar/services/geolocation_service.rb +83 -114
  73. data/lib/beskar/services/ip_whitelist.rb +31 -40
  74. data/lib/beskar/services/location_assessment.rb +109 -0
  75. data/lib/beskar/services/native_account_lock.rb +82 -0
  76. data/lib/beskar/services/notifications.rb +46 -0
  77. data/lib/beskar/services/rate_limiter.rb +99 -125
  78. data/lib/beskar/services/request_context.rb +64 -0
  79. data/lib/beskar/services/risk_assessment.rb +58 -0
  80. data/lib/beskar/services/session_revocation.rb +62 -0
  81. data/lib/beskar/services/waf.rb +311 -198
  82. data/lib/beskar/services/waf_request.rb +60 -0
  83. data/lib/beskar/version.rb +1 -1
  84. data/lib/beskar/warden_authentication.rb +53 -0
  85. data/lib/beskar.rb +54 -4
  86. data/lib/generators/beskar/install/install_generator.rb +158 -0
  87. data/lib/generators/beskar/install/templates/initializer.rb.tt +261 -0
  88. data/lib/tasks/beskar_tasks.rake +25 -20
  89. metadata +93 -12
  90. data/lib/beskar/templates/beskar_initializer.rb +0 -107
@@ -1,17 +1,592 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <html lang="en">
3
3
  <head>
4
- <title>Beskar</title>
4
+ <title>Beskar Security Dashboard</title>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
7
  <%= csrf_meta_tags %>
6
8
  <%= csp_meta_tag %>
7
9
 
8
- <%= yield :head %>
10
+ <!-- Embedded Styles (No external dependencies) -->
11
+ <style nonce="<%= content_security_policy_nonce %>">
12
+ :root {
13
+ --primary: #635BFF;
14
+ --primary-dark: #5243CC;
15
+ --danger: #E91E63;
16
+ --warning: #FF9800;
17
+ --success: #4CAF50;
18
+ --neutral: #6B7280;
19
+ --sidebar-width: 240px;
20
+ --header-height: 60px;
21
+ }
22
+
23
+ * {
24
+ margin: 0;
25
+ padding: 0;
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ body {
30
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
31
+ font-size: 14px;
32
+ line-height: 1.5;
33
+ color: #1A1F36;
34
+ background: #F6F9FC;
35
+ }
36
+
37
+ .dashboard-container {
38
+ display: flex;
39
+ min-height: 100vh;
40
+ }
41
+
42
+ /* Sidebar */
43
+ .sidebar {
44
+ width: var(--sidebar-width);
45
+ background: white;
46
+ border-right: 1px solid #E3E8EE;
47
+ position: fixed;
48
+ height: 100vh;
49
+ overflow-y: auto;
50
+ z-index: 100;
51
+ }
52
+
53
+ .sidebar-header {
54
+ padding: 1.5rem;
55
+ border-bottom: 1px solid #E3E8EE;
56
+ }
57
+
58
+ .sidebar-logo {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 0.75rem;
62
+ text-decoration: none;
63
+ color: #1A1F36;
64
+ }
65
+
66
+ .sidebar-logo-icon {
67
+ width: 32px;
68
+ height: 32px;
69
+ background: var(--primary);
70
+ border-radius: 8px;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ color: white;
75
+ font-weight: bold;
76
+ font-size: 18px;
77
+ }
78
+
79
+ .sidebar-logo-text {
80
+ font-size: 18px;
81
+ font-weight: 600;
82
+ letter-spacing: -0.02em;
83
+ }
84
+
85
+ .sidebar-nav {
86
+ padding: 1rem 0;
87
+ }
88
+
89
+ .nav-section {
90
+ padding: 0.5rem 1rem;
91
+ margin-bottom: 0.25rem;
92
+ }
93
+
94
+ .nav-section-title {
95
+ font-size: 11px;
96
+ font-weight: 600;
97
+ text-transform: uppercase;
98
+ letter-spacing: 0.05em;
99
+ color: #8792A2;
100
+ margin-bottom: 0.5rem;
101
+ }
102
+
103
+ .nav-link {
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 0.75rem;
107
+ padding: 0.625rem 1rem;
108
+ text-decoration: none;
109
+ color: #697386;
110
+ border-radius: 6px;
111
+ transition: all 0.15s ease;
112
+ margin-bottom: 0.125rem;
113
+ }
114
+
115
+ .nav-link:hover {
116
+ background: #F7FAFC;
117
+ color: #1A1F36;
118
+ }
119
+
120
+ .nav-link.active {
121
+ background: #F7F6FF;
122
+ color: var(--primary);
123
+ font-weight: 500;
124
+ }
125
+
126
+ .nav-icon {
127
+ width: 18px;
128
+ height: 18px;
129
+ opacity: 0.8;
130
+ }
131
+
132
+ /* Main Content */
133
+ .main-content {
134
+ flex: 1;
135
+ margin-left: var(--sidebar-width);
136
+ display: flex;
137
+ flex-direction: column;
138
+ }
139
+
140
+ /* Header */
141
+ .header {
142
+ height: var(--header-height);
143
+ background: white;
144
+ border-bottom: 1px solid #E3E8EE;
145
+ display: flex;
146
+ align-items: center;
147
+ justify-content: space-between;
148
+ padding: 0 2rem;
149
+ position: sticky;
150
+ top: 0;
151
+ z-index: 50;
152
+ }
153
+
154
+ .header-title {
155
+ font-size: 20px;
156
+ font-weight: 600;
157
+ color: #1A1F36;
158
+ }
159
+
160
+ .header-actions {
161
+ display: flex;
162
+ align-items: center;
163
+ gap: 1rem;
164
+ }
165
+
166
+ /* Content Area */
167
+ .content {
168
+ flex: 1;
169
+ padding: 2rem;
170
+ max-width: 1400px;
171
+ width: 100%;
172
+ margin: 0 auto;
173
+ }
174
+
175
+ /* Cards */
176
+ .card {
177
+ background: white;
178
+ border-radius: 8px;
179
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
180
+ margin-bottom: 1.5rem;
181
+ overflow: hidden;
182
+ }
183
+
184
+ .card-header {
185
+ padding: 1.25rem 1.5rem;
186
+ border-bottom: 1px solid #E3E8EE;
187
+ background: #FAFBFC;
188
+ }
189
+
190
+ .card-title {
191
+ font-size: 16px;
192
+ font-weight: 600;
193
+ color: #1A1F36;
194
+ margin: 0;
195
+ }
196
+
197
+ .card-subtitle {
198
+ font-size: 13px;
199
+ color: #697386;
200
+ margin-top: 0.25rem;
201
+ }
202
+
203
+ .card-body {
204
+ padding: 1.5rem;
205
+ }
206
+
207
+ /* Buttons */
208
+ .btn {
209
+ display: inline-flex;
210
+ align-items: center;
211
+ justify-content: center;
212
+ padding: 0.5rem 1rem;
213
+ font-size: 14px;
214
+ font-weight: 500;
215
+ border-radius: 6px;
216
+ border: none;
217
+ cursor: pointer;
218
+ text-decoration: none;
219
+ transition: all 0.15s ease;
220
+ gap: 0.5rem;
221
+ line-height: 1.5;
222
+ }
223
+
224
+ .btn-primary {
225
+ background: var(--primary);
226
+ color: white;
227
+ }
228
+
229
+ .btn-primary:hover {
230
+ background: var(--primary-dark);
231
+ }
232
+
233
+ .btn-secondary {
234
+ background: white;
235
+ color: #697386;
236
+ border: 1px solid #E3E8EE;
237
+ }
238
+
239
+ .btn-secondary:hover {
240
+ background: #F7FAFC;
241
+ border-color: #D1D9E0;
242
+ }
243
+
244
+ .btn-danger {
245
+ background: var(--danger);
246
+ color: white;
247
+ }
248
+
249
+ .btn-danger:hover {
250
+ background: #D81B60;
251
+ }
252
+
253
+ .btn-sm {
254
+ padding: 0.375rem 0.75rem;
255
+ font-size: 13px;
256
+ }
257
+
258
+ /* Badges */
259
+ .badge {
260
+ display: inline-flex;
261
+ align-items: center;
262
+ padding: 0.25rem 0.625rem;
263
+ font-size: 11px;
264
+ font-weight: 600;
265
+ text-transform: uppercase;
266
+ letter-spacing: 0.025em;
267
+ border-radius: 9999px;
268
+ }
269
+
270
+ .badge-success {
271
+ background: #E6F6E6;
272
+ color: #2E7D32;
273
+ }
274
+
275
+ .badge-warning {
276
+ background: #FFF3E0;
277
+ color: #E65100;
278
+ }
279
+
280
+ .badge-danger {
281
+ background: #FCE4EC;
282
+ color: #C2185B;
283
+ }
284
+
285
+ .badge-critical {
286
+ background: #E91E63;
287
+ color: white;
288
+ }
289
+
290
+ .badge-neutral {
291
+ background: #F4F5F7;
292
+ color: #697386;
293
+ }
9
294
 
10
- <%= stylesheet_link_tag "beskar/application", media: "all" %>
295
+ /* Tables */
296
+ .table-container {
297
+ overflow-x: auto;
298
+ }
299
+
300
+ table {
301
+ width: 100%;
302
+ border-collapse: collapse;
303
+ }
304
+
305
+ thead {
306
+ background: #FAFBFC;
307
+ border-top: 1px solid #E3E8EE;
308
+ border-bottom: 1px solid #E3E8EE;
309
+ }
310
+
311
+ th {
312
+ padding: 0.75rem 1rem;
313
+ text-align: left;
314
+ font-size: 12px;
315
+ font-weight: 600;
316
+ text-transform: uppercase;
317
+ letter-spacing: 0.025em;
318
+ color: #697386;
319
+ }
320
+
321
+ td {
322
+ padding: 1rem;
323
+ border-bottom: 1px solid #E3E8EE;
324
+ color: #1A1F36;
325
+ font-size: 14px;
326
+ }
327
+
328
+ tbody tr:hover {
329
+ background: #FAFBFC;
330
+ }
331
+
332
+ /* Stats Grid */
333
+ .stats-grid {
334
+ display: grid;
335
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
336
+ gap: 1.5rem;
337
+ margin-bottom: 2rem;
338
+ }
339
+
340
+ .stat-card {
341
+ background: white;
342
+ border-radius: 8px;
343
+ padding: 1.5rem;
344
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
345
+ }
346
+
347
+ .stat-label {
348
+ font-size: 12px;
349
+ font-weight: 600;
350
+ text-transform: uppercase;
351
+ letter-spacing: 0.025em;
352
+ color: #697386;
353
+ margin-bottom: 0.5rem;
354
+ }
355
+
356
+ .stat-value {
357
+ font-size: 28px;
358
+ font-weight: 600;
359
+ color: #1A1F36;
360
+ line-height: 1;
361
+ }
362
+
363
+ .stat-change {
364
+ font-size: 12px;
365
+ margin-top: 0.5rem;
366
+ color: #697386;
367
+ }
368
+
369
+ /* Forms */
370
+ .form-group {
371
+ margin-bottom: 1.25rem;
372
+ }
373
+
374
+ label {
375
+ display: block;
376
+ font-size: 13px;
377
+ font-weight: 500;
378
+ color: #1A1F36;
379
+ margin-bottom: 0.375rem;
380
+ }
381
+
382
+ input[type="text"],
383
+ input[type="email"],
384
+ input[type="password"],
385
+ input[type="number"],
386
+ input[type="date"],
387
+ input[type="datetime-local"],
388
+ select,
389
+ textarea {
390
+ width: 100%;
391
+ padding: 0.5rem 0.75rem;
392
+ font-size: 14px;
393
+ border: 1px solid #E3E8EE;
394
+ border-radius: 6px;
395
+ background: white;
396
+ transition: border-color 0.15s ease;
397
+ }
398
+
399
+ input:focus,
400
+ select:focus,
401
+ textarea:focus {
402
+ outline: none;
403
+ border-color: var(--primary);
404
+ box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.1);
405
+ }
406
+
407
+ /* Alerts */
408
+ .alert {
409
+ padding: 0.75rem 1rem;
410
+ border-radius: 6px;
411
+ margin-bottom: 1rem;
412
+ font-size: 14px;
413
+ }
414
+
415
+ .alert-info {
416
+ background: #E3F2FD;
417
+ color: #1565C0;
418
+ border: 1px solid #90CAF9;
419
+ }
420
+
421
+ .alert-success {
422
+ background: #E8F5E9;
423
+ color: #2E7D32;
424
+ border: 1px solid #81C784;
425
+ }
426
+
427
+ .alert-warning {
428
+ background: #FFF3E0;
429
+ color: #E65100;
430
+ border: 1px solid #FFB74D;
431
+ }
432
+
433
+ .alert-danger {
434
+ background: #FFEBEE;
435
+ color: #C62828;
436
+ border: 1px solid #EF5350;
437
+ }
438
+
439
+ /* Pagination */
440
+ .pagination {
441
+ display: flex;
442
+ align-items: center;
443
+ justify-content: center;
444
+ gap: 0.25rem;
445
+ margin-top: 2rem;
446
+ }
447
+
448
+ .pagination-link {
449
+ display: inline-flex;
450
+ align-items: center;
451
+ justify-content: center;
452
+ min-width: 32px;
453
+ height: 32px;
454
+ padding: 0 0.75rem;
455
+ font-size: 13px;
456
+ font-weight: 500;
457
+ color: #697386;
458
+ text-decoration: none;
459
+ border-radius: 6px;
460
+ transition: all 0.15s ease;
461
+ }
462
+
463
+ .pagination-link:hover {
464
+ background: #F7FAFC;
465
+ color: #1A1F36;
466
+ }
467
+
468
+ .pagination-link.active {
469
+ background: var(--primary);
470
+ color: white;
471
+ }
472
+
473
+ .pagination-link.disabled {
474
+ opacity: 0.5;
475
+ cursor: not-allowed;
476
+ }
477
+
478
+ /* Responsive */
479
+ @media (max-width: 768px) {
480
+ .sidebar {
481
+ transform: translateX(-100%);
482
+ }
483
+
484
+ .main-content {
485
+ margin-left: 0;
486
+ }
487
+
488
+ .stats-grid {
489
+ grid-template-columns: 1fr;
490
+ }
491
+ }
492
+ </style>
493
+
494
+ <%= yield :head %>
11
495
  </head>
12
- <body>
496
+ <body data-beskar-dashboard data-turbo="false">
497
+ <div class="dashboard-container">
498
+ <!-- Sidebar -->
499
+ <aside class="sidebar">
500
+ <div class="sidebar-header">
501
+ <%= link_to root_path, class: "sidebar-logo" do %>
502
+ <div class="sidebar-logo-icon">B</div>
503
+ <div class="sidebar-logo-text">Beskar</div>
504
+ <% end %>
505
+ </div>
506
+
507
+ <nav class="sidebar-nav">
508
+ <div class="nav-section">
509
+ <div class="nav-section-title">Overview</div>
510
+ <%= link_to dashboard_path, class: "nav-link #{controller_name == 'dashboard' ? 'active' : ''}" do %>
511
+ <svg class="nav-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
512
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
513
+ </svg>
514
+ Dashboard
515
+ <% end %>
516
+ </div>
517
+
518
+ <div class="nav-section">
519
+ <div class="nav-section-title">Security</div>
520
+ <%= link_to security_events_path, class: "nav-link #{controller_name == 'security_events' ? 'active' : ''}" do %>
521
+ <svg class="nav-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
522
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
523
+ </svg>
524
+ Security Events
525
+ <% end %>
526
+
527
+ <%= link_to banned_ips_path, class: "nav-link #{controller_name == 'banned_ips' ? 'active' : ''}" do %>
528
+ <svg class="nav-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
529
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path>
530
+ </svg>
531
+ Banned IPs
532
+ <% end %>
533
+ </div>
534
+
535
+ <div class="nav-section">
536
+ <%= link_to "Administrative Actions", administrative_actions_path,
537
+ class: "nav-link #{controller_name == 'administrative_actions' ? 'active' : ''}" %>
538
+ </div>
539
+
540
+ <div class="nav-section">
541
+ <div class="nav-section-title">Configuration</div>
542
+ <% if Beskar.configuration.monitor_only? %>
543
+ <div style="padding: 0.5rem 1rem;">
544
+ <div class="badge badge-warning" style="width: 100%; justify-content: center;">
545
+ Monitor Only Mode
546
+ </div>
547
+ </div>
548
+ <% end %>
549
+ </div>
550
+ </nav>
551
+ </aside>
552
+
553
+ <!-- Main Content -->
554
+ <main class="main-content">
555
+ <!-- Header -->
556
+ <header class="header">
557
+ <h1 class="header-title"><%= yield :page_title %></h1>
558
+
559
+ <div class="header-actions">
560
+ <%= yield :header_actions %>
561
+
562
+ <div style="display: flex; align-items: center; gap: 0.5rem; padding-left: 1rem; border-left: 1px solid #E3E8EE;">
563
+ <svg width="16" height="16" fill="currentColor" viewBox="0 0 24 24" style="color: #697386;">
564
+ <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"></path>
565
+ </svg>
566
+ <span style="font-size: 13px; color: #697386;">Protected by Beskar</span>
567
+ </div>
568
+ </div>
569
+ </header>
570
+
571
+ <!-- Content -->
572
+ <div class="content">
573
+ <% if notice.present? %>
574
+ <div class="alert alert-success">
575
+ <%= notice %>
576
+ </div>
577
+ <% end %>
578
+
579
+ <% if alert.present? %>
580
+ <div class="alert alert-danger">
581
+ <%= alert %>
582
+ </div>
583
+ <% end %>
13
584
 
14
- <%= yield %>
585
+ <%= yield %>
586
+ </div>
587
+ </main>
588
+ </div>
15
589
 
590
+ <%= render "layouts/beskar/behavior" %>
16
591
  </body>
17
592
  </html>
data/config/routes.rb CHANGED
@@ -1,2 +1,32 @@
1
1
  Beskar::Engine.routes.draw do
2
+ # Root route - dashboard
3
+ root to: "dashboard#index"
4
+
5
+ # Dashboard
6
+ get "dashboard", to: "dashboard#index", as: :dashboard
7
+
8
+ # Security Events
9
+ resources :security_events, only: [:index, :show] do
10
+ collection do
11
+ get "export"
12
+ end
13
+ end
14
+
15
+ # Banned IPs
16
+ resources :banned_ips do
17
+ member do
18
+ post "extend"
19
+ get "review"
20
+ end
21
+
22
+ collection do
23
+ post "bulk_action"
24
+ get "export"
25
+ end
26
+ end
27
+
28
+ resources :administrative_actions, only: [:index, :show]
29
+
30
+ # No versioned JSON API is implemented. Authenticated CSV/JSON exports live
31
+ # on the resources above; do not expose routes to nonexistent controllers.
2
32
  end
@@ -8,7 +8,7 @@ class CreateBeskarSecurityEvents < ActiveRecord::Migration[8.0]
8
8
  t.string :ip_address
9
9
  t.string :attempted_email
10
10
  t.text :user_agent
11
- t.json :metadata, default: {}
11
+ t.json :metadata, default: -> { "('{}')" }
12
12
  t.integer :risk_score
13
13
 
14
14
  t.timestamps
@@ -19,7 +19,7 @@ class CreateBeskarSecurityEvents < ActiveRecord::Migration[8.0]
19
19
  add_index :beskar_security_events, :attempted_email
20
20
  add_index :beskar_security_events, :created_at
21
21
  add_index :beskar_security_events, :risk_score
22
- add_index :beskar_security_events, [:ip_address, :event_type, :created_at],
23
- name: 'index_security_events_on_ip_event_time'
22
+ add_index :beskar_security_events, [:ip_address, :event_type, :created_at],
23
+ name: "index_security_events_on_ip_event_time"
24
24
  end
25
25
  end
@@ -0,0 +1,14 @@
1
+ class CreateBeskarSecurityStates < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :beskar_security_states do |t|
4
+ t.string :key, null: false
5
+ t.json :data, null: false, default: -> { "('{}')" }
6
+ t.datetime :expires_at
7
+ t.integer :lock_version, null: false, default: 0
8
+ t.timestamps
9
+ end
10
+ add_index :beskar_security_states, :key, unique: true
11
+ add_index :beskar_security_states, :expires_at
12
+ add_index :beskar_security_events, [:user_type, :user_id, :event_type, :created_at], name: "index_beskar_events_on_user_event_time"
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ class CreateBeskarAdministrativeActions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :beskar_administrative_actions do |t|
4
+ t.string :actor, null: false, limit: 200
5
+ t.string :action, null: false
6
+ t.bigint :target_id, null: false
7
+ t.string :operation_id, null: false, limit: 36
8
+ t.string :request_id, null: false, limit: 200
9
+ t.text :reason, null: false
10
+ # MySQL requires JSON defaults to be expressions, even for literals.
11
+ t.json :before_state, null: false, default: -> { "('{}')" }
12
+ t.json :after_state, null: false, default: -> { "('{}')" }
13
+ t.datetime :created_at, null: false
14
+ end
15
+ # No user/ban foreign key or cascading association: the action must survive
16
+ # deletion of its live target. Retention is a separate, explicit operation.
17
+ add_index :beskar_administrative_actions, [:operation_id, :target_id], unique: true,
18
+ name: "index_beskar_actions_on_operation_target"
19
+ add_index :beskar_administrative_actions, [:target_id, :id]
20
+ add_index :beskar_administrative_actions, :created_at
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ class ExpandAdministrativeActionTargets < ActiveRecord::Migration[8.0]
2
+ def change
3
+ add_column :beskar_administrative_actions, :target_type, :string, null: false, default: "BannedIp"
4
+ change_column_null :beskar_administrative_actions, :target_id, true
5
+ end
6
+ end