ariadna 1.3.0 → 2.0.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/ariadna.gemspec +0 -1
- data/data/agents/ariadna-codebase-mapper.md +34 -722
- data/data/agents/ariadna-debugger.md +44 -1139
- data/data/agents/ariadna-executor.md +75 -396
- data/data/agents/ariadna-planner.md +78 -1215
- data/data/agents/ariadna-roadmapper.md +55 -582
- data/data/agents/ariadna-verifier.md +60 -702
- data/data/ariadna/templates/config.json +8 -33
- data/data/ariadna/workflows/debug.md +28 -0
- data/data/ariadna/workflows/execute-phase.md +31 -513
- data/data/ariadna/workflows/map-codebase.md +20 -319
- data/data/ariadna/workflows/new-milestone.md +20 -365
- data/data/ariadna/workflows/new-project.md +19 -880
- data/data/ariadna/workflows/plan-phase.md +24 -443
- data/data/ariadna/workflows/progress.md +20 -376
- data/data/ariadna/workflows/quick.md +19 -221
- data/data/ariadna/workflows/roadmap-ops.md +28 -0
- data/data/ariadna/workflows/verify-work.md +23 -560
- data/data/commands/ariadna/add-phase.md +11 -22
- data/data/commands/ariadna/debug.md +11 -143
- data/data/commands/ariadna/execute-phase.md +12 -30
- data/data/commands/ariadna/insert-phase.md +7 -14
- data/data/commands/ariadna/map-codebase.md +16 -49
- data/data/commands/ariadna/new-milestone.md +12 -25
- data/data/commands/ariadna/new-project.md +22 -26
- data/data/commands/ariadna/plan-phase.md +13 -22
- data/data/commands/ariadna/progress.md +16 -6
- data/data/commands/ariadna/quick.md +9 -11
- data/data/commands/ariadna/remove-phase.md +9 -12
- data/data/commands/ariadna/verify-work.md +14 -19
- data/data/skills/rails-backend/API.md +138 -0
- data/data/skills/rails-backend/CONTROLLERS.md +154 -0
- data/data/skills/rails-backend/JOBS.md +132 -0
- data/data/skills/rails-backend/MODELS.md +213 -0
- data/data/skills/rails-backend/SKILL.md +169 -0
- data/data/skills/rails-frontend/ASSETS.md +154 -0
- data/data/skills/rails-frontend/COMPONENTS.md +253 -0
- data/data/skills/rails-frontend/SKILL.md +187 -0
- data/data/skills/rails-frontend/VIEWS.md +168 -0
- data/data/skills/rails-performance/PROFILING.md +106 -0
- data/data/skills/rails-performance/SKILL.md +217 -0
- data/data/skills/rails-security/AUDIT.md +118 -0
- data/data/skills/rails-security/SKILL.md +422 -0
- data/data/skills/rails-testing/FIXTURES.md +78 -0
- data/data/skills/rails-testing/SKILL.md +160 -0
- data/data/skills/rails-testing/SYSTEM-TESTS.md +73 -0
- data/lib/ariadna/installer.rb +11 -15
- data/lib/ariadna/tools/cli.rb +0 -12
- data/lib/ariadna/tools/config_manager.rb +10 -72
- data/lib/ariadna/tools/frontmatter.rb +23 -1
- data/lib/ariadna/tools/init.rb +201 -401
- data/lib/ariadna/tools/model_profiles.rb +6 -14
- data/lib/ariadna/tools/phase_manager.rb +1 -10
- data/lib/ariadna/tools/state_manager.rb +170 -451
- data/lib/ariadna/tools/template_filler.rb +4 -12
- data/lib/ariadna/tools/verification.rb +21 -399
- data/lib/ariadna/uninstaller.rb +9 -0
- data/lib/ariadna/version.rb +1 -1
- data/lib/ariadna.rb +1 -0
- metadata +20 -91
- data/data/agents/ariadna-backend-executor.md +0 -261
- data/data/agents/ariadna-frontend-executor.md +0 -259
- data/data/agents/ariadna-integration-checker.md +0 -418
- data/data/agents/ariadna-phase-researcher.md +0 -469
- data/data/agents/ariadna-plan-checker.md +0 -622
- data/data/agents/ariadna-project-researcher.md +0 -618
- data/data/agents/ariadna-research-synthesizer.md +0 -236
- data/data/agents/ariadna-test-executor.md +0 -266
- data/data/ariadna/references/checkpoints.md +0 -772
- data/data/ariadna/references/continuation-format.md +0 -249
- data/data/ariadna/references/decimal-phase-calculation.md +0 -65
- data/data/ariadna/references/git-integration.md +0 -248
- data/data/ariadna/references/git-planning-commit.md +0 -38
- data/data/ariadna/references/model-profile-resolution.md +0 -32
- data/data/ariadna/references/model-profiles.md +0 -73
- data/data/ariadna/references/phase-argument-parsing.md +0 -61
- data/data/ariadna/references/planning-config.md +0 -194
- data/data/ariadna/references/questioning.md +0 -153
- data/data/ariadna/references/rails-conventions.md +0 -416
- data/data/ariadna/references/tdd.md +0 -267
- data/data/ariadna/references/ui-brand.md +0 -160
- data/data/ariadna/references/verification-patterns.md +0 -853
- data/data/ariadna/templates/codebase/architecture.md +0 -481
- data/data/ariadna/templates/codebase/concerns.md +0 -380
- data/data/ariadna/templates/codebase/conventions.md +0 -434
- data/data/ariadna/templates/codebase/integrations.md +0 -328
- data/data/ariadna/templates/codebase/stack.md +0 -189
- data/data/ariadna/templates/codebase/structure.md +0 -418
- data/data/ariadna/templates/codebase/testing.md +0 -606
- data/data/ariadna/templates/context.md +0 -283
- data/data/ariadna/templates/continue-here.md +0 -78
- data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
- data/data/ariadna/templates/phase-prompt.md +0 -609
- data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
- data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
- data/data/ariadna/templates/research-project/FEATURES.md +0 -168
- data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
- data/data/ariadna/templates/research-project/STACK.md +0 -251
- data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
- data/data/ariadna/templates/state.md +0 -176
- data/data/ariadna/templates/summary-complex.md +0 -59
- data/data/ariadna/templates/summary-minimal.md +0 -41
- data/data/ariadna/templates/summary-standard.md +0 -48
- data/data/ariadna/templates/user-setup.md +0 -310
- data/data/ariadna/workflows/add-phase.md +0 -111
- data/data/ariadna/workflows/add-todo.md +0 -157
- data/data/ariadna/workflows/audit-milestone.md +0 -241
- data/data/ariadna/workflows/check-todos.md +0 -176
- data/data/ariadna/workflows/complete-milestone.md +0 -644
- data/data/ariadna/workflows/diagnose-issues.md +0 -219
- data/data/ariadna/workflows/discovery-phase.md +0 -289
- data/data/ariadna/workflows/discuss-phase.md +0 -408
- data/data/ariadna/workflows/execute-plan.md +0 -448
- data/data/ariadna/workflows/help.md +0 -470
- data/data/ariadna/workflows/insert-phase.md +0 -129
- data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
- data/data/ariadna/workflows/pause-work.md +0 -122
- data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
- data/data/ariadna/workflows/remove-phase.md +0 -154
- data/data/ariadna/workflows/research-phase.md +0 -74
- data/data/ariadna/workflows/resume-project.md +0 -306
- data/data/ariadna/workflows/set-profile.md +0 -80
- data/data/ariadna/workflows/settings.md +0 -145
- data/data/ariadna/workflows/transition.md +0 -493
- data/data/ariadna/workflows/update.md +0 -212
- data/data/ariadna/workflows/verify-phase.md +0 -226
- data/data/commands/ariadna/add-todo.md +0 -42
- data/data/commands/ariadna/audit-milestone.md +0 -42
- data/data/commands/ariadna/check-todos.md +0 -41
- data/data/commands/ariadna/complete-milestone.md +0 -136
- data/data/commands/ariadna/discuss-phase.md +0 -86
- data/data/commands/ariadna/help.md +0 -22
- data/data/commands/ariadna/list-phase-assumptions.md +0 -50
- data/data/commands/ariadna/pause-work.md +0 -35
- data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
- data/data/commands/ariadna/reapply-patches.md +0 -110
- data/data/commands/ariadna/research-phase.md +0 -187
- data/data/commands/ariadna/resume-work.md +0 -40
- data/data/commands/ariadna/set-profile.md +0 -34
- data/data/commands/ariadna/settings.md +0 -36
- data/data/commands/ariadna/update.md +0 -37
- data/data/guides/backend.md +0 -3069
- data/data/guides/frontend.md +0 -1479
- data/data/guides/performance.md +0 -1193
- data/data/guides/security.md +0 -1522
- data/data/guides/style-guide.md +0 -1091
- data/data/guides/testing.md +0 -504
- data/data/templates.md +0 -94
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-security-audit
|
|
3
|
+
description: Security audit checklist with grep patterns and file globs for systematic code review. Referenced from SKILL.md.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rails Security Audit Checklist
|
|
7
|
+
|
|
8
|
+
Systematic checklist for verifying security after each development phase. Each check includes a severity, grep pattern, and file scope so an agent or reviewer can scan changed files methodically.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## How to Use
|
|
13
|
+
|
|
14
|
+
### Step 1: Identify changed files
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
git diff --name-only HEAD~1
|
|
18
|
+
# For a full phase: git diff <phase-start-sha>..HEAD --name-only
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Step 2: Map files to applicable checks
|
|
22
|
+
|
|
23
|
+
| Changed file pattern | Applicable checks |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `app/models/**/*.rb` | 1.1a, 1.1b, 1.4a, 2.2c, 3.1b, 3.2c, 4.3a, 4.3b |
|
|
26
|
+
| `app/controllers/**/*.rb` | 1.1c, 1.1d, 1.2e, 2.1a, 2.2a, 2.2b, 2.3a, 3.1a, 3.1c, 3.2a, 3.2b, 3.2d, 3.3a |
|
|
27
|
+
| `app/views/**/*.erb` | 1.2a, 1.2b, 1.2c, 1.2d, 2.1b |
|
|
28
|
+
| `app/controllers/api/**/*.rb` | 5.2a, 5.2b, 5.2c |
|
|
29
|
+
| `config/routes.rb` | 2.1c, 2.3a |
|
|
30
|
+
| `config/environments/production.rb` | 4.1c, 5.1a |
|
|
31
|
+
| `config/initializers/**/*.rb` | 3.3b, 3.3c, 4.2a, 5.1b, 5.1c |
|
|
32
|
+
| `app/models/session.rb` | 3.3a, 3.3c |
|
|
33
|
+
| `db/migrate/**/*.rb` | 3.1b |
|
|
34
|
+
| `lib/**/*.rb` | 1.1b, 1.3a, 1.3b |
|
|
35
|
+
| `Gemfile` / `Gemfile.lock` | 5.3a |
|
|
36
|
+
| `package.json` / `yarn.lock` | 5.3b |
|
|
37
|
+
| `app/javascript/**/*.js` | 1.2d, 2.1b |
|
|
38
|
+
|
|
39
|
+
### Step 3: Report format
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
PASS: CHECK 1.1a — No string interpolation in SQL (scanned 3 files)
|
|
43
|
+
FAIL: CHECK 3.2a — Unscoped find in app/controllers/boards_controller.rb:15
|
|
44
|
+
SKIP: CHECK 4.3a — No file upload changes in this phase
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Full Checklist
|
|
50
|
+
|
|
51
|
+
| Check | Name | Severity | Grep pattern | Files |
|
|
52
|
+
|---|---|---|---|---|
|
|
53
|
+
| **1.1a** | No SQL string interpolation | Critical | `\.where\(["'].*#\{` | `app/models/**/*.rb`, `app/controllers/**/*.rb` |
|
|
54
|
+
| **1.1b** | Parameterized raw SQL | Critical | `\.execute\(["'].*#\{` | `app/models/**/*.rb`, `lib/**/*.rb` |
|
|
55
|
+
| **1.1c** | Allowlisted order/group columns | High | `\.order\(params` | `app/controllers/**/*.rb` |
|
|
56
|
+
| **1.1d** | Scoped find for user lookups | High | `\.find\(params\[` | `app/controllers/**/*.rb` |
|
|
57
|
+
| **1.2a** | No raw/html_safe on user input | Critical | `\.html_safe\|raw(` | `app/views/**/*.erb`, `app/helpers/**/*.rb` |
|
|
58
|
+
| **1.2b** | Sanitized rich text output | High | `\.to_s\.html_safe` | `app/views/**/*.erb` |
|
|
59
|
+
| **1.2c** | Safe link_to href values | High | `link_to.*params\[` | `app/views/**/*.erb` |
|
|
60
|
+
| **1.2d** | JSON escaped in script tags | High | `<script>.*to_json` | `app/views/**/*.erb` |
|
|
61
|
+
| **1.2e** | Content-Type for non-HTML responses | Medium | `render plain:` | `app/controllers/**/*.rb` |
|
|
62
|
+
| **1.3a** | No user input in shell calls | Critical | `system\(["'].*#\{` | `app/**/*.rb`, `lib/**/*.rb` |
|
|
63
|
+
| **1.3b** | Shellwords for unavoidable shell use | High | `Shellwords\.escape` | `app/**/*.rb`, `lib/**/*.rb` |
|
|
64
|
+
| **1.4a** | `\A`/`\z` not `^`/`$` in regex | High | `format:.*\/\^` | `app/models/**/*.rb`, `app/validators/**/*.rb` |
|
|
65
|
+
| **2.1a** | CSRF protection enabled | Critical | `skip_before_action :verify_authenticity` | `app/controllers/**/*.rb` |
|
|
66
|
+
| **2.1b** | Authenticity token in forms | High | `<form[^>]*method` | `app/views/**/*.erb` |
|
|
67
|
+
| **2.1c** | Non-GET for state-changing routes | High | `get.*destroy\|get.*delete\|get.*create` | `config/routes.rb` |
|
|
68
|
+
| **2.2a** | Strong parameters — no permit! | Critical | `params\.permit!` | `app/controllers/**/*.rb` |
|
|
69
|
+
| **2.2b** | No admin/role attrs in permit | High | `permit.*:admin\|permit.*:role` | `app/controllers/**/*.rb` |
|
|
70
|
+
| **2.2c** | Nested attributes scoped | Medium | `accepts_nested_attributes_for` | `app/models/**/*.rb` |
|
|
71
|
+
| **2.3a** | No open redirects from params | High | `redirect_to params\[` | `app/controllers/**/*.rb` |
|
|
72
|
+
| **2.3b** | External redirect allowlists | Medium | `allow_other_host: true` | `app/controllers/**/*.rb` |
|
|
73
|
+
| **3.1a** | Auth required on all controllers | Critical | `skip_before_action :authenticate` | `app/controllers/**/*.rb` |
|
|
74
|
+
| **3.1b** | Secure password storage | Critical | `Digest::MD5\|Digest::SHA1` | `app/models/**/*.rb` |
|
|
75
|
+
| **3.1c** | Timing-safe token comparison | High | `==.*token\|==.*secret` | `app/controllers/**/*.rb`, `app/models/**/*.rb` |
|
|
76
|
+
| **3.1d** | Rate limiting on auth endpoints | High | `rate_limit` | `app/controllers/sessions_controller.rb` |
|
|
77
|
+
| **3.2a** | Scoped resource lookups | Critical | `\.find\(params\[:id\]\)` | `app/controllers/**/*.rb` |
|
|
78
|
+
| **3.2b** | Authz check on destructive actions | High | `def destroy` | `app/controllers/**/*.rb` |
|
|
79
|
+
| **3.2c** | Tenant isolation in queries | Critical | `Current\.account` | `app/models/**/*.rb`, `app/controllers/**/*.rb` |
|
|
80
|
+
| **3.2d** | Nested resource scoped to parent | High | `Comment\.find\|\.find\(params\[:id\]\)` | `app/controllers/**/*.rb` |
|
|
81
|
+
| **3.3a** | Session regeneration after login | High | `reset_session` | `app/controllers/sessions_controller.rb` |
|
|
82
|
+
| **3.3b** | Secure cookie configuration | High | `session_store.*secure` | `config/environments/production.rb`, `config/initializers/**/*.rb` |
|
|
83
|
+
| **3.3c** | Session expiration configured | Medium | `expire_after` | `config/initializers/**/*.rb`, `app/models/session.rb` |
|
|
84
|
+
| **4.1a** | No hardcoded secrets | Critical | `API_KEY\|SECRET\|password.*=.*["']` | `app/**/*.rb`, `config/**/*.rb` |
|
|
85
|
+
| **4.1b** | Credentials encrypted, not committed | High | `secrets\.yml\|\.env` | `config/**/*.yml`, `.gitignore` |
|
|
86
|
+
| **4.1c** | Secret key base from credentials/env | Critical | `secret_key_base` | `config/environments/production.rb` |
|
|
87
|
+
| **4.2a** | All sensitive params filtered | High | `filter_parameters` | `config/initializers/filter_parameter_logging.rb` |
|
|
88
|
+
| **4.2b** | No PII/tokens in logs | High | `logger.*token\|logger.*password\|logger.*email` | `app/**/*.rb`, `lib/**/*.rb` |
|
|
89
|
+
| **4.3a** | File upload content type validated | High | `content_type:` | `app/models/**/*.rb` |
|
|
90
|
+
| **4.3b** | File upload size limited | High | `size:.*less_than` | `app/models/**/*.rb` |
|
|
91
|
+
| **4.3c** | Safe file download path | Critical | `send_file.*params` | `app/controllers/**/*.rb` |
|
|
92
|
+
| **4.3d** | No uploads in public directory | High | `public.*uploads` | `app/controllers/**/*.rb`, `config/storage.yml` |
|
|
93
|
+
| **5.1a** | Force SSL enabled in production | Critical | `force_ssl` | `config/environments/production.rb` |
|
|
94
|
+
| **5.1b** | CSP configured and restrictive | High | `content_security_policy` | `config/initializers/**/*.rb` |
|
|
95
|
+
| **5.1c** | Security headers set | Medium | `Referrer-Policy\|Permissions-Policy` | `config/initializers/**/*.rb` |
|
|
96
|
+
| **5.2a** | API authentication on all endpoints | Critical | `before_action.*authenticate` | `app/controllers/api/**/*.rb` |
|
|
97
|
+
| **5.2b** | API responses tenant-scoped | High | `Current\.account` | `app/controllers/api/**/*.rb` |
|
|
98
|
+
| **5.2c** | API rate limiting configured | Medium | `rate_limit\|Rack::Attack` | `app/controllers/api/**/*.rb`, `config/initializers/**/*.rb` |
|
|
99
|
+
| **5.3a** | No vulnerable gems | High | `bundle audit check --update` | `Gemfile.lock` |
|
|
100
|
+
| **5.3b** | No vulnerable JS packages | High | `yarn audit` | `package.json`, `yarn.lock` |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Step 4: Automated Tools
|
|
105
|
+
|
|
106
|
+
Run after manual checks:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Dependency audits
|
|
110
|
+
bundle audit check --update
|
|
111
|
+
yarn audit
|
|
112
|
+
|
|
113
|
+
# Static analysis
|
|
114
|
+
brakeman --no-pager -q
|
|
115
|
+
|
|
116
|
+
# Check for accidentally committed secrets/keys
|
|
117
|
+
git log --diff-filter=A --name-only -- "*.key" "*.pem" ".env*"
|
|
118
|
+
```
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-security
|
|
3
|
+
description: Ruby on Rails security conventions — authentication, authorization, OWASP protections, CSRF, input validation. Use when implementing auth, handling sensitive data, or reviewing security.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rails Security Conventions
|
|
7
|
+
|
|
8
|
+
Opinionated security conventions for Rails applications. Covers the most critical OWASP risks and Rails-specific pitfalls. For a full audit checklist with grep patterns, see [AUDIT.md](AUDIT.md).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Part 1: Input Handling & Injection
|
|
13
|
+
|
|
14
|
+
### SQL Injection — Always parameterize
|
|
15
|
+
|
|
16
|
+
Never interpolate or concatenate user input into SQL. ActiveRecord's hash and placeholder syntax handles this automatically.
|
|
17
|
+
|
|
18
|
+
**UNSAFE:**
|
|
19
|
+
```ruby
|
|
20
|
+
User.where("name = '#{params[:name]}'")
|
|
21
|
+
User.order("#{params[:sort]} #{params[:direction]}")
|
|
22
|
+
ActiveRecord::Base.connection.execute("UPDATE users SET name = '#{name}'")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**SAFE:**
|
|
26
|
+
```ruby
|
|
27
|
+
User.where(name: params[:name])
|
|
28
|
+
User.where("name = ?", params[:name])
|
|
29
|
+
ActiveRecord::Base.connection.exec_query("UPDATE users SET name = $1", "SQL", [name])
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
For `order`/`group`, allowlist the column before use:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
ALLOWED_SORT_COLUMNS = %w[name created_at updated_at].freeze
|
|
36
|
+
|
|
37
|
+
scope :sorted_by, ->(col) {
|
|
38
|
+
order(ALLOWED_SORT_COLUMNS.include?(col) ? col : :created_at)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### XSS — Rails escapes by default; do not bypass it
|
|
43
|
+
|
|
44
|
+
ERB auto-escapes `<%= %>`. Never call `.html_safe` or `raw()` on user-supplied or database content.
|
|
45
|
+
|
|
46
|
+
**UNSAFE:**
|
|
47
|
+
```erb
|
|
48
|
+
<%= params[:query].html_safe %>
|
|
49
|
+
<%= raw(@user.bio) %>
|
|
50
|
+
<%= @card.description.to_s.html_safe %>
|
|
51
|
+
<script>var data = <%= @data.to_json %>;</script>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**SAFE:**
|
|
55
|
+
```erb
|
|
56
|
+
<%= params[:query] %>
|
|
57
|
+
<%= sanitize(@user.bio) %>
|
|
58
|
+
<%= @card.description %>
|
|
59
|
+
<script>var data = <%= json_escape(@data.to_json) %>;</script>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Validate URL protocols before rendering user-supplied links:
|
|
63
|
+
```erb
|
|
64
|
+
<%= link_to "Website", @user.website_url if @user.website_url&.match?(%r{\Ahttps?://}) %>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Use `\A` / `\z` in regex validations — not `^` / `$` (line vs. string boundaries):
|
|
68
|
+
```ruby
|
|
69
|
+
validates :slug, format: { with: /\A[a-z0-9-]+\z/ } # SAFE
|
|
70
|
+
validates :slug, format: { with: /^[a-z0-9-]+$/ } # UNSAFE — allows newline injection
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Command Injection — Use array form for shell calls
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# UNSAFE — single string passes through the shell
|
|
77
|
+
system("convert #{params[:file]} output.png")
|
|
78
|
+
|
|
79
|
+
# SAFE — array form bypasses shell interpretation
|
|
80
|
+
system("convert", uploaded_file.path, "output.png")
|
|
81
|
+
stdout, _s = Open3.capture2("grep", "-r", query, "/data")
|
|
82
|
+
|
|
83
|
+
# If a shell string is unavoidable, escape the argument
|
|
84
|
+
system("tar -czf archive.tar.gz #{Shellwords.escape(directory)}")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Part 2: Request Integrity
|
|
90
|
+
|
|
91
|
+
### CSRF — Never skip `verify_authenticity_token` for browser controllers
|
|
92
|
+
|
|
93
|
+
Rails enables CSRF protection by default via `ApplicationController`. Only API controllers using token-based auth should opt out.
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
# UNSAFE — disables CSRF on a browser controller
|
|
97
|
+
class PaymentsController < ApplicationController
|
|
98
|
+
skip_before_action :verify_authenticity_token
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# SAFE — inherits CSRF from ApplicationController
|
|
102
|
+
class PaymentsController < ApplicationController
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Correct — ActionController::API excludes CSRF; authenticate with tokens instead
|
|
106
|
+
class Api::V1::BaseController < ActionController::API
|
|
107
|
+
before_action :authenticate_api_token
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Always use `form_with` (never hand-craft `<form>` tags without CSRF token). State-changing actions must use non-GET HTTP methods:
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# UNSAFE
|
|
115
|
+
get "posts/:id/publish", to: "posts#publish"
|
|
116
|
+
|
|
117
|
+
# SAFE
|
|
118
|
+
resource :publication, only: [:create, :destroy]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Strong Parameters — Always allowlist, never permit!
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
# UNSAFE
|
|
125
|
+
@user = User.create(params[:user])
|
|
126
|
+
@user.update(params.permit!)
|
|
127
|
+
|
|
128
|
+
# SAFE
|
|
129
|
+
def user_params
|
|
130
|
+
params.require(:user).permit(:name, :email)
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Never include privilege-escalation attributes (`admin`, `role`, `account_id`, `verified`) in permit lists — set them explicitly with authorization checks. Scope nested attributes:
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
def project_params
|
|
138
|
+
params.require(:project).permit(:name,
|
|
139
|
+
tasks_attributes: [:id, :title, :done, :_destroy])
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Open Redirects — Validate redirect targets
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
# UNSAFE
|
|
147
|
+
redirect_to params[:return_to]
|
|
148
|
+
|
|
149
|
+
# SAFE — only allow same-host redirects
|
|
150
|
+
def safe_redirect?(url)
|
|
151
|
+
uri = URI.parse(url.to_s)
|
|
152
|
+
uri.host.nil? || uri.host == request.host
|
|
153
|
+
rescue URI::InvalidURIError
|
|
154
|
+
false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# For external redirects, use an explicit allowlist
|
|
158
|
+
ALLOWED_REDIRECT_HOSTS = %w[accounts.example.com auth.example.com].freeze
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Part 3: Authentication & Authorization
|
|
164
|
+
|
|
165
|
+
### Authentication — Required by default, opt-out explicitly
|
|
166
|
+
|
|
167
|
+
Every controller inheriting from `ApplicationController` must require authentication. Use `allow_unauthenticated_access` only for intentionally public actions.
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
# ApplicationController
|
|
171
|
+
class ApplicationController < ActionController::Base
|
|
172
|
+
before_action :authenticate
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Only public actions opt out explicitly
|
|
176
|
+
class SessionsController < ApplicationController
|
|
177
|
+
allow_unauthenticated_access only: [:new, :create]
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Password storage:** Always use `has_secure_password` (bcrypt). Never MD5, SHA1, or plain text.
|
|
182
|
+
|
|
183
|
+
**Token comparison:** Use `ActiveSupport::SecurityUtils.secure_compare` to prevent timing attacks:
|
|
184
|
+
```ruby
|
|
185
|
+
if ActiveSupport::SecurityUtils.secure_compare(actual_token, expected_token)
|
|
186
|
+
process_webhook
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Rate limiting on auth endpoints:**
|
|
191
|
+
```ruby
|
|
192
|
+
class SessionsController < ApplicationController
|
|
193
|
+
rate_limit to: 10, within: 3.minutes, only: :create,
|
|
194
|
+
with: -> { redirect_to new_session_path, alert: "Try again later." }
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Authorization & IDOR — Always scope through the current user/account
|
|
199
|
+
|
|
200
|
+
Never look up records by raw ID without scoping:
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
# UNSAFE — any user can access any board
|
|
204
|
+
@board = Board.find(params[:id])
|
|
205
|
+
|
|
206
|
+
# SAFE — scoped through current user
|
|
207
|
+
@board = Current.user.boards.find(params[:id])
|
|
208
|
+
@card = Current.user.accessible_cards.find_by!(number: params[:card_id])
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
For multi-tenant applications, scope through `Current.account` at every query layer — including model scopes:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class Card < ApplicationRecord
|
|
215
|
+
scope :recent, -> {
|
|
216
|
+
where(account: Current.account).order(created_at: :desc).limit(10)
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Nested resource controllers must chain the scope to the parent — never look up child records globally:
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
# UNSAFE — any comment, not scoped to card
|
|
225
|
+
@comment = Comment.find(params[:id])
|
|
226
|
+
|
|
227
|
+
# SAFE
|
|
228
|
+
@card = Current.user.accessible_cards.find_by!(number: params[:card_id])
|
|
229
|
+
@comment = @card.comments.find(params[:id])
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Destructive actions need explicit authorization checks beyond authentication:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
def destroy
|
|
236
|
+
@board = Current.user.boards.find(params[:id])
|
|
237
|
+
if Current.user.can_administer_board?(@board)
|
|
238
|
+
@board.destroy
|
|
239
|
+
else
|
|
240
|
+
head :forbidden
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Session Security
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
# Regenerate session after login to prevent session fixation
|
|
249
|
+
def create
|
|
250
|
+
if user = User.authenticate_by(email: params[:email], password: params[:password])
|
|
251
|
+
reset_session
|
|
252
|
+
session[:user_id] = user.id
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Production session store configuration:
|
|
258
|
+
```ruby
|
|
259
|
+
Rails.application.config.session_store :cookie_store,
|
|
260
|
+
key: "_app_session",
|
|
261
|
+
secure: Rails.env.production?,
|
|
262
|
+
httponly: true,
|
|
263
|
+
same_site: :lax,
|
|
264
|
+
expire_after: 12.hours
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Part 4: Data Protection
|
|
270
|
+
|
|
271
|
+
### Secrets — Never hardcode; always use Rails credentials or ENV
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
# UNSAFE
|
|
275
|
+
API_KEY = "sk_live_abc123xyz"
|
|
276
|
+
|
|
277
|
+
# SAFE
|
|
278
|
+
API_KEY = Rails.application.credentials.stripe[:api_key]
|
|
279
|
+
# Or: ENV.fetch("STRIPE_API_KEY")
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Production secret_key_base must come from credentials or env — never from the development value.
|
|
283
|
+
|
|
284
|
+
### Log Filtering — Filter all sensitive fields
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
# config/initializers/filter_parameter_logging.rb
|
|
288
|
+
Rails.application.config.filter_parameters += [
|
|
289
|
+
:password, :password_confirmation,
|
|
290
|
+
:token, :api_key, :secret,
|
|
291
|
+
:credit_card, :card_number, :cvv,
|
|
292
|
+
:ssn, :social_security
|
|
293
|
+
]
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Never log user PII, tokens, or payment data. Log IDs, not values:
|
|
297
|
+
```ruby
|
|
298
|
+
Rails.logger.info "User login: user_id=#{user.id}" # SAFE
|
|
299
|
+
Rails.logger.info "User login: #{user.email}, token: #{user.auth_token}" # UNSAFE
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### File Uploads — Validate type, size, and storage location
|
|
303
|
+
|
|
304
|
+
```ruby
|
|
305
|
+
class Document < ApplicationRecord
|
|
306
|
+
has_one_attached :file
|
|
307
|
+
|
|
308
|
+
validates :file,
|
|
309
|
+
content_type: %w[application/pdf image/png image/jpeg],
|
|
310
|
+
size: { less_than: 10.megabytes }
|
|
311
|
+
|
|
312
|
+
validate :reject_dangerous_content_types
|
|
313
|
+
|
|
314
|
+
private
|
|
315
|
+
def reject_dangerous_content_types
|
|
316
|
+
dangerous = %w[text/html application/javascript application/x-httpd-php]
|
|
317
|
+
errors.add(:file, "type not allowed") if file.attached? && dangerous.include?(file.content_type)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Use ActiveStorage (files stored outside `public/`). Never store uploads directly in `public/uploads/`. Prevent path traversal in download endpoints:
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
def download
|
|
326
|
+
filename = File.basename(params[:filename])
|
|
327
|
+
path = Rails.root.join("uploads", filename)
|
|
328
|
+
path.to_s.start_with?(Rails.root.join("uploads").to_s) ? send_file(path) : head(:forbidden)
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Part 5: Infrastructure Security
|
|
335
|
+
|
|
336
|
+
### Force SSL and configure security headers
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
# config/environments/production.rb
|
|
340
|
+
config.force_ssl = true # Enables HSTS, redirects HTTP→HTTPS, marks cookies as secure
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Content Security Policy (avoid `unsafe-inline`/`unsafe-eval`):
|
|
344
|
+
```ruby
|
|
345
|
+
Rails.application.configure do
|
|
346
|
+
config.content_security_policy do |policy|
|
|
347
|
+
policy.default_src :self
|
|
348
|
+
policy.script_src :self
|
|
349
|
+
policy.style_src :self, :unsafe_inline
|
|
350
|
+
policy.img_src :self, :data, "https://storage.example.com"
|
|
351
|
+
policy.connect_src :self
|
|
352
|
+
end
|
|
353
|
+
config.content_security_policy_nonce_generator = ->(req) { req.session.id.to_s }
|
|
354
|
+
end
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Additional headers:
|
|
358
|
+
```ruby
|
|
359
|
+
# config/initializers/default_headers.rb
|
|
360
|
+
Rails.application.config.action_dispatch.default_headers.merge!(
|
|
361
|
+
"Referrer-Policy" => "strict-origin-when-cross-origin",
|
|
362
|
+
"Permissions-Policy" => "camera=(), microphone=(), geolocation=()",
|
|
363
|
+
"X-Content-Type-Options" => "nosniff"
|
|
364
|
+
)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### API Security
|
|
368
|
+
|
|
369
|
+
Token authentication on the base API controller, tenant-scoped responses:
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
class Api::V1::BaseController < ActionController::API
|
|
373
|
+
before_action :authenticate_api_token
|
|
374
|
+
|
|
375
|
+
private
|
|
376
|
+
def authenticate_api_token
|
|
377
|
+
token = request.headers["Authorization"]&.remove("Bearer ")
|
|
378
|
+
head :unauthorized unless token && ApiToken.active.exists?(token: token)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Rate-limit API endpoints via Rails built-in or Rack::Attack:
|
|
384
|
+
```ruby
|
|
385
|
+
# Rails built-in (7.1+)
|
|
386
|
+
rate_limit to: 100, within: 1.minute
|
|
387
|
+
|
|
388
|
+
# Rack::Attack
|
|
389
|
+
Rack::Attack.throttle("api/requests", limit: 100, period: 1.minute) do |req|
|
|
390
|
+
req.env["HTTP_AUTHORIZATION"]&.remove("Bearer ") if req.path.start_with?("/api/")
|
|
391
|
+
end
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Dependency Auditing
|
|
395
|
+
|
|
396
|
+
Run in CI on every build:
|
|
397
|
+
```bash
|
|
398
|
+
bundle audit check --update # Ruby gems
|
|
399
|
+
yarn audit # JavaScript packages
|
|
400
|
+
brakeman --no-pager -q # Static analysis
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
## Quick Reference: Severity Map
|
|
406
|
+
|
|
407
|
+
| Risk | Severity | Convention |
|
|
408
|
+
|---|---|---|
|
|
409
|
+
| SQL string interpolation | Critical | Use parameterized queries |
|
|
410
|
+
| XSS via `.html_safe` | Critical | Never bypass ERB escaping |
|
|
411
|
+
| CSRF disabled | Critical | Never skip `verify_authenticity_token` |
|
|
412
|
+
| `params.permit!` | Critical | Always allowlist params |
|
|
413
|
+
| Unscoped `find(params[:id])` | Critical | Scope through current user/account |
|
|
414
|
+
| Force SSL off | Critical | `config.force_ssl = true` in production |
|
|
415
|
+
| Hardcoded secrets | Critical | Use Rails credentials or ENV |
|
|
416
|
+
| Path traversal in uploads | Critical | Use ActiveStorage; never user-controlled paths |
|
|
417
|
+
| Command injection | Critical | Array form for shell calls |
|
|
418
|
+
| No rate limiting on auth | High | `rate_limit` on sessions controller |
|
|
419
|
+
| Timing attacks on tokens | High | `secure_compare` not `==` |
|
|
420
|
+
| Session fixation | High | `reset_session` before login |
|
|
421
|
+
|
|
422
|
+
For the full checklist with grep patterns and file globs, see [AUDIT.md](AUDIT.md).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Fixtures — Test Data Management
|
|
2
|
+
|
|
3
|
+
Part of the [Rails Testing Skill](SKILL.md).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Fixtures Over Factories
|
|
8
|
+
|
|
9
|
+
Use **YAML fixtures**, not FactoryBot. Fixtures are deterministic, fast (loaded once per suite in a transaction), and explicit. Same records every test run — no random IDs.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Location and Format
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
test/fixtures/
|
|
17
|
+
cards.yml
|
|
18
|
+
sessions.yml
|
|
19
|
+
boards.yml
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
# test/fixtures/cards.yml
|
|
24
|
+
logo:
|
|
25
|
+
title: "Logo Design"
|
|
26
|
+
board: writebook
|
|
27
|
+
state: open
|
|
28
|
+
|
|
29
|
+
header:
|
|
30
|
+
title: "Header Redesign"
|
|
31
|
+
board: writebook
|
|
32
|
+
state: closed
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use descriptive symbolic names (`logo`, `header`) — never generic names like `one`, `two`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Accessing Fixtures
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
card = cards(:logo)
|
|
43
|
+
session = sessions(:david)
|
|
44
|
+
board = boards(:writebook)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For associations, use the fixture name in YAML — Rails resolves the foreign key automatically.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Current.session and Sessions Fixtures
|
|
52
|
+
|
|
53
|
+
The sessions fixture drives the entire Current context cascade:
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
setup do
|
|
57
|
+
Current.session = sessions(:david)
|
|
58
|
+
# Sets Current.session → Current.user → Current.account
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
One line sets tenant scope, user context, and event tracking defaults. Define at least one named session per user persona.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Adding New Fixtures
|
|
67
|
+
|
|
68
|
+
1. Create `test/fixtures/<table_name>.yml`
|
|
69
|
+
2. Add named records with meaningful names and all required attributes
|
|
70
|
+
3. Reference in tests via `model_name(:fixture_name)`
|
|
71
|
+
|
|
72
|
+
Avoid `Model.create!` inside tests for arrangement — use existing fixtures. Reserve `create!` calls for tests that specifically verify creation behavior.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Fixture Isolation
|
|
77
|
+
|
|
78
|
+
Each test wraps in a transaction that rolls back after completion. Fixture records persist for the full suite run but mutations are rolled back between tests. Use `record.reload` to pick up database changes within the same test.
|