rails-guarddog 0.1.9 → 0.1.11

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae20382e818b53d7afdb3344fdd95711edd8c8890441ec4212231ccea489c41f
4
- data.tar.gz: a0ff9456c95b09c64983d80165ebbc6b59868dd4e6aef6b56aef3d1b21e7d467
3
+ metadata.gz: 859dc0da3cfd03afa3ceea5be6d2feba0f3f49786f109fb82c0a5b1c379c0837
4
+ data.tar.gz: a0251fa578fb369a59316ff36e62e4409cbfb0b08bdc4c4be1d2d75b764c1411
5
5
  SHA512:
6
- metadata.gz: cd08bde46e4cd0125ee004805cac926cc053fc66ddf655b58ddfa9d1bb5dd534ed02cf498c8695a44b51a4b96ad5321398b9a49dcad4771da544d2cc4b7c9760
7
- data.tar.gz: 78adf464a90b83119d33bcd54c652497da03125fa9356f3820df37d5405186601808522e03b5f7e789a0f8633a1fe6b530290cd0f0df55404b71d2452618d4c8
6
+ metadata.gz: 3394a5d46d8ed065003722151b6f5d74361fac8e9656a3c39760e02fc25e581c474cb3246aa9bb8337530a50f70d492dfc77127b1770f7fa82bc6d01460073e5
7
+ data.tar.gz: f52bea6bb7b20187c2ab4a1751267f5987116f1839657bda880dfb93e18a96a8840e4621fde4b490c4d4d318c9f42fbe453be04c6514aa0a42c26b4293a40840
data/README.md CHANGED
@@ -98,20 +98,28 @@ That's it! Scan your entire Rails app for security vulnerabilities.
98
98
  ### Data Protection
99
99
  - **CSRF Protection** — Disabled without documented reason
100
100
  - **Mass Assignment** — `permit!` vulnerabilities ⭐ IMPROVED in v0.1.8
101
- - **Hardcoded Secrets** — API keys, tokens, passwords in code (ALWAYS-ON)
101
+ - **Hardcoded Secrets** — API keys, tokens, passwords in code (ALWAYS-ON, false-positive-safe)
102
102
 
103
103
  ### Resource Management
104
104
  - **DoS/ReDoS** — Unbounded queries, dangerous regex patterns ⭐ ENHANCED in v0.1.8
105
105
  - **Supply Chain** — Typosquatted gems using Levenshtein distance ⭐ IMPROVED in v0.1.8
106
106
 
107
- ---
107
+ > **False-positive protection (v0.1.10)**
108
+ > - Patterns use word boundaries — `no_token:` or `reset_password_instructions:` in locale
109
+ > files will **not** trigger the secrets checker.
110
+ > - Secret values must be space-free (`[^\s'"]{6,}`) — human-readable sentences like
111
+ > `"You can't access this page..."` are never flagged.
112
+ > - `config/locales/` files are skipped entirely — i18n translations are not credentials.
113
+ > - `spec/` and `test/` trees suppress the secrets checker by default (configurable via
114
+ > `ignored_checks`).
115
+
108
116
 
109
117
  ## 📊 Example Output
110
118
 
111
119
  ### Console Report
112
120
  ```
113
121
  ============================================================
114
- Rails GuardDog Security Report v0.1.8
122
+ Rails GuardDog Security Report v0.1.10
115
123
  ============================================================
116
124
 
117
125
  [CRITICAL] (5 findings)
@@ -165,25 +173,23 @@ Create `config/initializers/guarddog.rb`:
165
173
 
166
174
  ```ruby
167
175
  Rails.application.config.after_initialize do
168
- # Enable only specific checkers
176
+ # --- Option A: Enable only specific checkers ---
169
177
  Rails.application.config.guarddog.enabled_checkers = %w[
170
178
  sql_injection xss csrf mass_assignment secrets
171
179
  ai_injection idor dos rate_limit supply_chain
172
180
  ]
173
181
 
182
+ # --- Option B: Disable only specific checkers (keep everything else) ---
183
+ # Easier when you want "all but a couple". Takes priority over enabled_checkers.
184
+ Rails.application.config.guarddog.disabled_checkers = %w[dos rate_limit]
185
+
174
186
  # Directories to skip entirely (all checks silenced).
