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,213 @@
|
|
|
1
|
+
# Authentication admission and account locks
|
|
2
|
+
|
|
3
|
+
Authentication enforcement is independent of audit persistence. Disabling
|
|
4
|
+
`security_tracking`, or a failed optional audit write, does not turn off rate
|
|
5
|
+
limits or risk-based locking. A request-local attempt ID correlates the decision
|
|
6
|
+
with any audit records that were successfully written.
|
|
7
|
+
|
|
8
|
+
## Dashboard access
|
|
9
|
+
|
|
10
|
+
The initializer generated by `bin/rails generate beskar:install` contains complete
|
|
11
|
+
dashboard examples for Rails built-in authentication and Devise. Uncomment one
|
|
12
|
+
example's three callbacks: `authenticate_admin`, `authorize_admin`, and
|
|
13
|
+
`audit_actor`. Adapt the example's `admin?` and `beskar_permissions` methods to
|
|
14
|
+
your application; neither Rails nor Devise provides those role/permission methods.
|
|
15
|
+
|
|
16
|
+
Beskar's dashboard controller inherits from `ActionController::Base`, not your
|
|
17
|
+
application controller. Your host's `Authentication` concern does not run there,
|
|
18
|
+
so `Current.session`, `Current.user`, or `current_user` cannot be assumed available.
|
|
19
|
+
The Rails example resolves `::Session` using `cookies.signed[:session_id]`, checks
|
|
20
|
+
`SessionRevocation.native_session_allowed?`, and shares the resolved user between
|
|
21
|
+
the three callbacks through a controller instance variable. It requires the
|
|
22
|
+
Rails-native model integration below. Adapt the lookup if your host changed the
|
|
23
|
+
generated cookie or session model.
|
|
24
|
+
|
|
25
|
+
If the session guard raises `NoMethodError` for `beskar_access_allowed?`, add
|
|
26
|
+
`include Beskar::Models::SecurityTrackableAuthenticable` to the host user model.
|
|
27
|
+
The similarly named `Beskar::Models::SecurityTrackable` concern is Devise-specific
|
|
28
|
+
and does not supply this method. Keep the revocation check in place so locked
|
|
29
|
+
accounts cannot access the dashboard through an existing session.
|
|
30
|
+
|
|
31
|
+
Devise uses Warden and does not require a Rails-native `Session` model. Its example
|
|
32
|
+
uses `request.env['warden'].authenticate(scope: :user)` to resume a session or run
|
|
33
|
+
configured strategies, then `.user(scope: :user)` to read that identity in the
|
|
34
|
+
permission and audit callbacks. The scope identifies a Devise mapping, not a role;
|
|
35
|
+
use `:admin` only for an app with that mapping. See the
|
|
36
|
+
[Warden authentication API](https://github.com/wardencommunity/warden/blob/master/lib/warden/proxy.rb).
|
|
37
|
+
|
|
38
|
+
Authentication alone does not grant dashboard permissions. `authorize_admin`
|
|
39
|
+
must return `true` for each allowed operation (`read`, `manage_bans`, `export`,
|
|
40
|
+
`read_audit`). Writes and exports additionally require a trusted `audit_actor`;
|
|
41
|
+
see [Audit lifecycle](audit-lifecycle.md).
|
|
42
|
+
|
|
43
|
+
## Devise
|
|
44
|
+
|
|
45
|
+
Include `Beskar::Models::SecurityTrackable` on each protected model. The engine
|
|
46
|
+
wraps Devise's database-password strategy: it identifies the target account and
|
|
47
|
+
reserves IP/account (and opt-in global) capacity **before password verification**. This includes
|
|
48
|
+
Devise HTTP Basic password authentication. Unknown accounts receive hashed,
|
|
49
|
+
normalized identity keys; password values are not used as counter keys.
|
|
50
|
+
|
|
51
|
+
The Warden outcome hook reuses the same admission instead of counting twice.
|
|
52
|
+
Protected-page visits without credentials and session fetches do not manufacture
|
|
53
|
+
failed logins. Devise scope aliases are resolved through `Devise.mappings`.
|
|
54
|
+
|
|
55
|
+
Risk-based Devise locking requires `:lockable`. A confirmed lock always rejects
|
|
56
|
+
the current attempt; `immediate_signout` defaults true and legacy false no longer
|
|
57
|
+
bypasses a lock. Normal persisted `locked_at` changes rotate a durable account
|
|
58
|
+
generation, invalidating all existing Devise sessions and remember-me cookies.
|
|
59
|
+
Unlock never restores an older generation. This includes Devise's own lockable
|
|
60
|
+
locks and manual model updates, not bulk SQL bypasses. Unrelated accounts/scopes
|
|
61
|
+
remain signed in. Generation reads bypass Active Record's query cache.
|
|
62
|
+
|
|
63
|
+
**Upgrade:** the new session/remember-cookie salt format signs out existing
|
|
64
|
+
Devise clients once. Drain old workers; mixed versions cannot safely enforce the
|
|
65
|
+
new credentials. All authentication models and state must use one writer pool.
|
|
66
|
+
|
|
67
|
+
Devise owns its own `unlock_strategy` and `unlock_in`. Beskar's `auto_unlock_time`
|
|
68
|
+
does **not** override Devise's class-level unlock policy. Configure it in Devise.
|
|
69
|
+
Standard custom Warden strategies now reserve IP capacity before `_run!`
|
|
70
|
+
verification; invalid tokens count. The account is charged once when an opaque
|
|
71
|
+
strategy identifies it. Warden `set_user` is guarded even with
|
|
72
|
+
`run_callbacks: false`, covering OAuth-style manual sign-in and stateless Warden.
|
|
73
|
+
Session fetch checks locks without counting a login. A custom strategy overriding
|
|
74
|
+
Warden's dispatcher, model serialization, or credential verifier needs its own
|
|
75
|
+
review. Beskar cannot revoke a host bearer token that omits the generation check.
|
|
76
|
+
See [Security hardening and rollout](../operations/security-hardening.md) for API and WebSocket adapters.
|
|
77
|
+
|
|
78
|
+
## Rails-native authentication: required integration
|
|
79
|
+
|
|
80
|
+
The old logging-only controller calls are insufficient. Upgrade the login
|
|
81
|
+
controller to reserve admission before `authenticate_by`, then guard session
|
|
82
|
+
creation:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class User < ApplicationRecord
|
|
86
|
+
has_secure_password
|
|
87
|
+
has_many :sessions, dependent: :destroy
|
|
88
|
+
normalizes :email_address, with: ->(email) { email.strip.downcase }
|
|
89
|
+
include Beskar::Models::SecurityTrackableAuthenticable
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class SessionsController < ApplicationController
|
|
93
|
+
include Authentication
|
|
94
|
+
include Beskar::Controllers::SecurityTracking
|
|
95
|
+
|
|
96
|
+
allow_unauthenticated_access only: %i[new create]
|
|
97
|
+
before_action -> { admit_authentication_attempt(User, :user) }, only: :create
|
|
98
|
+
|
|
99
|
+
def create
|
|
100
|
+
if (user = User.authenticate_by(params.permit(:email_address, :password)))
|
|
101
|
+
return unless complete_authentication(user) { start_new_session_for(user) }
|
|
102
|
+
redirect_to after_authentication_url
|
|
103
|
+
else
|
|
104
|
+
track_authentication_failure(User, :user)
|
|
105
|
+
redirect_to new_session_path, alert: "Try another email address or password."
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
For custom identity fields, pass an explicit identity-only hash through
|
|
112
|
+
`admit_authentication_attempt(User, :user, credentials: {...})`. Match the identity
|
|
113
|
+
lookup used by your authentication code. Never include passwords in that hash.
|
|
114
|
+
|
|
115
|
+
The session-creation block runs in a retryable database transaction; restrict it
|
|
116
|
+
to session creation and response-cookie assignment, not external notifications or
|
|
117
|
+
other non-idempotent external actions. Users, sessions, and Beskar state must use
|
|
118
|
+
the same writer connection pool for these transactions to be atomic. Sharded or
|
|
119
|
+
cross-database authentication models need a separately designed adapter.
|
|
120
|
+
|
|
121
|
+
Existing-session readers must also honor the persistent lock. In Rails'
|
|
122
|
+
`Authentication` concern, adapt `resume_session` along these lines:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
def resume_session
|
|
126
|
+
Current.session ||= find_session_by_cookie
|
|
127
|
+
if Current.session && !Beskar::Services::SessionRevocation.native_session_allowed?(Current.session, request: request)
|
|
128
|
+
Current.session = nil
|
|
129
|
+
cookies.delete(:session_id)
|
|
130
|
+
end
|
|
131
|
+
Current.session
|
|
132
|
+
end
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The dummy application contains the exercised integration. Include the check on
|
|
136
|
+
every other path that resumes a native session (including custom API or websocket
|
|
137
|
+
authentication). In-flight requests cannot be retroactively canceled by a lock.
|
|
138
|
+
|
|
139
|
+
Native locks use `beskar_security_states`; no lock columns on the user table are
|
|
140
|
+
required. The default strategy selects native locking for native models. A lock
|
|
141
|
+
revokes all database sessions, never compares their IDs to the unrelated Rack
|
|
142
|
+
session ID, and serializes with guarded new-session creation. If a host session
|
|
143
|
+
destruction callback fails or aborts, the persistent lock remains authoritative,
|
|
144
|
+
access is denied by the reader guard, and physical cleanup failure is logged.
|
|
145
|
+
That lock becomes manual-only so expiry cannot reactivate old sessions. Explicit
|
|
146
|
+
unlock retries cleanup and only clears the lock if every session was removed.
|
|
147
|
+
|
|
148
|
+
`auto_unlock_time` controls native lock duration. Set it to `nil` for manual-only
|
|
149
|
+
unlocking. To explicitly unlock:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
Beskar::Services::AccountLocker.new(user, risk_score: 0).unlock!
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Native lock rows retain their deadline inside their data; periodic expired-state
|
|
156
|
+
cleanup does not erase manual locks. Account-lifecycle cleanup remains separate.
|
|
157
|
+
|
|
158
|
+
## Policy and failure boundaries
|
|
159
|
+
|
|
160
|
+
- Monitor mode and whitelisted IPs suppress Beskar automatic account locks,
|
|
161
|
+
sign-outs, and emergency password resets. Observed authentication counters do
|
|
162
|
+
not consume enforced global/account capacity.
|
|
163
|
+
- Devise's own failed-attempt Lockable policy and host Rails rate limiters are
|
|
164
|
+
independent. Beskar does not disable those host policies in monitor mode.
|
|
165
|
+
- Admission denials return HTTP 429 with a retry deadline. Native lock denials
|
|
166
|
+
return HTTP 403. Required authentication-state or enforced risk-assessment
|
|
167
|
+
failures reject authentication with HTTP 503. Optional audit/enrichment failures
|
|
168
|
+
are logged and do not reject an otherwise allowed attempt.
|
|
169
|
+
- Direct model tracking methods preserve their optional `SecurityEvent` return
|
|
170
|
+
contract; that return is **not** an admission decision. Native controllers should
|
|
171
|
+
use `complete_authentication`, not infer authorization from whether an event
|
|
172
|
+
saved. `track_authentication_success` now returns a boolean for custom callers;
|
|
173
|
+
ignoring it is unsafe and does not provide the concurrent session guard.
|
|
174
|
+
|
|
175
|
+
## Audit and recovery
|
|
176
|
+
|
|
177
|
+
Known failed targets are associated with their user record for account-history
|
|
178
|
+
analysis. Rejected admissions/sessions use `authentication_blocked`, not
|
|
179
|
+
`login_success`. `metadata["authentication"]` records the attempt ID, scope,
|
|
180
|
+
admission outcome, and actual lock result. Lock events reference that same attempt
|
|
181
|
+
ID; the login audit row may not yet exist or may be disabled.
|
|
182
|
+
|
|
183
|
+
Deleting an account retains its linked events unchanged, including the original
|
|
184
|
+
`user_type` and `user_id`. This is not anonymization; no event expiry or automatic
|
|
185
|
+
purge is added. See [Audit lifecycle](audit-lifecycle.md) for retention, missing-user
|
|
186
|
+
presentation, and the separate required journal for dashboard ban changes.
|
|
187
|
+
|
|
188
|
+
Authentication context no longer stores Rack session IDs. Referrers are restricted
|
|
189
|
+
to HTTP(S), with credentials, queries, and fragments removed. Other WAF/export
|
|
190
|
+
privacy findings are still tracked separately; this is not a complete audit-data
|
|
191
|
+
redaction policy.
|
|
192
|
+
|
|
193
|
+
Emergency reset remains opt-in. Thresholds count confirmed account-lock evidence,
|
|
194
|
+
not strings containing a false travel/device flag or duplicate success events.
|
|
195
|
+
Password invalidation, session revocation, and the mandatory recovery audit commit
|
|
196
|
+
together; failure rolls them back. `require_manual_unlock: true` removes the native
|
|
197
|
+
automatic-unlock deadline. Notification hooks run after all enclosing transactions
|
|
198
|
+
commit.
|
|
199
|
+
|
|
200
|
+
Account-lock and native emergency-reset notifications now use opt-in post-commit
|
|
201
|
+
Action Mailer jobs. The three notification flags default to false; configure a
|
|
202
|
+
sender, HTTPS recovery entry page, and any security-team recipients before enabling
|
|
203
|
+
them. See [Notifications and recovery](notifications-and-recovery.md) for retries,
|
|
204
|
+
legacy hook overrides, delivery/privacy limits, and the tested native recovery
|
|
205
|
+
handoff. A successful password reset does not bypass a Beskar manual lock. Real
|
|
206
|
+
production delivery and host-specific recovery verification remain necessary.
|
|
207
|
+
Normalized risk evidence and removal of unsafe automatic trust discounts are
|
|
208
|
+
documented in [Risk scoring](risk-scoring.md).
|
|
209
|
+
|
|
210
|
+
See [Configuration](configuration.md) for validated lock strategies and the
|
|
211
|
+
optional host-owned, post-commit analysis hook. The old `:custom` placeholder is
|
|
212
|
+
rejected; use a supported strategy or explicit `:none`. No built-in background
|
|
213
|
+
analyzer is supplied; analysis and notification delivery are separate job paths.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Configuration and supported capabilities
|
|
2
|
+
|
|
3
|
+
Beskar validates configuration during Rails startup, after main autoloader setup
|
|
4
|
+
and host `after_initialize` callbacks registered by configuration files. Invalid settings raise
|
|
5
|
+
`Beskar::Configuration::Error` and stop boot. Errors identify known setting paths
|
|
6
|
+
without interpolating supplied values or unknown keys.
|
|
7
|
+
|
|
8
|
+
## Changes and defaults
|
|
9
|
+
|
|
10
|
+
Prefer `Beskar.configure` in `config/initializers/beskar.rb`:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
Beskar.configure do |config|
|
|
14
|
+
config.monitor_only = true
|
|
15
|
+
config.rate_limiting = {ip_attempts: {limit: 20}}
|
|
16
|
+
end
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Whole-section assignment overlays **library defaults**, including nested defaults.
|
|
20
|
+
The example keeps the default IP period and account/global limits. It also resets
|
|
21
|
+
any previous customizations in that section. To retain existing customizations,
|
|
22
|
+
edit individual entries inside the configure block:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
Beskar.configure do |config|
|
|
26
|
+
config.rate_limiting[:ip_attempts][:limit] = 20
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`configure` copies the current settings, yields the copy, validates it, and
|
|
31
|
+
publishes another copy. A raised exception or validation failure leaves the
|
|
32
|
+
active configuration unchanged. Retaining the block's nested hashes/arrays does
|
|
33
|
+
not provide a reference to the published values. Proc callbacks and classes
|
|
34
|
+
remain trusted host objects; their behavior is not sandboxed or deep-copied.
|
|
35
|
+
|
|
36
|
+
Configuration is sealed after startup validation. Direct/nested mutation and
|
|
37
|
+
wholesale replacement are then rejected. Prefer initializer changes plus a
|
|
38
|
+
coordinated restart. Runtime `Beskar.configure(actor:, reason:, request_id:)`
|
|
39
|
+
requires an explicit `authorize_configuration` grant, validates a detached copy,
|
|
40
|
+
and records a mandatory filtered before/after journal before publishing it.
|
|
41
|
+
Missing authorization, invalid settings, open database transactions, or failed
|
|
42
|
+
journaling cannot publish. Publication is serialized within the Ruby process.
|
|
43
|
+
This is not a distributed configuration store or a per-request snapshot; runtime
|
|
44
|
+
changes affect only this process. The journal records authorized publication
|
|
45
|
+
intent, not proof every worker adopted it. A process crash after journaling can
|
|
46
|
+
prevent publication. Boot/deployment edits and changes inside trusted callback
|
|
47
|
+
implementations must be audited by the host deployment/source-control workflow.
|
|
48
|
+
See [Security hardening and rollout](../operations/security-hardening.md) for the complete contract.
|
|
49
|
+
|
|
50
|
+
During initialization, `configure` defers resolution of named host jobs until the
|
|
51
|
+
final startup check, when `app/jobs` can be autoloaded. Runtime `configure` and an
|
|
52
|
+
explicit `validate!` resolve enabled jobs immediately.
|
|
53
|
+
|
|
54
|
+
## What is validated
|
|
55
|
+
|
|
56
|
+
- Known symbol-keyed section schemas; unknown, misspelled, string, and removed
|
|
57
|
+
keys are rejected. Partial section assignments fill missing defaults; deleting
|
|
58
|
+
required entries in place is invalid.
|
|
59
|
+
- Actual booleans, not strings such as `"false"`; dashboard authorization and
|
|
60
|
+
`audit_actor` callbacks must each be a Proc or nil. Nil authorization denies all
|
|
61
|
+
dashboard access; nil `audit_actor` permits authenticated reads but rejects writes.
|
|
62
|
+
Validation does not execute callbacks, grant access, or verify their decisions.
|
|
63
|
+
- Finite positive windows, cache lifetimes, decay half-lives, block durations,
|
|
64
|
+
and thresholds; integer attempt/history/emergency counts. Authentication risk
|
|
65
|
+
thresholds must be between 0 and 100. Native `auto_unlock_time` and WAF
|
|
66
|
+
`permanent_block_after` accept nil for manual-only unlock/no permanent escalation.
|
|
67
|
+
- Valid IP/CIDR whitelist strings; WAF exception policies, nonempty block-duration
|
|
68
|
+
arrays, regexp exclusions, and method/category names.
|
|
69
|
+
- Supported lock strategies and geolocation providers; configured authentication
|
|
70
|
+
scope-name syntax. MaxMind requires an existing readable database path.
|
|
71
|
+
- An explicit Active Job subclass when automatic analysis is active.
|
|
72
|
+
- Sender/recipient mailbox syntax and an explicit HTTPS recovery entry page when
|
|
73
|
+
user notifications are enabled; a nonempty recipient list for enabled team alerts.
|
|
74
|
+
|
|
75
|
+
Validation does not query application tables or `Rails.cache`. Any Rails.cache
|
|
76
|
+
backend remains supported; coordinated enforcement still uses the database
|
|
77
|
+
contract in [State storage](../operations/state-storage.md). Validation does not prove host
|
|
78
|
+
model/adapter readiness, MaxMind file integrity, queue availability, or production
|
|
79
|
+
database correctness. Runtime enrichment failure handling remains separate.
|
|
80
|
+
|
|
81
|
+
## Supported capabilities
|
|
82
|
+
|
|
83
|
+
| Area | Contract |
|
|
84
|
+
| --- | --- |
|
|
85
|
+
| Account locks | `:devise_lockable`, `:rails_auth`, or explicit `:none`. The placeholder `:custom` and unknown values are rejected. Host adapter requirements remain in [Authentication](authentication.md). |
|
|
86
|
+
| Geolocation | `:mock` or `:maxmind`. IP2Location and unknown providers are rejected, including by direct service construction. Mock results do not supply geographic risk evidence. |
|
|
87
|
+
| Background analysis | Off by default. A host-owned Active Job can opt into the post-commit hook below; no built-in analyzer is supplied. |
|
|
88
|
+
| Administration API | No versioned API; authenticated dashboard resource exports remain available. |
|
|
89
|
+
| Administrative history | Dashboard ban changes require a trusted `audit_actor` and per-request `audit_reason`, with mandatory transactional history. See [Audit lifecycle](audit-lifecycle.md) for configuration and the new migration. |
|
|
90
|
+
| Notifications/recovery delivery | Opt-in Action Mailer notices for account locks and native emergency resets, with post-commit jobs and bounded retries. Explicit sender/recovery/team configuration is required. The host still owns password-reset/unlock flows; see [Notifications and recovery](notifications-and-recovery.md). |
|
|
91
|
+
|
|
92
|
+
Unsupported lock strategies also raise when read at runtime. With risk enforcement
|
|
93
|
+
enabled, authentication rejects this misconfiguration with 503 rather than
|
|
94
|
+
silently skipping the lock; both supported authentication paths are regression
|
|
95
|
+
tested. `:none` deliberately disables locking and is not a custom adapter hook.
|
|
96
|
+
|
|
97
|
+
## Optional host background analysis
|
|
98
|
+
|
|
99
|
+
Provide a host job and explicitly enable it. Prefer a class-name string so each
|
|
100
|
+
invocation resolves the current Rails-autoloaded class:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
# app/jobs/security_review_job.rb
|
|
104
|
+
class SecurityReviewJob < ApplicationJob
|
|
105
|
+
def perform(user_type:, user_id:, event_type:)
|
|
106
|
+
# Restrict supported identities explicitly; adapt for your host models.
|
|
107
|
+
return unless user_type == "User" && event_type == "login_success"
|
|
108
|
+
user = User.find_by(id: user_id)
|
|
109
|
+
return unless user
|
|
110
|
+
|
|
111
|
+
suspicious = user.suspicious_login_pattern?
|
|
112
|
+
Rails.logger.info("Security review user_id=#{user.id} suspicious=#{suspicious}")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# config/initializers/beskar.rb
|
|
117
|
+
Beskar.configure do |config|
|
|
118
|
+
config.security_tracking[:analysis_job] = "SecurityReviewJob"
|
|
119
|
+
config.security_tracking[:auto_analyze_patterns] = true
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Automatic invocation occurs on the successful-login tracking path after an
|
|
124
|
+
admitted attempt, with successful-login tracking enabled. It is optional analysis,
|
|
125
|
+
not asynchronous authentication enforcement. A failed optional audit write does
|
|
126
|
+
not by itself prevent invocation. The arguments contain only the model's base
|
|
127
|
+
class name, user ID, and `"login_success"`; no email, credential, session, raw
|
|
128
|
+
request context, or audit-event ID is passed. IDs still identify users and require
|
|
129
|
+
appropriate queue access/retention policy.
|
|
130
|
+
|
|
131
|
+
Monitor-only and whitelisted observations can invoke the hook too. Arguments do
|
|
132
|
+
not contain the policy mode; host jobs must not infer permission to lock, revoke,
|
|
133
|
+
or reset an account from receiving a job. Keep this hook read-only enrichment;
|
|
134
|
+
final admission and applicable policy remain synchronous.
|
|
135
|
+
|
|
136
|
+
Enqueueing waits for all enclosing Active Record transactions to commit. An outer
|
|
137
|
+
rollback or rolled-back savepoint cancels its callback. With no open transaction,
|
|
138
|
+
enqueueing runs immediately. Preparation errors, queue exceptions, and aborted
|
|
139
|
+
enqueue attempts are logged without raw exception messages and do not undo a
|
|
140
|
+
committed login/update or change admission.
|
|
141
|
+
|
|
142
|
+
This is **not a transactional outbox**: a process crash between commit and enqueue
|
|
143
|
+
can lose analysis. Backends/retries may duplicate work. Hosts own queue selection,
|
|
144
|
+
workers, retry/idempotence policy, and any eventual delivery. The local tests use
|
|
145
|
+
real database commits/rollbacks and the Active Job test adapter, not a production
|
|
146
|
+
worker or durable delivery service. The public `analyze_suspicious_patterns_async`
|
|
147
|
+
helper is also callable directly, but is not an admission/authorization check.
|
|
148
|
+
|
|
149
|
+
## Upgrade notes
|
|
150
|
+
|
|
151
|
+
- Configure `authorize_admin(request, permission)` separately from authentication;
|
|
152
|
+
missing permissions deny dashboard access. Grants are `:read`, `:manage_bans`,
|
|
153
|
+
`:export`, and `:read_audit`. Exports now require actor/reason/history as well.
|
|
154
|
+
- Global login limits and request-wide authentication-quota blocking default off.
|
|
155
|
+
Explicitly opting in restores their distributed-denial/shared-NAT tradeoffs.
|
|
156
|
+
- A real lock always rejects the login and invalidates prior Devise credentials;
|
|
157
|
+
`immediate_signout: false` is a deprecated compatibility value, not a bypass.
|
|
158
|
+
|
|
159
|
+
- Dashboard mutations now require a server-derived `audit_actor` callback and
|
|
160
|
+
`audit_reason`. Deploy the administrative-action migration before new workers;
|
|
161
|
+
see [Audit lifecycle](audit-lifecycle.md). Missing actor configuration returns
|
|
162
|
+
503 for writes, without disabling authenticated reads.
|
|
163
|
+
- Lock/user-reset/team notification flags now default to false. Their old true
|
|
164
|
+
defaults were log-only placeholders. Explicitly configure delivery before
|
|
165
|
+
enabling them; see [Notifications and recovery](notifications-and-recovery.md).
|
|
166
|
+
- Automatic analysis now defaults to false. Enabling it requires `analysis_job`;
|
|
167
|
+
the former silent discovery of a nonexistent `Beskar::SecurityAnalysisJob` is
|
|
168
|
+
removed. Adapt any existing custom job to the explicit keyword contract above.
|
|
169
|
+
- Replace `:custom`/IP2Location configuration with a supported capability; these
|
|
170
|
+
placeholders no longer silently do nothing.
|
|
171
|
+
- Remove obsolete `waf[:block_threshold]` and `waf[:monitor_only]`. Use cumulative
|
|
172
|
+
`waf[:score_threshold]` and top-level `monitor_only`, respectively. A violation
|
|
173
|
+
count is not interchangeable with a cumulative score.
|
|
174
|
+
- MaxMind configuration with a missing/unreadable path now stops startup. Supply
|
|
175
|
+
a readable database or explicitly choose `:mock` without geographic risk evidence.
|
|
176
|
+
- Review partial section assignments for the default-overlay behavior above.
|
|
177
|
+
The sixth batch required no new migration; the eighth batch adds the history table.
|
|
178
|
+
|
|
179
|
+
The Ruby 4.0.6 regressions exercise invalid/valid application boots, host job
|
|
180
|
+
autoloading, rejected configure-block publication, real commit/rollback behavior,
|
|
181
|
+
and optional queue failures. Production workers, actual class reloading, MaxMind
|
|
182
|
+
database reload/load behavior, and host-specific adapter readiness remain unverified.
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Dashboard and search contract
|
|
2
|
+
|
|
3
|
+
Reporting/search behavior introduced in the fifth repair batch, which required
|
|
4
|
+
no new migration. The eighth batch adds a history table and required actor/reason
|
|
5
|
+
for dashboard writes; see [Audit lifecycle](audit-lifecycle.md).
|
|
6
|
+
See [Audit data and WAF](audit-and-waf.md) for capture, redaction, export paging,
|
|
7
|
+
and privacy limitations, and [Repair status](../audits/repair-status.md) for open work.
|
|
8
|
+
|
|
9
|
+
## One set of reporting bands
|
|
10
|
+
|
|
11
|
+
`Beskar::RiskLevel` supplies model predicates/scopes, dashboard counts, event
|
|
12
|
+
filters (including exports), badges, labels, colors, and filter option text:
|
|
13
|
+
|
|
14
|
+
| Band | Event score | Badge |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Low | 0–29 | success |
|
|
17
|
+
| Medium | 30–69 | warning |
|
|
18
|
+
| High | 70–89 | danger |
|
|
19
|
+
| Critical | 90–100 | critical |
|
|
20
|
+
|
|
21
|
+
The model's existing high/critical boundaries of 70 and 90 are authoritative.
|
|
22
|
+
`high_risk` and the dashboard's High Risk Events total include critical events;
|
|
23
|
+
the `risk_level=high` filter selects only 70–89. This distinguishes a minimum
|
|
24
|
+
threat threshold from an exclusive histogram bucket.
|
|
25
|
+
|
|
26
|
+
Invalid/missing/out-of-range scores are unknown, not critical. They receive a
|
|
27
|
+
neutral badge and are excluded from score-band counts, while still counting as
|
|
28
|
+
events in overall totals. Fractional aggregate scores use the same boundaries:
|
|
29
|
+
69.9 remains medium, 89.9 remains high.
|
|
30
|
+
|
|
31
|
+
These are reporting bands, not a change to scoring weights, configured account
|
|
32
|
+
lock thresholds, WAF severity points, or cumulative WAF ban thresholds.
|
|
33
|
+
Older dashboard thresholds of 61/86 and the conflicting boundary at 30 are
|
|
34
|
+
removed. Review saved filters/report consumers that relied on those old ranges.
|
|
35
|
+
|
|
36
|
+
## Statistics and user presentation
|
|
37
|
+
|
|
38
|
+
Overview event totals and failed-login counts reuse one grouped event-type
|
|
39
|
+
query. Risk-band and high/critical counts reuse one grouped score query. The
|
|
40
|
+
selected statistics period has both a lower bound and a current-time upper
|
|
41
|
+
bound, excluding future-dated records. The recent-activity list remains the
|
|
42
|
+
latest ten events across periods, as its own section describes.
|
|
43
|
+
|
|
44
|
+
These separate queries are not a transactional snapshot: concurrent writes can
|
|
45
|
+
still change results between queries. Grouped counts reduce repeated queries
|
|
46
|
+
but do not remove the cost of scanning a large selected period.
|
|
47
|
+
|
|
48
|
+
Ban details compute count, average, maximum, first seen, and last seen from all
|
|
49
|
+
events for that IP. The table still shows only the latest 20. Related-event
|
|
50
|
+
tables preload their user associations; lists with equal timestamps use IDs
|
|
51
|
+
as a descending tie-breaker. Pagination remains offset-based for HTML lists;
|
|
52
|
+
exports retain their separate descending-ID cursor contract.
|
|
53
|
+
|
|
54
|
+
Associated user labels support both Devise `email` and native `email_address`.
|
|
55
|
+
The same bounded, filtered presentation is used by dashboard/event/ban views
|
|
56
|
+
and exports. Native associated email addresses honor both `email_address` and
|
|
57
|
+
the common `email` filter name; this does not rewrite the host user record.
|
|
58
|
+
Attempted-email and metadata fields retain their own audit-key filtering
|
|
59
|
+
contract. Configure a broad `:email` filter to cover those email-bearing keys.
|
|
60
|
+
Where no address is available, views use an attempted email or user-ID fallback.
|
|
61
|
+
|
|
62
|
+
## Search semantics and database support
|
|
63
|
+
|
|
64
|
+
Event index and both export formats use `Services::EventSearch`:
|
|
65
|
+
|
|
66
|
+
- General search covers IP, User-Agent, attempted email, event type, and a text
|
|
67
|
+
representation of metadata on all three database families below.
|
|
68
|
+
- The email filter checks the attempted-email column, falling back only when
|
|
69
|
+
that column is null to the top-level `metadata.attempted_email` value.
|
|
70
|
+
Unrelated metadata prose and the user's current email are not email matches.
|
|
71
|
+
- Terms are bounded to 256 characters, lowercased for ASCII-insensitive matching,
|
|
72
|
+
and passed as SQL values. Percent, underscore, and the chosen escape character
|
|
73
|
+
(`!`) are literal text, not user-controlled LIKE wildcards.
|
|
74
|
+
- Missing/empty/non-text search values do not add a filter. Non-ASCII case folding,
|
|
75
|
+
collation behavior, and serialized-JSON escaping remain database-dependent.
|
|
76
|
+
|
|
77
|
+
JSON cannot be handled as an ordinary string column everywhere. PostgreSQL
|
|
78
|
+
extracts legacy email text with `->>`; SQLite uses `json_extract`; MySQL uses
|
|
79
|
+
`JSON_EXTRACT` plus `JSON_UNQUOTE`, preserving JSON-null behavior. The centralized
|
|
80
|
+
expressions follow the respective primary references:
|
|
81
|
+
[PostgreSQL JSON operators](https://www.postgresql.org/docs/current/functions-json.html),
|
|
82
|
+
[SQLite JSON functions](https://www.sqlite.org/json1.html), and
|
|
83
|
+
[MySQL JSON search functions](https://dev.mysql.com/doc/refman/8.4/en/json-search-functions.html).
|
|
84
|
+
|
|
85
|
+
General metadata search uses a TEXT cast on PostgreSQL/SQLite and a CHAR cast on
|
|
86
|
+
MySQL (Mysql2/Trilogy), instead of applying LIKE directly to a JSON column.
|
|
87
|
+
This is text search, not a structured JSON query language or a full-text index.
|
|
88
|
+
|
|
89
|
+
SQLite execution, request/export integration, literal-wildcard handling, and
|
|
90
|
+
PostgreSQL/MySQL SQL generation are tested locally. No live PostgreSQL/MySQL
|
|
91
|
+
server, production collation matrix, query plan, or throughput benchmark has
|
|
92
|
+
been exercised by this batch. Other adapters have no implemented legacy-email
|
|
93
|
+
extractor and fail explicitly when that filter is used.
|
|
94
|
+
|
|
95
|
+
Search operates on stored values, before read-time model filtering. It can
|
|
96
|
+
therefore match sensitive legacy values that are hidden when rendered, revealing
|
|
97
|
+
record membership to an authorized administrator. Read-time redaction is not
|
|
98
|
+
historical erasure; remediate legacy storage separately if that inference is
|
|
99
|
+
unacceptable. Searches over newly redacted values cannot recover the original.
|
|
100
|
+
Leading-substring and JSON-text search can still be expensive despite paging.
|
|
101
|
+
|
|
102
|
+
## Public routes and remaining work
|
|
103
|
+
|
|
104
|
+
Removed the unimplemented `/beskar/api/v1/*` routes and their route helpers.
|
|
105
|
+
They previously pointed to missing controllers. Supported read exports remain
|
|
106
|
+
`/beskar/security_events/export.csv|json` and
|
|
107
|
+
`/beskar/banned_ips/export.csv|json`, behind dashboard authorization.
|
|
108
|
+
There is no versioned programmatic administration API.
|
|
109
|
+
|
|
110
|
+
The fifth batch verified rendered HTML and controllers. Batch nine additionally
|
|
111
|
+
exercises the forms in Chromium as described below. Administrative lifecycle and
|
|
112
|
+
opt-in notifications have their own contracts in [Audit lifecycle](audit-lifecycle.md)
|
|
113
|
+
and [Notifications and recovery](notifications-and-recovery.md).
|
|
114
|
+
|
|
115
|
+
## Ban forms and timezones
|
|
116
|
+
|
|
117
|
+
New/edit expiry fields are explicitly **UTC**, independent of browser timezone and
|
|
118
|
+
the host application's `Time.zone`. A timezone-free dashboard value such as
|
|
119
|
+
`2030-11-03T01:30` means 01:30 UTC, not a DST-dependent local wall time. Scripted
|
|
120
|
+
dashboard requests may also supply ISO 8601 timestamps with `Z` or a numeric
|
|
121
|
+
`+HH:MM`/`-HH:MM` offset. These are normalized to the identified UTC instant.
|
|
122
|
+
The parser accepts valid calendar dates with four-digit positive years, hours
|
|
123
|
+
0–23, optional seconds, and up to six fractional digits. Malformed dates, overflow
|
|
124
|
+
times, leap seconds, non-string shapes, and excessive precision return 422 rather
|
|
125
|
+
than normalizing to another date or silently selecting a default.
|
|
126
|
+
|
|
127
|
+
HTML datetime controls use millisecond precision. Edit forms mark that precision
|
|
128
|
+
with `expiry_precision=milliseconds`; submitting the unchanged displayed instant
|
|
129
|
+
preserves existing database microseconds. Scripted requests without that form
|
|
130
|
+
marker retain their explicit precision. This is not optimistic locking against
|
|
131
|
+
stale form edits. Report timestamps outside these inputs continue to use their
|
|
132
|
+
existing Rails/application-zone presentation; they are not reinterpreted as UTC
|
|
133
|
+
wall-clock input.
|
|
134
|
+
|
|
135
|
+
Creation presets are computed on the server, once: a positive integer number of
|
|
136
|
+
seconds, capped at 90 days. An explicit custom expiry takes precedence; absent/empty
|
|
137
|
+
duration and custom expiry use 24 hours. Invalid supplied presets and unknown ban
|
|
138
|
+
types return 422. Permanent creation ignores temporary duration/expiry values.
|
|
139
|
+
This restriction applies to dashboard creation/the manager, not a new global ban
|
|
140
|
+
duration cap. Direct `BannedIp.ban!` and administrative extension contracts remain
|
|
141
|
+
separate. Valid custom dates can be in the past, permitting an explicit expiry;
|
|
142
|
+
the database does not silently advance them.
|
|
143
|
+
|
|
144
|
+
Quick edit buttons add elapsed UTC hours to the later of the displayed expiry or
|
|
145
|
+
the browser's current time; they never convert a UTC string through the browser's
|
|
146
|
+
local timezone. They only edit the field until the operator submits. Switching a
|
|
147
|
+
permanent ban to temporary requires an expiry; JavaScript suggests 24 hours if the
|
|
148
|
+
field is empty. Switching to permanent disables temporary controls, and server
|
|
149
|
+
normalization clears expiry. Presets are not converted to client-clock timestamps
|
|
150
|
+
on submission. Create-form validation retries retain the selected duration rather
|
|
151
|
+
than turning it into a stale custom expiry. Existing custom reasons remain selectable,
|
|
152
|
+
and blank-expiry validation renders without a secondary view error.
|
|
153
|
+
|
|
154
|
+
## Script policy and native navigation
|
|
155
|
+
|
|
156
|
+
One layout behavior script carries the host-generated nonce. Inline `onclick`,
|
|
157
|
+
`onchange`, and submission handlers are removed from the dashboard. Delegated
|
|
158
|
+
listeners are installed once per document; initial load and restored pages refresh
|
|
159
|
+
the control state without adding duplicate handlers. Validation errors and notices
|
|
160
|
+
are no longer automatically removed after five seconds.
|
|
161
|
+
|
|
162
|
+
The engine does not loosen the host's CSP. Hosts enforcing script CSP must provide
|
|
163
|
+
a nonce generator and allow that nonce in `script-src`, for example:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
# Host CSP configuration: merge with your existing policy, do not discard it.
|
|
167
|
+
config.content_security_policy_nonce_generator = ->(_) { SecureRandom.base64(24) }
|
|
168
|
+
config.content_security_policy_nonce_directives = %w[script-src style-src]
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Local tests enforce `script-src 'self' 'nonce-…'`, `script-src-attr 'none'`, and no
|
|
172
|
+
script `unsafe-inline` or `unsafe-eval`. The layout's style block also receives a
|
|
173
|
+
nonce, but the existing views still contain **inline style attributes**. Tests
|
|
174
|
+
explicitly allow `style-src-attr 'unsafe-inline'`; hosts forbidding those attributes
|
|
175
|
+
will not get the intended styling. This is script-policy compatibility, not full
|
|
176
|
+
strict-style CSP support. Moving the remaining styles into classes/assets remains
|
|
177
|
+
open; hosts should not weaken script policy to accommodate dashboard controls.
|
|
178
|
+
|
|
179
|
+
The dashboard body explicitly opts out of Turbo Drive (`data-turbo="false"`).
|
|
180
|
+
Links and forms use native navigation even when the host has loaded Turbo. The
|
|
181
|
+
handwritten method-link/form synthesizer is removed; Rails forms carry their own
|
|
182
|
+
CSRF token and `_method` where needed. Beskar does not require Rails UJS, Turbo,
|
|
183
|
+
an import map, or a JavaScript bundler for its dashboard. Mutation redirects use
|
|
184
|
+
the existing Rails redirect flow; no new client-side submission protocol is added.
|
|
185
|
+
|
|
186
|
+
With JavaScript disabled or the behavior script blocked, normal new/edit/review
|
|
187
|
+
forms still work. Temporary fields and bulk controls remain available; server
|
|
188
|
+
authorization, reasons, validation, and CSRF remain mandatory. Quick edit buttons,
|
|
189
|
+
preview, selection helpers, automatic page-size submission, and bulk confirmation
|
|
190
|
+
dialogs are enhancements. A visible page-size submit button works without scripts.
|
|
191
|
+
Changing page size preserves filters, drops the old page number, and avoids duplicate
|
|
192
|
+
hidden `per_page` inputs. Row unban actions still require a separate review form;
|
|
193
|
+
bulk confirmation dialogs require JavaScript.
|
|
194
|
+
|
|
195
|
+
## Browser verification and remaining limits
|
|
196
|
+
|
|
197
|
+
Browser tests use the standard [Rails system-test integration](https://api.rubyonrails.org/v8.0/classes/ActionDispatch/SystemTestCase.html)
|
|
198
|
+
with Capybara and Selenium, installed only in the development project's test bundle.
|
|
199
|
+
Turbo Rails is a test-only dependency used to load the real host library, not a new
|
|
200
|
+
runtime dependency of Beskar. Run separately from the ordinary suite:
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
mise exec -- env PARALLEL_WORKERS=1 bin/rails test test/system/dashboard_test.rb
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Use an installed Chrome/Chromium and matching ChromeDriver. Optional
|
|
207
|
+
`BESKAR_BROWSER_BINARY` and `BESKAR_BROWSER_DRIVER` select their paths; otherwise the
|
|
208
|
+
harness discovers local binaries/driver, with Selenium's usual fallback when no
|
|
209
|
+
driver is installed. `BESKAR_BROWSER_LOG` enables verbose ChromeDriver logs at the
|
|
210
|
+
specified path, appended across tests. Each test gets a fresh browser process.
|
|
211
|
+
The tests do not use a production account, copy the project runtime, or disable
|
|
212
|
+
Chromium's sandbox. Failed startup does not trigger another browser launch for
|
|
213
|
+
screenshots or teardown.
|
|
214
|
+
|
|
215
|
+
Coverage includes UTC edits with multiple browser zones and DST-boundary dates,
|
|
216
|
+
microsecond preservation, server-relative presets with a skewed browser clock,
|
|
217
|
+
permanent/temporary toggles, persistent validation feedback/retry, bulk clear/cancel/
|
|
218
|
+
confirm, page-size filters, actual Turbo-loaded navigation, back navigation, no
|
|
219
|
+
duplicate audit operations, and JavaScript-disabled create/review/delete with real
|
|
220
|
+
CSRF protection. Browser console checks reject script/CSP errors (excluding a
|
|
221
|
+
missing favicon and the deliberately tested validation response's HTTP 422).
|
|
222
|
+
Screenshot inspection supplemented functional checks.
|
|
223
|
+
|
|
224
|
+
Native form submissions now wait for the destination URL before checking the
|
|
225
|
+
success notice. Scoped notice checks also wait for same-URL redirects, such as
|
|
226
|
+
bulk unban. This avoids reading the outgoing document during navigation: in
|
|
227
|
+
[CI run 35353219531](https://github.com/AuditBadger-com/beskar/actions/runs/35353219531),
|
|
228
|
+
ChromeDriver 153 reported `Node with given id does not belong to the document`
|
|
229
|
+
while the update itself succeeded. A delayed-submit regression with matching
|
|
230
|
+
text in the outgoing page fails without the destination wait and passes with it.
|
|
231
|
+
The export test similarly waits for the export URL before checking the response.
|
|
232
|
+
With Chrome/ChromeDriver 153.0.8010.52 and mise Ruby 4.0.7, all 11 browser tests
|
|
233
|
+
(118 assertions) pass with seeds `42702`, `101`, `202`, `303`, and `20260918`.
|
|
234
|
+
|
|
235
|
+
CI includes a separate Ubuntu 24.04 browser job with a
|
|
236
|
+
[matching Chrome/driver setup](https://github.com/browser-actions/setup-chrome)
|
|
237
|
+
and system dependencies. A per-binary AppArmor rule permits the downloaded
|
|
238
|
+
Chrome to use its sandbox without disabling global user-namespace restrictions.
|
|
239
|
+
Startup is verified by the Selenium suite itself. The standalone `--dump-dom`
|
|
240
|
+
preflight hung with Chrome for Testing 153.0.8010.52, reproduced locally even
|
|
241
|
+
though all ten browser tests passed with that exact browser/driver pair.
|
|
242
|
+
The `browser-diagnostics` artifact retains verbose driver logs (including Chrome
|
|
243
|
+
startup diagnostics) and failure screenshots for three days. Browser startup now
|
|
244
|
+
passes in hosted CI; the navigation-synchronization repair still needs hosted
|
|
245
|
+
confirmation. Other browsers, full strict-style CSP,
|
|
246
|
+
mobile/accessibility review, arbitrary host layouts/authentication integrations,
|
|
247
|
+
Turbo Frames/Streams, and transactional stale-form protection remain unverified
|
|
248
|
+
or out of scope for this batch. No new database migration is required for batch
|
|
249
|
+
nine; the preceding administrative-history migration and actor configuration still
|
|
250
|
+
apply. Update scripted callers that previously relied on local-zone timestamps or
|
|
251
|
+
loosely parsed duration strings before deploying.
|