scryer 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/README.md +367 -0
- data/exe/scryer +7 -0
- data/lib/generators/scryer/USAGE +66 -0
- data/lib/generators/scryer/install_generator.rb +29 -0
- data/lib/generators/scryer/templates/scryer_initializer.rb +28 -0
- data/lib/scryer/ai_client.rb +53 -0
- data/lib/scryer/ai_fix_suggester.rb +138 -0
- data/lib/scryer/ast.rb +277 -0
- data/lib/scryer/cache_extractor.rb +124 -0
- data/lib/scryer/cli.rb +193 -0
- data/lib/scryer/dependency_audit.rb +225 -0
- data/lib/scryer/duplicate_detector.rb +103 -0
- data/lib/scryer/finding.rb +21 -0
- data/lib/scryer/method_extractor.rb +55 -0
- data/lib/scryer/performance_rules/inefficient_save_loop_rule.rb +108 -0
- data/lib/scryer/performance_rules/missing_pagination_rule.rb +132 -0
- data/lib/scryer/performance_rules/n_plus_one_query_rule.rb +221 -0
- data/lib/scryer/performance_rules/unbounded_table_scan_rule.rb +78 -0
- data/lib/scryer/query_extractor.rb +123 -0
- data/lib/scryer/query_watcher.rb +250 -0
- data/lib/scryer/railtie.rb +12 -0
- data/lib/scryer/report_renderer.rb +546 -0
- data/lib/scryer/rule.rb +43 -0
- data/lib/scryer/rule_set.rb +19 -0
- data/lib/scryer/rules/command_injection_rule.rb +61 -0
- data/lib/scryer/rules/csrf_protection_rule.rb +89 -0
- data/lib/scryer/rules/hardcoded_secret_rule.rb +96 -0
- data/lib/scryer/rules/mass_assignment_rule.rb +103 -0
- data/lib/scryer/rules/open_redirect_rule.rb +57 -0
- data/lib/scryer/rules/sql_injection_rule.rb +63 -0
- data/lib/scryer/rules/unsafe_deserialization_rule.rb +71 -0
- data/lib/scryer/rules/weak_crypto_rule.rb +66 -0
- data/lib/scryer/rules/xss_unsafe_html_rule.rb +70 -0
- data/lib/scryer/scanner.rb +129 -0
- data/lib/scryer/version.rb +3 -0
- data/lib/scryer.rb +65 -0
- data/lib/tasks/scryer.rake +172 -0
- metadata +106 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
require "ripper"
|
|
2
|
+
require "set"
|
|
3
|
+
|
|
4
|
+
module Scryer
|
|
5
|
+
# Ties parsing + rules + duplicate detection together across a directory
|
|
6
|
+
# tree. This is the one entry point both the `sentinel:scan` rake task and
|
|
7
|
+
# any generator/CLI should call — everything else in this gem is a building
|
|
8
|
+
# block this class composes.
|
|
9
|
+
class Scanner
|
|
10
|
+
DEFAULT_GLOB_DIRS = %w[app lib config db].freeze
|
|
11
|
+
SKIP_DIR_SEGMENTS = %w[vendor node_modules tmp log .git spec test].freeze
|
|
12
|
+
|
|
13
|
+
# Duplicate-code detection only makes sense within hand-written business
|
|
14
|
+
# logic. Migrations in particular are mostly boilerplate (create_table /
|
|
15
|
+
# t.string / t.integer calls) that normalize to near-identical token
|
|
16
|
+
# streams and drown out real duplicates with false positives — so unlike
|
|
17
|
+
# the security/performance rules (which still scan every file under
|
|
18
|
+
# DEFAULT_GLOB_DIRS), duplicate detection is restricted to models,
|
|
19
|
+
# controllers, helpers, and concerns (wherever they're nested).
|
|
20
|
+
DUPLICATE_DETECTION_APP_SUBDIRS = %w[models controllers helpers].freeze
|
|
21
|
+
|
|
22
|
+
# Query and cache-value duplicates use a slightly higher bar than whole
|
|
23
|
+
# methods: they're much shorter fragments, so a coincidental match at the
|
|
24
|
+
# method threshold (0.6) is more likely — see DuplicateDetector's shingle
|
|
25
|
+
# comment for why short token streams are more sensitive to this.
|
|
26
|
+
QUERY_SIMILARITY_THRESHOLD = 0.7
|
|
27
|
+
CACHE_SIMILARITY_THRESHOLD = 0.7
|
|
28
|
+
|
|
29
|
+
Result = Struct.new(:security_findings, :performance_findings, :duplicate_groups, :files_scanned, :parse_errors, keyword_init: true)
|
|
30
|
+
|
|
31
|
+
# `skip_rules` silences specific checks by rule_id (e.g. a known false
|
|
32
|
+
# positive on this codebase) without editing/removing the rule itself —
|
|
33
|
+
# accepts strings or symbols, matched against Rule.rule_id.
|
|
34
|
+
def initialize(root:, dirs: DEFAULT_GLOB_DIRS, skip_rules: [])
|
|
35
|
+
@root = File.expand_path(root)
|
|
36
|
+
@dirs = dirs
|
|
37
|
+
@skip_rules = Set.new(skip_rules.map(&:to_s))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call
|
|
41
|
+
files = collect_files
|
|
42
|
+
all_methods = []
|
|
43
|
+
all_queries = []
|
|
44
|
+
all_cache_calls = []
|
|
45
|
+
security_findings = []
|
|
46
|
+
performance_findings = []
|
|
47
|
+
parse_errors = []
|
|
48
|
+
|
|
49
|
+
files.each do |abs_path|
|
|
50
|
+
rel_path = abs_path.sub(/\A#{Regexp.escape(@root)}\/?/, "")
|
|
51
|
+
source = File.read(abs_path)
|
|
52
|
+
|
|
53
|
+
sexp = begin
|
|
54
|
+
Ripper.sexp(source)
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
parse_errors << { file: rel_path, error: e.message }
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if sexp.nil?
|
|
61
|
+
parse_errors << { file: rel_path, error: "Ripper could not parse this file (possibly a syntax error, or Ruby syntax newer than this gem's Ruby runtime supports)" } unless parse_errors.any? { |pe| pe[:file] == rel_path }
|
|
62
|
+
next
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
RuleSet.all.each do |rule_class|
|
|
66
|
+
next if @skip_rules.include?(rule_class.rule_id.to_s)
|
|
67
|
+
|
|
68
|
+
bucket =
|
|
69
|
+
case rule_class.category
|
|
70
|
+
when "security" then security_findings
|
|
71
|
+
when "performance" then performance_findings
|
|
72
|
+
end
|
|
73
|
+
next unless bucket
|
|
74
|
+
|
|
75
|
+
bucket.concat(rule_class.new(file: rel_path, source: source, sexp: sexp).scan)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if duplicate_detection_target?(rel_path)
|
|
79
|
+
all_methods.concat(MethodExtractor.extract(file: rel_path, source: source, sexp: sexp))
|
|
80
|
+
all_queries.concat(QueryExtractor.extract(file: rel_path, source: source, sexp: sexp))
|
|
81
|
+
all_cache_calls.concat(CacheExtractor.extract(file: rel_path, source: source, sexp: sexp))
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Same computed value cached under the same key from multiple call
|
|
86
|
+
# sites is normal (just reusing the cache). Only flag it when the
|
|
87
|
+
# *keys* differ too — that's either a redundant cache entry or a key
|
|
88
|
+
# that drifted out of sync with a copy-pasted sibling.
|
|
89
|
+
cache_groups = DuplicateDetector.call(all_cache_calls, threshold: CACHE_SIMILARITY_THRESHOLD, kind: "cache_duplicate")
|
|
90
|
+
.select { |g| g.members.map(&:cache_key).uniq.size > 1 }
|
|
91
|
+
|
|
92
|
+
duplicate_groups =
|
|
93
|
+
DuplicateDetector.call(all_methods, kind: "method_duplicate") +
|
|
94
|
+
DuplicateDetector.call(all_queries, threshold: QUERY_SIMILARITY_THRESHOLD, kind: "query_duplicate") +
|
|
95
|
+
cache_groups
|
|
96
|
+
|
|
97
|
+
Result.new(
|
|
98
|
+
security_findings: security_findings,
|
|
99
|
+
performance_findings: performance_findings,
|
|
100
|
+
duplicate_groups: duplicate_groups,
|
|
101
|
+
files_scanned: files.size,
|
|
102
|
+
parse_errors: parse_errors
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def duplicate_detection_target?(relative_path)
|
|
109
|
+
segments = relative_path.split("/")
|
|
110
|
+
return true if segments.include?("concerns")
|
|
111
|
+
|
|
112
|
+
segments[0] == "app" && DUPLICATE_DETECTION_APP_SUBDIRS.include?(segments[1])
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def collect_files
|
|
116
|
+
@dirs.flat_map { |dir| Dir.glob(File.join(@root, dir, "**", "*.rb")) }
|
|
117
|
+
.reject do |path|
|
|
118
|
+
# Only check segments of the path *relative to @root* — checking
|
|
119
|
+
# the full absolute path would wrongly exclude a project simply
|
|
120
|
+
# because some ancestor directory outside the project happens to
|
|
121
|
+
# be named e.g. "tmp" or "test".
|
|
122
|
+
relative = path.sub(/\A#{Regexp.escape(@root)}\/?/, "")
|
|
123
|
+
SKIP_DIR_SEGMENTS.any? { |seg| relative.split("/").include?(seg) }
|
|
124
|
+
end
|
|
125
|
+
.sort
|
|
126
|
+
.uniq
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/scryer.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require "scryer/version"
|
|
2
|
+
require "scryer/ast"
|
|
3
|
+
require "scryer/finding"
|
|
4
|
+
require "scryer/rule_set"
|
|
5
|
+
require "scryer/rule"
|
|
6
|
+
require "scryer/method_extractor"
|
|
7
|
+
require "scryer/query_extractor"
|
|
8
|
+
require "scryer/cache_extractor"
|
|
9
|
+
require "scryer/duplicate_detector"
|
|
10
|
+
require "scryer/scanner"
|
|
11
|
+
require "scryer/report_renderer"
|
|
12
|
+
require "scryer/dependency_audit"
|
|
13
|
+
require "scryer/ai_client"
|
|
14
|
+
require "scryer/ai_fix_suggester"
|
|
15
|
+
|
|
16
|
+
Dir[File.join(__dir__, "scryer", "rules", "*.rb")].sort.each { |f| require f }
|
|
17
|
+
Dir[File.join(__dir__, "scryer", "performance_rules", "*.rb")].sort.each { |f| require f }
|
|
18
|
+
|
|
19
|
+
module Scryer
|
|
20
|
+
class Configuration
|
|
21
|
+
# `branch`, when set, overrides the git_branch value recorded in the
|
|
22
|
+
# report (instead of the actual checked-out branch from `git rev-parse
|
|
23
|
+
# --abbrev-ref HEAD`) — set this when the branch you want tracked isn't
|
|
24
|
+
# necessarily the one the scan happens to run on (e.g. a CI runner in
|
|
25
|
+
# detached-HEAD state, or you always want releases attributed to "main").
|
|
26
|
+
#
|
|
27
|
+
# `ai_client`, when set, opts into rewriting every finding's
|
|
28
|
+
# `suggested_fix` via an LLM (see README's "AI-assisted fix
|
|
29
|
+
# suggestions") — any object or Proc responding to #call(prompt) (or
|
|
30
|
+
# #complete(prompt)). nil by default: off, no network calls, no
|
|
31
|
+
# provider assumed.
|
|
32
|
+
#
|
|
33
|
+
# `skip_rules` silences specific checks by rule_id (e.g. a known false
|
|
34
|
+
# positive on this codebase) without editing/removing the rule itself —
|
|
35
|
+
# strings or symbols, matched against each Rule's rule_id. Empty by
|
|
36
|
+
# default: every registered rule runs. The `scryer` executable's
|
|
37
|
+
# `--skip RULE_ID` flag adds to this list for a single run rather than
|
|
38
|
+
# replacing it.
|
|
39
|
+
attr_accessor :project_name, :dirs, :branch, :ai_client, :skip_rules
|
|
40
|
+
|
|
41
|
+
def initialize
|
|
42
|
+
@dirs = Scryer::Scanner::DEFAULT_GLOB_DIRS
|
|
43
|
+
@skip_rules = []
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
def configure
|
|
49
|
+
yield configuration
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def configuration
|
|
53
|
+
@configuration ||= Configuration.new
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# QueryWatcher instruments a *running* app (ActiveRecord query notifications,
|
|
59
|
+
# association reader hooks) rather than the one-shot static scan the rest of
|
|
60
|
+
# this file wires up, and is opt-in (Scryer::QueryWatcher.enable!) — so it
|
|
61
|
+
# isn't required eagerly here to avoid loading active_support/notifications
|
|
62
|
+
# machinery for the (default) case where nobody asked for it. Require it
|
|
63
|
+
# yourself where you call .enable! (see README's "Runtime query watcher").
|
|
64
|
+
|
|
65
|
+
require "scryer/railtie" if defined?(Rails::Railtie)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
# Deliberately does NOT depend on the Rails `:environment` task — the
|
|
5
|
+
# scanning engine itself needs no Rails/bundler to run, so this task works
|
|
6
|
+
# equally via `bin/rails scryer:report` inside a Rails app or plain
|
|
7
|
+
# `bundle exec rake -f <this file> scryer:report` outside one. Avoids
|
|
8
|
+
# ActiveSupport methods (`.presence`/`.blank?`) for the same reason — plain
|
|
9
|
+
# Ruby nil-or-empty checks only.
|
|
10
|
+
namespace :scryer do
|
|
11
|
+
desc "Run Scryer and write a report. Args are any mix of json/html/csv (which formats " \
|
|
12
|
+
"to write — default json,html), the token 'deps' (fold a dependency audit — OSV.dev " \
|
|
13
|
+
"vulnerable gems + insecure git/http sources, same checks as scryer:audit_dependencies " \
|
|
14
|
+
"— into this report instead of running it separately), plus at most one path (a " \
|
|
15
|
+
"directory when writing more than one format, or an exact file for a single format). " \
|
|
16
|
+
"Rake splits bracket args on every comma, so pass each token as its own item rather " \
|
|
17
|
+
"than one comma-joined string. e.g. rails scryer:report, rails 'scryer:report[html]', " \
|
|
18
|
+
"rails 'scryer:report[json,doc/security.json]', rails 'scryer:report[json,html]', " \
|
|
19
|
+
"rails 'scryer:report[html,deps]', rails 'scryer:report[csv]'"
|
|
20
|
+
task :report, [:format] do |_, args|
|
|
21
|
+
root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
|
|
22
|
+
dirs = Scryer.configuration.dirs
|
|
23
|
+
|
|
24
|
+
# Ignore the declared :format name and read every positional value Rake
|
|
25
|
+
# was given (args.to_a) — the whole point is accepting a variable number
|
|
26
|
+
# of format/deps tokens plus one path, which a single named param can't do.
|
|
27
|
+
formats, path_arg, include_deps = parse_report_args(args.to_a)
|
|
28
|
+
|
|
29
|
+
skip_rules = Scryer.configuration.skip_rules
|
|
30
|
+
puts "Scryer: skipping #{skip_rules.join(', ')}." if skip_rules.any?
|
|
31
|
+
|
|
32
|
+
result = Scryer::Scanner.new(root: root, dirs: dirs, skip_rules: skip_rules).call
|
|
33
|
+
|
|
34
|
+
dependency_findings = []
|
|
35
|
+
if include_deps
|
|
36
|
+
puts "Scryer: checking Gemfile.lock sources (offline)..."
|
|
37
|
+
puts "Scryer: querying OSV.dev for known vulnerabilities (needs network)..."
|
|
38
|
+
dependency_findings = Scryer::DependencyAudit.insecure_sources(root) + Scryer::DependencyAudit.vulnerable_gems(root)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
if Scryer.configuration.ai_client
|
|
42
|
+
puts "Scryer: rewriting suggested fixes via the configured AI client..."
|
|
43
|
+
Scryer::AiFixSuggester.enhance_result!(result)
|
|
44
|
+
Scryer::AiFixSuggester.enhance_many!(dependency_findings) unless dependency_findings.empty?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
renderer = Scryer::ReportRenderer.new(
|
|
48
|
+
result: result,
|
|
49
|
+
project_name: blank_to_nil(Scryer.configuration.project_name) || File.basename(root),
|
|
50
|
+
release_label: ScryerTasks.git_release_label,
|
|
51
|
+
git_commit_sha: ScryerTasks.git_commit_sha,
|
|
52
|
+
git_branch: blank_to_nil(Scryer.configuration.branch) || ScryerTasks.git_branch,
|
|
53
|
+
dependency_findings: dependency_findings
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
paths = output_paths(root: root, formats: formats, path_arg: path_arg)
|
|
57
|
+
|
|
58
|
+
paths.each do |format, path|
|
|
59
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
60
|
+
content = case format
|
|
61
|
+
when "json" then renderer.as_json
|
|
62
|
+
when "csv" then renderer.as_csv
|
|
63
|
+
else renderer.as_html
|
|
64
|
+
end
|
|
65
|
+
File.write(path, content)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
puts "Scryer: #{result.files_scanned} files scanned, " \
|
|
69
|
+
"#{result.security_findings.size} security findings, " \
|
|
70
|
+
"#{result.performance_findings.size} performance findings, " \
|
|
71
|
+
"#{result.duplicate_groups.size} duplicate groups" \
|
|
72
|
+
"#{include_deps ? ", #{dependency_findings.size} dependency findings" : ""}."
|
|
73
|
+
puts "Report written to #{paths.values.join(', ')}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
desc "Check Gemfile.lock for known-vulnerable gem versions (via OSV.dev — needs network) " \
|
|
77
|
+
"and insecure git/http gem sources (offline). Exits non-zero if anything is found, so " \
|
|
78
|
+
"this can gate CI the same way `bundle-audit check` does."
|
|
79
|
+
task :audit_dependencies do
|
|
80
|
+
root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
|
|
81
|
+
|
|
82
|
+
puts "Scryer: checking Gemfile.lock sources (offline)..."
|
|
83
|
+
insecure = Scryer::DependencyAudit.insecure_sources(root)
|
|
84
|
+
|
|
85
|
+
puts "Scryer: querying OSV.dev for known vulnerabilities (needs network)..."
|
|
86
|
+
vulnerable = Scryer::DependencyAudit.vulnerable_gems(root)
|
|
87
|
+
|
|
88
|
+
(insecure + vulnerable).each do |f|
|
|
89
|
+
label = f.kind == "insecure_source" ? "[#{f.severity.upcase}] #{f.message}" : "[#{f.severity.upcase}] #{f.gem_name} #{f.installed_version} - #{f.advisory_id}: #{f.title}"
|
|
90
|
+
puts label
|
|
91
|
+
puts " fix: #{f.suggested_fix}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
total = insecure.size + vulnerable.size
|
|
95
|
+
puts "\nScryer: #{total} dependency finding(s) (#{insecure.size} insecure source, #{vulnerable.size} vulnerable gem)."
|
|
96
|
+
abort("Scryer: dependency audit failed.") if total.positive?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
VALID_FORMATS = %w[json html csv].freeze
|
|
100
|
+
EXTENSION_FOR_FORMAT = { "json" => "json", "html" => "html", "csv" => "csv" }.freeze
|
|
101
|
+
DEPS_TOKEN = "deps".freeze
|
|
102
|
+
|
|
103
|
+
# tokens is every bracket arg Rake was given, e.g. %w[json doc/security.json] or
|
|
104
|
+
# %w[json html] or %w[html deps] or []. Returns [formats, path_arg,
|
|
105
|
+
# include_deps] — any token matching a known format is a format, the
|
|
106
|
+
# literal "deps" token opts into folding a dependency audit into the
|
|
107
|
+
# report, and at most one other token is allowed, which is the path.
|
|
108
|
+
def parse_report_args(tokens)
|
|
109
|
+
tokens = tokens.map { |t| blank_to_nil(t) }.compact
|
|
110
|
+
include_deps = tokens.any? { |t| t.downcase == DEPS_TOKEN }
|
|
111
|
+
tokens = tokens.reject { |t| t.downcase == DEPS_TOKEN }
|
|
112
|
+
|
|
113
|
+
format_tokens, other_tokens = tokens.partition { |t| VALID_FORMATS.include?(t.downcase) }
|
|
114
|
+
|
|
115
|
+
if other_tokens.size > 1
|
|
116
|
+
abort "Scryer: only one path is allowed — got #{other_tokens.join(', ')}."
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
formats = format_tokens.map(&:downcase).uniq
|
|
120
|
+
formats = %w[json html] if formats.empty?
|
|
121
|
+
|
|
122
|
+
[formats, other_tokens.first, include_deps]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# No path given: default filenames under tmp/. A path given with a single
|
|
126
|
+
# format whose extension matches that format is treated as an exact file
|
|
127
|
+
# target; otherwise the path is treated as a directory (created if needed)
|
|
128
|
+
# and each format gets its default filename inside it.
|
|
129
|
+
def output_paths(root:, formats:, path_arg:)
|
|
130
|
+
default_names = formats.each_with_object({}) { |f, h| h[f] = "scryer_report.#{EXTENSION_FOR_FORMAT[f]}" }
|
|
131
|
+
return default_names.transform_values { |name| File.join(root, "tmp", name) } if path_arg.nil?
|
|
132
|
+
|
|
133
|
+
resolved = File.absolute_path?(path_arg) ? path_arg : File.join(root, path_arg)
|
|
134
|
+
ext = File.extname(resolved).delete_prefix(".").downcase
|
|
135
|
+
|
|
136
|
+
if formats.size == 1 && ext == formats.first
|
|
137
|
+
{ formats.first => resolved }
|
|
138
|
+
else
|
|
139
|
+
formats.each_with_object({}) { |f, h| h[f] = File.join(resolved, default_names[f]) }
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def blank_to_nil(value)
|
|
144
|
+
return nil if value.nil?
|
|
145
|
+
|
|
146
|
+
str = value.to_s.strip
|
|
147
|
+
str.empty? ? nil : str
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
module ScryerTasks
|
|
152
|
+
module_function
|
|
153
|
+
|
|
154
|
+
def git_commit_sha
|
|
155
|
+
shell_out("git rev-parse HEAD")
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def git_branch
|
|
159
|
+
shell_out("git rev-parse --abbrev-ref HEAD")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def git_release_label
|
|
163
|
+
shell_out("git describe --tags --always")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def shell_out(cmd)
|
|
167
|
+
output = `#{cmd} 2>/dev/null`.strip
|
|
168
|
+
output.empty? ? nil : output
|
|
169
|
+
rescue StandardError
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: scryer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ram Laxman Yadav
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-08-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rake
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '13.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '13.0'
|
|
27
|
+
description: |
|
|
28
|
+
Scans a Rails app's own source with Ruby's stdlib Ripper parser (no Rails/bundler needed to
|
|
29
|
+
run the scan itself) and reports: security findings (SQL injection, mass assignment, command
|
|
30
|
+
injection, hardcoded secrets, unsafe deserialization, XSS-prone unescaped HTML, CSRF gaps,
|
|
31
|
+
weak crypto, open redirects), near-duplicate code (token-normalized similarity across
|
|
32
|
+
methods), and performance heuristics (N+1 queries, missing pagination, inefficient per-record
|
|
33
|
+
save loops, unbounded full-table iteration). Every finding includes a human-reviewable
|
|
34
|
+
suggested fix — nothing is auto-applied. Writes a detailed report as JSON and/or
|
|
35
|
+
self-contained HTML (tmp/scryer_report.{json,html}). Ships a `scryer` executable for
|
|
36
|
+
running outside a Rails app too, e.g. `scryer -o report.json -o report.html`. Also includes a
|
|
37
|
+
runtime query watcher (N+1 / unused-eager-load detection via ActiveRecord instrumentation,
|
|
38
|
+
opt-in) and a dependency vulnerability + insecure-source audit against OSV.dev.
|
|
39
|
+
email:
|
|
40
|
+
executables:
|
|
41
|
+
- scryer
|
|
42
|
+
extensions: []
|
|
43
|
+
extra_rdoc_files: []
|
|
44
|
+
files:
|
|
45
|
+
- README.md
|
|
46
|
+
- exe/scryer
|
|
47
|
+
- lib/generators/scryer/USAGE
|
|
48
|
+
- lib/generators/scryer/install_generator.rb
|
|
49
|
+
- lib/generators/scryer/templates/scryer_initializer.rb
|
|
50
|
+
- lib/scryer.rb
|
|
51
|
+
- lib/scryer/ai_client.rb
|
|
52
|
+
- lib/scryer/ai_fix_suggester.rb
|
|
53
|
+
- lib/scryer/ast.rb
|
|
54
|
+
- lib/scryer/cache_extractor.rb
|
|
55
|
+
- lib/scryer/cli.rb
|
|
56
|
+
- lib/scryer/dependency_audit.rb
|
|
57
|
+
- lib/scryer/duplicate_detector.rb
|
|
58
|
+
- lib/scryer/finding.rb
|
|
59
|
+
- lib/scryer/method_extractor.rb
|
|
60
|
+
- lib/scryer/performance_rules/inefficient_save_loop_rule.rb
|
|
61
|
+
- lib/scryer/performance_rules/missing_pagination_rule.rb
|
|
62
|
+
- lib/scryer/performance_rules/n_plus_one_query_rule.rb
|
|
63
|
+
- lib/scryer/performance_rules/unbounded_table_scan_rule.rb
|
|
64
|
+
- lib/scryer/query_extractor.rb
|
|
65
|
+
- lib/scryer/query_watcher.rb
|
|
66
|
+
- lib/scryer/railtie.rb
|
|
67
|
+
- lib/scryer/report_renderer.rb
|
|
68
|
+
- lib/scryer/rule.rb
|
|
69
|
+
- lib/scryer/rule_set.rb
|
|
70
|
+
- lib/scryer/rules/command_injection_rule.rb
|
|
71
|
+
- lib/scryer/rules/csrf_protection_rule.rb
|
|
72
|
+
- lib/scryer/rules/hardcoded_secret_rule.rb
|
|
73
|
+
- lib/scryer/rules/mass_assignment_rule.rb
|
|
74
|
+
- lib/scryer/rules/open_redirect_rule.rb
|
|
75
|
+
- lib/scryer/rules/sql_injection_rule.rb
|
|
76
|
+
- lib/scryer/rules/unsafe_deserialization_rule.rb
|
|
77
|
+
- lib/scryer/rules/weak_crypto_rule.rb
|
|
78
|
+
- lib/scryer/rules/xss_unsafe_html_rule.rb
|
|
79
|
+
- lib/scryer/scanner.rb
|
|
80
|
+
- lib/scryer/version.rb
|
|
81
|
+
- lib/tasks/scryer.rake
|
|
82
|
+
homepage: https://github.com/ramlaxmanyadav/scryer
|
|
83
|
+
licenses:
|
|
84
|
+
- MIT
|
|
85
|
+
metadata: {}
|
|
86
|
+
post_install_message:
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
require_paths:
|
|
89
|
+
- lib
|
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: 2.7.0
|
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0'
|
|
100
|
+
requirements: []
|
|
101
|
+
rubygems_version: 3.5.11
|
|
102
|
+
signing_key:
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: 'Static code analysis for Rails apps: security vulnerabilities, duplicate
|
|
105
|
+
code, and performance heuristics.'
|
|
106
|
+
test_files: []
|