scryer 1.2.0 → 1.2.1
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/CHANGELOG.md +31 -0
- data/docs/architecture.md +11 -6
- data/lib/scryer/dependency_audit.rb +16 -1
- data/lib/scryer/report_renderer.rb +60 -21
- data/lib/scryer/version.rb +1 -1
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 47762d5ce6faa0ec795a2f4966f61dee19625e4dd803e0219a0f47440c1d9892
|
|
4
|
+
data.tar.gz: 9d892e071867b5ff6e2023bd0dfa0f1e98c4b7730b4652ad9d1a5bdaf204eb6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 51b48a7aad2906b43d3d86a0a82a19275c7c6fb19860ec23da7671f765648d47ca29454e88d9ad959b4b8870ba91d1ed5b5590c0651306f8560889baa3acee0f
|
|
7
|
+
data.tar.gz: a8d456858bca651e77ceb583304645f5f69ae827c6669c09efb9caf88eb707de72f66177faea7f935415095b72874c282c762b4a5db78252be10a90155708aa6
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ All notable changes to this project are documented here. Format loosely follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.2.1] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
- Fixed: a platform-specific gem's `Gemfile.lock` spec line (e.g. `nokogiri (1.19.4-x86_64-linux-musl)`)
|
|
11
|
+
fed its unstripped `1.19.4-x86_64-linux-musl` version straight into `DependencyAudit.vulnerable_gems`'s
|
|
12
|
+
OSV.dev query, which parsed the platform suffix as a semver prerelease marker — sorting the
|
|
13
|
+
version as *older* than the plain release and matching every advisory fixed at-or-before it as
|
|
14
|
+
still open. Confirmed directly against OSV.dev: querying the unstripped string for `nokogiri`
|
|
15
|
+
returned 8 vulnerabilities; querying the real `1.19.4` returned 0. `DependencyAudit.parse_lockfile`
|
|
16
|
+
now strips the hyphen-separated platform suffix (RubyGems versions never contain a hyphen, so this
|
|
17
|
+
never touches a real prerelease version like `1.0.0.pre1`). Covered by a new `test/dependency_audit_test.rb`.
|
|
18
|
+
- Fixed: the HTML report's Summary table showed a `—` placeholder for every severity column on the
|
|
19
|
+
Dependency audit row (no breakdown at all), and its Total row's sum silently excluded dependency
|
|
20
|
+
findings entirely — so a report with only dependency findings showed `0/0/0/0` in this table's
|
|
21
|
+
Total row while the severity bars in the executive summary just above it (which do count
|
|
22
|
+
dependency findings) showed nonzero counts, two different totals on the same page. Both rows now
|
|
23
|
+
count dependency findings by severity like every other row. Covered by a new
|
|
24
|
+
`test/report_renderer_summary_table_test.rb`.
|
|
25
|
+
- Security score changes: `info`-severity findings weighed too heavily — `SCORE_SEVERITY_WEIGHT["info"]`
|
|
26
|
+
dropped from `1` to `0.25` (critical stays `15`, warning stays `6`); an info finding is closer to
|
|
27
|
+
a cosmetic note than a real risk (e.g. `csrf_protection_disabled`'s narrowly-scoped-skip case), so
|
|
28
|
+
a codebase with only info-severity findings should barely move off 100/A even with several of
|
|
29
|
+
them. Performance findings (N+1 queries, missing pagination, etc.) now count toward the score too,
|
|
30
|
+
at a new `SCORE_CATEGORY_WEIGHT["performance"] = 0.2` — a fifth of security's weight at the same
|
|
31
|
+
severity/confidence, since a slow app is a real cost but not a *security* one; previously
|
|
32
|
+
performance findings were excluded from the score entirely. Duplicate-code groups and style
|
|
33
|
+
findings (`frozen_string_literal`) remain permanently excluded — a `DuplicateDetector::DuplicateGroup`
|
|
34
|
+
isn't backed by a `Finding` with a severity to weigh in the first place, and style findings are
|
|
35
|
+
cosmetic, not risk. No change to critical/warning weighting or confidence weighting. Covered by a
|
|
36
|
+
new `test/security_score_test.rb` (11 tests) — there was no prior test coverage of the score
|
|
37
|
+
formula at all.
|
|
38
|
+
|
|
8
39
|
## [1.2.0] - 2026-08-16
|
|
9
40
|
|
|
10
41
|
- Added `c.detect_duplicates` (`Scryer::Configuration`, default `true`): duplicate-code detection
|
data/docs/architecture.md
CHANGED
|
@@ -103,9 +103,11 @@ Security Score: 11/100 (F)
|
|
|
103
103
|
The formula (`ReportRenderer#security_score` in `lib/scryer/report_renderer.rb`): every security
|
|
104
104
|
and dependency finding costs points, weighted by both its severity *and* its confidence (a
|
|
105
105
|
`low`-confidence `idor` finding costs less than a `high`-confidence `sql_injection` finding at the
|
|
106
|
-
same severity),
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
same severity); performance findings cost points too, but at a fifth of that weight (a slow app is
|
|
107
|
+
a real cost, just not a *security* one, so it nudges the score rather than driving it). Combined
|
|
108
|
+
via exponential decay from 100 rather than linear subtraction — one critical finding visibly moves
|
|
109
|
+
the score (100 → ~86) without a handful of findings driving any real app straight to a hard-clamped
|
|
110
|
+
0.
|
|
109
111
|
|
|
110
112
|
Two things worth being precise about before you treat this number as meaningful:
|
|
111
113
|
|
|
@@ -114,9 +116,12 @@ Two things worth being precise about before you treat this number as meaningful:
|
|
|
114
116
|
exposure, not a rate. That makes it useful for tracking *one project's own trend* over time (did
|
|
115
117
|
the next scan score higher or lower), not for comparing two differently-sized codebases against
|
|
116
118
|
each other.
|
|
117
|
-
- **
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
- **Code-quality findings are never part of it.** Duplicate-code groups and the one style check
|
|
120
|
+
(`frozen_string_literal`) are cosmetic, not risk, and stay out entirely — a duplicate-code group
|
|
121
|
+
isn't even backed by a `Finding` with a severity/confidence to weigh in the first place (see
|
|
122
|
+
`Scryer::DuplicateDetector`). Check the "Code Quality" row in the summary box separately for
|
|
123
|
+
that; performance findings, unlike code-quality ones, do count toward the score (lightly — see
|
|
124
|
+
above).
|
|
120
125
|
|
|
121
126
|
Alongside the score, every scan also reports a **rule-level pass rate** — "how many of Scryer's
|
|
122
127
|
registered checks fired zero findings":
|
|
@@ -108,7 +108,22 @@ module Scryer
|
|
|
108
108
|
next unless in_specs
|
|
109
109
|
|
|
110
110
|
name = Regexp.last_match(1)
|
|
111
|
-
|
|
111
|
+
# A platform-specific gem's lockfile spec line looks like
|
|
112
|
+
# "nokogiri (1.19.4-x86_64-linux-musl)" — Bundler appends the
|
|
113
|
+
# platform as a hyphen-separated suffix on the version itself.
|
|
114
|
+
# RubyGems version numbers (Gem::Version) never contain a
|
|
115
|
+
# hyphen — only dots — so a hyphen here always demarcates that
|
|
116
|
+
# suffix, never part of the version proper (including for
|
|
117
|
+
# prerelease gems, which use a dot: "1.0.0.pre1", not
|
|
118
|
+
# "1.0.0-pre1"). Left unstripped, this string gets sent
|
|
119
|
+
# straight to OSV.dev's version filter in vulnerable_gems,
|
|
120
|
+
# which parses "-x86_64-linux-musl" as a semver prerelease
|
|
121
|
+
# marker — sorting the version as *older* than the plain
|
|
122
|
+
# release and matching every advisory fixed at-or-before it as
|
|
123
|
+
# if it were still open. Confirmed directly against OSV.dev:
|
|
124
|
+
# querying nokogiri "1.19.4-x86_64-linux-musl" returns 8 vulns;
|
|
125
|
+
# querying "1.19.4" (the real, fully-patched version) returns 0.
|
|
126
|
+
version = Regexp.last_match(2).split("-", 2).first
|
|
112
127
|
# A gem can legitimately appear under more than one block only
|
|
113
128
|
# in pathological Gemfiles; last one wins, consistent with how
|
|
114
129
|
# Bundler itself resolves a single spec per gem name.
|
|
@@ -106,16 +106,21 @@ module Scryer
|
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
# A single 0-100 number (plus a letter grade) summarizing this scan's
|
|
109
|
-
# security risk exposure — security findings and dependency findings
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
109
|
+
# security risk exposure — security findings and dependency findings at
|
|
110
|
+
# full weight, plus performance findings at a small fraction of that
|
|
111
|
+
# weight (a slow app is a real cost, just not a *security* one, so it
|
|
112
|
+
# nudges the score rather than driving it — see SCORE_CATEGORY_WEIGHT).
|
|
113
|
+
# Code-quality findings (duplicate-code groups, frozen_string_literal)
|
|
114
|
+
# are never part of this on purpose: a `DuplicateDetector::DuplicateGroup`
|
|
115
|
+
# isn't even a `Finding` (no severity/confidence to weigh in the first
|
|
116
|
+
# place — see duplicate_detector.rb), and style findings are cosmetic,
|
|
117
|
+
# not risk. Deliberately NOT normalized by files-scanned or lines of
|
|
118
|
+
# code: it reflects this scan's absolute finding exposure, so it's
|
|
119
|
+
# meaningful for tracking one project's trend over time (does the next
|
|
120
|
+
# scan score higher or lower), not for comparing two differently-sized
|
|
121
|
+
# codebases against each other — a bigger app with the same finding
|
|
122
|
+
# *density* will naturally score lower here, and that's a documented
|
|
123
|
+
# limitation, not a bug.
|
|
119
124
|
#
|
|
120
125
|
# Exponential decay rather than linear subtraction from 100: a single
|
|
121
126
|
# critical/high finding should visibly move the score (100 -> ~86) without
|
|
@@ -123,11 +128,24 @@ module Scryer
|
|
|
123
128
|
# which would make the score useless for comparing "bad" against "worse."
|
|
124
129
|
# weighted_penalty combines severity (the dominant factor) with
|
|
125
130
|
# confidence (a low-confidence rule's finding shouldn't hurt the score as
|
|
126
|
-
# much as a high-confidence one saying the same severity) —
|
|
127
|
-
# philosophy as SARIF's `rank` (see sarif_rank above), computed once
|
|
128
|
-
# for a single scan-level number instead of per-result.
|
|
129
|
-
|
|
131
|
+
# much as a high-confidence one saying the same severity) and category —
|
|
132
|
+
# same philosophy as SARIF's `rank` (see sarif_rank above), computed once
|
|
133
|
+
# here for a single scan-level number instead of per-result.
|
|
134
|
+
# "info" is deliberately far below critical/warning, not just a step
|
|
135
|
+
# down from them — an info-severity finding is closer to a cosmetic
|
|
136
|
+
# note than a real risk (see e.g. csrf_protection_disabled's
|
|
137
|
+
# narrowly-scoped-skip case), so a codebase with only info findings
|
|
138
|
+
# should barely move off 100 even with a fair number of them, rather
|
|
139
|
+
# than accumulating like a smaller warning would.
|
|
140
|
+
SCORE_SEVERITY_WEIGHT = { "critical" => 15, "warning" => 6, "info" => 0.25 }.freeze
|
|
130
141
|
SCORE_CONFIDENCE_WEIGHT = { "high" => 1.0, "medium" => 0.7, "low" => 0.4 }.freeze
|
|
142
|
+
# `category` is "security" for every Scryer::Finding in security_findings
|
|
143
|
+
# and every DependencyAudit::Finding (which has no `category` field at
|
|
144
|
+
# all — `f["category"]` is nil for those, so they fall through to the
|
|
145
|
+
# 1.0 default below, same full weight as security). "performance" is the
|
|
146
|
+
# only other category ever passed into this method's `findings` — style
|
|
147
|
+
# findings and duplicate-code groups never reach here at all (see above).
|
|
148
|
+
SCORE_CATEGORY_WEIGHT = { "security" => 1.0, "performance" => 0.2 }.freeze
|
|
131
149
|
SCORE_DECAY_CONSTANT = 100.0
|
|
132
150
|
|
|
133
151
|
# Deliberately reads @result/@dependency_findings directly rather than
|
|
@@ -140,10 +158,14 @@ module Scryer
|
|
|
140
158
|
# (has "confidence") and DependencyAudit::Finding (doesn't — a plain
|
|
141
159
|
# Hash returns nil for a missing key rather than raising, unlike
|
|
142
160
|
# calling #confidence directly on a struct that has no such member).
|
|
143
|
-
|
|
161
|
+
# style_findings/duplicate_groups are never included — see this
|
|
162
|
+
# method's doc comment above for why.
|
|
163
|
+
findings = (@result.security_findings + @result.performance_findings + @dependency_findings).map(&:to_h)
|
|
144
164
|
|
|
145
165
|
weighted_penalty = findings.sum do |f|
|
|
146
|
-
(SCORE_SEVERITY_WEIGHT[f["severity"]] || 3) *
|
|
166
|
+
(SCORE_SEVERITY_WEIGHT[f["severity"]] || 3) *
|
|
167
|
+
(SCORE_CONFIDENCE_WEIGHT[f["confidence"]] || 0.7) *
|
|
168
|
+
(SCORE_CATEGORY_WEIGHT[f["category"]] || 1.0)
|
|
147
169
|
end
|
|
148
170
|
|
|
149
171
|
score = (100 * Math.exp(-weighted_penalty / SCORE_DECAY_CONSTANT)).round
|
|
@@ -368,7 +390,8 @@ module Scryer
|
|
|
368
390
|
</div>
|
|
369
391
|
<div class="score-details">
|
|
370
392
|
<p class="score-label">Security Score — #{score["finding_count"]} security + dependency
|
|
371
|
-
finding(s)
|
|
393
|
+
+ performance finding(s) (performance weighted far lighter than security), weighted
|
|
394
|
+
by severity and this rule's confidence. Not normalized by app
|
|
372
395
|
size — see the README for what this number does and doesn't mean.
|
|
373
396
|
<strong>#{clean_rate["clean"]}/#{clean_rate["total"]}</strong> rules clean
|
|
374
397
|
(#{clean_rate["percent"]}%) — a rule-level pass rate, a different (and not always
|
|
@@ -399,7 +422,16 @@ module Scryer
|
|
|
399
422
|
sec_counts = count_by_severity(security)
|
|
400
423
|
perf_counts = count_by_severity(performance)
|
|
401
424
|
style_counts = count_by_severity(style)
|
|
402
|
-
|
|
425
|
+
# Dependency findings carry a real severity (see DependencyAudit::
|
|
426
|
+
# Finding#severity) same as any other finding — previously left out of
|
|
427
|
+
# this row (rendered as a "—" placeholder) and out of the Total row's
|
|
428
|
+
# sum below, which made the Total row silently undercount relative to
|
|
429
|
+
# what a reader would expect from a row literally labeled "Total" at
|
|
430
|
+
# the bottom of this same table, and inconsistent with the executive
|
|
431
|
+
# summary's severity bars above (which do include dependency findings
|
|
432
|
+
# in their own count).
|
|
433
|
+
deps_counts = count_by_severity(dependency_findings)
|
|
434
|
+
total_counts = SEVERITY_ORDER.each_with_object({}) { |s, acc| acc[s] = sec_counts[s] + perf_counts[s] + style_counts[s] + deps_counts[s] }
|
|
403
435
|
|
|
404
436
|
header = "<tr><th>Category</th>" + SEVERITY_ORDER.map { |s| "<th>#{SEVERITY_LABELS[s]}</th>" }.join + "<th>Total</th></tr>"
|
|
405
437
|
# Category rows link via the same search-filter mechanism as the OWASP
|
|
@@ -411,13 +443,20 @@ module Scryer
|
|
|
411
443
|
perf_row = summary_row("Performance", perf_counts, filter_term: "performance")
|
|
412
444
|
style_row = summary_row("Style", style_counts, filter_term: "style")
|
|
413
445
|
# The Total row has no single category tag to filter by (it's the sum
|
|
414
|
-
# across all
|
|
446
|
+
# across all four), so its per-severity cells link straight to the
|
|
415
447
|
# matching #sev-* heading instead — same anchors, same precision, as
|
|
416
|
-
# the severity distribution chart at the top of the report.
|
|
448
|
+
# the severity distribution chart at the top of the report. Same
|
|
449
|
+
# caveat that chart's own comment already notes: #sev-* only groups
|
|
450
|
+
# security/performance/style findings, so a reader following this link
|
|
451
|
+
# for a severity that's only present via a dependency finding won't
|
|
452
|
+
# see it highlighted there — still the right destination for the bulk
|
|
453
|
+
# of what's counted, and dependency findings are one section away via
|
|
454
|
+
# the Dependency audit row's own link just below.
|
|
417
455
|
total_row = summary_row("Total", total_counts, css_class: "total", severity_anchors: true)
|
|
418
456
|
dup_row = "<tr><th>Duplicate code</th><td colspan=\"#{SEVERITY_ORDER.size}\">—</td>" \
|
|
419
457
|
"<td><a class=\"jump-link\" href=\"#duplicates\">#{duplicate_groups.size} group(s)</a></td></tr>"
|
|
420
|
-
|
|
458
|
+
deps_cells = SEVERITY_ORDER.map { |s| "<td>#{deps_counts[s]}</td>" }.join
|
|
459
|
+
deps_row = "<tr><th>Dependency audit</th>#{deps_cells}" \
|
|
421
460
|
"<td><a class=\"jump-link\" href=\"#dependency-audit\">#{dependency_findings.size} finding(s)</a></td></tr>"
|
|
422
461
|
|
|
423
462
|
"<table class=\"summary\">#{header}#{sec_row}#{perf_row}#{style_row}#{dup_row}#{deps_row}#{total_row}</table>"
|
data/lib/scryer/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scryer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ram Laxman Yadav
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rake
|
|
@@ -56,7 +55,6 @@ description: |
|
|
|
56
55
|
rewritten against your actual code by any LLM you configure. Reports in JSON, self-contained
|
|
57
56
|
HTML, CSV, or SARIF (for GitHub Code Scanning). Zero runtime dependencies beyond Ruby's own
|
|
58
57
|
stdlib.
|
|
59
|
-
email:
|
|
60
58
|
executables:
|
|
61
59
|
- scryer
|
|
62
60
|
extensions: []
|
|
@@ -148,7 +146,6 @@ metadata:
|
|
|
148
146
|
changelog_uri: https://github.com/ramlaxmanyadav/scryer/blob/main/CHANGELOG.md
|
|
149
147
|
bug_tracker_uri: https://github.com/ramlaxmanyadav/scryer/issues
|
|
150
148
|
rubygems_mfa_required: 'true'
|
|
151
|
-
post_install_message:
|
|
152
149
|
rdoc_options: []
|
|
153
150
|
require_paths:
|
|
154
151
|
- lib
|
|
@@ -163,8 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
163
160
|
- !ruby/object:Gem::Version
|
|
164
161
|
version: '0'
|
|
165
162
|
requirements: []
|
|
166
|
-
rubygems_version: 3.
|
|
167
|
-
signing_key:
|
|
163
|
+
rubygems_version: 3.6.9
|
|
168
164
|
specification_version: 4
|
|
169
165
|
summary: An all-in-one security auditing and static analysis tool for Ruby on Rails
|
|
170
166
|
applications — tells you what to fix first, across security, performance, dependencies,
|