maquina-generators 0.5.0 → 0.7.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/README.md +125 -15
- data/lib/generators/maquina/app/app_generator.rb +9 -4
- data/lib/generators/maquina/app/templates/README.md.erb +6 -4
- data/lib/generators/maquina/app/templates/app/views/home/index.html.erb +18 -5
- data/lib/generators/maquina/app/templates/config/database.yml.tt +12 -0
- data/lib/generators/maquina/clave/clave_generator.rb +48 -0
- data/lib/generators/maquina/clave/templates/app/controllers/registration/verifications_controller.rb.tt +10 -1
- data/lib/generators/maquina/clave/templates/app/controllers/session/verifications_controller.rb.tt +10 -1
- data/lib/generators/maquina/clave/templates/app/models/email_verification.rb.tt +18 -0
- data/lib/generators/maquina/clave/templates/config/locales/clave.en.yml +2 -0
- data/lib/generators/maquina/clave/templates/config/locales/clave.es.yml +2 -0
- data/lib/generators/maquina/csp_reports/USAGE +18 -0
- data/lib/generators/maquina/csp_reports/csp_reports_generator.rb +231 -0
- data/lib/generators/maquina/csp_reports/templates/app/controllers/csp_reports_controller.rb +24 -0
- data/lib/generators/maquina/csp_reports/templates/app/models/security/csp_violation.rb +89 -0
- data/lib/generators/maquina/csp_reports/templates/app/models/security/csp_violation_report.rb +30 -0
- data/lib/generators/maquina/csp_reports/templates/app/views/backstage/security/_csp_violations.html.erb +42 -0
- data/lib/generators/maquina/csp_reports/templates/db/create_csp_violations.rb.tt +23 -0
- data/lib/generators/maquina/mission_control_jobs/mission_control_jobs_generator.rb +32 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/controllers/backstage_dashboard_controller.rb +47 -0
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/backstage_dashboard/index.html.erb.tt +93 -0
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/layouts/_admin_navigation.html.erb.tt +18 -6
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/layouts/admin.html.erb +21 -0
- data/lib/generators/maquina/rack_attack/USAGE +8 -6
- data/lib/generators/maquina/rack_attack/rack_attack_generator.rb +6 -2
- data/lib/generators/maquina/rack_attack/templates/config/initializers/rack_attack.rb.tt +139 -57
- data/lib/generators/maquina/security/USAGE +15 -0
- data/lib/generators/maquina/security/security_generator.rb +278 -0
- data/lib/generators/maquina/security/templates/app/controllers/backstage/security_controller.rb.tt +45 -0
- data/{test/tmp/app/controllers/backstage_controller.rb → lib/generators/maquina/security/templates/app/controllers/backstage_controller.rb.tt} +0 -1
- data/lib/generators/maquina/security/templates/app/controllers/backstage_dashboard_controller.rb +47 -0
- data/lib/generators/maquina/security/templates/app/models/security/abuse_event.rb +36 -0
- data/lib/generators/maquina/security/templates/app/models/security/abuse_report.rb +112 -0
- data/lib/generators/maquina/security/templates/app/models/security/record.rb +6 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_addresses.html.erb +37 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_blocked.html.erb +53 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_by_day.html.erb +33 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_hosts.html.erb +31 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_paths.html.erb +33 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_recent.html.erb +37 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_utc_time.html.erb +2 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/show.html.erb +47 -0
- data/lib/generators/maquina/security/templates/app/views/backstage_dashboard/index.html.erb.tt +93 -0
- data/lib/generators/maquina/security/templates/app/views/layouts/_admin_navigation.html.erb.tt +40 -0
- data/lib/generators/maquina/security/templates/app/views/layouts/admin.html.erb +21 -0
- data/{test/tmp/app/views/layouts/solid_errors/application.html.erb → lib/generators/maquina/security/templates/app/views/layouts/backstage/security.html.erb} +1 -1
- data/lib/generators/maquina/security/templates/config/initializers/rack_attack_events.rb +34 -0
- data/lib/generators/maquina/security/templates/db/security_schema.rb.tt +27 -0
- data/lib/generators/maquina/solid_errors/solid_errors_generator.rb +32 -1
- data/lib/generators/maquina/solid_errors/templates/app/controllers/backstage_dashboard_controller.rb +47 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/backstage_dashboard/index.html.erb.tt +93 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/layouts/_admin_navigation.html.erb.tt +18 -6
- data/lib/generators/maquina/solid_errors/templates/app/views/layouts/admin.html.erb +21 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/error_mailer/error_occurred.html.erb +65 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/error_mailer/error_occurred.text.erb +30 -0
- data/lib/generators/maquina/solid_errors/templates/config/initializers/solid_errors.rb.tt +12 -0
- data/lib/maquina_generators/version.rb +1 -1
- data/test/generators/maquina/app_generator_test.rb +21 -0
- data/test/generators/maquina/clave_generator_test.rb +74 -0
- data/test/generators/maquina/csp_reports_generator_test.rb +151 -0
- data/test/generators/maquina/mission_control_jobs_generator_test.rb +28 -4
- data/test/generators/maquina/rack_attack_generator_test.rb +41 -19
- data/test/generators/maquina/security_generator_test.rb +380 -0
- data/test/generators/maquina/solid_errors_generator_test.rb +59 -4
- data/test/tmp/.gitignore +5 -0
- data/test/tmp/.rubocop.yml +11 -0
- data/test/tmp/.standard.yml +1 -0
- data/test/tmp/Gemfile +12 -2
- data/test/tmp/Procfile.dev +3 -0
- data/test/tmp/README.md +89 -0
- data/test/tmp/app/controllers/home_controller.rb +6 -0
- data/test/tmp/app/javascript/application.js +1 -0
- data/test/tmp/app/views/home/index.html.erb +204 -0
- data/test/tmp/app/views/layouts/application.html.erb +10 -0
- data/test/tmp/config/application.rb +15 -0
- data/test/tmp/config/database.yml +90 -0
- data/test/tmp/config/database.yml.example +90 -0
- data/test/tmp/config/environments/development.rb +8 -0
- data/test/tmp/config/environments/production.rb +6 -0
- data/test/tmp/config/initializers/generators.rb +4 -0
- data/test/tmp/config/routes.rb +1 -1
- metadata +60 -32
- data/test/tmp/app/agents/error_redactor.rb +0 -34
- data/test/tmp/app/agents/errors_query.rb +0 -84
- data/test/tmp/app/agents/failure_triage.rb +0 -21
- data/test/tmp/app/agents/get_exception_tool.rb +0 -26
- data/test/tmp/app/agents/list_failures_tool.rb +0 -20
- data/test/tmp/app/agents/top_exceptions_tool.rb +0 -18
- data/test/tmp/app/helpers/solid_errors_helper.rb +0 -9
- data/test/tmp/app/javascript/controllers/backtrace_filter_controller.js +0 -27
- data/test/tmp/app/javascript/controllers/clipboard_controller.js +0 -36
- data/test/tmp/app/views/layouts/_admin_navigation.html.erb +0 -28
- data/test/tmp/app/views/solid_errors/errors/_actions.html.erb +0 -22
- data/test/tmp/app/views/solid_errors/errors/_delete_button.html.erb +0 -10
- data/test/tmp/app/views/solid_errors/errors/_error_card.html.erb +0 -63
- data/test/tmp/app/views/solid_errors/errors/_resolve_button.html.erb +0 -11
- data/test/tmp/app/views/solid_errors/errors/index.html.erb +0 -37
- data/test/tmp/app/views/solid_errors/errors/show/_actions.html.erb +0 -33
- data/test/tmp/app/views/solid_errors/errors/show/_error_details.html.erb +0 -95
- data/test/tmp/app/views/solid_errors/errors/show/_header.html.erb +0 -18
- data/test/tmp/app/views/solid_errors/errors/show/_properties.html.erb +0 -65
- data/test/tmp/app/views/solid_errors/errors/show.html.erb +0 -23
- data/test/tmp/app/views/solid_errors/occurrences/_backtrace_line.html.erb +0 -27
- data/test/tmp/app/views/solid_errors/occurrences/_collection.html.erb +0 -80
- data/test/tmp/app/views/solid_errors/occurrences/_occurrence.html.erb +0 -129
- data/test/tmp/bin/failures +0 -37
- data/test/tmp/bin/failures-mcp +0 -21
- data/test/tmp/config/initializers/solid_errors.rb +0 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3a31ee05a1ed941cc88119c5721629c3c5089081dde3e7833d1768cb9598b202
|
|
4
|
+
data.tar.gz: 5bc0462a7009085c8a752791090347501a59886cab23eb74ba7bdd5ff0ff2f08
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 85c73f484e3ed4e09e28ec2bebf42ca13ea1dd84693b9e7abbffa0746f14aa69930d95607c0c592085ad6acd88ba2a5d81afd90e602c8265cf9122e05d86a3ef
|
|
7
|
+
data.tar.gz: bf4cec22c128729f8425b71d345a6ef166e6c9f4d18172860ef1eca3bbb894997397ce227297168f54218d6d26c87dc707dbedbd7b268594620543d22f1b6437
|
data/README.md
CHANGED
|
@@ -19,12 +19,14 @@ A collection of Rails generators from the Maquina umbrella. Each generator produ
|
|
|
19
19
|
- **Migrations:** 4 migrations (accounts, users, sessions, email_verifications)
|
|
20
20
|
- **Test helper:** `sign_in_as(user)` and `sign_out` for integration tests
|
|
21
21
|
|
|
22
|
+
Sign-in codes are encrypted at rest with Active Record encryption and compared in constant time. The generator adds the encryption keys to `config/credentials.yml.enc` when they are missing; without a master key, run `bin/rails db:encryption:init`.
|
|
23
|
+
|
|
22
24
|
#### Installation
|
|
23
25
|
|
|
24
26
|
Add to your Gemfile:
|
|
25
27
|
|
|
26
28
|
```ruby
|
|
27
|
-
gem "
|
|
29
|
+
gem "maquina-generators", group: :development
|
|
28
30
|
```
|
|
29
31
|
|
|
30
32
|
Run the generator:
|
|
@@ -55,6 +57,7 @@ All generated code lives in your app -- edit it directly:
|
|
|
55
57
|
- **Session duration:** Edit `authentication.rb` (default: 30 days)
|
|
56
58
|
- **Code expiration:** Edit controllers (default: 15 minutes)
|
|
57
59
|
- **Cooldown between codes:** Edit `EmailVerification::COOLDOWN_MINUTES` (default: 15)
|
|
60
|
+
- **Wrong guesses before a code locks:** Edit `EmailVerification::MAX_ATTEMPTS` (default: 5)
|
|
58
61
|
- **Colors/styling:** Edit view templates (default: indigo)
|
|
59
62
|
- **Email sender:** Edit `app/mailers/verification_mailer.rb`
|
|
60
63
|
- **Translations:** Edit `config/locales/clave.*.yml`
|
|
@@ -114,8 +117,10 @@ All generated code lives in your app -- edit it directly:
|
|
|
114
117
|
|
|
115
118
|
- **BackstageController:** Inherits from `ActionController::Base` (bypasses app's ApplicationController concerns)
|
|
116
119
|
- **Initializer:** Credentials-first auth with ENV variable fallback, database connection config
|
|
117
|
-
- **Route:** Mounts `SolidErrors::Engine`
|
|
118
|
-
- **
|
|
120
|
+
- **Route:** Mounts `SolidErrors::Engine` at `<prefix>/errors`
|
|
121
|
+
- **Mailer templates:** Self-contained `error_occurred` HTML and text templates, so `send_emails` works without the dashboard's helpers
|
|
122
|
+
- **Admin navigation:** Shared navigation bar with Overview, Errors, Jobs and Security tabs
|
|
123
|
+
- **Admin overview:** `BackstageDashboardController` at the prefix root with cards for each installed dashboard and a `@metrics` slot for your own aggregate counts; answers 503 until backstage credentials are set
|
|
119
124
|
- **Custom layout:** Tailwind-styled layout with admin navigation and toast flash messages
|
|
120
125
|
- **Stimulus controllers:** `clipboard_controller.js` and `backtrace_filter_controller.js`
|
|
121
126
|
- **Custom views:** Tailwind-styled views to override the gem defaults (included by default, use `--no-copy-views` to skip)
|
|
@@ -156,8 +161,9 @@ Credentials are resolved in order:
|
|
|
156
161
|
- **BackstageController:** Inherits from `ActionController::Base` with maquina_components helpers (bypasses app's ApplicationController concerns)
|
|
157
162
|
- **Helper:** `MissionControlHelper` with `job_status_badge_variant` and `nav_icon_for_section`
|
|
158
163
|
- **Initializer:** Sets base controller class, credentials-first auth with ENV variable fallback
|
|
159
|
-
- **Route:** Mounts `MissionControl::Jobs::Engine`
|
|
160
|
-
- **Admin navigation:** Shared navigation bar
|
|
164
|
+
- **Route:** Mounts `MissionControl::Jobs::Engine` at `<prefix>/jobs`
|
|
165
|
+
- **Admin navigation:** Shared navigation bar with Overview, Errors, Jobs and Security tabs
|
|
166
|
+
- **Admin overview:** `BackstageDashboardController` at the prefix root with cards for each installed dashboard and a `@metrics` slot for your own aggregate counts; answers 503 until backstage credentials are set
|
|
161
167
|
- **Custom layout:** Tailwind-styled layout with admin navigation, toast flash messages, application/server selection, and tab navigation
|
|
162
168
|
- **Custom views:** Tailwind-styled views for jobs, queues, workers, and recurring tasks (included by default, use `--no-copy-views` to skip)
|
|
163
169
|
|
|
@@ -218,28 +224,132 @@ rails g maquina:solid_queue --database postgresql # PostgreSQL
|
|
|
218
224
|
|
|
219
225
|
### Rack Attack -- Request Protection
|
|
220
226
|
|
|
221
|
-
**Rack Attack** installs the [rack-attack](https://github.com/rack/rack-attack) gem with
|
|
227
|
+
**Rack Attack** installs the [rack-attack](https://github.com/rack/rack-attack) gem with rules that ban vulnerability scanners and addresses that ignore the throttle.
|
|
222
228
|
|
|
223
229
|
#### What it generates
|
|
224
230
|
|
|
225
|
-
- **Initializer:** `config/initializers/rack_attack.rb` with
|
|
231
|
+
- **Initializer:** `config/initializers/rack_attack.rb` with the bans, throttles, safelist, 403 responder and an `[ATTACK]` log line per refusal. Its knobs are constants on `Rack::Attack` (`SCANNER_*`, `GENERAL_*`, `FLOOD_*`, `LOGIN_*`) and `Rack::Attack.banned?(ip)` answers whether an address is banned now.
|
|
226
232
|
|
|
227
233
|
#### Usage
|
|
228
234
|
|
|
229
235
|
```bash
|
|
230
236
|
rails g maquina:rack_attack
|
|
237
|
+
rails g maquina:rack_attack --login-path /sign_in # Throttle a different sign-in path
|
|
231
238
|
```
|
|
232
239
|
|
|
233
240
|
The generator automatically runs `bundle install`.
|
|
234
241
|
|
|
235
242
|
#### Default Protections
|
|
236
243
|
|
|
237
|
-
- **
|
|
244
|
+
- **Scanner ban (Fail2Ban):** three scanner paths in 10 minutes bans the IP for 7 days. Scanner paths are PHP files (`*.php`), WordPress paths (`wp-admin`, `wp-login`, etc.), sensitive files anywhere in the path (`.env`, `.git`, `/etc/passwd`, etc.), backup archives (`.zip`, `.sql`, `.tar.gz`, `.bak`, etc.) and scanner targets (`phpmyadmin`, `cgi-bin`, etc.). `/rails/active_storage` is exempt.
|
|
245
|
+
- **Flood ban (Allow2Ban):** 600 requests in 5 minutes (twice the general throttle) bans the IP for 1 day.
|
|
246
|
+
- **Throttles:** 300 requests/5min per IP (general; `/assets` and `/rails/active_storage` exempt), 5 `POST /session`/20s per IP
|
|
238
247
|
- **Safelists:** Localhost (`127.0.0.1`, `::1`)
|
|
239
|
-
- **
|
|
240
|
-
|
|
248
|
+
- **Responses:** 403 Forbidden for blocklisted and banned, 429 Too Many Requests for throttled
|
|
249
|
+
|
|
250
|
+
Bans live in `Rails.cache`, so they need a shared cache store (Solid Cache) in production. Customize rules in `config/initializers/rack_attack.rb`. To see the refusals, add `maquina:security`.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### Security -- Rack::Attack with an Abuse Dashboard
|
|
255
|
+
|
|
256
|
+
**Security** records every Rack::Attack refusal in its own database and summarises the last week on a backstage page next to Solid Errors and Mission Control Jobs.
|
|
257
|
+
|
|
258
|
+
#### What it generates
|
|
259
|
+
|
|
260
|
+
- **Rack::Attack rules:** runs `maquina:rack_attack` unless `config/initializers/rack_attack.rb` already defines `Rack::Attack.banned?` (an older initializer without it is replaced)
|
|
261
|
+
- **Subscriber:** `config/initializers/rack_attack_events.rb` writes a `Security::AbuseEvent` per throttled, blocked or banned request; a failed write never turns a 403 into a 500
|
|
262
|
+
- **Models:** `Security::Record` (`connects_to` the `security` database), `Security::AbuseEvent` (30-day retention), `Security::AbuseReport` (the page's figures)
|
|
263
|
+
- **Database:** `db/security_schema.rb` and a `security:` entry in every multi-database environment of `config/database.yml` (and `config/database.yml.example`)
|
|
264
|
+
- **Retention:** a daily `purge_abuse_events` task in `config/recurring.yml`
|
|
265
|
+
- **BackstageController**, **controller** (`Backstage::SecurityController`), **route** (`<prefix>/security`), **layout** and **views**: stats, throttled addresses, blocked addresses (banned now, expired or not yet banned), scanner paths, sign-in throttles, targeted hosts, refusals by day, recent refusals (views skipped with `--no-copy-views`)
|
|
266
|
+
- **Admin navigation:** a Security tab, added to an existing `_admin_navigation` partial too
|
|
267
|
+
- **Admin overview:** the shared dashboard at the prefix root, if Solid Errors or Mission Control Jobs have not installed it already
|
|
268
|
+
|
|
269
|
+
#### What the page shows
|
|
270
|
+
|
|
271
|
+
The last 7 days of refusals, top five of each, read from `Security::AbuseEvent` through `Security::AbuseReport`. It is a briefing, not a log viewer: no filters, no pagination. Times are UTC.
|
|
272
|
+
|
|
273
|
+
- **Stats:** refused requests, throttled, blocked or banned, and distinct addresses
|
|
274
|
+
- **Throttled addresses:** over the general or sign-in limit, busiest first, with the rules they hit
|
|
275
|
+
- **Blocked addresses:** every address a blocklist refused, marked **Banned now** (Rack::Attack's own answer at render time), **Expired**, or **Not banned** -- an address with a strike or two on the scanner rule, or a slow scanner that stays under three paths in ten minutes
|
|
276
|
+
- **Most requested blocked paths:** what the scanners were after. A banned address is refused on every path, its ordinary pages included, so only the paths `Rack::Attack.scanner_path?` recognises are listed
|
|
277
|
+
- **Sign-in throttles:** addresses refused on `POST <login-path>`, kept apart so scanner noise never buries them
|
|
278
|
+
- **Targeted hosts:** which hostnames the refusals were aimed at
|
|
279
|
+
- **By day:** throttled, blocked and banned per day, zeros kept so a spike reads against its neighbours
|
|
280
|
+
- **Recent refusals:** the last 20, newest first
|
|
281
|
+
|
|
282
|
+
#### Usage
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
rails g maquina:security --prefix /admin
|
|
286
|
+
bin/rails db:prepare
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The generator automatically runs `bundle install`. The page needs maquina_components.
|
|
290
|
+
|
|
291
|
+
#### Options
|
|
292
|
+
|
|
293
|
+
```bash
|
|
294
|
+
rails g maquina:security --prefix /admin # Default
|
|
295
|
+
rails g maquina:security --prefix /admin --login-path /sign_in # Different sign-in path
|
|
296
|
+
rails g maquina:security --prefix /admin --no-copy-views # Without views
|
|
297
|
+
rails g maquina:security --prefix /backstage \
|
|
298
|
+
--user-env-var ADMIN_USER --password-env-var ADMIN_PASSWORD # Custom env vars
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### Authentication
|
|
302
|
+
|
|
303
|
+
HTTP Basic Auth, with credentials resolved in order:
|
|
304
|
+
|
|
305
|
+
1. `Rails.application.credentials.backstage.username` / `.password`
|
|
306
|
+
2. `ENV["SECURITY_USER"]` / `ENV["SECURITY_PASSWORD"]` (configurable)
|
|
307
|
+
|
|
308
|
+
The page answers 503 until one of them is set, so it is never left open.
|
|
309
|
+
|
|
310
|
+
#### Trying it locally
|
|
311
|
+
|
|
312
|
+
Localhost is safelisted, so your own requests are never refused. Rack::Attack trusts `X-Forwarded-For` from `127.0.0.1`, so send one to stand in for another address:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
for path in /wp-login.php /.env /backup.zip /; do
|
|
316
|
+
curl -s -o /dev/null -w "%{http_code} $path\n" -H "X-Forwarded-For: 203.0.113.10" http://localhost:3000$path
|
|
317
|
+
done
|
|
318
|
+
# 403 /wp-login.php
|
|
319
|
+
# 403 /.env
|
|
320
|
+
# 403 /backup.zip <- third scanner path: banned for 7 days
|
|
321
|
+
# 403 / <- banned, so every path is refused
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Development's `:memory_store` keeps bans inside the server process, so they reset on restart and a `bin/rails runner` script cannot see them. Production needs a shared store (Solid Cache).
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
### CSP Reports -- Content Security Policy Violation Endpoint
|
|
329
|
+
|
|
330
|
+
**CSP Reports** gives the browser somewhere to send Content Security Policy violations and shows them on the Security page. It needs `maquina:security`, whose `security` database stores them.
|
|
331
|
+
|
|
332
|
+
#### What it generates
|
|
333
|
+
|
|
334
|
+
- **Controller:** `CspReportsController` at `POST /csp-reports` -- an API controller with no session or CSRF that accepts both the legacy `report-uri` body and the Reporting API body, capped at 64 KB. Reported URLs lose their query and fragment, and browser-extension violations are dropped
|
|
335
|
+
- **Models:** `Security::CspViolation` (30-day retention) and `Security::CspViolationReport` (the page's figures)
|
|
336
|
+
- **Database:** a migration in `db/security_migrate` and the `csp_violations` table in `db/security_schema.rb`
|
|
337
|
+
- **Retention:** a daily `purge_csp_violations` task in `config/recurring.yml`
|
|
338
|
+
- **Security page:** a section listing the last 7 days of violations, most frequent first
|
|
339
|
+
- **Policy:** points `report_uri` at the endpoint in `config/initializers/content_security_policy.rb` (no `report-to`, which switches Chrome to the batched Reporting API) and gives anonymous requests a nonce -- Rails' session-id nonce is empty without a session
|
|
340
|
+
- **Rack::Attack:** takes the endpoint out of the general throttle and caps it at 100 reports per 5 minutes per IP
|
|
341
|
+
|
|
342
|
+
Every step is idempotent. Anything it cannot place safely is listed after the install instead of being forced.
|
|
343
|
+
|
|
344
|
+
#### Usage
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
rails g maquina:csp_reports
|
|
348
|
+
rails g maquina:csp_reports --path /security/csp-reports # Different endpoint path
|
|
349
|
+
bin/rails db:migrate
|
|
350
|
+
```
|
|
241
351
|
|
|
242
|
-
|
|
352
|
+
Watch the Security page while the policy is report-only, then set `content_security_policy_report_only = false` to enforce it.
|
|
243
353
|
|
|
244
354
|
---
|
|
245
355
|
|
|
@@ -259,10 +369,10 @@ Customize rules in `config/initializers/rack_attack.rb`.
|
|
|
259
369
|
8. Sets up ActiveStorage JavaScript imports
|
|
260
370
|
9. Adds turbo morphing, `yield :head`, and simplifies `<main>` tag in layout
|
|
261
371
|
10. Optionally installs authentication (`maquina:clave` or `maquina:registration`)
|
|
262
|
-
11. Invokes sub-generators: `maquina:
|
|
263
|
-
12. Runs external installers: `solid_queue:install`, `solid_errors:install`, `solid_cache:install`, `solid_cable:install`, `maquina_components:install`
|
|
372
|
+
11. Invokes sub-generators: `maquina:mission_control_jobs`, `maquina:solid_errors`
|
|
373
|
+
12. Runs external installers: `solid_queue:install`, `solid_errors:install`, `solid_cache:install`, `solid_cable:install`, `maquina_components:install`, then `maquina:security` (which installs the `maquina:rack_attack` rules)
|
|
264
374
|
13. Restores custom layouts overwritten by gem installers
|
|
265
|
-
14. Configures multi-database `database.yml` (primary, queue, cache, cable, errors)
|
|
375
|
+
14. Configures multi-database `database.yml` (primary, queue, cache, cable, errors, security)
|
|
266
376
|
15. Creates a HomeController with root route
|
|
267
377
|
16. Generates a README and `database.yml.example`
|
|
268
378
|
17. Runs `db:prepare`
|
|
@@ -277,7 +387,7 @@ rails g maquina:app --auth registration
|
|
|
277
387
|
|
|
278
388
|
#### Options
|
|
279
389
|
|
|
280
|
-
- `--prefix` (default: `/admin`) -- Base path prefix for backstage tools (Solid Errors, Mission Control Jobs)
|
|
390
|
+
- `--prefix` (default: `/admin`) -- Base path prefix for backstage tools (Solid Errors, Mission Control Jobs, Security)
|
|
281
391
|
- `--port` (default: `3000`) -- Default port for the development server
|
|
282
392
|
- `--auth` (default: `none`) -- Authentication type: `none`, `clave`, or `registration`
|
|
283
393
|
|
|
@@ -199,7 +199,6 @@ module Maquina
|
|
|
199
199
|
def install_maquina_generators
|
|
200
200
|
return unless rails_app?
|
|
201
201
|
|
|
202
|
-
generate "maquina:rack_attack", "--quiet"
|
|
203
202
|
generate "maquina:mission_control_jobs", "--prefix", options[:prefix], "--quiet"
|
|
204
203
|
generate "maquina:solid_errors", "--prefix", options[:prefix], "--quiet"
|
|
205
204
|
|
|
@@ -210,6 +209,10 @@ module Maquina
|
|
|
210
209
|
system("bin/rails generate solid_cable:install", chdir: destination_root)
|
|
211
210
|
system("bin/rails generate maquina_components:install", chdir: destination_root)
|
|
212
211
|
end
|
|
212
|
+
|
|
213
|
+
# After solid_queue:install, so config/recurring.yml exists for the purge.
|
|
214
|
+
# Installs the maquina:rack_attack rules too.
|
|
215
|
+
generate "maquina:security", "--prefix", options[:prefix], "--quiet"
|
|
213
216
|
end
|
|
214
217
|
|
|
215
218
|
# 13. Restore custom layouts overwritten by gem installers
|
|
@@ -323,10 +326,12 @@ module Maquina
|
|
|
323
326
|
say " 2. Start the app: bin/dev"
|
|
324
327
|
say ""
|
|
325
328
|
say "Installed components:", :yellow
|
|
326
|
-
say " - Rack::Attack:
|
|
329
|
+
say " - Rack::Attack: scanner and flood bans, throttles (config/initializers/rack_attack.rb)"
|
|
330
|
+
say " - Security: Rack::Attack refusals at #{options[:prefix]}/security"
|
|
327
331
|
say " - Solid Queue: background jobs (config/solid_queue.yml)"
|
|
328
|
-
say " - Mission Control Jobs: job dashboard at #{options[:prefix]}/
|
|
329
|
-
say " - Solid Errors: error tracking at #{options[:prefix]}/
|
|
332
|
+
say " - Mission Control Jobs: job dashboard at #{options[:prefix]}/jobs"
|
|
333
|
+
say " - Solid Errors: error tracking at #{options[:prefix]}/errors"
|
|
334
|
+
say " - Admin overview: #{options[:prefix]}"
|
|
330
335
|
say " - Maquina Components: UI components"
|
|
331
336
|
if options[:auth] != "none"
|
|
332
337
|
say ""
|
|
@@ -34,15 +34,17 @@ Your app will be running at **http://localhost:<%= options[:port] %>**.
|
|
|
34
34
|
| **WebSockets** | Solid Cable |
|
|
35
35
|
| **Rich Text** | Action Text |
|
|
36
36
|
| **File Storage** | Active Storage (S3-ready in production) |
|
|
37
|
-
| **Multi-database** | Separate databases for queue, cache, cable, and
|
|
37
|
+
| **Multi-database** | Separate databases for queue, cache, cable, errors, and security |
|
|
38
38
|
|
|
39
39
|
## Development URLs
|
|
40
40
|
|
|
41
41
|
| URL | Description |
|
|
42
42
|
|-----|-------------|
|
|
43
43
|
| http://localhost:<%= options[:port] %> | Application |
|
|
44
|
-
| http://localhost:<%= options[:port] %><%= options[:prefix]
|
|
45
|
-
| http://localhost:<%= options[:port] %><%= options[:prefix] %>/
|
|
44
|
+
| http://localhost:<%= options[:port] %><%= options[:prefix] %> | Admin overview |
|
|
45
|
+
| http://localhost:<%= options[:port] %><%= options[:prefix] %>/errors | Errors dashboard |
|
|
46
|
+
| http://localhost:<%= options[:port] %><%= options[:prefix] %>/jobs | Jobs dashboard |
|
|
47
|
+
| http://localhost:<%= options[:port] %><%= options[:prefix] %>/security | Security dashboard |
|
|
46
48
|
<% if options[:auth] != "none" -%>
|
|
47
49
|
| http://localhost:<%= options[:port] %>/session/new | Sign in |
|
|
48
50
|
<% if options[:auth] == "registration" -%>
|
|
@@ -65,7 +67,7 @@ Edit credentials with:
|
|
|
65
67
|
bin/rails credentials:edit
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
Add backstage credentials for the
|
|
70
|
+
Add backstage credentials for the admin dashboards (overview, errors, jobs, security):
|
|
69
71
|
|
|
70
72
|
```yaml
|
|
71
73
|
backstage:
|
|
@@ -32,17 +32,17 @@
|
|
|
32
32
|
<%% if authenticated? %>
|
|
33
33
|
<section class="max-w-5xl mx-auto px-5 py-12">
|
|
34
34
|
<p class="text-sm font-semibold uppercase tracking-wider text-primary mb-4">Admin Tools</p>
|
|
35
|
-
<div class="grid grid-cols-1 md:grid-cols-
|
|
35
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
36
36
|
<%%= render "components/card" do %>
|
|
37
37
|
<%%= render "components/card/header" do %>
|
|
38
38
|
<div class="flex items-center gap-2">
|
|
39
|
-
<%%= icon_for(:
|
|
39
|
+
<%%= icon_for(:triangle_alert, class: "size-5 text-primary") %>
|
|
40
40
|
<%%= render "components/card/title", text: "Solid Errors" %>
|
|
41
41
|
</div>
|
|
42
42
|
<%%= render "components/card/description", text: "Browse, resolve, and clean up application exceptions in real time." %>
|
|
43
43
|
<%% end %>
|
|
44
44
|
<%%= render "components/card/footer" do %>
|
|
45
|
-
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/
|
|
45
|
+
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/errors", data: { component: "button", variant: "outline", size: "sm" } %>
|
|
46
46
|
<%% end %>
|
|
47
47
|
<%% end %>
|
|
48
48
|
|
|
@@ -55,7 +55,20 @@
|
|
|
55
55
|
<%%= render "components/card/description", text: "Monitor queues, retry failed jobs, and manage background processing." %>
|
|
56
56
|
<%% end %>
|
|
57
57
|
<%%= render "components/card/footer" do %>
|
|
58
|
-
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/
|
|
58
|
+
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/jobs", data: { component: "button", variant: "outline", size: "sm" } %>
|
|
59
|
+
<%% end %>
|
|
60
|
+
<%% end %>
|
|
61
|
+
|
|
62
|
+
<%%= render "components/card" do %>
|
|
63
|
+
<%%= render "components/card/header" do %>
|
|
64
|
+
<div class="flex items-center gap-2">
|
|
65
|
+
<%%= icon_for(:slash, class: "size-5 text-primary") %>
|
|
66
|
+
<%%= render "components/card/title", text: "Security" %>
|
|
67
|
+
</div>
|
|
68
|
+
<%%= render "components/card/description", text: "See what Rack::Attack throttled, blocked, and banned this week." %>
|
|
69
|
+
<%% end %>
|
|
70
|
+
<%%= render "components/card/footer" do %>
|
|
71
|
+
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/security", data: { component: "button", variant: "outline", size: "sm" } %>
|
|
59
72
|
<%% end %>
|
|
60
73
|
<%% end %>
|
|
61
74
|
</div>
|
|
@@ -109,7 +122,7 @@
|
|
|
109
122
|
<%%= render "components/card/title", text: "Rate Limiting" %>
|
|
110
123
|
<%% end %>
|
|
111
124
|
<%%= render "components/card/content" do %>
|
|
112
|
-
<p class="text-sm text-muted-foreground">Rack::Attack
|
|
125
|
+
<p class="text-sm text-muted-foreground">Rack::Attack bans scanners and floods and throttles sign-in. Every refusal shows on the Security dashboard.</p>
|
|
113
126
|
<%% end %>
|
|
114
127
|
<%% end %>
|
|
115
128
|
|
|
@@ -29,6 +29,10 @@ development:
|
|
|
29
29
|
<<: *default
|
|
30
30
|
database: storage/development_errors.sqlite3
|
|
31
31
|
migrations_paths: db/errors_migrate
|
|
32
|
+
security:
|
|
33
|
+
<<: *default
|
|
34
|
+
database: storage/development_security.sqlite3
|
|
35
|
+
migrations_paths: db/security_migrate
|
|
32
36
|
|
|
33
37
|
# Warning: The database defined as "test" will be erased and
|
|
34
38
|
# re-generated from your development database when you run "rake".
|
|
@@ -53,6 +57,10 @@ test:
|
|
|
53
57
|
<<: *default
|
|
54
58
|
database: storage/test_errors.sqlite3
|
|
55
59
|
migrations_paths: db/errors_migrate
|
|
60
|
+
security:
|
|
61
|
+
<<: *default
|
|
62
|
+
database: storage/test_security.sqlite3
|
|
63
|
+
migrations_paths: db/security_migrate
|
|
56
64
|
|
|
57
65
|
# Store production database in the storage/ directory, which by default
|
|
58
66
|
# is mounted as a persistent Docker volume in config/deploy.yml.
|
|
@@ -76,3 +84,7 @@ production:
|
|
|
76
84
|
<<: *default
|
|
77
85
|
database: storage/production_errors.sqlite3
|
|
78
86
|
migrations_paths: db/errors_migrate
|
|
87
|
+
security:
|
|
88
|
+
<<: *default
|
|
89
|
+
database: storage/production_security.sqlite3
|
|
90
|
+
migrations_paths: db/security_migrate
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "rails/generators"
|
|
2
2
|
require "rails/generators/migration"
|
|
3
|
+
require "active_support/encrypted_configuration"
|
|
3
4
|
|
|
4
5
|
module Maquina
|
|
5
6
|
module Generators
|
|
@@ -42,6 +43,23 @@ module Maquina
|
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# 2. Controllers
|
|
46
|
+
# EmailVerification encrypts its code, which needs Active Record
|
|
47
|
+
# encryption keys. They go into the app's credentials when missing; with
|
|
48
|
+
# no master key to write them, the post-install message says what to run.
|
|
49
|
+
def configure_encryption_keys
|
|
50
|
+
credentials = app_credentials
|
|
51
|
+
if credentials.nil?
|
|
52
|
+
@encryption_pending = true
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
content = credentials.read
|
|
57
|
+
return if content.match?(/^active_record_encryption:/)
|
|
58
|
+
|
|
59
|
+
credentials.write(content.rstrip + (content.strip.empty? ? "" : "\n\n") + encryption_keys_yaml)
|
|
60
|
+
@encryption_written = true
|
|
61
|
+
end
|
|
62
|
+
|
|
45
63
|
def create_controllers
|
|
46
64
|
template "app/controllers/concerns/authentication.rb.tt",
|
|
47
65
|
"app/controllers/concerns/authentication.rb"
|
|
@@ -172,6 +190,15 @@ module Maquina
|
|
|
172
190
|
say " 4. Visit /registration/new # Sign-up page"
|
|
173
191
|
end
|
|
174
192
|
say ""
|
|
193
|
+
if @encryption_written
|
|
194
|
+
say "Added Active Record encryption keys to config/credentials.yml.enc (EmailVerification encrypts its code).", :yellow
|
|
195
|
+
say ""
|
|
196
|
+
elsif @encryption_pending
|
|
197
|
+
say "EmailVerification encrypts its code. Generate keys and add them to your credentials:", :yellow
|
|
198
|
+
say " bin/rails db:encryption:init"
|
|
199
|
+
say " bin/rails credentials:edit"
|
|
200
|
+
say ""
|
|
201
|
+
end
|
|
175
202
|
say "Optional:", :yellow
|
|
176
203
|
say " - Edit app/controllers/concerns/authentication.rb to customize redirect URLs"
|
|
177
204
|
say " - Edit config/locales/clave.*.yml to customize messages"
|
|
@@ -181,6 +208,27 @@ module Maquina
|
|
|
181
208
|
|
|
182
209
|
private
|
|
183
210
|
|
|
211
|
+
# The app's credentials, or nil when there is no key to decrypt them with.
|
|
212
|
+
def app_credentials
|
|
213
|
+
key_path = File.join(destination_root, "config/master.key")
|
|
214
|
+
return unless File.exist?(key_path) || ENV["RAILS_MASTER_KEY"].present?
|
|
215
|
+
|
|
216
|
+
ActiveSupport::EncryptedConfiguration.new(
|
|
217
|
+
config_path: File.join(destination_root, "config/credentials.yml.enc"),
|
|
218
|
+
key_path: key_path, env_key: "RAILS_MASTER_KEY", raise_if_missing_key: false
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The same shape `bin/rails db:encryption:init` prints.
|
|
223
|
+
def encryption_keys_yaml
|
|
224
|
+
<<~YAML
|
|
225
|
+
active_record_encryption:
|
|
226
|
+
primary_key: #{SecureRandom.alphanumeric(32)}
|
|
227
|
+
deterministic_key: #{SecureRandom.alphanumeric(32)}
|
|
228
|
+
key_derivation_salt: #{SecureRandom.alphanumeric(32)}
|
|
229
|
+
YAML
|
|
230
|
+
end
|
|
231
|
+
|
|
184
232
|
def migration_version
|
|
185
233
|
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
|
|
186
234
|
end
|
|
@@ -26,7 +26,16 @@ class Registration::VerificationsController < ApplicationController
|
|
|
26
26
|
return
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# Too many wrong guesses: kill the code so a fresh one is required.
|
|
30
|
+
if @verification.locked?
|
|
31
|
+
@verification.update!(expires_at: Time.current)
|
|
32
|
+
Rails.logger.warn "[AUTH] Verification locked after #{@verification.attempts} attempts for #{@email} from #{request.remote_ip}"
|
|
33
|
+
flash.now[:alert] = t("flash.registrations.code_locked")
|
|
34
|
+
render :new, status: :unprocessable_entity
|
|
35
|
+
return
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
unless @verification.code_matches?(params[:code])
|
|
30
39
|
@verification.increment!(:attempts)
|
|
31
40
|
Rails.logger.warn "[AUTH] Invalid verification code attempt for #{@email} from #{request.remote_ip} (attempt #{@verification.attempts})"
|
|
32
41
|
flash.now[:alert] = t("flash.registrations.invalid_code")
|
data/lib/generators/maquina/clave/templates/app/controllers/session/verifications_controller.rb.tt
CHANGED
|
@@ -27,7 +27,16 @@ class Session::VerificationsController < ApplicationController
|
|
|
27
27
|
return
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
# Too many wrong guesses: kill the code so a fresh one is required.
|
|
31
|
+
if @verification.locked?
|
|
32
|
+
@verification.update!(expires_at: Time.current)
|
|
33
|
+
Rails.logger.warn "[AUTH] Verification locked after #{@verification.attempts} attempts for #{@email} from #{request.remote_ip}"
|
|
34
|
+
flash.now[:alert] = t("flash.sessions.code_locked")
|
|
35
|
+
render :new, status: :unprocessable_entity
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
unless @verification.code_matches?(params[:code])
|
|
31
40
|
@verification.increment!(:attempts)
|
|
32
41
|
Rails.logger.warn "[AUTH] Invalid verification code attempt for #{@email} from #{request.remote_ip} (attempt #{@verification.attempts})"
|
|
33
42
|
flash.now[:alert] = t("flash.sessions.invalid_code")
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
class EmailVerification < ApplicationRecord
|
|
2
2
|
VERIFICATION_TYPES = %w[signup login].freeze
|
|
3
3
|
COOLDOWN_MINUTES = 15
|
|
4
|
+
MAX_ATTEMPTS = 5
|
|
4
5
|
|
|
5
6
|
validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
|
|
6
7
|
validates :code, presence: true
|
|
@@ -10,6 +11,10 @@ class EmailVerification < ApplicationRecord
|
|
|
10
11
|
normalizes :email, with: ->(email) { email.strip.downcase }
|
|
11
12
|
normalizes :code, with: ->(code) { code.strip.upcase }
|
|
12
13
|
|
|
14
|
+
# Encrypted at rest, not digested: the mail goes out through deliver_later,
|
|
15
|
+
# whose job reloads this row by GlobalID and must read the code back.
|
|
16
|
+
encrypts :code
|
|
17
|
+
|
|
13
18
|
scope :pending, -> { where(verified_at: nil) }
|
|
14
19
|
scope :expired, -> { where("expires_at < ?", Time.current) }
|
|
15
20
|
scope :active, -> { pending.where("expires_at >= ?", Time.current) }
|
|
@@ -20,6 +25,19 @@ class EmailVerification < ApplicationRecord
|
|
|
20
25
|
expires_at < Time.current
|
|
21
26
|
end
|
|
22
27
|
|
|
28
|
+
# A code is single-use against a bounded number of guesses: once the wrong-code
|
|
29
|
+
# count hits the ceiling the record is dead regardless of how many IPs an
|
|
30
|
+
# attacker rotates through, since each code only ever maps to one row.
|
|
31
|
+
def locked?
|
|
32
|
+
attempts >= MAX_ATTEMPTS
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Constant-time, so response timing reveals nothing about how close a guess
|
|
36
|
+
# came. Normalized the same way the stored code is.
|
|
37
|
+
def code_matches?(candidate)
|
|
38
|
+
ActiveSupport::SecurityUtils.secure_compare(code, candidate.to_s.strip.upcase)
|
|
39
|
+
end
|
|
40
|
+
|
|
23
41
|
def verified?
|
|
24
42
|
verified_at.present?
|
|
25
43
|
end
|
|
@@ -8,6 +8,7 @@ en:
|
|
|
8
8
|
code_sent: "We sent you a verification code."
|
|
9
9
|
invalid_code: "The code is incorrect. Please try again."
|
|
10
10
|
code_expired: "The code has expired. Request a new one."
|
|
11
|
+
code_locked: "Too many incorrect tries. Request a new code to keep going."
|
|
11
12
|
code_resent: "We sent you a new code."
|
|
12
13
|
cooldown: "You must wait %{minutes} minutes before requesting another code."
|
|
13
14
|
code_already_sent: "We already sent you a code. Check your email (including spam). You can request a new one in %{minutes} minutes."
|
|
@@ -19,6 +20,7 @@ en:
|
|
|
19
20
|
email_taken: "An account with this email already exists. Would you like to sign in?"
|
|
20
21
|
invalid_code: "The code is incorrect. Please try again."
|
|
21
22
|
code_expired: "The code has expired. Request a new one."
|
|
23
|
+
code_locked: "Too many incorrect tries. Request a new code to keep going."
|
|
22
24
|
code_resent: "We sent you a new code."
|
|
23
25
|
cooldown: "You must wait %{minutes} minutes before requesting another code."
|
|
24
26
|
code_already_sent: "We already sent you a code. Check your email (including spam). You can request a new one in %{minutes} minutes."
|
|
@@ -8,6 +8,7 @@ es:
|
|
|
8
8
|
code_sent: "Te enviamos un codigo de verificacion."
|
|
9
9
|
invalid_code: "El codigo es incorrecto. Por favor, intenta de nuevo."
|
|
10
10
|
code_expired: "El codigo ha expirado. Solicita uno nuevo."
|
|
11
|
+
code_locked: "Demasiados intentos incorrectos. Solicita un codigo nuevo para continuar."
|
|
11
12
|
code_resent: "Te enviamos un nuevo codigo."
|
|
12
13
|
cooldown: "Debes esperar %{minutes} minutos antes de solicitar otro codigo."
|
|
13
14
|
code_already_sent: "Ya te enviamos un codigo. Revisa tu correo (incluyendo spam). Podras solicitar uno nuevo en %{minutes} minutos."
|
|
@@ -19,6 +20,7 @@ es:
|
|
|
19
20
|
email_taken: "Ya existe una cuenta con este correo. Quieres iniciar sesion?"
|
|
20
21
|
invalid_code: "El codigo es incorrecto. Por favor, intenta de nuevo."
|
|
21
22
|
code_expired: "El codigo ha expirado. Solicita uno nuevo."
|
|
23
|
+
code_locked: "Demasiados intentos incorrectos. Solicita un codigo nuevo para continuar."
|
|
22
24
|
code_resent: "Te enviamos un nuevo codigo."
|
|
23
25
|
cooldown: "Debes esperar %{minutes} minutos antes de solicitar otro codigo."
|
|
24
26
|
code_already_sent: "Ya te enviamos un codigo. Revisa tu correo (incluyendo spam). Podras solicitar uno nuevo en %{minutes} minutos."
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Adds a Content Security Policy violation report endpoint and shows the
|
|
3
|
+
reports on the backstage Security page. Needs maquina:security, whose
|
|
4
|
+
`security` database stores them.
|
|
5
|
+
|
|
6
|
+
Installs CspReportsController (POST <path>, no session or CSRF, both report
|
|
7
|
+
formats, 64 KB cap, browser-extension noise dropped), the
|
|
8
|
+
Security::CspViolation model and its migration in db/security_migrate, the
|
|
9
|
+
csp_violations table in db/security_schema.rb, a daily purge in
|
|
10
|
+
config/recurring.yml and a section on the Security page. It points the
|
|
11
|
+
policy's report-uri at the endpoint, gives anonymous requests a nonce
|
|
12
|
+
(Rails' session-id nonce is empty without a session), exempts the endpoint
|
|
13
|
+
from the general Rack::Attack throttle and caps it on its own. Anything it
|
|
14
|
+
cannot place safely is listed after the install instead of being forced.
|
|
15
|
+
|
|
16
|
+
Examples:
|
|
17
|
+
rails g maquina:csp_reports
|
|
18
|
+
rails g maquina:csp_reports --path /security/csp-reports
|