175
187
  # Default: %w[vendor node_modules]
176
188
  Rails.application.config.guarddog.excluded_paths = %w[vendor node_modules]
177
189
 
178
190
  # Suppress specific checkers for files under certain path substrings.
179
191
  # Keys are path fragments; values are arrays of checker names.
180
- # This is useful to avoid false positives in test/spec trees where
181
- # dummy passwords and tokens are intentional.
182
- #
183
- # Default (built-in):
184
- # { "spec" => %w[secrets], "test" => %w[secrets] }
185
- #
186
- # Override or extend:
192
+ # Default: { "spec" => %w[secrets], "test" => %w[secrets] }
187
193
  Rails.application.config.guarddog.ignored_checks = {
188
194
  "spec" => %w[secrets], # no secrets alerts in spec/
189
195
  "test" => %w[secrets], # no secrets alerts in test/
@@ -198,16 +204,20 @@ Rails.application.config.after_initialize do
198
204
  end
199
205
  ```
200
206
 
207
+ ### Disabling specific checkers
201
208
 
202
- ### Suppressing checks per directory
203
-
204
- If you see false positives like dummy passwords flagged in test/spec files:
209
+ If you want to keep all checkers **except a couple**, use `disabled_checkers`
210
+ instead of maintaining a long `enabled_checkers` list:
205
211
 
212
+ ```ruby
213
+ # All 12 checkers run, except dos and rate_limit
214
+ Rails.application.config.guarddog.disabled_checkers = %w[dos rate_limit]
206
215
  ```
207
- Secrets — Hardcoded secret detected
208
- /app/spec/controllers/api/v1/users_controller_spec.rb:1975
209
- Fix: Use ENV variables or Rails credentials
210
- ```
216
+
217
+ This is the recommended approach when you hit false positives in one checker
218
+ but still want full coverage from all others.
219
+
220
+ ### Suppressing checks per directory
211
221
 
212
222
  GuardDog already silences `secrets` in `spec/` and `test/` by default. To add
213
223
  more suppressions or override the defaults, set `ignored_checks` in your
@@ -215,6 +225,7 @@ initializer (see example above).
215
225
 
216
226
  ---
217
227
 
228
+
218
229
  ## 🔄 CI/CD Integration
219
230
 
220
231
  ### GitHub Actions
@@ -279,6 +290,6 @@ MIT License - Free to use and modify.
279
290
 
280
291
  ---
281
292
 
282
- **Rails GuardDog v0.1.8 — Production Ready**
293
+ **Rails GuardDog v0.1.10 — Production Ready**
283
294
 
284
295
  *Beyond brakeman. Detect what others miss.* 🐕🔒
@@ -1,38 +1,63 @@
1
- module Rails
2
- module Guarddog
3
- module Checkers
4
- class DosChecker < BaseChecker
5
- def run
6
- glob_files('app/**/*.rb').each do |file|
7
- content = File.read(file)
8
- content.each_line.with_index do |line, idx|
9
- # Check for unbounded queries
10
- if line.match?(/\.where\(.*\)\.all/) || line.match?(/\.all\s*$/)
11
- add_finding(
12
- severity: :high,
13
- message: "Potential DoS: unbounded database query without limit",
14
- file: file,
15
- line: idx + 1,
16
- snippet: line.strip,
17
- remediation: "Add .limit() to control result size"
18
- )
19
- end
20
- # Check for regex vulnerabilities
21
- if line.match?(/\/.+\*\+.*\*\+.+\//) || line.match?(/match\?.*\(.+\*\+/)
22
- add_finding(
23
- severity: :high,
24
- message: "Potential ReDoS vulnerability: dangerous regex pattern",
25
- file: file,
26
- line: idx + 1,
27
- snippet: line.strip,
28
- remediation: "Simplify regex or use timeout mechanisms"
29
- )
30
- end
31
- end
32
- end
33
- findings
34
- end
35
- end
36
- end
37
- end
38
- end
1
+ module Rails
2
+ module Guarddog
3
+ module Checkers
4
+ class DosChecker < BaseChecker
5
+ # Matches .all only when it is the final call on an ActiveRecord-style
6
+ # model class (PascalCase constant), e.g.:
7
+ # User.all => flagged
8
+ # Post.where(...).all => flagged
9
+ # But NOT:
10
+ # current_user.permissions.track_manage.all => NOT flagged (chained on object)
11
+ # collection.all? { } => NOT flagged (.all? method)
12
+ # track_manage.all => NOT flagged (lowercase receiver)
13
+ UNBOUNDED_QUERY_PATTERN = /
14
+ (?:
15
+ [A-Z][A-Za-z0-9_]* # PascalCase model class e.g. User, TrackItem
16
+ (?:\.[a-z_]+\(.*?\))* # optional chained scopes e.g. .where(...).order(...)
17
+ )
18
+ \.all # the .all call
19
+ (?!\?) # not .all? (Enumerable method, not AR query)
20
+ \s*(?:[,)\]#\n]|$) # followed by end-of-expression
21
+ /x.freeze
22
+
23
+ # Patterns known to be dangerous nested regex (ReDoS)
24
+ REDOS_PATTERNS = [
25
+ /\/.+\*\+.*\*\+.+\//,
26
+ /match\?.*\(.+\*\+/
27
+ ].freeze
28
+
29
+ def run
30
+ glob_files('app/**/*.rb').each do |file|
31
+ content = File.read(file) rescue next
32
+ content.each_line.with_index do |line, idx|
33
+ next if line.strip.start_with?('#')
34
+
35
+ if line.match?(UNBOUNDED_QUERY_PATTERN)
36
+ add_finding(
37
+ severity: :high,
38
+ message: "Potential DoS: unbounded database query without limit",
39
+ file: file,
40
+ line: idx + 1,
41
+ snippet: line.strip,
42
+ remediation: "Add .limit() to control result size"
43
+ )
44
+ end
45
+
46
+ if REDOS_PATTERNS.any? { |pat| line.match?(pat) }
47
+ add_finding(
48
+ severity: :high,
49
+ message: "Potential ReDoS vulnerability: dangerous regex pattern",
50
+ file: file,
51
+ line: idx + 1,
52
+ snippet: line.strip,
53
+ remediation: "Simplify regex or use timeout mechanisms"
54
+ )
55
+ end
56
+ end
57
+ end
58
+ findings
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -2,20 +2,33 @@ module Rails
2
2
  module Guarddog
3
3
  module Checkers
4
4
  class SecretsChecker < BaseChecker
5
+ # Paths whose files should never be checked for secrets.
6
+ # Locale/i18n YAML files contain human-readable strings, not credentials.
7
+ SKIP_PATH_FRAGMENTS = %w[config/locales].freeze
8
+
9
+ # Each pattern uses:
10
+ # \b / \w* — boundary/prefix so compound names like AUTH_TOKEN,
11
+ # access_token, devise_token are all caught.
12
+ # [^\s'"]{N,} — secret value must be space-free and at least N chars;
13
+ # this eliminates human-readable sentences (false positives)
14
+ # e.g. no_token: "You can't access this page..." won't match.
5
15
  PATTERNS = [
6
- /api[_-]?key\s*[=:]\s*['"][^'"]+['"]/i,
7
- /secret[_-]?key\s*[=:]\s*['"][^'"]+['"]/i,
8
- /password\s*[=:]\s*['"][^'"]+['"]/i,
9
- /token\s*[=:]\s*['"][^'"]+['"]/i
10
- ]
16
+ /\bapi[_-]?key\s*[=:]\s*['"][^\s'"]{6,}['"]/i,
17
+ /\bsecret[_-]?key\s*[=:]\s*['"][^\s'"]{6,}['"]/i,
18
+ /\bpassword\s*[=:]\s*['"][^\s'"]{4,}['"]/i,
19
+ # Matches: token, AUTH_TOKEN, access_token, devise_token, reset_token, etc.
20
+ /\b\w*token\w*\s*[=:]\s*['"][^\s'"]{6,}['"]/i
21
+ ].freeze
11
22
 
12
23
  def run
13
- %w[*.rb *.yml .env .env.local].each do |pattern|
14
- glob_files("**/{#{pattern}}").each do |file|
24
+ %w[*.rb *.yml .env .env.local].each do |file_pattern|
25
+ glob_files("**/{#{file_pattern}}").each do |file|
26
+ next if skip_file?(file)
27
+
15
28
  content = File.read(file) rescue next
16
29
  content.each_line.with_index do |line, idx|
17
- PATTERNS.each do |pattern|
18
- if line.match?(pattern) && !line.strip.start_with?('#')
30
+ PATTERNS.each do |pat|
31
+ if line.match?(pat) && !line.strip.start_with?('#')
19
32
  add_finding(
20
33
  severity: :critical,
21
34
  message: "Hardcoded secret detected",
@@ -30,6 +43,12 @@ module Rails
30
43
  end
31
44
  findings
32
45
  end
46
+
47
+ private
48
+
49
+ def skip_file?(file)
50
+ SKIP_PATH_FRAGMENTS.any? { |fragment| file.include?(fragment) }
51
+ end
33
52
  end
34
53
  end
35
54
  end
@@ -2,11 +2,16 @@ module Rails
2
2
  module Guarddog
3
3
  class Configuration
4
4
  attr_writer :root
5
- attr_accessor :enabled_checkers, :excluded_paths, :output_format, :ignored_checks
5
+ attr_accessor :enabled_checkers, :disabled_checkers, :excluded_paths, :output_format, :ignored_checks
6
6
 
7
7
  def initialize
8
8
  @root = nil
9
9
  @enabled_checkers = all_checkers
10
+ # Checkers to turn off without having to list every other checker in
11
+ # enabled_checkers. Takes priority over enabled_checkers.
12
+ # Example: disable just dos and rate_limit:
13
+ # config.disabled_checkers = %w[dos rate_limit]
14
+ @disabled_checkers = []
10
15
  @excluded_paths = %w[vendor node_modules]
11
16
  # Per-directory checker suppression: keys are path substrings, values are
12
17
  # arrays of checker names (same format as enabled_checkers).
@@ -19,6 +24,12 @@ module Rails
19
24
  @output_format = :console
20
25
  end
21
26
 
27
+ # The final resolved list of checkers to run:
28
+ # enabled_checkers minus anything in disabled_checkers.
29
+ def active_checkers
30
+ enabled_checkers - Array(@disabled_checkers).map(&:to_s)
31
+ end
32
+
22
33
  def root
23
34
  @root || Rails.root.to_s
24
35
  end
@@ -20,20 +20,21 @@ module Rails
20
20
  private
21
21
 
22
22
  def load_checkers
23
- [
24
- Checkers::SqlInjectionChecker,
25
- Checkers::XssChecker,
26
- Checkers::CsrfChecker,
27
- Checkers::MassAssignmentChecker,
28
- Checkers::OpenRedirectChecker,
29
- Checkers::SecretsChecker,
30
- Checkers::DosChecker,
31
- Checkers::IdorChecker,
32
- Checkers::AiInjectionChecker,
33
- Checkers::RateLimitChecker,
34
- Checkers::DependencyChecker,
35
- Checkers::GraphqlChecker
36
- ]
23
+ checker_map = {
24
+ 'sql_injection' => Checkers::SqlInjectionChecker,
25
+ 'xss' => Checkers::XssChecker,
26
+ 'csrf' => Checkers::CsrfChecker,
27
+ 'mass_assignment'=> Checkers::MassAssignmentChecker,
28
+ 'open_redirect' => Checkers::OpenRedirectChecker,
29
+ 'secrets' => Checkers::SecretsChecker,
30
+ 'dos' => Checkers::DosChecker,
31
+ 'idor' => Checkers::IdorChecker,
32
+ 'ai_injection' => Checkers::AiInjectionChecker,
33
+ 'rate_limit' => Checkers::RateLimitChecker,
34
+ 'dependency' => Checkers::DependencyChecker,
35
+ 'graphql' => Checkers::GraphqlChecker
36
+ }
37
+ @configuration.active_checkers.filter_map { |name| checker_map[name] }
37
38
  end
38
39
 
39
40
  def severity_order(severity)
@@ -1,5 +1,5 @@
1
1
  module Rails
2
2
  module Guarddog
3
- VERSION = "0.1.9"
3
+ VERSION = "0.1.11"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-guarddog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Security Team
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-12 00:00:00.000000000 Z
11
+ date: 2026-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties