claude-agent-framework 1.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.
- package/README.md +128 -0
- package/bin/claude-framework +3 -0
- package/framework/agents/design-lead.md +240 -0
- package/framework/agents/product-owner.md +179 -0
- package/framework/agents/tech-lead.md +226 -0
- package/framework/commands/ayuda.md +127 -0
- package/framework/commands/a/303/261adir.md +98 -0
- package/framework/commands/backup.md +397 -0
- package/framework/commands/cambiar.md +110 -0
- package/framework/commands/cloud.md +457 -0
- package/framework/commands/code.md +142 -0
- package/framework/commands/debug.md +334 -0
- package/framework/commands/deploy.md +383 -0
- package/framework/commands/deshacer.md +120 -0
- package/framework/commands/estado.md +218 -0
- package/framework/commands/explica.md +227 -0
- package/framework/commands/feature.md +120 -0
- package/framework/commands/git.md +427 -0
- package/framework/commands/historial.md +202 -0
- package/framework/commands/learn.md +408 -0
- package/framework/commands/movil.md +245 -0
- package/framework/commands/nuevo.md +118 -0
- package/framework/commands/plan.md +134 -0
- package/framework/commands/prd.md +113 -0
- package/framework/commands/probar.md +148 -0
- package/framework/commands/revisar.md +208 -0
- package/framework/commands/seeds.md +230 -0
- package/framework/commands/seguridad.md +226 -0
- package/framework/commands/tasks.md +157 -0
- package/framework/skills/architecture/algorithms.md +970 -0
- package/framework/skills/architecture/clean-code.md +1080 -0
- package/framework/skills/architecture/design-patterns.md +1984 -0
- package/framework/skills/architecture/functional-programming.md +972 -0
- package/framework/skills/architecture/solid.md +991 -0
- package/framework/skills/cloud/cloud-aws.md +848 -0
- package/framework/skills/cloud/cloud-azure.md +931 -0
- package/framework/skills/cloud/cloud-gcp.md +848 -0
- package/framework/skills/cloud/message-queues.md +1229 -0
- package/framework/skills/core/accessibility.md +401 -0
- package/framework/skills/core/api.md +474 -0
- package/framework/skills/core/authentication.md +306 -0
- package/framework/skills/core/authorization.md +388 -0
- package/framework/skills/core/background-jobs.md +341 -0
- package/framework/skills/core/caching.md +473 -0
- package/framework/skills/core/code-review.md +341 -0
- package/framework/skills/core/controllers.md +290 -0
- package/framework/skills/core/cua.md +285 -0
- package/framework/skills/core/documentation.md +472 -0
- package/framework/skills/core/file-uploads.md +351 -0
- package/framework/skills/core/hotwire-native.md +296 -0
- package/framework/skills/core/hotwire.md +278 -0
- package/framework/skills/core/i18n.md +334 -0
- package/framework/skills/core/imports-exports.md +750 -0
- package/framework/skills/core/infrastructure.md +337 -0
- package/framework/skills/core/models.md +228 -0
- package/framework/skills/core/notifications.md +672 -0
- package/framework/skills/core/payments.md +581 -0
- package/framework/skills/core/performance.md +361 -0
- package/framework/skills/core/rails-scaffold.md +131 -0
- package/framework/skills/core/search.md +518 -0
- package/framework/skills/core/security.md +565 -0
- package/framework/skills/core/seeds.md +307 -0
- package/framework/skills/core/seo.md +542 -0
- package/framework/skills/core/testing.md +393 -0
- package/framework/skills/core/views.md +260 -0
- package/framework/skills/core/websockets.md +564 -0
- package/framework/skills/data/advanced-sql.md +1204 -0
- package/framework/skills/data/nosql.md +1141 -0
- package/framework/skills/devops/containers-advanced.md +1237 -0
- package/framework/skills/devops/debugging.md +834 -0
- package/framework/skills/devops/git-workflow.md +752 -0
- package/framework/skills/devops/networking.md +932 -0
- package/framework/skills/devops/shell-scripting.md +1132 -0
- package/framework/sub-agents/architecture-patterns-agent.md +1450 -0
- package/framework/sub-agents/cloud-agent.md +677 -0
- package/framework/sub-agents/data.md +504 -0
- package/framework/sub-agents/debugging-agent.md +554 -0
- package/framework/sub-agents/devops.md +483 -0
- package/framework/sub-agents/docs.md +176 -0
- package/framework/sub-agents/frontend-dev.md +349 -0
- package/framework/sub-agents/git-workflow-agent.md +697 -0
- package/framework/sub-agents/integrations.md +630 -0
- package/framework/sub-agents/native-dev.md +434 -0
- package/framework/sub-agents/qa.md +138 -0
- package/framework/sub-agents/rails-dev.md +375 -0
- package/framework/sub-agents/security.md +526 -0
- package/framework/sub-agents/ui.md +437 -0
- package/framework/sub-agents/ux.md +284 -0
- package/framework/templates/api-spec.md +500 -0
- package/framework/templates/component-spec.md +248 -0
- package/framework/templates/feature.json +13 -0
- package/framework/templates/model-spec.md +318 -0
- package/framework/templates/prd-template.md +80 -0
- package/framework/templates/task-plan.md +122 -0
- package/framework/templates/task-user-story.md +52 -0
- package/framework/templates/technical-spec.md +260 -0
- package/framework/templates/user-story.md +95 -0
- package/package.json +42 -0
- package/project-templates/CLAUDE.md +42 -0
- package/project-templates/contexts/architecture.md +25 -0
- package/project-templates/contexts/conventions.md +46 -0
- package/project-templates/contexts/design-system.md +47 -0
- package/project-templates/contexts/requirements.md +38 -0
- package/project-templates/contexts/stack.md +30 -0
- package/project-templates/history/active/models.md +11 -0
- package/project-templates/history/changelog.md +15 -0
- package/project-templates/workspace/.gitkeep +0 -0
- package/src/cli.js +52 -0
- package/src/init.js +104 -0
- package/src/status.js +75 -0
- package/src/update.js +88 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
# Skill: Security
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Implementar y mantener la seguridad de aplicaciones Rails siguiendo las mejores prácticas y estándares de la industria (OWASP).
|
|
6
|
+
|
|
7
|
+
## Herramientas de auditoría
|
|
8
|
+
|
|
9
|
+
### Brakeman (análisis estático)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Gemfile
|
|
13
|
+
group :development do
|
|
14
|
+
gem "brakeman", require: false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Ejecutar
|
|
18
|
+
bundle exec brakeman
|
|
19
|
+
|
|
20
|
+
# Generar reporte HTML
|
|
21
|
+
bundle exec brakeman -o tmp/brakeman-report.html
|
|
22
|
+
|
|
23
|
+
# Solo warnings altos
|
|
24
|
+
bundle exec brakeman -w2
|
|
25
|
+
|
|
26
|
+
# Ignorar falsos positivos (crear config/brakeman.ignore)
|
|
27
|
+
bundle exec brakeman -I
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Bundler Audit (dependencias vulnerables)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Gemfile
|
|
34
|
+
group :development do
|
|
35
|
+
gem "bundler-audit", require: false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Actualizar DB de vulnerabilidades
|
|
39
|
+
bundle exec bundle-audit update
|
|
40
|
+
|
|
41
|
+
# Ejecutar auditoría
|
|
42
|
+
bundle exec bundle-audit check
|
|
43
|
+
|
|
44
|
+
# Actualizar gems vulnerables automáticamente
|
|
45
|
+
bundle exec bundle-audit check --update
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### CI/CD Integration
|
|
49
|
+
|
|
50
|
+
```yaml
|
|
51
|
+
# .github/workflows/security.yml
|
|
52
|
+
name: Security
|
|
53
|
+
|
|
54
|
+
on: [push, pull_request]
|
|
55
|
+
|
|
56
|
+
jobs:
|
|
57
|
+
security:
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v4
|
|
61
|
+
|
|
62
|
+
- name: Set up Ruby
|
|
63
|
+
uses: ruby/setup-ruby@v1
|
|
64
|
+
with:
|
|
65
|
+
ruby-version: '3.3'
|
|
66
|
+
bundler-cache: true
|
|
67
|
+
|
|
68
|
+
- name: Run Brakeman
|
|
69
|
+
run: bundle exec brakeman -q -w2
|
|
70
|
+
|
|
71
|
+
- name: Run Bundle Audit
|
|
72
|
+
run: |
|
|
73
|
+
bundle exec bundle-audit update
|
|
74
|
+
bundle exec bundle-audit check
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## OWASP Top 10 - Prevención
|
|
78
|
+
|
|
79
|
+
### A01: Broken Access Control
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# ✅ Usar Pundit para autorización
|
|
83
|
+
class PostsController < ApplicationController
|
|
84
|
+
def show
|
|
85
|
+
@post = Post.find(params[:id])
|
|
86
|
+
authorize @post # Verificar permisos
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def index
|
|
90
|
+
@posts = policy_scope(Post) # Solo posts autorizados
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# ✅ Verificar ownership
|
|
95
|
+
class PostPolicy < ApplicationPolicy
|
|
96
|
+
def update?
|
|
97
|
+
record.user == user || user.admin?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# ✅ No exponer IDs secuenciales (usar UUIDs o tokens)
|
|
102
|
+
class Post < ApplicationRecord
|
|
103
|
+
has_secure_token :public_id
|
|
104
|
+
|
|
105
|
+
def to_param
|
|
106
|
+
public_id
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### A02: Cryptographic Failures
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
# ✅ Usar has_secure_password (bcrypt)
|
|
115
|
+
class User < ApplicationRecord
|
|
116
|
+
has_secure_password
|
|
117
|
+
|
|
118
|
+
# Password requirements
|
|
119
|
+
validates :password, length: { minimum: 12 }, if: :password_required?
|
|
120
|
+
validates :password, format: {
|
|
121
|
+
with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
|
|
122
|
+
message: "must include uppercase, lowercase, and number"
|
|
123
|
+
}, if: :password_required?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# ✅ Encriptar datos sensibles con Active Record Encryption
|
|
127
|
+
class User < ApplicationRecord
|
|
128
|
+
encrypts :ssn
|
|
129
|
+
encrypts :api_key, deterministic: true # Para búsquedas
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# ✅ Configurar encryption
|
|
133
|
+
# config/credentials.yml.enc
|
|
134
|
+
active_record_encryption:
|
|
135
|
+
primary_key: xxx
|
|
136
|
+
deterministic_key: xxx
|
|
137
|
+
key_derivation_salt: xxx
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### A03: Injection
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
# SQL Injection Prevention
|
|
144
|
+
|
|
145
|
+
# ❌ VULNERABLE
|
|
146
|
+
User.where("email = '#{params[:email]}'")
|
|
147
|
+
User.where("role IN (#{params[:roles].join(',')})")
|
|
148
|
+
|
|
149
|
+
# ✅ SEGURO
|
|
150
|
+
User.where(email: params[:email])
|
|
151
|
+
User.where("email = ?", params[:email])
|
|
152
|
+
User.where(role: params[:roles])
|
|
153
|
+
|
|
154
|
+
# XSS Prevention - Ya escapado por defecto en ERB
|
|
155
|
+
# Solo usar html_safe o raw cuando sea absolutamente necesario
|
|
156
|
+
|
|
157
|
+
# ✅ Sanitizar HTML permitido
|
|
158
|
+
<%= sanitize @post.body, tags: %w[p br strong em a], attributes: %w[href class] %>
|
|
159
|
+
|
|
160
|
+
# Command Injection Prevention
|
|
161
|
+
# ❌ VULNERABLE
|
|
162
|
+
system("convert #{params[:filename]} output.png")
|
|
163
|
+
|
|
164
|
+
# ✅ SEGURO
|
|
165
|
+
system("convert", params[:filename], "output.png")
|
|
166
|
+
# O usar Shellwords
|
|
167
|
+
system("convert #{Shellwords.escape(params[:filename])} output.png")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### A04: Insecure Design
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# ✅ Validar entrada temprano
|
|
174
|
+
class User < ApplicationRecord
|
|
175
|
+
validates :email, presence: true,
|
|
176
|
+
format: { with: URI::MailTo::EMAIL_REGEXP },
|
|
177
|
+
length: { maximum: 255 }
|
|
178
|
+
|
|
179
|
+
validates :name, presence: true,
|
|
180
|
+
length: { minimum: 2, maximum: 100 }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# ✅ Principio de mínimo privilegio
|
|
184
|
+
class ApplicationPolicy
|
|
185
|
+
def initialize(user, record)
|
|
186
|
+
@user = user
|
|
187
|
+
@record = record
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Denegar todo por defecto
|
|
191
|
+
def index? = false
|
|
192
|
+
def show? = false
|
|
193
|
+
def create? = false
|
|
194
|
+
def update? = false
|
|
195
|
+
def destroy? = false
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# ✅ Defense in depth
|
|
199
|
+
class PostsController < ApplicationController
|
|
200
|
+
before_action :authenticate_user! # Capa 1: Autenticación
|
|
201
|
+
before_action :set_post, only: [:show, :edit, :update, :destroy]
|
|
202
|
+
after_action :verify_authorized # Capa 2: Autorización
|
|
203
|
+
|
|
204
|
+
def update
|
|
205
|
+
authorize @post # Capa 3: Verificación explícita
|
|
206
|
+
# ...
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### A05: Security Misconfiguration
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# config/environments/production.rb
|
|
215
|
+
Rails.application.configure do
|
|
216
|
+
# ✅ Forzar SSL
|
|
217
|
+
config.force_ssl = true
|
|
218
|
+
|
|
219
|
+
# ✅ No mostrar errores detallados
|
|
220
|
+
config.consider_all_requests_local = false
|
|
221
|
+
|
|
222
|
+
# ✅ Logs seguros
|
|
223
|
+
config.log_level = :info
|
|
224
|
+
|
|
225
|
+
# ✅ Configurar hosts permitidos
|
|
226
|
+
config.hosts << "myapp.com"
|
|
227
|
+
config.hosts << /.*\.myapp\.com/
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# config/initializers/content_security_policy.rb
|
|
231
|
+
Rails.application.configure do
|
|
232
|
+
config.content_security_policy do |policy|
|
|
233
|
+
policy.default_src :self
|
|
234
|
+
policy.font_src :self, :data
|
|
235
|
+
policy.img_src :self, :data, :blob, "https:"
|
|
236
|
+
policy.object_src :none
|
|
237
|
+
policy.script_src :self
|
|
238
|
+
policy.style_src :self, :unsafe_inline
|
|
239
|
+
policy.frame_ancestors :none
|
|
240
|
+
policy.base_uri :self
|
|
241
|
+
policy.form_action :self
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
config.content_security_policy_nonce_generator = ->(request) {
|
|
245
|
+
SecureRandom.base64(16)
|
|
246
|
+
}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# config/initializers/permissions_policy.rb
|
|
250
|
+
Rails.application.config.permissions_policy do |policy|
|
|
251
|
+
policy.camera :none
|
|
252
|
+
policy.microphone :none
|
|
253
|
+
policy.geolocation :none
|
|
254
|
+
policy.usb :none
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### A06: Vulnerable Components
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
# Gemfile - Mantener actualizadas
|
|
262
|
+
ruby "3.3.0"
|
|
263
|
+
|
|
264
|
+
gem "rails", "~> 8.0"
|
|
265
|
+
|
|
266
|
+
# Verificar regularmente
|
|
267
|
+
# bundle outdated
|
|
268
|
+
# bundle update --conservative
|
|
269
|
+
|
|
270
|
+
# Automatizar con Dependabot
|
|
271
|
+
# .github/dependabot.yml
|
|
272
|
+
version: 2
|
|
273
|
+
updates:
|
|
274
|
+
- package-ecosystem: "bundler"
|
|
275
|
+
directory: "/"
|
|
276
|
+
schedule:
|
|
277
|
+
interval: "weekly"
|
|
278
|
+
open-pull-requests-limit: 5
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### A07: Authentication Failures
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
# ✅ Rate limiting para login
|
|
285
|
+
# config/initializers/rack_attack.rb
|
|
286
|
+
class Rack::Attack
|
|
287
|
+
throttle("logins/ip", limit: 5, period: 60.seconds) do |req|
|
|
288
|
+
req.ip if req.path == "/session" && req.post?
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
throttle("logins/email", limit: 5, period: 60.seconds) do |req|
|
|
292
|
+
if req.path == "/session" && req.post?
|
|
293
|
+
req.params.dig("session", "email")&.downcase
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# ✅ Account lockout
|
|
299
|
+
class User < ApplicationRecord
|
|
300
|
+
MAX_LOGIN_ATTEMPTS = 5
|
|
301
|
+
LOCKOUT_DURATION = 15.minutes
|
|
302
|
+
|
|
303
|
+
def locked?
|
|
304
|
+
locked_at.present? && locked_at > LOCKOUT_DURATION.ago
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def increment_failed_attempts!
|
|
308
|
+
increment!(:failed_attempts)
|
|
309
|
+
update!(locked_at: Time.current) if failed_attempts >= MAX_LOGIN_ATTEMPTS
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def reset_failed_attempts!
|
|
313
|
+
update!(failed_attempts: 0, locked_at: nil)
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# ✅ Secure session management
|
|
318
|
+
# config/initializers/session_store.rb
|
|
319
|
+
Rails.application.config.session_store :cookie_store,
|
|
320
|
+
key: "_myapp_session",
|
|
321
|
+
secure: Rails.env.production?,
|
|
322
|
+
httponly: true,
|
|
323
|
+
same_site: :lax,
|
|
324
|
+
expire_after: 12.hours
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### A08: Data Integrity Failures
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
# ✅ Verificar integridad de uploads
|
|
331
|
+
class Document < ApplicationRecord
|
|
332
|
+
has_one_attached :file
|
|
333
|
+
|
|
334
|
+
validates :file, content_type: {
|
|
335
|
+
in: %w[application/pdf image/png image/jpeg],
|
|
336
|
+
message: "must be a PDF or image"
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
validates :file, size: { less_than: 10.megabytes }
|
|
340
|
+
|
|
341
|
+
# Verificar checksum
|
|
342
|
+
after_attach :verify_checksum
|
|
343
|
+
|
|
344
|
+
private
|
|
345
|
+
|
|
346
|
+
def verify_checksum
|
|
347
|
+
return unless file.attached?
|
|
348
|
+
|
|
349
|
+
expected = file.checksum
|
|
350
|
+
actual = Digest::MD5.base64digest(file.download)
|
|
351
|
+
|
|
352
|
+
raise "Checksum mismatch" unless expected == actual
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# ✅ Signed URLs para downloads
|
|
357
|
+
<%= rails_blob_url(@document.file, disposition: "attachment", expires_in: 1.hour) %>
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### A09: Logging & Monitoring
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# ✅ Log eventos de seguridad
|
|
364
|
+
class SecurityLogger
|
|
365
|
+
def self.log_event(event_type, details = {})
|
|
366
|
+
Rails.logger.info({
|
|
367
|
+
type: "security_event",
|
|
368
|
+
event: event_type,
|
|
369
|
+
timestamp: Time.current.iso8601,
|
|
370
|
+
ip: Current.request&.remote_ip,
|
|
371
|
+
user_id: Current.user&.id,
|
|
372
|
+
**details
|
|
373
|
+
}.to_json)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Usar en controllers
|
|
378
|
+
class SessionsController < ApplicationController
|
|
379
|
+
def create
|
|
380
|
+
if user&.authenticate(params[:password])
|
|
381
|
+
SecurityLogger.log_event("login_success", email: params[:email])
|
|
382
|
+
# ...
|
|
383
|
+
else
|
|
384
|
+
SecurityLogger.log_event("login_failure", email: params[:email])
|
|
385
|
+
# ...
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# ✅ Filtrar parámetros sensibles
|
|
391
|
+
# config/initializers/filter_parameter_logging.rb
|
|
392
|
+
Rails.application.config.filter_parameters += [
|
|
393
|
+
:password, :password_confirmation,
|
|
394
|
+
:token, :secret, :api_key,
|
|
395
|
+
:credit_card, :cvv, :ssn,
|
|
396
|
+
:authorization
|
|
397
|
+
]
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### A10: Server-Side Request Forgery (SSRF)
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
# ✅ Validar URLs antes de hacer requests
|
|
404
|
+
class UrlValidator
|
|
405
|
+
BLOCKED_HOSTS = %w[localhost 127.0.0.1 0.0.0.0 ::1]
|
|
406
|
+
BLOCKED_NETWORKS = [
|
|
407
|
+
IPAddr.new("10.0.0.0/8"),
|
|
408
|
+
IPAddr.new("172.16.0.0/12"),
|
|
409
|
+
IPAddr.new("192.168.0.0/16"),
|
|
410
|
+
IPAddr.new("169.254.0.0/16")
|
|
411
|
+
]
|
|
412
|
+
|
|
413
|
+
def self.safe?(url)
|
|
414
|
+
uri = URI.parse(url)
|
|
415
|
+
return false unless %w[http https].include?(uri.scheme)
|
|
416
|
+
return false if BLOCKED_HOSTS.include?(uri.host)
|
|
417
|
+
|
|
418
|
+
ip = IPAddr.new(Resolv.getaddress(uri.host))
|
|
419
|
+
BLOCKED_NETWORKS.none? { |net| net.include?(ip) }
|
|
420
|
+
rescue StandardError
|
|
421
|
+
false
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# Usar antes de hacer requests externos
|
|
426
|
+
def fetch_external_resource(url)
|
|
427
|
+
raise "Unsafe URL" unless UrlValidator.safe?(url)
|
|
428
|
+
|
|
429
|
+
HTTP.get(url)
|
|
430
|
+
end
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Headers de seguridad
|
|
434
|
+
|
|
435
|
+
```ruby
|
|
436
|
+
# app/controllers/application_controller.rb
|
|
437
|
+
class ApplicationController < ActionController::Base
|
|
438
|
+
before_action :set_security_headers
|
|
439
|
+
|
|
440
|
+
private
|
|
441
|
+
|
|
442
|
+
def set_security_headers
|
|
443
|
+
# Prevenir clickjacking
|
|
444
|
+
response.headers["X-Frame-Options"] = "DENY"
|
|
445
|
+
|
|
446
|
+
# Prevenir MIME sniffing
|
|
447
|
+
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
448
|
+
|
|
449
|
+
# XSS filter (legacy browsers)
|
|
450
|
+
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
451
|
+
|
|
452
|
+
# Referrer policy
|
|
453
|
+
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
454
|
+
|
|
455
|
+
# Permissions policy
|
|
456
|
+
response.headers["Permissions-Policy"] = "geolocation=(), microphone=(), camera=()"
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Manejo de secretos
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
# Generar master key
|
|
465
|
+
rails credentials:edit
|
|
466
|
+
|
|
467
|
+
# Estructura recomendada
|
|
468
|
+
# config/credentials.yml.enc
|
|
469
|
+
secret_key_base: generado_automaticamente
|
|
470
|
+
|
|
471
|
+
# Base de datos
|
|
472
|
+
database:
|
|
473
|
+
password: xxx
|
|
474
|
+
|
|
475
|
+
# Servicios externos
|
|
476
|
+
stripe:
|
|
477
|
+
secret_key: sk_xxx
|
|
478
|
+
publishable_key: pk_xxx
|
|
479
|
+
webhook_secret: whsec_xxx
|
|
480
|
+
|
|
481
|
+
aws:
|
|
482
|
+
access_key_id: xxx
|
|
483
|
+
secret_access_key: xxx
|
|
484
|
+
bucket: myapp-production
|
|
485
|
+
|
|
486
|
+
# Email
|
|
487
|
+
smtp:
|
|
488
|
+
user_name: xxx
|
|
489
|
+
password: xxx
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
# Acceder a credentials
|
|
494
|
+
Rails.application.credentials.stripe[:secret_key]
|
|
495
|
+
|
|
496
|
+
# En config
|
|
497
|
+
config.stripe_key = Rails.application.credentials.dig(:stripe, :secret_key)
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
## Testing de seguridad
|
|
501
|
+
|
|
502
|
+
```ruby
|
|
503
|
+
# spec/requests/security_spec.rb
|
|
504
|
+
RSpec.describe "Security", type: :request do
|
|
505
|
+
describe "headers" do
|
|
506
|
+
before { get root_path }
|
|
507
|
+
|
|
508
|
+
it "sets X-Frame-Options" do
|
|
509
|
+
expect(response.headers["X-Frame-Options"]).to eq("DENY")
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
it "sets X-Content-Type-Options" do
|
|
513
|
+
expect(response.headers["X-Content-Type-Options"]).to eq("nosniff")
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
it "sets CSP header" do
|
|
517
|
+
expect(response.headers["Content-Security-Policy"]).to be_present
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
describe "authentication" do
|
|
522
|
+
it "requires login for protected resources" do
|
|
523
|
+
get dashboard_path
|
|
524
|
+
expect(response).to redirect_to(new_session_path)
|
|
525
|
+
end
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
describe "authorization" do
|
|
529
|
+
let(:user) { create(:user) }
|
|
530
|
+
let(:other_user) { create(:user) }
|
|
531
|
+
let(:post) { create(:post, user: other_user) }
|
|
532
|
+
|
|
533
|
+
it "prevents unauthorized access" do
|
|
534
|
+
sign_in user
|
|
535
|
+
patch post_path(post), params: { post: { title: "Hacked" } }
|
|
536
|
+
expect(response).to have_http_status(:forbidden)
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
describe "rate limiting" do
|
|
541
|
+
it "blocks excessive login attempts" do
|
|
542
|
+
6.times do
|
|
543
|
+
post session_path, params: { email: "test@test.com", password: "wrong" }
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
expect(response).to have_http_status(:too_many_requests)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
## Checklist de seguridad
|
|
553
|
+
|
|
554
|
+
### Antes de deploy
|
|
555
|
+
|
|
556
|
+
- [ ] Brakeman sin warnings críticos/altos
|
|
557
|
+
- [ ] bundler-audit sin vulnerabilidades
|
|
558
|
+
- [ ] Secrets en credentials, no en código
|
|
559
|
+
- [ ] HTTPS forzado
|
|
560
|
+
- [ ] Headers de seguridad configurados
|
|
561
|
+
- [ ] CSP implementado
|
|
562
|
+
- [ ] Rate limiting activo
|
|
563
|
+
- [ ] Logs filtrados (sin passwords/tokens)
|
|
564
|
+
- [ ] Backups configurados
|
|
565
|
+
- [ ] Monitoreo de errores activo
|