rails-guarddog 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +17 -0
- data/README.md +66 -0
- data/bin/guarddog +17 -0
- data/lib/rails/guarddog/checkers/ai_injection_checker.rb +31 -0
- data/lib/rails/guarddog/checkers/base_checker.rb +36 -0
- data/lib/rails/guarddog/checkers/csrf_checker.rb +24 -0
- data/lib/rails/guarddog/checkers/dependency_checker.rb +27 -0
- data/lib/rails/guarddog/checkers/dos_checker.rb +38 -0
- data/lib/rails/guarddog/checkers/graphql_checker.rb +27 -0
- data/lib/rails/guarddog/checkers/idor_checker.rb +26 -0
- data/lib/rails/guarddog/checkers/mass_assignment_checker.rb +28 -0
- data/lib/rails/guarddog/checkers/open_redirect_checker.rb +26 -0
- data/lib/rails/guarddog/checkers/rate_limit_checker.rb +33 -0
- data/lib/rails/guarddog/checkers/secrets_checker.rb +37 -0
- data/lib/rails/guarddog/checkers/sql_injection_checker.rb +26 -0
- data/lib/rails/guarddog/checkers/xss_checker.rb +26 -0
- data/lib/rails/guarddog/configuration.rb +21 -0
- data/lib/rails/guarddog/finding.rb +29 -0
- data/lib/rails/guarddog/railtie.rb +11 -0
- data/lib/rails/guarddog/reporters/console_reporter.rb +35 -0
- data/lib/rails/guarddog/reporters/html_reporter.rb +103 -0
- data/lib/rails/guarddog/reporters/json_reporter.rb +29 -0
- data/lib/rails/guarddog/scanner.rb +37 -0
- data/lib/rails/guarddog/version.rb +5 -0
- data/lib/rails/guarddog.rb +27 -0
- data/lib/rails-guarddog.rb +1 -0
- data/lib/tasks/guarddog.rake +45 -0
- metadata +114 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: aa16a224273a4b8cb806307781b714908b081fbc7a6d245714bed775e6f99749
|
|
4
|
+
data.tar.gz: 9f5d59f9e0d7445a1a2c5c471c087881cd3524ed93f636ed27b64f3e439e3d01
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f36099494083f49b05cb40792e1f092b013cdca187f1b01e8de5c340e9927cd7b4203c03900560e04e6bd5d4807687a14511e8d57d05043040e4e67b7b69ebc4
|
|
7
|
+
data.tar.gz: 0221476bc569b4025a2cadc8088de4961efbad611af4e1d465f14dac67aa7b37e7415a0089daad390b2944987d84b34e98a9cd8ce4606ad7c4e8f944b8ef6022
|
data/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Rails GuardDog
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
data/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Rails GuardDog š
|
|
2
|
+
|
|
3
|
+
Advanced security scanning for Rails applications. Beyond brakeman ā AI injection, DoS patterns, supply chain attacks, GraphQL auth, and more.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### Core Checks
|
|
8
|
+
- SQL Injection (improved detection)
|
|
9
|
+
- XSS in views
|
|
10
|
+
- CSRF protection
|
|
11
|
+
- Mass assignment vulnerabilities
|
|
12
|
+
- Open redirects
|
|
13
|
+
- Hardcoded secrets (always-on)
|
|
14
|
+
|
|
15
|
+
### Original Features ā
|
|
16
|
+
- **AI/LLM Prompt Injection** ā Detects user input flowing into LLM calls
|
|
17
|
+
- **DoS & ReDoS Detection** ā Regex catastrophe and unbounded query patterns
|
|
18
|
+
- **Supply Chain** ā Typosquatting detection with Levenshtein distance
|
|
19
|
+
- **GraphQL Auth Gaps** ā Missing field-level authorization
|
|
20
|
+
- **Rate Limiting Audit** ā Checks rack-attack configuration
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Add to Gemfile:
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'rails-guarddog'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run:
|
|
30
|
+
```bash
|
|
31
|
+
bundle install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### CLI
|
|
37
|
+
```bash
|
|
38
|
+
guarddog scan # Console output
|
|
39
|
+
guarddog report # HTML + JSON reports
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Rake Tasks
|
|
43
|
+
```bash
|
|
44
|
+
rake guarddog:scan # Run scan
|
|
45
|
+
rake guarddog:report # Generate reports
|
|
46
|
+
rake guarddog:ci # CI integration (exits 1 on critical)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Report Formats
|
|
50
|
+
|
|
51
|
+
- **Console** ā Color-coded terminal output
|
|
52
|
+
- **HTML** ā Interactive dashboard with filtering
|
|
53
|
+
- **JSON** ā Structured format for CI/CD integration
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
Create `config/initializers/guarddog.rb`:
|
|
58
|
+
```ruby
|
|
59
|
+
Rails.application.config.guarddog.enabled_checkers = %w[
|
|
60
|
+
sql_injection xss csrf mass_assignment
|
|
61
|
+
]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
data/bin/guarddog
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'rails/guarddog'
|
|
3
|
+
|
|
4
|
+
if ARGV[0] == 'scan'
|
|
5
|
+
scanner = Rails::Guarddog::Scanner.new
|
|
6
|
+
findings = scanner.run
|
|
7
|
+
reporter = Rails::Guarddog::Reporters::ConsoleReporter.new(findings)
|
|
8
|
+
reporter.report
|
|
9
|
+
elsif ARGV[0] == 'report'
|
|
10
|
+
scanner = Rails::Guarddog::Scanner.new
|
|
11
|
+
findings = scanner.run
|
|
12
|
+
html_reporter = Rails::Guarddog::Reporters::HtmlReporter.new(findings)
|
|
13
|
+
path = html_reporter.report
|
|
14
|
+
puts "Report generated: #{path}"
|
|
15
|
+
else
|
|
16
|
+
puts "Usage: guarddog [scan|report]"
|
|
17
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class AiInjectionChecker < BaseChecker
|
|
5
|
+
AI_GEMS = %w[ruby-openai anthropic langchainrb openai]
|
|
6
|
+
|
|
7
|
+
def run
|
|
8
|
+
glob_files('app/**/*.rb').each do |file|
|
|
9
|
+
content = File.read(file)
|
|
10
|
+
content.each_line.with_index do |line, idx|
|
|
11
|
+
# Check for AI gem calls with user input
|
|
12
|
+
if line.match?(/\.create.*messages/) || line.match?(/\.chat\.completions/)
|
|
13
|
+
if line.include?('params') || line.include?('user_input')
|
|
14
|
+
add_finding(
|
|
15
|
+
severity: :critical,
|
|
16
|
+
message: "AI prompt injection risk: user input passed to LLM without sanitization",
|
|
17
|
+
file: file,
|
|
18
|
+
line: idx + 1,
|
|
19
|
+
snippet: line.strip,
|
|
20
|
+
remediation: "Sanitize user input before passing to LLM; use system prompts safely"
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
findings
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class BaseChecker
|
|
5
|
+
attr_accessor :findings
|
|
6
|
+
|
|
7
|
+
def initialize(root = Rails.root.to_s)
|
|
8
|
+
@root = root
|
|
9
|
+
@findings = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
raise NotImplementedError, "Subclasses must implement run"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
def add_finding(severity:, message:, file:, line:, snippet: "", remediation: "")
|
|
19
|
+
findings << Finding.new(
|
|
20
|
+
severity: severity,
|
|
21
|
+
category: self.class.name.demodulize.gsub(/Checker$/, ''),
|
|
22
|
+
message: message,
|
|
23
|
+
file: file,
|
|
24
|
+
line: line,
|
|
25
|
+
code_snippet: snippet,
|
|
26
|
+
remediation: remediation
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def glob_files(pattern)
|
|
31
|
+
Dir.glob(File.join(@root, pattern))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class CsrfChecker < BaseChecker
|
|
5
|
+
def run
|
|
6
|
+
glob_files('app/controllers/**/*.rb').each do |file|
|
|
7
|
+
content = File.read(file)
|
|
8
|
+
has_skip = content.include?('skip_before_action :verify_authenticity_token')
|
|
9
|
+
if has_skip && !content.include?('# CSRF disabled for specific reason')
|
|
10
|
+
add_finding(
|
|
11
|
+
severity: :critical,
|
|
12
|
+
message: "CSRF protection disabled without documented reason",
|
|
13
|
+
file: file,
|
|
14
|
+
line: 1,
|
|
15
|
+
remediation: "Remove skip_before_action or add documented reason"
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
findings
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class DependencyChecker < BaseChecker
|
|
5
|
+
def run
|
|
6
|
+
gemfile = File.join(@root, 'Gemfile.lock')
|
|
7
|
+
return [] unless File.exist?(gemfile)
|
|
8
|
+
|
|
9
|
+
content = File.read(gemfile)
|
|
10
|
+
|
|
11
|
+
# Check for typosquatted gems
|
|
12
|
+
if content.match?(/raills|raill\s|rails-rails|active-model/)
|
|
13
|
+
add_finding(
|
|
14
|
+
severity: :critical,
|
|
15
|
+
message: "Possible typosquatted gem detected in Gemfile.lock",
|
|
16
|
+
file: gemfile,
|
|
17
|
+
line: 1,
|
|
18
|
+
remediation: "Verify gem names carefully; check rubygems.org"
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
findings
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class GraphqlChecker < BaseChecker
|
|
5
|
+
def run
|
|
6
|
+
glob_files('app/graphql/**/*.rb').each do |file|
|
|
7
|
+
content = File.read(file)
|
|
8
|
+
|
|
9
|
+
if content.include?('field') || content.include?('def resolve')
|
|
10
|
+
unless content.include?('authorize') || content.include?('current_user')
|
|
11
|
+
add_finding(
|
|
12
|
+
severity: :high,
|
|
13
|
+
message: "GraphQL field missing authorization check",
|
|
14
|
+
file: file,
|
|
15
|
+
line: 1,
|
|
16
|
+
snippet: "GraphQL resolver without auth",
|
|
17
|
+
remediation: "Add authorization: authorize @object or Pundit check"
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
findings
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class IdorChecker < BaseChecker
|
|
5
|
+
def run
|
|
6
|
+
glob_files('app/controllers/**/*.rb').each do |file|
|
|
7
|
+
content = File.read(file)
|
|
8
|
+
content.each_line.with_index do |line, idx|
|
|
9
|
+
if line.match?(/find\(params\[[:']id[']\]/) && !content.include?('authorize')
|
|
10
|
+
add_finding(
|
|
11
|
+
severity: :high,
|
|
12
|
+
message: "Potential IDOR: object accessed by ID without ownership check",
|
|
13
|
+
file: file,
|
|
14
|
+
line: idx + 1,
|
|
15
|
+
snippet: line.strip,
|
|
16
|
+
remediation: "Add authorization check: authorize @object"
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
findings
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class MassAssignmentChecker < 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
|
+
if line.include?('permit!') || line.include?('permit(:')
|
|
10
|
+
if line.include?('permit!')
|
|
11
|
+
add_finding(
|
|
12
|
+
severity: :critical,
|
|
13
|
+
message: "Mass assignment vulnerability: permit! allows all parameters",
|
|
14
|
+
file: file,
|
|
15
|
+
line: idx + 1,
|
|
16
|
+
snippet: line.strip,
|
|
17
|
+
remediation: "Use specific permits: permit(:field1, :field2)"
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
findings
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class OpenRedirectChecker < BaseChecker
|
|
5
|
+
def run
|
|
6
|
+
glob_files('app/controllers/**/*.rb').each do |file|
|
|
7
|
+
content = File.read(file)
|
|
8
|
+
content.each_line.with_index do |line, idx|
|
|
9
|
+
if line.match?(/redirect_to\s+params\[:/) || line.match?(/redirect_to\s+request\./)
|
|
10
|
+
add_finding(
|
|
11
|
+
severity: :high,
|
|
12
|
+
message: "Potential open redirect: user-controlled redirect URL",
|
|
13
|
+
file: file,
|
|
14
|
+
line: idx + 1,
|
|
15
|
+
snippet: line.strip,
|
|
16
|
+
remediation: "Whitelist allowed redirect URLs"
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
findings
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class RateLimitChecker < BaseChecker
|
|
5
|
+
def run
|
|
6
|
+
config_file = File.join(@root, 'config/initializers/rack_attack.rb')
|
|
7
|
+
|
|
8
|
+
if !File.exist?(config_file)
|
|
9
|
+
add_finding(
|
|
10
|
+
severity: :medium,
|
|
11
|
+
message: "Rate limiting not configured: rack_attack.rb missing",
|
|
12
|
+
file: config_file,
|
|
13
|
+
line: 1,
|
|
14
|
+
remediation: "Create config/initializers/rack_attack.rb with rate limiting rules"
|
|
15
|
+
)
|
|
16
|
+
else
|
|
17
|
+
content = File.read(config_file)
|
|
18
|
+
unless content.include?('throttle') && (content.include?('login') || content.include?('api'))
|
|
19
|
+
add_finding(
|
|
20
|
+
severity: :medium,
|
|
21
|
+
message: "Rate limiting rules not configured for critical endpoints",
|
|
22
|
+
file: config_file,
|
|
23
|
+
line: 1,
|
|
24
|
+
remediation: "Add throttle rules for /login, /api/auth, /password_reset"
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
findings
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class SecretsChecker < BaseChecker
|
|
5
|
+
PATTERNS = [
|
|
6
|
+
/api[_-]?key\s*[=:]\s*['"][^'"]+['"]/i,
|
|
7
|
+
/secret[_-]?key\s*[=:]\s*['"][^'"]+['"]/i,
|
|
8
|
+
/password\s*[=:]\s*['"][^'"]+['"]/i,
|
|
9
|
+
/token\s*[=:]\s*['"][^'"]+['"]/i
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
%w[*.rb *.yml .env .env.local].each do |pattern|
|
|
14
|
+
glob_files("**/{#{pattern}}").each do |file|
|
|
15
|
+
next if file.include?('node_modules') || file.include?('vendor')
|
|
16
|
+
content = File.read(file) rescue next
|
|
17
|
+
content.each_line.with_index do |line, idx|
|
|
18
|
+
PATTERNS.each do |pattern|
|
|
19
|
+
if line.match?(pattern) && !line.strip.start_with?('#')
|
|
20
|
+
add_finding(
|
|
21
|
+
severity: :critical,
|
|
22
|
+
message: "Hardcoded secret detected",
|
|
23
|
+
file: file,
|
|
24
|
+
line: idx + 1,
|
|
25
|
+
remediation: "Use ENV variables or Rails credentials"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
findings
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class SqlInjectionChecker < 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
|
+
if line.match?(/\.where\s*\(\s*['"]\s*#{.*}/) || line.match?(/\.find_by_sql\s*\(/)
|
|
10
|
+
add_finding(
|
|
11
|
+
severity: :high,
|
|
12
|
+
message: "Potential SQL injection: using string interpolation in queries",
|
|
13
|
+
file: file,
|
|
14
|
+
line: idx + 1,
|
|
15
|
+
snippet: line.strip,
|
|
16
|
+
remediation: "Use parameterized queries: .where('column = ?', value)"
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
findings
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Checkers
|
|
4
|
+
class XssChecker < BaseChecker
|
|
5
|
+
def run
|
|
6
|
+
glob_files('app/views/**/*.erb').each do |file|
|
|
7
|
+
content = File.read(file)
|
|
8
|
+
content.each_line.with_index do |line, idx|
|
|
9
|
+
if line.include?('<%=') && (line.include?('params') || line.include?('@')) && !line.include?('sanitize') && !line.include?('h(')
|
|
10
|
+
add_finding(
|
|
11
|
+
severity: :high,
|
|
12
|
+
message: "Potential XSS vulnerability: unsanitized user input in view",
|
|
13
|
+
file: file,
|
|
14
|
+
line: idx + 1,
|
|
15
|
+
snippet: line.strip,
|
|
16
|
+
remediation: "Use <%= h() %> or sanitize() helper"
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
findings
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
class Configuration
|
|
4
|
+
attr_accessor :root, :enabled_checkers, :excluded_paths, :output_format
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@root = Rails.root.to_s
|
|
8
|
+
@enabled_checkers = all_checkers
|
|
9
|
+
@excluded_paths = %w[vendor spec test node_modules]
|
|
10
|
+
@output_format = :console
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def all_checkers
|
|
14
|
+
%w[
|
|
15
|
+
sql_injection xss csrf mass_assignment open_redirect secrets
|
|
16
|
+
dos idor ai_injection rate_limit dependency graphql
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
class Finding
|
|
4
|
+
attr_accessor :severity, :category, :message, :file, :line, :code_snippet, :remediation
|
|
5
|
+
|
|
6
|
+
def initialize(severity:, category:, message:, file:, line:, code_snippet: "", remediation: "")
|
|
7
|
+
@severity = severity
|
|
8
|
+
@category = category
|
|
9
|
+
@message = message
|
|
10
|
+
@file = file
|
|
11
|
+
@line = line
|
|
12
|
+
@code_snippet = code_snippet
|
|
13
|
+
@remediation = remediation
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_h
|
|
17
|
+
{
|
|
18
|
+
severity: severity,
|
|
19
|
+
category: category,
|
|
20
|
+
message: message,
|
|
21
|
+
file: file,
|
|
22
|
+
line: line,
|
|
23
|
+
code_snippet: code_snippet,
|
|
24
|
+
remediation: remediation
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Reporters
|
|
4
|
+
class ConsoleReporter
|
|
5
|
+
def initialize(findings)
|
|
6
|
+
@findings = findings
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def report
|
|
10
|
+
puts "\n" + "="*60
|
|
11
|
+
puts "Rails GuardDog Security Report".center(60)
|
|
12
|
+
puts "="*60 + "\n"
|
|
13
|
+
|
|
14
|
+
if @findings.empty?
|
|
15
|
+
puts "ā No security issues found!".green
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@findings.group_by(&:severity).each do |severity, findings|
|
|
20
|
+
puts "\n[#{severity.upcase}] (#{findings.count})"
|
|
21
|
+
findings.each do |finding|
|
|
22
|
+
puts " #{finding.category} ā #{finding.message}"
|
|
23
|
+
puts " #{finding.file}:#{finding.line}"
|
|
24
|
+
puts " Fix: #{finding.remediation}\n"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
puts "\n" + "="*60
|
|
29
|
+
puts "Total findings: #{@findings.count}"
|
|
30
|
+
puts "="*60 + "\n"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
module Reporters
|
|
4
|
+
class HtmlReporter
|
|
5
|
+
def initialize(findings)
|
|
6
|
+
@findings = findings
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def report(output_path = "guarddog_report.html")
|
|
10
|
+
html = generate_html
|
|
11
|
+
File.write(output_path, html)
|
|
12
|
+
output_path
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def generate_html
|
|
18
|
+
severity_breakdown = @findings.group_by(&:severity).transform_values(&:count)
|
|
19
|
+
|
|
20
|
+
html = <<~HTML
|
|
21
|
+
<!DOCTYPE html>
|
|
22
|
+
<html>
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="UTF-8">
|
|
25
|
+
<title>Rails GuardDog Security Report</title>
|
|
26
|
+
<style>
|
|
27
|
+
* { box-sizing: border-box; }
|
|
28
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
|
|
29
|
+
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
|
|
30
|
+
h1 { color: #333; margin-top: 0; }
|
|
31
|
+
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin: 20px 0; }
|
|
32
|
+
.stat-card { padding: 16px; border-radius: 6px; text-align: center; }
|
|
33
|
+
.stat-card.critical { background: #fee; color: #c00; }
|
|
34
|
+
.stat-card.high { background: #fef3cd; color: #856404; }
|
|
35
|
+
.stat-card.medium { background: #cfe2ff; color: #084298; }
|
|
36
|
+
.stat-card.low { background: #d1e7dd; color: #0f5132; }
|
|
37
|
+
.stat-card h3 { margin: 0 0 10px; font-size: 28px; }
|
|
38
|
+
.stat-card p { margin: 0; font-size: 12px; }
|
|
39
|
+
.findings { margin-top: 30px; }
|
|
40
|
+
.finding { border-left: 4px solid #ccc; padding: 16px; margin: 16px 0; background: #fafafa; border-radius: 4px; }
|
|
41
|
+
.finding.critical { border-color: #c00; background: #fee; }
|
|
42
|
+
.finding.high { border-color: #ff9800; background: #fff3cd; }
|
|
43
|
+
.finding.medium { border-color: #2196f3; background: #d1ecf1; }
|
|
44
|
+
.finding.low { border-color: #4caf50; background: #d4edda; }
|
|
45
|
+
.finding-severity { font-weight: bold; font-size: 12px; text-transform: uppercase; }
|
|
46
|
+
.finding-title { font-size: 16px; font-weight: 600; margin: 8px 0; }
|
|
47
|
+
.finding-meta { font-size: 13px; color: #666; margin: 8px 0; }
|
|
48
|
+
.finding-code { background: #f0f0f0; padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; margin: 8px 0; overflow-x: auto; }
|
|
49
|
+
.finding-remediation { margin-top: 10px; padding: 10px; background: rgba(0,0,0,0.05); border-radius: 4px; font-size: 13px; }
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div class="container">
|
|
54
|
+
<h1>š Rails GuardDog Security Report</h1>
|
|
55
|
+
<p>Generated: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}</p>
|
|
56
|
+
|
|
57
|
+
<div class="stats">
|
|
58
|
+
<div class="stat-card critical">
|
|
59
|
+
<h3>#{severity_breakdown[:critical] || 0}</h3>
|
|
60
|
+
<p>Critical</p>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="stat-card high">
|
|
63
|
+
<h3>#{severity_breakdown[:high] || 0}</h3>
|
|
64
|
+
<p>High</p>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="stat-card medium">
|
|
67
|
+
<h3>#{severity_breakdown[:medium] || 0}</h3>
|
|
68
|
+
<p>Medium</p>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="stat-card low">
|
|
71
|
+
<h3>#{severity_breakdown[:low] || 0}</h3>
|
|
72
|
+
<p>Low</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="findings">
|
|
77
|
+
HTML
|
|
78
|
+
|
|
79
|
+
@findings.each do |finding|
|
|
80
|
+
html += %{
|
|
81
|
+
<div class="finding #{finding.severity}">
|
|
82
|
+
<div class="finding-severity">#{finding.severity.upcase}</div>
|
|
83
|
+
<div class="finding-title">#{finding.category} - #{finding.message}</div>
|
|
84
|
+
<div class="finding-meta">#{finding.file}:#{finding.line}</div>
|
|
85
|
+
<div class="finding-code">#{finding.code_snippet}</div>
|
|
86
|
+
<div class="finding-remediation"><strong>Fix:</strong> #{finding.remediation}</div>
|
|
87
|
+
</div>
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
html += <<~HTML
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
HTML
|
|
97
|
+
|
|
98
|
+
html
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module Rails
|
|
4
|
+
module Guarddog
|
|
5
|
+
module Reporters
|
|
6
|
+
class JsonReporter
|
|
7
|
+
def initialize(findings)
|
|
8
|
+
@findings = findings
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def report
|
|
12
|
+
output = {
|
|
13
|
+
timestamp: Time.now.iso8601,
|
|
14
|
+
total_findings: @findings.count,
|
|
15
|
+
severity_breakdown: severity_breakdown,
|
|
16
|
+
findings: @findings.map(&:to_h)
|
|
17
|
+
}
|
|
18
|
+
JSON.pretty_generate(output)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def severity_breakdown
|
|
24
|
+
@findings.group_by(&:severity).transform_values(&:count)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Rails
|
|
2
|
+
module Guarddog
|
|
3
|
+
class Scanner
|
|
4
|
+
attr_accessor :configuration, :findings
|
|
5
|
+
|
|
6
|
+
def initialize(config = nil)
|
|
7
|
+
@configuration = config || Configuration.new
|
|
8
|
+
@findings = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
checkers = load_checkers
|
|
13
|
+
checkers.each do |checker|
|
|
14
|
+
checker_instance = checker.new(@configuration.root)
|
|
15
|
+
checker_instance.run
|
|
16
|
+
@findings.concat(checker_instance.findings)
|
|
17
|
+
end
|
|
18
|
+
@findings.sort_by { |f| severity_order(f.severity) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def load_checkers
|
|
24
|
+
checkers_dir = File.expand_path('../guarddog/checkers', __FILE__)
|
|
25
|
+
Dir.glob("#{checkers_dir}/*_checker.rb").map do |file|
|
|
26
|
+
require file
|
|
27
|
+
class_name = File.basename(file, '.rb').camelize
|
|
28
|
+
Checkers.const_get(class_name)
|
|
29
|
+
end.compact
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def severity_order(severity)
|
|
33
|
+
{ critical: 0, high: 1, medium: 2, low: 3 }[severity] || 4
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'rails'
|
|
2
|
+
require_relative 'guarddog/version'
|
|
3
|
+
require_relative 'guarddog/configuration'
|
|
4
|
+
require_relative 'guarddog/finding'
|
|
5
|
+
require_relative 'guarddog/scanner'
|
|
6
|
+
require_relative 'guarddog/checkers/base_checker'
|
|
7
|
+
require_relative 'guarddog/checkers/sql_injection_checker'
|
|
8
|
+
require_relative 'guarddog/checkers/xss_checker'
|
|
9
|
+
require_relative 'guarddog/checkers/csrf_checker'
|
|
10
|
+
require_relative 'guarddog/checkers/mass_assignment_checker'
|
|
11
|
+
require_relative 'guarddog/checkers/open_redirect_checker'
|
|
12
|
+
require_relative 'guarddog/checkers/secrets_checker'
|
|
13
|
+
require_relative 'guarddog/checkers/dos_checker'
|
|
14
|
+
require_relative 'guarddog/checkers/idor_checker'
|
|
15
|
+
require_relative 'guarddog/checkers/ai_injection_checker'
|
|
16
|
+
require_relative 'guarddog/checkers/rate_limit_checker'
|
|
17
|
+
require_relative 'guarddog/checkers/dependency_checker'
|
|
18
|
+
require_relative 'guarddog/checkers/graphql_checker'
|
|
19
|
+
require_relative 'guarddog/reporters/console_reporter'
|
|
20
|
+
require_relative 'guarddog/reporters/json_reporter'
|
|
21
|
+
require_relative 'guarddog/reporters/html_reporter'
|
|
22
|
+
require_relative 'guarddog/railtie'
|
|
23
|
+
|
|
24
|
+
module Rails
|
|
25
|
+
module Guarddog
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative 'rails/guarddog'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
namespace :guarddog do
|
|
2
|
+
task :scan do
|
|
3
|
+
require 'rails/guarddog'
|
|
4
|
+
|
|
5
|
+
scanner = Rails::Guarddog::Scanner.new
|
|
6
|
+
findings = scanner.run
|
|
7
|
+
|
|
8
|
+
reporter = Rails::Guarddog::Reporters::ConsoleReporter.new(findings)
|
|
9
|
+
reporter.report
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
task :report do
|
|
13
|
+
require 'rails/guarddog'
|
|
14
|
+
|
|
15
|
+
scanner = Rails::Guarddog::Scanner.new
|
|
16
|
+
findings = scanner.run
|
|
17
|
+
|
|
18
|
+
html_reporter = Rails::Guarddog::Reporters::HtmlReporter.new(findings)
|
|
19
|
+
path = html_reporter.report("guarddog_report.html")
|
|
20
|
+
puts "ā HTML report generated: #{path}"
|
|
21
|
+
|
|
22
|
+
json_reporter = Rails::Guarddog::Reporters::JsonReporter.new(findings)
|
|
23
|
+
File.write("guarddog_report.json", json_reporter.report)
|
|
24
|
+
puts "ā JSON report generated: guarddog_report.json"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
task :ci do
|
|
28
|
+
require 'rails/guarddog'
|
|
29
|
+
|
|
30
|
+
scanner = Rails::Guarddog::Scanner.new
|
|
31
|
+
findings = scanner.run
|
|
32
|
+
|
|
33
|
+
critical = findings.select { |f| f.severity == :critical }
|
|
34
|
+
|
|
35
|
+
puts Rails::Guarddog::Reporters::JsonReporter.new(findings).report
|
|
36
|
+
|
|
37
|
+
if critical.any?
|
|
38
|
+
puts "\nā CRITICAL VULNERABILITIES FOUND: #{critical.count}"
|
|
39
|
+
exit 1
|
|
40
|
+
else
|
|
41
|
+
puts "\nā No critical vulnerabilities"
|
|
42
|
+
exit 0
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails-guarddog
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Security Team
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-05 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: railties
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '6.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '6.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: parser
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.0'
|
|
55
|
+
description: 'Rails GuardDog: Beyond brakeman ā AI injection, DoS, supply chain, GraphQL
|
|
56
|
+
auth, and more'
|
|
57
|
+
email:
|
|
58
|
+
- security@example.com
|
|
59
|
+
executables:
|
|
60
|
+
- guarddog
|
|
61
|
+
extensions: []
|
|
62
|
+
extra_rdoc_files: []
|
|
63
|
+
files:
|
|
64
|
+
- LICENSE
|
|
65
|
+
- README.md
|
|
66
|
+
- bin/guarddog
|
|
67
|
+
- lib/rails-guarddog.rb
|
|
68
|
+
- lib/rails/guarddog.rb
|
|
69
|
+
- lib/rails/guarddog/checkers/ai_injection_checker.rb
|
|
70
|
+
- lib/rails/guarddog/checkers/base_checker.rb
|
|
71
|
+
- lib/rails/guarddog/checkers/csrf_checker.rb
|
|
72
|
+
- lib/rails/guarddog/checkers/dependency_checker.rb
|
|
73
|
+
- lib/rails/guarddog/checkers/dos_checker.rb
|
|
74
|
+
- lib/rails/guarddog/checkers/graphql_checker.rb
|
|
75
|
+
- lib/rails/guarddog/checkers/idor_checker.rb
|
|
76
|
+
- lib/rails/guarddog/checkers/mass_assignment_checker.rb
|
|
77
|
+
- lib/rails/guarddog/checkers/open_redirect_checker.rb
|
|
78
|
+
- lib/rails/guarddog/checkers/rate_limit_checker.rb
|
|
79
|
+
- lib/rails/guarddog/checkers/secrets_checker.rb
|
|
80
|
+
- lib/rails/guarddog/checkers/sql_injection_checker.rb
|
|
81
|
+
- lib/rails/guarddog/checkers/xss_checker.rb
|
|
82
|
+
- lib/rails/guarddog/configuration.rb
|
|
83
|
+
- lib/rails/guarddog/finding.rb
|
|
84
|
+
- lib/rails/guarddog/railtie.rb
|
|
85
|
+
- lib/rails/guarddog/reporters/console_reporter.rb
|
|
86
|
+
- lib/rails/guarddog/reporters/html_reporter.rb
|
|
87
|
+
- lib/rails/guarddog/reporters/json_reporter.rb
|
|
88
|
+
- lib/rails/guarddog/scanner.rb
|
|
89
|
+
- lib/rails/guarddog/version.rb
|
|
90
|
+
- lib/tasks/guarddog.rake
|
|
91
|
+
homepage: https://github.com/example/rails-guarddog
|
|
92
|
+
licenses:
|
|
93
|
+
- MIT
|
|
94
|
+
metadata: {}
|
|
95
|
+
post_install_message:
|
|
96
|
+
rdoc_options: []
|
|
97
|
+
require_paths:
|
|
98
|
+
- lib
|
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0'
|
|
109
|
+
requirements: []
|
|
110
|
+
rubygems_version: 3.5.22
|
|
111
|
+
signing_key:
|
|
112
|
+
specification_version: 4
|
|
113
|
+
summary: Advanced security checker for Rails apps
|
|
114
|
+
test_files: []
|