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.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/ariadna.gemspec +0 -1
  3. data/data/agents/ariadna-codebase-mapper.md +34 -722
  4. data/data/agents/ariadna-debugger.md +44 -1139
  5. data/data/agents/ariadna-executor.md +75 -396
  6. data/data/agents/ariadna-planner.md +78 -1215
  7. data/data/agents/ariadna-roadmapper.md +55 -582
  8. data/data/agents/ariadna-verifier.md +60 -702
  9. data/data/ariadna/templates/config.json +8 -33
  10. data/data/ariadna/workflows/debug.md +28 -0
  11. data/data/ariadna/workflows/execute-phase.md +31 -513
  12. data/data/ariadna/workflows/map-codebase.md +20 -319
  13. data/data/ariadna/workflows/new-milestone.md +20 -365
  14. data/data/ariadna/workflows/new-project.md +19 -880
  15. data/data/ariadna/workflows/plan-phase.md +24 -443
  16. data/data/ariadna/workflows/progress.md +20 -376
  17. data/data/ariadna/workflows/quick.md +19 -221
  18. data/data/ariadna/workflows/roadmap-ops.md +28 -0
  19. data/data/ariadna/workflows/verify-work.md +23 -560
  20. data/data/commands/ariadna/add-phase.md +11 -22
  21. data/data/commands/ariadna/debug.md +11 -143
  22. data/data/commands/ariadna/execute-phase.md +12 -30
  23. data/data/commands/ariadna/insert-phase.md +7 -14
  24. data/data/commands/ariadna/map-codebase.md +16 -49
  25. data/data/commands/ariadna/new-milestone.md +12 -25
  26. data/data/commands/ariadna/new-project.md +22 -26
  27. data/data/commands/ariadna/plan-phase.md +13 -22
  28. data/data/commands/ariadna/progress.md +16 -6
  29. data/data/commands/ariadna/quick.md +9 -11
  30. data/data/commands/ariadna/remove-phase.md +9 -12
  31. data/data/commands/ariadna/verify-work.md +14 -19
  32. data/data/skills/rails-backend/API.md +138 -0
  33. data/data/skills/rails-backend/CONTROLLERS.md +154 -0
  34. data/data/skills/rails-backend/JOBS.md +132 -0
  35. data/data/skills/rails-backend/MODELS.md +213 -0
  36. data/data/skills/rails-backend/SKILL.md +169 -0
  37. data/data/skills/rails-frontend/ASSETS.md +154 -0
  38. data/data/skills/rails-frontend/COMPONENTS.md +253 -0
  39. data/data/skills/rails-frontend/SKILL.md +187 -0
  40. data/data/skills/rails-frontend/VIEWS.md +168 -0
  41. data/data/skills/rails-performance/PROFILING.md +106 -0
  42. data/data/skills/rails-performance/SKILL.md +217 -0
  43. data/data/skills/rails-security/AUDIT.md +118 -0
  44. data/data/skills/rails-security/SKILL.md +422 -0
  45. data/data/skills/rails-testing/FIXTURES.md +78 -0
  46. data/data/skills/rails-testing/SKILL.md +160 -0
  47. data/data/skills/rails-testing/SYSTEM-TESTS.md +73 -0
  48. data/lib/ariadna/installer.rb +11 -15
  49. data/lib/ariadna/tools/cli.rb +0 -12
  50. data/lib/ariadna/tools/config_manager.rb +10 -72
  51. data/lib/ariadna/tools/frontmatter.rb +23 -1
  52. data/lib/ariadna/tools/init.rb +201 -401
  53. data/lib/ariadna/tools/model_profiles.rb +6 -14
  54. data/lib/ariadna/tools/phase_manager.rb +1 -10
  55. data/lib/ariadna/tools/state_manager.rb +170 -451
  56. data/lib/ariadna/tools/template_filler.rb +4 -12
  57. data/lib/ariadna/tools/verification.rb +21 -399
  58. data/lib/ariadna/uninstaller.rb +9 -0
  59. data/lib/ariadna/version.rb +1 -1
  60. data/lib/ariadna.rb +1 -0
  61. metadata +20 -91
  62. data/data/agents/ariadna-backend-executor.md +0 -261
  63. data/data/agents/ariadna-frontend-executor.md +0 -259
  64. data/data/agents/ariadna-integration-checker.md +0 -418
  65. data/data/agents/ariadna-phase-researcher.md +0 -469
  66. data/data/agents/ariadna-plan-checker.md +0 -622
  67. data/data/agents/ariadna-project-researcher.md +0 -618
  68. data/data/agents/ariadna-research-synthesizer.md +0 -236
  69. data/data/agents/ariadna-test-executor.md +0 -266
  70. data/data/ariadna/references/checkpoints.md +0 -772
  71. data/data/ariadna/references/continuation-format.md +0 -249
  72. data/data/ariadna/references/decimal-phase-calculation.md +0 -65
  73. data/data/ariadna/references/git-integration.md +0 -248
  74. data/data/ariadna/references/git-planning-commit.md +0 -38
  75. data/data/ariadna/references/model-profile-resolution.md +0 -32
  76. data/data/ariadna/references/model-profiles.md +0 -73
  77. data/data/ariadna/references/phase-argument-parsing.md +0 -61
  78. data/data/ariadna/references/planning-config.md +0 -194
  79. data/data/ariadna/references/questioning.md +0 -153
  80. data/data/ariadna/references/rails-conventions.md +0 -416
  81. data/data/ariadna/references/tdd.md +0 -267
  82. data/data/ariadna/references/ui-brand.md +0 -160
  83. data/data/ariadna/references/verification-patterns.md +0 -853
  84. data/data/ariadna/templates/codebase/architecture.md +0 -481
  85. data/data/ariadna/templates/codebase/concerns.md +0 -380
  86. data/data/ariadna/templates/codebase/conventions.md +0 -434
  87. data/data/ariadna/templates/codebase/integrations.md +0 -328
  88. data/data/ariadna/templates/codebase/stack.md +0 -189
  89. data/data/ariadna/templates/codebase/structure.md +0 -418
  90. data/data/ariadna/templates/codebase/testing.md +0 -606
  91. data/data/ariadna/templates/context.md +0 -283
  92. data/data/ariadna/templates/continue-here.md +0 -78
  93. data/data/ariadna/templates/debug-subagent-prompt.md +0 -91
  94. data/data/ariadna/templates/phase-prompt.md +0 -609
  95. data/data/ariadna/templates/planner-subagent-prompt.md +0 -117
  96. data/data/ariadna/templates/research-project/ARCHITECTURE.md +0 -439
  97. data/data/ariadna/templates/research-project/FEATURES.md +0 -168
  98. data/data/ariadna/templates/research-project/PITFALLS.md +0 -406
  99. data/data/ariadna/templates/research-project/STACK.md +0 -251
  100. data/data/ariadna/templates/research-project/SUMMARY.md +0 -247
  101. data/data/ariadna/templates/state.md +0 -176
  102. data/data/ariadna/templates/summary-complex.md +0 -59
  103. data/data/ariadna/templates/summary-minimal.md +0 -41
  104. data/data/ariadna/templates/summary-standard.md +0 -48
  105. data/data/ariadna/templates/user-setup.md +0 -310
  106. data/data/ariadna/workflows/add-phase.md +0 -111
  107. data/data/ariadna/workflows/add-todo.md +0 -157
  108. data/data/ariadna/workflows/audit-milestone.md +0 -241
  109. data/data/ariadna/workflows/check-todos.md +0 -176
  110. data/data/ariadna/workflows/complete-milestone.md +0 -644
  111. data/data/ariadna/workflows/diagnose-issues.md +0 -219
  112. data/data/ariadna/workflows/discovery-phase.md +0 -289
  113. data/data/ariadna/workflows/discuss-phase.md +0 -408
  114. data/data/ariadna/workflows/execute-plan.md +0 -448
  115. data/data/ariadna/workflows/help.md +0 -470
  116. data/data/ariadna/workflows/insert-phase.md +0 -129
  117. data/data/ariadna/workflows/list-phase-assumptions.md +0 -178
  118. data/data/ariadna/workflows/pause-work.md +0 -122
  119. data/data/ariadna/workflows/plan-milestone-gaps.md +0 -256
  120. data/data/ariadna/workflows/remove-phase.md +0 -154
  121. data/data/ariadna/workflows/research-phase.md +0 -74
  122. data/data/ariadna/workflows/resume-project.md +0 -306
  123. data/data/ariadna/workflows/set-profile.md +0 -80
  124. data/data/ariadna/workflows/settings.md +0 -145
  125. data/data/ariadna/workflows/transition.md +0 -493
  126. data/data/ariadna/workflows/update.md +0 -212
  127. data/data/ariadna/workflows/verify-phase.md +0 -226
  128. data/data/commands/ariadna/add-todo.md +0 -42
  129. data/data/commands/ariadna/audit-milestone.md +0 -42
  130. data/data/commands/ariadna/check-todos.md +0 -41
  131. data/data/commands/ariadna/complete-milestone.md +0 -136
  132. data/data/commands/ariadna/discuss-phase.md +0 -86
  133. data/data/commands/ariadna/help.md +0 -22
  134. data/data/commands/ariadna/list-phase-assumptions.md +0 -50
  135. data/data/commands/ariadna/pause-work.md +0 -35
  136. data/data/commands/ariadna/plan-milestone-gaps.md +0 -40
  137. data/data/commands/ariadna/reapply-patches.md +0 -110
  138. data/data/commands/ariadna/research-phase.md +0 -187
  139. data/data/commands/ariadna/resume-work.md +0 -40
  140. data/data/commands/ariadna/set-profile.md +0 -34
  141. data/data/commands/ariadna/settings.md +0 -36
  142. data/data/commands/ariadna/update.md +0 -37
  143. data/data/guides/backend.md +0 -3069
  144. data/data/guides/frontend.md +0 -1479
  145. data/data/guides/performance.md +0 -1193
  146. data/data/guides/security.md +0 -1522
  147. data/data/guides/style-guide.md +0 -1091
  148. data/data/guides/testing.md +0 -504
  149. 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.