Class: Scryer::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/scryer/cli.rb

Overview

Backs the scryer executable (see exe/scryer) — a standalone, Rails-free way to run a scan, mirroring brakeman -o report.json. Deliberately separate from lib/scryer.rb: OptionParser/Shellwords are only needed for this CLI entry point, not when the gem is required inside a host app.

Constant Summary collapse

EXTENSION_FORMATS =
{ ".json" => "json", ".html" => "html", ".htm" => "html", ".csv" => "csv", ".sarif" => "sarif" }.freeze

Instance Method Summary collapse

Constructor Details

#initialize(argv, stdout: $stdout, stderr: $stderr) ⇒ CLI

Returns a new instance of CLI.



14
15
16
17
18
# File 'lib/scryer/cli.rb', line 14

def initialize(argv, stdout: $stdout, stderr: $stderr)
  @argv = argv
  @stdout = stdout
  @stderr = stderr
end

Instance Method Details

#runObject

Returns a process exit code: 0 if clean, 1 if security findings were found (so scryer -o report.json can gate CI the way brakeman -o report.json does), 2 on a usage error.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/scryer/cli.rb', line 23

def run
  options = parse(@argv)
  return 0 if options[:exit_early]

  # The standalone executable has no equivalent of a Rails app's
  # config/initializers/scryer.rb getting autoloaded at boot — this is
  # the only way to run Scryer.configure (set c.ai_client, c.skip_rules,
  # c.dirs, ...) before a scan starts outside Rails.
  Array(options[:require]).each { |path| require File.expand_path(path) }

  return check_gem(options[:check_gem]) if options[:check_gem]

  root = File.expand_path(options[:path] || Dir.pwd)
  return audit_deps(root) if options[:audit_deps]

  skip_rules = Scryer.configuration.skip_rules + (options[:skip] || [])
  @stdout.puts "Scryer: skipping #{skip_rules.join(', ')}." if skip_rules.any?

  result = Scanner.new(root: root, dirs: Scryer.configuration.dirs, skip_rules: skip_rules).call

  # Dependency auditing (OSV.dev) runs by default — a single `scryer`
  # invocation is meant to cover the same ground as RuboCop + Brakeman +
  # bundler-audit + Reek run separately, and that story isn't true if
  # the dependency half is silently skipped unless you remember a flag.
  # `--no-deps` opts back out for a fast, fully offline run (e.g. no
  # network in this environment, or you only want the static scan).
  ran_deps = !options[:no_deps]
  dependency_findings = []
  if ran_deps
    @stdout.puts "Scryer: querying OSV.dev for known-vulnerable gems (needs network)..."
    dependency_findings = DependencyAudit.insecure_sources(root) + DependencyAudit.vulnerable_gems(root) +
                           DependencyAudit.ruby_eol_check(root) + DependencyAudit.credentials_exposure_check(root)
  end

  if Scryer.configuration.ai_client
    @stdout.puts "Scryer: rewriting suggested fixes via the configured AI client..."
    AiFixSuggester.enhance_result!(result)
    AiFixSuggester.enhance_many!(dependency_findings) unless dependency_findings.empty?
  end

  renderer = ReportRenderer.new(
    result: result,
    project_name: options[:project_name] || File.basename(root),
    release_label: git(root, "describe --tags --always"),
    git_commit_sha: git(root, "rev-parse HEAD"),
    git_branch: options[:branch] || git(root, "rev-parse --abbrev-ref HEAD"),
    dependency_findings: dependency_findings
  )

  outputs = options[:outputs].empty? ? default_outputs(root) : options[:outputs]
  outputs.each { |path| write_report(renderer, path) }

  print_summary(result: result, dependency_findings: dependency_findings, ran_deps: ran_deps, outputs: outputs)

  result.security_findings.empty? && dependency_findings.empty? ? 0 : 1
rescue UsageError => e
  @stderr.puts "scryer: #{e.message}"
  2
end