Class: Scryer::ReportRenderer

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

Overview

Turns a Scanner::Result into the exact JSON shape documented in CONTRACT2.md (also the ingest POST body shape) and a self-contained HTML report (inline CSS, no external assets except optionally linking out — here, none at all, so it works offline too). The HTML report is laid out similarly to a Brakeman report: an overview, a summary of counts, the full list of checks that ran, a breakdown of warnings by type, and then every finding in detail.

Constant Summary collapse

SEVERITY_ORDER =
%w[critical warning info].freeze
SEVERITY_LABELS =
{ "critical" => "Critical", "warning" => "Warning", "info" => "Info" }.freeze
CSV_HEADERS =
%w[kind identifier severity location message suggested_fix code_snippet url].freeze
SARIF_LEVEL_BY_SEVERITY =
{ "critical" => "error", "warning" => "warning", "info" => "note" }.freeze

Instance Method Summary collapse

Constructor Details

#initialize(result:, project_name:, release_label: nil, git_commit_sha: nil, git_branch: nil, dependency_findings: [], scanned_at: Time.now) ⇒ ReportRenderer

dependency_findings is an optional array of Scryer::DependencyAudit:: Finding (insecure_sources + vulnerable_gems) — pass it to fold a bundler-audit-like dependency audit into the same report as the static scan, instead of the audit living in separate --audit-deps output. Defaults to empty so existing callers that only run the static scan are unaffected.



23
24
25
26
27
28
29
30
31
32
# File 'lib/scryer/report_renderer.rb', line 23

def initialize(result:, project_name:, release_label: nil, git_commit_sha: nil, git_branch: nil,
               dependency_findings: [], scanned_at: Time.now)
  @result = result
  @project_name = project_name
  @release_label = release_label
  @git_commit_sha = git_commit_sha
  @git_branch = git_branch
  @dependency_findings = dependency_findings
  @scanned_at = scanned_at
end

Instance Method Details

#as_csvObject

Flat, one-row-per-finding CSV — security + performance findings plus any dependency findings, in that order — for dropping into a spreadsheet or importing into a ticketing tool. Deliberately excludes duplicate-code groups: they're nested member lists, not a single actionable item, so they don't fit a flat "one row = one thing to fix" table (see as_json for the full nested data). No csv stdlib dependency — RFC4180-style quoting is small enough to hand-roll, same reasoning as this gem's other hand-rolled parsers/writers.



65
66
67
68
69
70
71
72
73
74
# File 'lib/scryer/report_renderer.rb', line 65

def as_csv
  h = as_hash
  rows = [CSV_HEADERS]
  h["security_findings"].each { |f| rows << static_csv_row(f) }
  h["performance_findings"].each { |f| rows << static_csv_row(f) }
  h["style_findings"].each { |f| rows << static_csv_row(f) }
  h["dependency_findings"].each { |f| rows << dependency_csv_row(f) }

  rows.map { |row| row.map { |field| csv_field(field) }.join(",") }.join("\n")
end

#as_hashObject



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/scryer/report_renderer.rb', line 34

def as_hash
  {
    "project_name" => @project_name,
    "scryer_version" => Scryer::VERSION,
    "ruby_version" => RUBY_VERSION,
    "scanned_at" => @scanned_at.utc.iso8601,
    "release_label" => @release_label,
    "git_commit_sha" => @git_commit_sha,
    "git_branch" => @git_branch,
    "files_scanned" => @result.files_scanned,
    "parse_errors" => @result.parse_errors.map { |pe| { "file" => pe[:file], "error" => pe[:error] } },
    "security_findings" => @result.security_findings.map(&:to_h),
    "performance_findings" => @result.performance_findings.map(&:to_h),
    "style_findings" => @result.style_findings.map(&:to_h),
    "duplicate_groups" => @result.duplicate_groups.map { |g| duplicate_group_hash(g) },
    "dependency_findings" => @dependency_findings.map(&:to_h)
  }
end

#as_htmlObject



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/scryer/report_renderer.rb', line 88

def as_html
  h = as_hash
  security = h["security_findings"]
  performance = h["performance_findings"]
  style = h["style_findings"]
  all_findings = security + performance + style
  by_severity = all_findings.group_by { |f| f["severity"] }
  duplicate_groups = h["duplicate_groups"]
  dependency_findings = h["dependency_findings"]

  <<~HTML
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Scryer report — #{escape(@project_name)}</title>
      <style>#{CSS}</style>
    </head>
    <body>
      <h1>Scryer report</h1>
      <p class="meta">
        #{escape(@project_name)} &middot; #{escape(h["release_label"] || "no release label")} &middot;
        #{escape(h["scanned_at"])} &middot; #{h["files_scanned"]} files scanned
        #{h["parse_errors"].any? ? "&middot; <span class=\"crit\">#{h["parse_errors"].size} parse error(s)</span>" : ""}
      </p>

      #{render_toc(h)}

      <section id="overview">
        <h2>Overview</h2>
        #{render_overview_table(h)}
      </section>

      <section id="summary">
        <h2>Summary</h2>
        #{render_summary_table(security, performance, style, duplicate_groups, dependency_findings)}
      </section>

      <section id="checks-performed">
        <h2>Checks performed</h2>
        #{render_checks_performed}
      </section>

      <section id="warnings-by-type">
        <h2>Warnings by type</h2>
        #{render_warnings_by_type(all_findings)}
      </section>

      <section id="findings">
        <h2>Findings (#{all_findings.size}) #{expand_collapse_controls("#findings")}</h2>
        #{render_severity_section("critical", by_severity["critical"] || [])}
        #{render_severity_section("warning", by_severity["warning"] || [])}
        #{render_severity_section("info", by_severity["info"] || [])}
      </section>

      <section id="duplicates">
        <h2>Duplicate code groups (#{duplicate_groups.size}) #{expand_collapse_controls("#duplicates")}</h2>
        #{render_duplicate_groups(duplicate_groups)}
      </section>

      <section id="dependency-audit">
        <h2>Dependency audit (#{dependency_findings.size}) #{expand_collapse_controls("#dependency-audit")}</h2>
        #{render_dependency_findings(dependency_findings)}
      </section>

      <section id="errors">
        <h2>Files that couldn't be parsed (#{h["parse_errors"].size})</h2>
        #{render_parse_errors(h["parse_errors"])}
      </section>

      <p class="footer">
        Generated by Scryer v#{h["scryer_version"]} (Ruby #{escape(h["ruby_version"])}) &middot;
        heuristic static analysis, not full data-flow/taint analysis — review every finding in
        its surrounding context before acting on it.
      </p>

      <script>#{JS}</script>
    </body>
    </html>
  HTML
end

#as_jsonObject



53
54
55
# File 'lib/scryer/report_renderer.rb', line 53

def as_json
  JSON.pretty_generate(as_hash)
end

#as_sarifObject

SARIF 2.1.0 (docs.oasis-open.org/sarif/sarif/v2.1.0) — the format GitHub Code Scanning (and other CI security dashboards) natively ingest, turning findings into inline PR annotations and Security-tab entries instead of a report file nobody opens. Pure data mapping of what's already in as_hash — no new detection logic, and every finding behaves identically to how it does in the other formats.



84
85
86
# File 'lib/scryer/report_renderer.rb', line 84

def as_sarif
  JSON.pretty_generate(sarif_hash)
end