rails-guarddog 0.1.10 → 0.1.12
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 +32 -19
- data/lib/rails/guarddog/checkers/dos_checker.rb +107 -38
- 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: 97cdcd83511d9b89a1786584d4a21dcf403123b3ea3911e7341a85a3bda88446
|
|
4
|
+
data.tar.gz: e6634444d93c30e0ddf147cb51db25ee7f04f6f2ee9a17f09d9c1de7b59d7da0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1e72af9e429215d30599e42276ced36f320f052002274827beb6b4060adfa29f907c42af0a88da4a13eead365ef84e32cbb079994534084112e60151671a09f
|
|
7
|
+
data.tar.gz: 2af6a3569f894d95fc2e944ebe14bce13696bc89e0120f13aa5c3cdd9e305e44ecbaba77a6e212dc1911ae1bc2efba267c58093e141bb5c544e59c39979aa3d3
|
data/README.md
CHANGED
|
@@ -98,20 +98,30 @@ 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 - v0.1.12)**
|
|
108
|
+
> - **Secrets**: Patterns use word boundaries — `no_token:` or `reset_password_instructions:` in locale
|
|
109
|
+
> files will **not** trigger the secrets checker.
|
|
110
|
+
> - **Secrets**: Secret values must be space-free (`[^\s'"]{6,}`) — human-readable sentences like
|
|
111
|
+
> `"You can't access this page..."` are never flagged.
|
|
112
|
+
> - **Secrets**: `config/locales/` files are skipped entirely — i18n translations are not credentials.
|
|
113
|
+
> - **Secrets**: `spec/` and `test/` trees suppress the secrets checker by default (configurable via
|
|
114
|
+
> `ignored_checks`).
|
|
115
|
+
> - **DoS**: Smart multiline `.limit()` check detection (v0.1.12) — lookahead up to 3 lines handles chained
|
|
116
|
+
> `.limit()` calls on subsequent lines or reassignment to the same variable, e.g. `things = Thing.all; things = things.limit(100)` or `.limit(...)` on the next line.
|
|
117
|
+
|
|
108
118
|
|
|
109
119
|
## 📊 Example Output
|
|
110
120
|
|
|
111
121
|
### Console Report
|
|
112
122
|
```
|
|
113
123
|
============================================================
|
|
114
|
-
Rails GuardDog Security Report v0.1.
|
|
124
|
+
Rails GuardDog Security Report v0.1.12
|
|
115
125
|
============================================================
|
|
116
126
|
|
|
117
127
|
[CRITICAL] (5 findings)
|
|
@@ -165,25 +175,23 @@ Create `config/initializers/guarddog.rb`:
|
|
|
165
175
|
|
|
166
176
|
```ruby
|
|
167
177
|
Rails.application.config.after_initialize do
|
|
168
|
-
# Enable only specific checkers
|
|
178
|
+
# --- Option A: Enable only specific checkers ---
|
|
169
179
|
Rails.application.config.guarddog.enabled_checkers = %w[
|
|
170
180
|
sql_injection xss csrf mass_assignment secrets
|
|
171
181
|
ai_injection idor dos rate_limit supply_chain
|
|
172
182
|
]
|
|
173
183
|
|
|
184
|
+
# --- Option B: Disable only specific checkers (keep everything else) ---
|
|
185
|
+
# Easier when you want "all but a couple". Takes priority over enabled_checkers.
|
|
186
|
+
Rails.application.config.guarddog.disabled_checkers = %w[dos rate_limit]
|
|
187
|
+
|
|
174
188
|
# Directories to skip entirely (all checks silenced).
|
|
175
189
|
# Default: %w[vendor node_modules]
|
|
176
190
|
Rails.application.config.guarddog.excluded_paths = %w[vendor node_modules]
|
|
177
191
|
|
|
178
192
|
# Suppress specific checkers for files under certain path substrings.
|
|
179
193
|
# 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:
|
|
194
|
+
# Default: { "spec" => %w[secrets], "test" => %w[secrets] }
|
|
187
195
|
Rails.application.config.guarddog.ignored_checks = {
|
|
188
196
|
"spec" => %w[secrets], # no secrets alerts in spec/
|
|
189
197
|
"test" => %w[secrets], # no secrets alerts in test/
|
|
@@ -198,16 +206,20 @@ Rails.application.config.after_initialize do
|
|
|
198
206
|
end
|
|
199
207
|
```
|
|
200
208
|
|
|
209
|
+
### Disabling specific checkers
|
|
201
210
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
If you see false positives like dummy passwords flagged in test/spec files:
|
|
211
|
+
If you want to keep all checkers **except a couple**, use `disabled_checkers`
|
|
212
|
+
instead of maintaining a long `enabled_checkers` list:
|
|
205
213
|
|
|
214
|
+
```ruby
|
|
215
|
+
# All 12 checkers run, except dos and rate_limit
|
|
216
|
+
Rails.application.config.guarddog.disabled_checkers = %w[dos rate_limit]
|
|
206
217
|
```
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
|
|
219
|
+
This is the recommended approach when you hit false positives in one checker
|
|
220
|
+
but still want full coverage from all others.
|
|
221
|
+
|
|
222
|
+
### Suppressing checks per directory
|
|
211
223
|
|
|
212
224
|
GuardDog already silences `secrets` in `spec/` and `test/` by default. To add
|
|
213
225
|
more suppressions or override the defaults, set `ignored_checks` in your
|
|
@@ -215,6 +227,7 @@ initializer (see example above).
|
|
|
215
227
|
|
|
216
228
|
---
|
|
217
229
|
|
|
230
|
+
|
|
218
231
|
## 🔄 CI/CD Integration
|
|
219
232
|
|
|
220
233
|
### GitHub Actions
|
|
@@ -279,6 +292,6 @@ MIT License - Free to use and modify.
|
|
|
279
292
|
|
|
280
293
|
---
|
|
281
294
|
|
|
282
|
-
**Rails GuardDog v0.1.
|
|
295
|
+
**Rails GuardDog v0.1.12 — Production Ready**
|
|
283
296
|
|
|
284
297
|
*Beyond brakeman. Detect what others miss.* 🐕🔒
|
|
@@ -1,38 +1,107 @@
|
|
|
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 (lowercase receiver)
|
|
11
|
+
# collection.all? { } => NOT flagged (.all? method)
|
|
12
|
+
UNBOUNDED_QUERY_PATTERN = /
|
|
13
|
+
(?:
|
|
14
|
+
[A-Z][A-Za-z0-9_]* # PascalCase model class e.g. User, TrackItem
|
|
15
|
+
(?:\.[a-z_]+\(.*?\))* # optional chained scopes e.g. .where(...).order(...)
|
|
16
|
+
)
|
|
17
|
+
\.all # the .all call
|
|
18
|
+
(?!\?) # not .all? (Enumerable method, not AR query)
|
|
19
|
+
\s*(?:[,)\]#\n]|$) # followed by end-of-expression (not .limit etc.)
|
|
20
|
+
/x.freeze
|
|
21
|
+
|
|
22
|
+
# Patterns known to be dangerous nested regex (ReDoS)
|
|
23
|
+
REDOS_PATTERNS = [
|
|
24
|
+
/\/.+\*\+.*\*\+.+\//,
|
|
25
|
+
/match\?.*\(.+\*\+/
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
# How many lines ahead to search for a .limit() call.
|
|
29
|
+
# Covers both chained-on-next-line and separate-assignment styles.
|
|
30
|
+
LOOKAHEAD_LINES = 3
|
|
31
|
+
|
|
32
|
+
def run
|
|
33
|
+
glob_files('app/**/*.rb').each do |file|
|
|
34
|
+
lines = File.readlines(file) rescue next
|
|
35
|
+
|
|
36
|
+
lines.each_with_index do |line, idx|
|
|
37
|
+
next if line.strip.start_with?('#')
|
|
38
|
+
|
|
39
|
+
if line.match?(UNBOUNDED_QUERY_PATTERN) && !limit_applied?(lines, idx, line)
|
|
40
|
+
add_finding(
|
|
41
|
+
severity: :high,
|
|
42
|
+
message: "Potential DoS: unbounded database query without limit",
|
|
43
|
+
file: file,
|
|
44
|
+
line: idx + 1,
|
|
45
|
+
snippet: line.strip,
|
|
46
|
+
remediation: "Add .limit() to control result size"
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if REDOS_PATTERNS.any? { |pat| line.match?(pat) }
|
|
51
|
+
add_finding(
|
|
52
|
+
severity: :high,
|
|
53
|
+
message: "Potential ReDoS vulnerability: dangerous regex pattern",
|
|
54
|
+
file: file,
|
|
55
|
+
line: idx + 1,
|
|
56
|
+
snippet: line.strip,
|
|
57
|
+
remediation: "Simplify regex or use timeout mechanisms"
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
findings
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Returns true if a .limit() call is found either:
|
|
68
|
+
# 1. On the same line as .all (e.g. Thing.all.limit(100))
|
|
69
|
+
# 2. Chained on the very next line (e.g. Thing.all\n .limit(100))
|
|
70
|
+
# 3. Applied to the assigned variable within LOOKAHEAD_LINES lines
|
|
71
|
+
# (e.g. things = Thing.all \n things = things.limit(100))
|
|
72
|
+
def limit_applied?(lines, current_idx, current_line)
|
|
73
|
+
# 1. Same-line limit (Thing.all.limit(100)) — already excluded by
|
|
74
|
+
# UNBOUNDED_QUERY_PATTERN's end-of-expression anchor, but double-check.
|
|
75
|
+
return true if current_line.match?(/\.limit\s*\(/)
|
|
76
|
+
|
|
77
|
+
# Extract the variable being assigned, supporting local vars, instance
|
|
78
|
+
# vars (@foo) and class-instance vars (@@foo).
|
|
79
|
+
assigned_var = current_line.match(/([@]{0,2}\w+)\s*=\s*[A-Z]/)&.[](1)
|
|
80
|
+
|
|
81
|
+
lookahead = lines[(current_idx + 1), LOOKAHEAD_LINES] || []
|
|
82
|
+
|
|
83
|
+
lookahead.each do |next_line|
|
|
84
|
+
stripped = next_line.lstrip
|
|
85
|
+
|
|
86
|
+
# 2. Chained on next line: the continuation starts with .limit
|
|
87
|
+
return true if stripped.start_with?('.limit')
|
|
88
|
+
|
|
89
|
+
# 3. Assigned variable subsequently limited:
|
|
90
|
+
# things = things.limit(100) => local var (word boundary ok)
|
|
91
|
+
# @things = @things.limit(100) => instance var (@ is non-word, use look-behind)
|
|
92
|
+
# things.limit(100)
|
|
93
|
+
if assigned_var
|
|
94
|
+
# Build an anchor that works for both `@var` and plain `var`:
|
|
95
|
+
# require that the var is preceded by start-of-line, whitespace or `=`.
|
|
96
|
+
anchor = '(?:^|[\s=(])'
|
|
97
|
+
escaped = Regexp.escape(assigned_var)
|
|
98
|
+
return true if next_line.match?(/#{anchor}#{escaped}(?:\s*=\s*#{escaped})?\.limit\s*\(/)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
false
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
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.12
|
|
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
|