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 +4 -4
- data/README.md +30 -19
- data/lib/rails/guarddog/checkers/dos_checker.rb +63 -38
- data/lib/rails/guarddog/checkers/secrets_checker.rb +28 -9
- data/lib/rails/guarddog/configuration.rb +12 -1
- data/lib/rails/guarddog/scanner.rb +15 -14
- data/lib/rails/guarddog/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 859dc0da3cfd03afa3ceea5be6d2feba0f3f49786f109fb82c0a5b1c379c0837
|
|
4
|
+
data.tar.gz: a0251fa578fb369a59316ff36e62e4409cbfb0b08bdc4c4be1d2d75b764c1411
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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.
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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 |
|
|
14
|
-
glob_files("**/{#{
|
|
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 |
|
|
18
|
-
if line.match?(
|
|
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)
|
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.
|
|
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-
|
|
11
|
+
date: 2026-07-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: railties
|