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
@@ -0,0 +1,216 @@
1
+ # Review remediation
2
+
3
+ Tracks [Original project review](project-review.md), which remains the historical baseline.
4
+ Ten implementation batches are complete; this is not a claim that all findings are fixed.
5
+ The runtime is the project's mise default Ruby **4.0.6**.
6
+
7
+ Batch ten addresses the renewed audit findings 2–6. See
8
+ [Security hardening and rollout](../operations/security-hardening.md) for the entry-point coverage matrix,
9
+ revocation semantics, separate administrative permissions, required export/config
10
+ history, changed availability defaults, and the host/database validation gates
11
+ that remain open. Account deletion still retains events unchanged.
12
+
13
+ ## Implemented and locally verified
14
+
15
+ | Finding | Change and evidence |
16
+ | --- | --- |
17
+ | F01 | Devise database-password/HTTP Basic admission now reserves and enforces IP/account and opt-in global limits before password verification. Failed known targets count against their account; unknown identities use normalized hashed keys. Native controllers have explicit admission/session guards. Outcome callbacks do not double-count, and credential-free page visits create no attempts. Standard custom Warden strategies now have IP pre-verification admission and account binding after identity resolution; set_user is guarded even when callbacks are disabled. API/Cable adapters and remaining host integration boundaries are documented in docs/operations/security-hardening.md. |
18
+ | F02 | Warden sign-out uses the current request's actual lock result, not recent audit rows, and affects only the current scope. Confirmed locks now always reject admission, rotate a durable account generation, and invalidate all prior Devise sessions/remember cookies. Unlock never restores them; unrelated accounts remain signed in. |
19
+ | F03 | Beskar automatic account locks, sign-outs, and emergency resets honor monitor/whitelist policy. Whitelisted authentication observations cannot consume enforced global capacity. Risk-enabled audits now record would-lock eligibility and adapter availability without claiming an actual lock. Host Devise/Rails policies remain independent. |
20
+ | F04 | Implemented persistent native locks, expiry/manual unlock, all-session revocation, and a transactional session-creation guard. Existing-session readers honor lock state. Separate-connection races and failed/aborted destruction callbacks are covered; failed cleanup requires successful cleanup before manual unlock. Host integration and same-writer-pool requirements are explicit in docs/guides/authentication.md. |
21
+ | F05 | Geographic history pairs normalized JSON locations with their own timestamps/IDs, uses actual elapsed seconds and explicit chronology, and rejects unusable coordinates/times. Real Devise/native requests exercise persisted travel evidence through actual locks. Mock locations never create travel/country risk. Bounded history and geolocation limitations are explicit in docs/guides/risk-scoring.md; production accuracy remains unverified. |
22
+ | F06 | Middleware, authentication audit, rate limiting, and WAF attribution use the Rails-resolved `remote_ip`. Trusted-proxy regression coverage added. Hosts must configure trusted proxies correctly. |
23
+ | F07 | Ban enforcement reads the database, never cached booleans. Regressions cover rollback, IP edits, expiry edits, stale positives, and stale negatives. |
24
+ | F08 | Permanent flag is authoritative; permanent bans are excluded from expiry cleanup and normalize expiry to nil on save. Temporary bans require expiry. Legacy-data review remains an upgrade prerequisite. |
25
+ | F09 | Database-backed state, unique keys, transactional updates, optimistic conflict detection, and bounded retries replace cache read/modify/write. Separate-connection SQLite regressions exercise counters, admissions, WAF history, and repeated bans. Other database concurrency/load runs remain unverified. |
26
+ | F13 | Minitest 5 pinned for Rails 8.0; lint dependencies updated for Ruby 4. CI now includes Ruby 4.0.6 and 3.4; duplicate automatic workflow made manual. Local verification below; hosted CI has not been run here. |
27
+ | F14 | Removed the disconnected `ip_auth_failures` cache representation. Request-wide IP-quota blocking and its fixed-window denial/auto-ban counter are now opt-in; unrelated shared-NAT traffic remains accessible by default. |
28
+ | F15 | Read-only previews, configured counting windows, bounded attempt storage, enforced backoff deadlines, longest retry selection, global backoff separation, and explicit global/IP-denial reset coverage. Global capacity is now opt-in, removing the default shared lock hot spot and distributed-denial budget. |
29
+ | F16 | Added bounded path canonicalization, root/segment-aware signatures, exact-format query matching, and method/path/category exclusions. Ordinary Rails exceptions no longer score without independent scanner evidence by default; broad scoring requires opt-in. Middleware records at most one charge for path plus exception. Benign/attack corpora cover boundary false positives, encoded traversal, query poisoning, malformed input, and exclusions. Production route coverage and false-positive rates remain unverified; this remains a cumulative scanner heuristic, not first-request exploit prevention. |
30
+ | F19 | Monitor WAF/rate observations use separate state and never create automatic active IP bans. WAF audit `would_be_blocked` respects whitelist and auto-block policy. Automatic account actions now honor the same monitor/whitelist policy. |
31
+
32
+ ## Partially addressed
33
+
34
+ | Finding | Completed so far | Still open |
35
+ | --- | --- | --- |
36
+ | F10 | Dashboard callbacks that render/redirect are not rendered twice; missing-config logging no longer interpolates secrets; credential recipes reject blank/missing values and use secure comparison; recursive installer example removed. | Broader host-adapter authorization coverage and review of custom callbacks. |
37
+ | F11 | Authentication context drops session IDs/forwarded headers and cleans referrers. WAF sinks retain bounded rule evidence, not raw URLs/headers/exception messages. Audit models bound/filter metadata and text using built-in plus host parameter filters, including on legacy model reads. Rescued-exception logs use classes rather than messages. CSV exporters quote cells and visibly prefix formula-like text; JSON projects known fields. Associated Devise/native email presentation now shares filtering across views and exports. Export authorization and no-store behavior are tested. | Arbitrary secrets in allowed free text/paths, historical rows/logs/backups, raw SQL/bulk API bypasses, retention/access policy, and actual spreadsheet-client import/save/reopen QA. Stored-value search can reveal legacy record membership despite read-time redaction. Audit filtering does not anonymize enforcement fields such as ban IPs. |
38
+ | F12 | Logger fallback no longer recurses. Cache availability is not required for authoritative state. Required authentication-state, lock-persistence, and enforced risk-assessment failures reject with 503; optional authentication and WAF audit writes are isolated with savepoints. A failed optional WAF audit does not prevent state updates or bans. Optional host analysis queues only after enclosing commits; rollback cancels it and queue failures do not undo admission/committed work or log raw exception messages. | Unified dependency-failure policy for the other middleware/WAF paths and database-specific fault tests. The optional analysis hook has no outbox/durable-delivery guarantee. |
39
+ | F17 | One assessment provides explicit factors whose points/caps sum to the score and flow into lock evidence. Corrected bot/reason flags, browser captures, and midnight scoring. Removed implicit IP/unlock trust discounts and geographic bypasses; only explicitly admitted, unlocked-success history supplies travel observations. The geographic pattern helper is implemented. Observation history does not raise enforced risk. | Production calibration/false-positive evaluation and any future verified-device/recovery trust mechanism. User-Agent and IP geography remain spoofable/approximate heuristics, not identity proof. |
40
+ | F18 | Native unlock deadlines/manual locks work; Devise's own unlock policy is explicitly separate. Emergency thresholds count confirmed locks with true flags, not duplicated successes or false-flag strings. Password invalidation, session revocation, and mandatory recovery audit are transactional. Opt-in Action Mailer lock/reset/team notices now use independent post-commit jobs with five-attempt retries, validated sender/recipients/recovery page, and isolated failures. Native recovery is exercised through the host reset email/token redemption and authorized manual unlock; resetting the native password alone cannot bypass the lock. A real Devise reset-email/token handoff now verifies its own email-unlock policy, replay rejection and continued revocation of old sessions. | Real SMTP/production worker and broader host recovery verification; durable outbox/reconciliation, duplicate/bounce handling, delivery-status history, and broader host recovery/identity-verification policies. |
41
+ | F20 | Correct migration source/destination paths, destination-aware mount detection, migration numbering, idempotence with engine-copied migrations, monitor-first initializer, truthful immediate installation instructions. Copied migrations run successfully against a fresh SQLite database. | Full fresh host-app boot/authentication exercise and remaining older documentation claims. |
42
+ | F21 | README and gem metadata describe scanner-path and User-Agent heuristics without promising SQLi/XSS filtering, JavaScript challenges, or honeypots. Removed versioned API routes that pointed to missing controllers; authenticated resource exports remain. Devise's invalid callback-registration path was removed. Automatic background analysis now defaults off and requires an explicit host Active Job with a documented post-commit contract. Both authentication paths invoke it only after admission. Removed custom-lock/IP2Location no-op placeholders; unsupported settings fail explicitly. Notification defaults now opt out and their enabled paths perform real Action Mailer delivery, not log-only intent. Current operational-contract documents ship in the gem. | Remaining historical capability/configuration claims. No built-in analyzer, production worker validation, custom lock adapter API, IP2Location integration, or standalone recovery/token endpoint is supplied. |
43
+ | F22 | Added a composite user/event/time index; risk-history projections are ordered/bounded and one device/geographic snapshot supplies scoring/evidence. Exports are capped at 1,000 rows with cursors. Centralized PostgreSQL/SQLite/MySQL JSON search expressions, exact legacy email fallback, bound text values, and literal LIKE escaping. Overview counts reuse grouped queries; related-event tables preload users. Query-count regressions pass. | Live PostgreSQL/MySQL execution, adapter/collation/query-plan/load testing, further query reduction, and retention. JSON-text substring search remains expensive; paging and grouped counts are not transactional snapshots. |
44
+ | F23 | Account deletion retains linked security events unchanged, including original polymorphic IDs, as explicitly chosen. Raw-row comparisons and dashboard/export regressions cover both adapters. Dashboard ban changes require a trusted actor and reason, with bounded before/after snapshots, server operation UUID, and request correlation in a separate journal that survives target/actor deletion. Changes and history share a transaction and ban coordination; bulk selections are bounded, deduplicated, and all-or-nothing. Required-history failures and aborted destruction never report success. SecurityEvent and administrative history reject normal instance CRUD rewrites. Exports require separate permission, actor/reason and a mandatory preparation journal; runtime configuration has its own grant and before/after journal; manual extensions no longer fabricate violations. | No automatic retention period/purge, historical backfill, universal auditing of direct model/automatic changes, failed-attempt journal, database tamper resistance, or production adapter/load verification. Low-level SQL/bulk/counter APIs and privileged host code can bypass model protections. Boot/deployment changes need host source-control/deployment auditing. |
45
+ | F24 | Whitelist parsing follows replaced/mutated configuration; default getters and Devise scope mappings are corrected. Geolocation caches are isolated by provider/database identity, and readers follow database generations. Startup/configure validation checks section schemas, booleans, numeric bounds, whitelist/exclusions, supported providers/strategies, readable MaxMind paths, and enabled host jobs. Partial section assignments merge library defaults; invalid configure blocks do not publish partial changes. Boot/job-autoload and direct unsupported-strategy authentication regressions pass. | Host model/adapter readiness checks, real MaxMind hot-reload/load testing and distributed configuration. Configuration is sealed at boot; authorized runtime changes serialize local publication with required history. This is process-local, not a distributed or crash-atomic publication protocol. |
46
+ | F25 | Lowercase Rack 3 response headers and actual retry deadlines; verified using Rack::Lint. | Content negotiation for API clients. |
47
+ | F26 | Reporting bands, full-history statistics, native-user identity/filtering, and stable event ordering are unified. Ban expiry fields now explicitly use UTC with strict server parsing, browser-compatible milliseconds, and unchanged-value microsecond preservation. Presets use server time; invalid inputs are rejected, custom reasons survive edits, and validation retries preserve duration selections. One nonce-bearing script replaces inline handlers and handwritten method-link submission. Native navigation opts out of host Turbo; real forms retain CSRF and work without JavaScript. Page-size changes preserve filters without duplicate/stale fields; validation feedback stays visible. Chromium regressions cover timezone/DST, skewed clocks, precision, toggles, bulk confirmations, Turbo/back navigation, and no-script forms. | Full strict-style CSP (inline style attributes remain), other browsers, mobile/accessibility and broader host-layout/auth QA, Turbo Frames/Streams, stale-form protection, and remaining dashboard semantics. |
48
+
49
+ ## Next implementation batch
50
+
51
+ First run the new PostgreSQL/MySQL CI jobs and host multi-worker load/failure
52
+ tests; Docker daemon access was denied locally, including outside the sandbox.
53
+ Verify host API/Cable/native adapters and deployment configuration audit coverage.
54
+ Then address F25 content negotiation and the remaining F26 strict-style CSP/host UI work.
55
+ Continue the remaining privacy and
56
+ real-client validation work in F11.
57
+ Complete the remaining portions of F10/F12/F18/F20/F21/F22/F24/F25 alongside those changes.
58
+
59
+ ## Verification
60
+
61
+ - Batch ten regular suite: **839 tests, 4,369 assertions, 0 failures/errors,
62
+ 3 existing MaxMind-data skips**, Ruby 4.0.6, seeds `20260915` and `20260916`.
63
+ - Batch ten Chromium suite: **10 tests, 97 assertions, 0 failures/errors/skips**,
64
+ Chromium/ChromeDriver **153.0.8010.36**, seeds `20260915` and `20260916`. Includes the new
65
+ reason-bearing export form, required history, and existing form regressions.
66
+ - Batch ten lint: **180 files, no offenses**. Zeitwerk and CI YAML parsing pass.
67
+ The existing dummy mailer-preview eager-load warning remains. New test-only
68
+ adapters are pg 1.6.3 and mysql2 0.5.7; their servers/CI jobs were not run locally.
69
+ - New authentication regressions cover custom Warden failure admission, disabled
70
+ callbacks, same-scope identity substitution, whitelist isolation, multiple
71
+ browser/remember-cookie revocation, lock rollback, revocation during verification,
72
+ generic token issuance, stale native sessions, deleted cached Cable users,
73
+ inbound/outbound Cable denial, and Devise recovery-token replay rejection.
74
+ - New administration/availability regressions cover separate permissions,
75
+ required export actor/reason/history, append-only events, sealed/runtime config
76
+ publication, failed journal/transaction rejection, distributed login capacity,
77
+ shared-NAT page access, eliminated default quota queries and database failures.
78
+
79
+ - Before implementation, restored baseline: **595 tests, 2,342 assertions,
80
+ 0 failures, 0 errors, 4 skips**.
81
+ - First batch: **626 tests, 2,465 assertions, 0 failures, 0 errors, 4 skips**.
82
+ - Second batch: **655 tests, 2,606 assertions, 0 failures, 0 errors, 4 skips**.
83
+ - Third batch: **679 tests, 2,753 assertions, 0 failures, 0 errors, 4 skips**.
84
+ - Fourth batch: **700 tests, 2,951 assertions, 0 failures, 0 errors, 4 skips**.
85
+ - Fifth batch: **718 tests, 3,177 assertions, 0 failures, 0 errors, 4 skips**.
86
+ - Sixth batch: **747 tests, 3,388 assertions, 0 failures, 0 errors, 3 skips**.
87
+ - Seventh batch: **768 tests, 3,623 assertions, 0 failures, 0 errors, 3 skips**.
88
+ - Eighth batch: **791 tests, 3,932 assertions, 0 failures, 0 errors, 3 skips**.
89
+ - Ninth-batch regular suite: **798 tests, 4,073 assertions, 0 failures, 0 errors,
90
+ 3 existing MaxMind database skips**, Ruby 4.0.6, seeds `20260923` and `20260924`.
91
+ - Ninth-batch Chromium system suite: **9 tests, 91 assertions, 0 failures, 0 errors,
92
+ 0 skips**, Chromium/ChromeDriver 152.0.7977.82, seeds `20260923` and `20260924`.
93
+ The formerly assertion-free ban-deletion test now verifies a 404 response.
94
+ - Standard Ruby lint passes (164 files). Zeitwerk eager-load verification passes;
95
+ the dummy application's mailer-preview directory is outside eager-load paths.
96
+ - New regressions include separate database connections (not transactional fixture
97
+ connection-sharing), null/unavailable caches, retry/rollback, counter isolation,
98
+ long windows, ban permanence, monitor isolation, trusted proxies, Rack responses,
99
+ dashboard challenges, and fresh migration execution.
100
+ - Authentication regressions additionally exercise real Devise/Warden requests,
101
+ pre-password denial, HTTP Basic, independent scopes, disabled/unavailable audits,
102
+ state/risk failures, native lock/session races, cleanup callbacks, and transactional
103
+ password-reset rollback.
104
+ - Risk regressions cover persisted travel through both adapters, matching score/
105
+ lock evidence, history order and bounds, removal of implicit trust, observation
106
+ isolation, malformed geography/times, midnight and browser scoring, cache/provider
107
+ generation isolation, and unavailable optional caches. Old discount assertions
108
+ were replaced with regressions that reject unsupported trust assumptions.
109
+ - Audit/WAF regressions cover nested/host redaction, metadata bounds, legacy model
110
+ reads and state projection, partial model queries, CSV formulas/column boundaries,
111
+ filtered-email admission accounting, both export cursors/authorization, canonical
112
+ matching/exclusions, benign exception policy, single charges, and optional audit
113
+ failures. Existing tests that required raw emails or exception messages now assert
114
+ redaction; attack-accounting checks use user associations and hashed counters.
115
+ - Dashboard/search regressions cover reporting boundaries across model/UI/export,
116
+ invalid/fractional scores, selected-time statistics, full-history ban totals,
117
+ native/Devise display privacy, stable equal-time ordering, absent phantom API
118
+ routes, literal search text, legacy JSON email fallback, adapter-specific SQL
119
+ generation, composition across pages/exports, and aggregate/preload query counts.
120
+ - Configuration/analysis regressions cover real application startup and host job
121
+ autoloading, host configuration callbacks, invalid-block publication, nested
122
+ default merging, unsupported providers/strategies, both authentication paths,
123
+ native final-session denial, real enclosing commits/savepoint rollbacks, and
124
+ queue exceptions/aborted enqueues. The formerly skipped background-analysis
125
+ test now exercises the supported opt-in contract.
126
+ - Notification regressions exercise actual Devise/native lock requests, real
127
+ commits/savepoint rollbacks, reset-token invalidation, independent user/team
128
+ hooks, enqueue aborts/failures, bounded delivery retries and failed retry enqueue,
129
+ malformed recipients, deleted users, recipient-list changes, delivery switches,
130
+ sanitized transport/job logging, and the full dummy native recovery handoff.
131
+ Only the Rails test delivery backend was used; no real emails were sent.
132
+ - Lifecycle/admin regressions compare every stored event field after native/Devise
133
+ account deletion, including legacy evidence, and exercise missing-user pages and
134
+ exports without rewriting retained rows. Administrative tests cover trusted actor
135
+ resolution, real CSRF rejection, required-history rollback, later-item bulk
136
+ failure, deletion callbacks, strict selections/durations, no-ops and subsecond
137
+ changes, ordinary history CRUD rejection, legacy read filtering, bounded history
138
+ paging, escaped HTML, and confirmation-form verbs. Separate SQLite connections
139
+ exercise concurrent manual extensions and mixed manual/automatic updates.
140
+ - Form/browser regressions additionally cover non-UTC application-zone parsing,
141
+ UTC and offset instants, invalid calendar dates/types/overflow/precision, strict
142
+ creation presets/defaults, permanent/temporary validation, nonce-bearing markup,
143
+ native navigation, and filter-preserving page sizes. Actual Chromium tests use
144
+ multiple browser zones, DST-boundary dates, a skewed browser clock, real CSRF,
145
+ host Turbo loaded from its package, JavaScript disabled, and browser console
146
+ assertions. They verify persistent validation feedback, precise expiry retention,
147
+ and no duplicate administrative operations. A separate browser CI job is added.
148
+ - PostgreSQL/MySQL concurrency, production throughput, hosted CI, and broader
149
+ cross-browser/host UI testing remain unverified. The tested CSP forbids inline
150
+ script handlers but explicitly allows the existing inline style attributes.
151
+
152
+ ## Rollout contract
153
+
154
+ See [State storage](../operations/state-storage.md): apply the migration before starting new
155
+ workers, drain old cache-based workers, share one authoritative writer database,
156
+ review legacy bans, and schedule `beskar:cleanup_security_state`. Cache counters
157
+ are not imported. The test database has been migrated locally; no host production
158
+ database was changed.
159
+
160
+ Rails-native hosts must also adopt the login and existing-session guards in
161
+ [Authentication](../guides/authentication.md). No additional migration beyond the
162
+ shared-state table is required for batches two through seven. Custom lock strategies
163
+ and cross-database authentication models remain unsupported. Batch ten's
164
+ API/Cable/Warden adapters are documented in docs/operations/security-hardening.md.
165
+
166
+ Risk weights were not calibrated against production traffic. Review the corrected
167
+ factors and removal of implicit trust discounts in [Risk scoring](../guides/risk-scoring.md)
168
+ before enabling enforcement or emergency resets. Legacy records without explicit
169
+ admission evidence are not automatically promoted into travel history.
170
+
171
+ Review [Audit data and WAF](../guides/audit-and-waf.md) for changed capture/export fields,
172
+ host email-filter behavior, the 1,000-row export cursor contract, visible CSV text
173
+ prefixes, and the narrower default exception policy. No historical personal data
174
+ or existing bans were bulk-rewritten or deleted by this batch.
175
+
176
+ Review [Dashboard and search](../guides/dashboard-and-search.md) for the corrected reporting
177
+ bands, search behavior/privacy limits, native-user display, and removal of unused
178
+ versioned API routes/helpers. Authentication risk weights and lock thresholds were
179
+ not changed by the dashboard repair.
180
+
181
+ Review [Configuration](../guides/configuration.md) before deploying the sixth batch:
182
+ invalid/obsolete settings now stop startup, partial section assignments overlay
183
+ library defaults, and `:custom`/IP2Location placeholders are rejected. Automatic
184
+ analysis defaults off; opt in with a host job and the new keyword contract. The
185
+ analysis hook supplies neither a built-in analyzer nor recovery delivery.
186
+
187
+ Review [Notifications and recovery](../guides/notifications-and-recovery.md) for batch seven.
188
+ The three notification flags now default to false; enabling them requires explicit
189
+ sender/recovery/team settings and a worker consuming `beskar_notifications`.
190
+ Notifications use their own post-commit delivery jobs. They do not issue recovery
191
+ tokens, unlock accounts, guarantee delivery, or supply an outbox. Production
192
+ transports and host recovery/support procedures must be configured and verified.
193
+
194
+ Review [Audit lifecycle](../guides/audit-lifecycle.md) for batch eight. Apply the new
195
+ administrative-action migration and configure a trusted `audit_actor` before
196
+ dashboard writes; forms/scripted callers must supply `audit_reason`. Drain old
197
+ workers, whose code can still delete events with accounts or make unaudited ban
198
+ changes. Events now survive account deletion unchanged, not anonymized. No audit
199
+ retention period, purge, historical reconstruction, or production data rewrite
200
+ was performed. Journal protections are not a database tamper-resistance guarantee.
201
+
202
+ Review the form/browser sections of [Dashboard and search](../guides/dashboard-and-search.md)
203
+ for batch nine. No additional migration is needed. Expiry inputs are now UTC;
204
+ scripted callers must use valid UTC/offset timestamps and valid bounded duration
205
+ seconds. The host must allow its nonce for the behavior script; the engine does
206
+ not loosen CSP. Inline style attributes still require separate cleanup for hosts
207
+ with strict style policies. Native navigation is deliberate, including with host
208
+ Turbo loaded; it is not support for arbitrary Turbo Frames/Streams integrations.
209
+
210
+ For batch ten, apply `ExpandAdministrativeActionTargets`, configure explicit
211
+ dashboard grants and actor resolution, and update exports to supply a reason.
212
+ Adopt the API/Cable/native adapters, bind credential generations, and plan the
213
+ one-time Devise session invalidation. Configuration is now sealed after boot.
214
+ Global login capacity and request-wide quota blocking are explicit opt-ins.
215
+ Read [Security hardening and rollout](../operations/security-hardening.md) before rolling out; host
216
+ integration, deployment auditing and PostgreSQL/MySQL/load gates remain open.
@@ -0,0 +1,175 @@
1
+ # Audit data, exports, and WAF matching
2
+
3
+ This contract was introduced in the fourth remediation batch. The fifth batch
4
+ adds [consistent dashboard reporting and search](dashboard-and-search.md).
5
+ The eighth batch adds [account-deletion retention and administrative history](audit-lifecycle.md).
6
+ Earlier examples
7
+ that retain full URLs/exception messages or score every Rails exception are
8
+ historical. See [Repair status](../audits/repair-status.md) for remaining findings.
9
+
10
+ ## Audit capture and disclosure
11
+
12
+ WAF matching uses request data transiently. Persisted WAF evidence contains rule
13
+ IDs, static descriptions/categories, severity, rules version, decoding-pass count,
14
+ an allowlisted HTTP method/exception class, timestamps, and scores. The event
15
+ retains the Rails-resolved IP for attribution, subject to host audit filters.
16
+ Raw/canonical paths, query strings, exception messages, and User-Agent headers are
17
+ not copied into WAF events, ban metadata, state, or WAF messages. Public
18
+ `record_violation` calls also project old-style analysis onto this bounded schema.
19
+ The state API strips legacy raw-path/description fields on reads and on the next
20
+ normal write; it does not bulk-rewrite historical database contents.
21
+
22
+ Authentication audits still retain bounded paths, cleaned HTTP(S) referrers,
23
+ device/geographic evidence, IPs, and bounded User-Agent text. Referrers exclude
24
+ credentials, queries, and fragments; session IDs and raw forwarded headers are
25
+ not captured. These records remain sensitive and require access controls.
26
+
27
+ `Services::AuditData` bounds metadata and applies both built-in sensitive-key
28
+ filters and `Rails.application.config.filter_parameters`. Built-ins cover
29
+ passwords, secrets, tokens, authorization, cookies, session IDs, CSRF,
30
+ `exception_message`, `fullpath`, and `matched_path`. Nested keys are filtered.
31
+ Host email filters now redact attempted emails and associated-user presentation
32
+ in views and exports; account associations and hashed admission counters remain intact.
33
+ Plaintext email searches cannot find newly redacted email values.
34
+
35
+ Model validation and model loading sanitize:
36
+
37
+ | Model | Filtered audit fields |
38
+ | --- | --- |
39
+ | SecurityEvent | Metadata, event type (100 characters), IP text (64), User-Agent (500), attempted email (320) |
40
+ | BannedIp | Metadata, reason (100 characters), details (2,048) |
41
+ | AdministrativeAction | Actor/request ID (200), reason (1,000), bounded before/after state projections |
42
+ | Native lock state | Metadata supplied when locking; lock authority is separate |
43
+
44
+ Ban IP addresses, permanence, expiry, IDs, and other enforcement fields are
45
+ deliberately not passed through audit filters. They remain visible to authorized
46
+ dashboard/export readers. Redacting a ban IP must never disable enforcement.
47
+ Host filters targeting event types, geolocation, or authentication evidence can
48
+ reduce audit reporting and history-based risk enrichment; review that policy
49
+ explicitly. Admission state does not depend on a successful optional audit write.
50
+
51
+ Metadata admits JSON-compatible values, at most 64 entries per hash, 50 per
52
+ array, depth 10, and a 512-node traversal budget. Keys are at most 128 bytes;
53
+ string values are at most 2,048 characters, with controls replaced and invalid
54
+ UTF-8 repaired. Non-finite numbers become null. Excessively large serialized
55
+ metadata becomes `{"_truncated":true}` (64-KiB limit); depth/budget exhaustion uses
56
+ `[TRUNCATED]`. Do not treat truncated audit data as complete evidence.
57
+
58
+ Read-time filtering changes loaded objects, not stored legacy rows. Raw SQL,
59
+ `pluck`/`pick`, bulk inserts, and writes that skip validation bypass model
60
+ sanitization. No historic rows, logs, backups, or exports were purged by this
61
+ repair. Arbitrary secrets embedded in allowed free text, path segments, or
62
+ User-Agent text cannot be reliably identified by key-based filters. Hosts must
63
+ filter/drop those fields if their application places secrets there.
64
+
65
+ Beskar's rescued-exception log messages now use the exception class instead of
66
+ its message. This does not sanitize the host application's own request logs,
67
+ exception reporters, custom callbacks, or externally supplied free-text log
68
+ arguments. Account deletion now retains events unchanged, and dashboard ban changes
69
+ require transactional administrative history; see [Audit lifecycle](audit-lifecycle.md).
70
+ An operational retention period, historical cleanup, and tamper-resistant archival
71
+ remain separate work.
72
+
73
+ ## Export contract
74
+
75
+ Both event and ban exports require dashboard authentication, an explicit `:export`
76
+ permission, trusted actor, and a nonblank reason (`audit_reason` or
77
+ `X-Beskar-Audit-Reason`). Each page requires a persisted administrative export
78
+ record before data is sent; see [Audit lifecycle](audit-lifecycle.md).
79
+ CSV and JSON return at most 1,000 records in descending ID order. They set
80
+ `Cache-Control: private, no-store`, `X-Content-Type-Options: nosniff`, and:
81
+
82
+ - `X-Beskar-Export-Limit: 1000`.
83
+ - `X-Beskar-Export-Truncated: true|false`.
84
+ - `X-Beskar-Next-Cursor` when more rows remain.
85
+
86
+ Pass that cursor as `before_id` with the same filters and format for the next
87
+ page. Malformed/nonpositive/out-of-range IDs return 422. Exports no longer load
88
+ the entire relation or silently override ordering with `find_each`.
89
+ Pagination is not a transactional snapshot: concurrent edits/deletions and
90
+ relative-time filters can change membership. Newly inserted higher IDs require
91
+ starting a new export. The 1,000-row cap bounds rows, not database query cost.
92
+
93
+ CSV fields are quoted/escaped, bounded, and dangerous textual prefixes receive a
94
+ visible `text: ` marker. Detection covers leading `=`, `+`, `-`, and `@`,
95
+ including preceding controls/whitespace/formatting characters and Unicode
96
+ compatibility variants. Numeric database fields stay numeric. This intentionally
97
+ changes exported text; JSON preserves the filtered text without a spreadsheet
98
+ marker. Neither format exports an associated user object's custom serialization:
99
+ only its ID and filtered email are included.
100
+
101
+ Quoting alone is not a formula defense, and spreadsheet import/save/reopen
102
+ behavior varies. The visible prefix avoids relying solely on a removable
103
+ apostrophe. See [OWASP's CSV injection guidance](https://owasp.org/www-community/attacks/CSV_Injection).
104
+ Automated tests cover emitted CSV cells and parsed column boundaries, not actual
105
+ Excel/LibreOffice/Sheets clients or their save/reopen behavior. Test the supported
106
+ client workflow before claiming spreadsheet-client safety.
107
+
108
+ ## WAF matching contract
109
+
110
+ The WAF is a scanner-path heuristic, not a general SQL injection/XSS engine.
111
+ It does not parse request bodies or implement JavaScript challenges/honeypots.
112
+
113
+ - Match at most 8,192 path bytes, with at most two percent-decoding passes.
114
+ - Convert backslashes to slashes, preserve literal plus signs, and retain dot
115
+ segments so traversal is detectable rather than erased by normalization.
116
+ - Flag oversized paths, malformed escapes, invalid UTF-8/control bytes, and
117
+ excessive encoding as medium-severity malformed-path evidence.
118
+ - Use root/segment boundaries to avoid matches inside ordinary words.
119
+ Ordinary `.well-known` routes are not scanner signatures.
120
+ - Ignore arbitrary query text. Only a flat, exact `format` query key with an
121
+ allowlisted executable value is checked; queries over 8,192 bytes are skipped.
122
+ - Retain matched rule IDs with `rules_version: 1`, not the matching input.
123
+
124
+ Use narrow exclusions for legitimate host routes:
125
+
126
+ ```ruby
127
+ Beskar.configure do |config|
128
+ config.waf[:exception_detection] = :suspicious
129
+ config.waf[:request_exclusions] = [
130
+ {path: %r{\A/wp-content/}, methods: ["GET", "HEAD"],
131
+ categories: [:wordpress_static]},
132
+ {path: %r{\A/reports/}, methods: ["GET"], categories: [:unknown_format]}
133
+ ]
134
+ end
135
+ ```
136
+
137
+ Exclusions use the decoded path. Omitted methods/categories mean all methods/
138
+ categories for that path. A static-file exclusion does not exclude traversal or
139
+ configuration-file rules. Existing `record_not_found_exclusions` still apply
140
+ to RecordNotFound exception analysis, not independent path signatures.
141
+
142
+ Exception policies:
143
+
144
+ - `:suspicious` (default): ordinary known Rails exceptions need independent
145
+ path/format evidence. A missing record or unsupported format alone is not abuse.
146
+ - `:all`: opt into broad scoring of the four known exception classes; legitimate
147
+ errors can accumulate enough points to ban an IP.
148
+ - `:none`: disable exception scoring, without disabling request-path matching.
149
+
150
+ The known classes are UnknownFormat, InvalidType, RecordNotFound, and
151
+ IpSpoofAttackError (matched by exact class name). Middleware only attributes
152
+ IP-spoof exceptions when an IP was safely resolved; it never substitutes an
153
+ attacker-supplied forwarded header. Application exceptions still propagate.
154
+
155
+ One middleware pass records at most one violation. Matching several path rules
156
+ uses the highest path severity; a subsequent application exception neither
157
+ adds another charge nor upgrades that earlier charge. Scores remain cumulative:
158
+ one critical signature adds 95 points, below the default threshold of 150.
159
+ This is not a promise to block the first exploit request. Monitor/whitelist
160
+ observations remain isolated from enforcement state.
161
+
162
+ ## Rollout and remaining verification
163
+
164
+ The fourth-batch changes require only the first batch's shared-state migration;
165
+ batch eight also requires the administrative-action table described in
166
+ [Audit lifecycle](audit-lifecycle.md). Review changed audit fields, host filters, cursor-based export
167
+ consumers, and default exception policy before deployment. Existing enforced bans
168
+ are not automatically forgiven by the narrower matching policy.
169
+
170
+ Run monitor-first against real host routes and mounted paths. Local benign/attack
171
+ corpora cover percent/double encoding, backslashes, query poisoning, boundaries,
172
+ exclusions, ordinary Rails exceptions, legacy state, and single-charge behavior.
173
+ These tests do not establish production false-positive rates or complete scanner
174
+ coverage. Production database fault/load tests, spreadsheet-client QA, historical
175
+ data cleanup, and operational retention policies remain unverified or unimplemented.
@@ -0,0 +1,172 @@
1
+ # Audit lifecycle and administrative changes
2
+
3
+ This is the eighth remediation batch's contract. Earlier documentation describing
4
+ account-deletion cascades or unaudited dashboard ban changes is historical.
5
+
6
+ ## Account deletion: retain events unchanged
7
+
8
+ Deleting a native or Devise account no longer destroys its linked security events.
9
+ Beskar leaves every stored event field unchanged, including `user_type`, `user_id`,
10
+ timestamps, attempted email, IP address, and metadata. The association resolves to
11
+ nil once the account is gone; dashboard pages and exports tolerate that absence.
12
+ JSON exports retain the original polymorphic identifiers without embedding a
13
+ nonexistent user. Existing read-time privacy filters still apply without rewriting
14
+ stored rows.
15
+
16
+ This is **retention, not anonymization**. Identifiers and evidence can still identify
17
+ people. Beskar does not nullify the linkage, impose an expiry, or supply an automatic
18
+ event purge. Unidentified failed-login events also remain independent of account
19
+ deletion. Previously cascade-deleted events are not recovered, and this change
20
+ does not rewrite historical data or backups.
21
+
22
+ Hosts must choose and operate any retention period, access controls, backup policy,
23
+ or explicitly authorized erasure workflow separately. Avoid reusing account IDs;
24
+ the retained polymorphic reference can resolve to a replacement row with the same
25
+ type/ID. Model renames/removals also require a host migration strategy. Host-defined
26
+ callbacks, bulk SQL, and database rules can override this lifecycle. Normal
27
+ SecurityEvent save/update/update_columns/destroy/delete operations are now
28
+ rejected after creation. This is append-only model behavior, not tamper-proof
29
+ database storage or a restriction on privileged host code.
30
+
31
+ ## Trusted administrative actor
32
+
33
+ Dashboard writes require both the existing authorization callback and a separate
34
+ server-side `audit_actor` resolver. For a host with a Devise `User#admin?` role:
35
+
36
+ ```ruby
37
+ Beskar.configure do |config|
38
+ config.authenticate_admin = ->(request) do
39
+ request.env["warden"]&.authenticate(scope: :user)&.admin?
40
+ end
41
+ config.audit_actor = ->(request) do
42
+ user = request.env["warden"]&.user(scope: :user)
43
+ "User:#{user.id}" if user&.admin?
44
+ end
45
+ config.authorize_admin = ->(request, permission) do
46
+ user = request.env["warden"]&.user(scope: :user)
47
+ user&.admin? && user.beskar_permissions.include?(permission.to_s)
48
+ end
49
+ end
50
+ ```
51
+
52
+ Adapt the scope, role, and `beskar_permissions` lookup to the host. Permission
53
+ names are `read`, `manage_bans`, `export`, and `read_audit`; none is implied by
54
+ another or by successful authentication. Missing grants deny access (403).
55
+ These callbacks run in controller
56
+ context; the actor callback runs only after dashboard authorization and CSRF
57
+ verification, once per mutation request. Native hosts should derive the identifier
58
+ from their already authenticated administrative session. A shared Basic/token
59
+ credential may use an explicit stable service identifier, but that records a
60
+ shared identity, not which individual used it.
61
+
62
+ The resolver must return an opaque string of 1–200 ASCII characters, starting with
63
+ a letter/digit and otherwise containing letters, digits, `:`, `_`, `.`, `/`, or `-`.
64
+ Use a stable type/ID, never an email, password, token, request parameter, or
65
+ unverified header. Beskar cannot verify the truth of a host callback's identity.
66
+ Authorization's boolean result is not inferred to be an actor.
67
+
68
+ `audit_actor` defaults to nil. Separately authorized reads still work, but mutations and exports return
69
+ 503 when the resolver is missing, invalid, or raises. Startup validates Proc-or-nil
70
+ without executing the callback. Every mutation also requires `audit_reason`, a
71
+ nonblank string of at most 1,000 characters; invalid input returns 422. Prefer a
72
+ case reference and short explanation. Key-based filtering cannot identify arbitrary
73
+ secrets embedded in free text.
74
+
75
+ ## Journal and transactional behavior
76
+
77
+ Apply `ExpandAdministrativeActionTargets` as well as the original journal migration.
78
+ Entries now distinguish `BannedIp`, `SecurityEvent`, and `Configuration` targets;
79
+ collection/configuration entries have no target ID. Export preparation records
80
+ the actor, required reason, request, format, filtered query, count, ID bounds,
81
+ and truncation before sending data. A journal failure returns 503 without an
82
+ export body. Supply `audit_reason` or `X-Beskar-Audit-Reason` on every cursor page.
83
+ The record is not proof of successful client download. Runtime configuration
84
+ publication has a separate permission and journal; see [Configuration](configuration.md).
85
+
86
+ `beskar_administrative_actions` records dashboard ban creation, updates, unbans,
87
+ extensions, and conversion to permanent bans. Each changed target receives:
88
+
89
+ - A server-resolved actor, required reason, action name, ban ID, and creation time.
90
+ - A server-generated operation UUID shared by all targets of one bulk operation.
91
+ - The Rails request ID, bounded to 200 characters, for correlation only. It may
92
+ originate in a client `X-Request-ID` header and is not proof of identity.
93
+ - Filtered, bounded before/after projections of ban ID/IP, reason, details,
94
+ permanence, ban/expiry times, violation count, and metadata. Creation has an empty
95
+ before-state; unban has an empty after-state.
96
+
97
+ The projections use [AuditData filtering and bounds](audit-and-waf.md), not a raw
98
+ database copy. Timestamp precision, redaction, and truncation can make projections
99
+ look identical despite a real stored change; such changes still receive an entry.
100
+ The journal is not a full-fidelity restore backup.
101
+
102
+ Ban changes and required journal inserts share one writer-database transaction and
103
+ the same coordination keys used by automatic escalation. Bulk requests accept
104
+ 1–100 positive ban IDs, deduplicate and sort them, and require every target to
105
+ exist. Missing targets return 404; malformed inputs and unsupported operations or
106
+ extension durations return 422. Supported extensions are `1h`, `6h`, `24h`, `7d`,
107
+ and `30d`; making a ban permanent is a separate operation. Existing permanent bans
108
+ cannot be extended. Manual extensions do not increment violation counts; automatic
109
+ `BannedIp.extend_ban!` behavior is unchanged. Dashboard edits cannot change a ban's
110
+ IP identity.
111
+
112
+ There is no partial bulk success. A failed target callback or required history
113
+ insert rolls back the transaction, including earlier items. Success messages follow
114
+ successful completion; Active Record errors and rejected persistence callbacks
115
+ return 503 rather than claiming success (other unexpected exceptions propagate).
116
+ If a connection fails during commit, the client may not know whether the
117
+ transaction committed: reload state/history before retrying. Operation UUIDs are
118
+ correlation, not a client retry/idempotency protocol. Exact no-op updates create no
119
+ history, and the bulk response reports the count actually changed.
120
+
121
+ Authenticated history pages at `/beskar/administrative_actions` and `/:id` are
122
+ read-only, paginated (at most 100 rows/page), HTML-escaped, and marked `no-store`.
123
+ Filter the index with `target_id`. There are no history edit/delete/export routes.
124
+ History has no actor or ban foreign key and survives their deletion. Ordinary
125
+ instance save/update, `update_columns`, destroy, and delete attempts are rejected.
126
+ This is **not tamper-proof storage**: low-level counter/bulk APIs, raw SQL, and
127
+ privileged database access can bypass model protections. Database access policy,
128
+ external archival, and tamper detection remain separate work.
129
+
130
+ Unban and row-extension links now open review pages with real forms and required
131
+ reasons; their GET requests do not mutate state. New/edit/inline-extension/bulk
132
+ forms also require reasons. Batch nine consolidates behavior in a nonce-bearing
133
+ script, removes inline event handlers/method-link synthesis, and adopts native
134
+ navigation. See [Dashboard and search](dashboard-and-search.md) for UTC inputs,
135
+ JavaScript-disabled behavior, browser verification, and the remaining style-CSP limit.
136
+
137
+ ## Coverage and deployment boundaries
138
+
139
+ The journal is prospective and covers dashboard exports, audited runtime
140
+ configuration publication, and dashboard/explicit `AdministrativeBans` changes.
141
+ It does **not** automatically journal every
142
+ direct model/manager/console write, WAF transition, expiry cleanup, or failed action
143
+ attempt. Existing administrative history is not reconstructed. For host-owned
144
+ manual workflows, authorize the operator first, then create one service instance
145
+ per operation with trusted `actor:`, required `reason:`, and a nonblank `request_id:`:
146
+
147
+ ```ruby
148
+ Beskar::Services::AdministrativeBans.new(
149
+ actor: "Operator:42", reason: "Case 123: false positive", request_id: SecureRandom.uuid
150
+ ).change!([ban.id], action: "unban")
151
+ ```
152
+
153
+ The service itself is not an authorization boundary. As with coordinated security
154
+ state, bans, journal, and state rows must use the same authoritative writer pool.
155
+ Any `Rails.cache` backend remains supported.
156
+
157
+ Before rollout, copy and apply `CreateBeskarAdministrativeActions` and
158
+ `ExpandAdministrativeActionTargets` along with any
159
+ missing earlier migrations (`bin/rails beskar:install:migrations`, then
160
+ `bin/rails db:migrate`). Configure `authorize_admin` and `audit_actor`, update scripted dashboard callers
161
+ to supply `audit_reason`, and drain/restart old workers: older code can still delete
162
+ events on account deletion or mutate bans without the journal. The new table was
163
+ applied only to the local test database; no production database was changed.
164
+
165
+ No cleanup job is added for either audit table. Per-entry bounds do not bound total
166
+ storage; hosts must monitor growth and choose retention deliberately. Local tests
167
+ cover both account adapters, unchanged raw rows after deletion/read/export, real
168
+ CSRF rejection, required-history and later-bulk-item rollback, escaped HTML/forms,
169
+ fresh migration execution, and simultaneous manual/automatic updates using separate
170
+ SQLite connections. Batch nine adds Chromium form/CSRF/navigation checks. Live
171
+ PostgreSQL/MySQL, production load, broader host browser flows, and tamper-resistant
172
+ external archival remain unverified.