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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 688af786218666d530e02b72ec75afbd2e64fb811cda54e7bb732531bb69af1d
4
- data.tar.gz: ffb6e7eba883bfaa1165462db1278a59e2cbe97e0d16adbf19b44fe25ac27293
3
+ metadata.gz: 47762d5ce6faa0ec795a2f4966f61dee19625e4dd803e0219a0f47440c1d9892
4
+ data.tar.gz: 9d892e071867b5ff6e2023bd0dfa0f1e98c4b7730b4652ad9d1a5bdaf204eb6b
5
5
  SHA512:
6
- metadata.gz: 02c51d96853d124cdf7c33b5072f29e35fede7d1df080b41131429daa1e251d4f807043ad1eb51de081d49486b08d7932a34925671895332dec22c88dcba37e7
7
- data.tar.gz: ce8d5f7770abff15b0d8a37979982a3ba1a11a07f44f217d83fb0277e038029b44cce8fb7623ef96c2cbd5f29a30efec3084015c7a99ae751d85e16b64eb376a
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), combined via exponential decay from 100 rather than linear subtraction — one
107
- critical finding visibly moves the score (100 ~86) without a handful of findings driving any
108
- real app straight to a hard-clamped 0.
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
- - **Performance and code-quality findings aren't part of it.** This is a *security* score only
118
- security and dependency findings count. A slow app with clean security findings still scores
119
- well here; check the "Performance"/"Code Quality" rows in the summary box separately for that.
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
- version = Regexp.last_match(2)
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
- # only (performance/code-quality findings aren't security risk, so
111
- # they're deliberately excluded; a slow app doesn't lower a *security*
112
- # score). Deliberately NOT normalized by files-scanned or lines of code:
113
- # it reflects this scan's absolute finding exposure, so it's meaningful
114
- # for tracking one project's trend over time (does the next scan score
115
- # higher or lower), not for comparing two differently-sized codebases
116
- # against each other a bigger app with the same finding *density* will
117
- # naturally score lower here, and that's a documented limitation, not a
118
- # bug.
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
+ # placesee 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) — same
127
- # philosophy as SARIF's `rank` (see sarif_rank above), computed once here
128
- # for a single scan-level number instead of per-result.
129
- SCORE_SEVERITY_WEIGHT = { "critical" => 15, "warning" => 6, "info" => 1 }.freeze
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
- findings = (@result.security_findings + @dependency_findings).map(&:to_h)
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) * (SCORE_CONFIDENCE_WEIGHT[f["confidence"]] || 0.7)
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), weighted by severity and this rule's confidence. Not normalized by app
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
- total_counts = SEVERITY_ORDER.each_with_object({}) { |s, acc| acc[s] = sec_counts[s] + perf_counts[s] + style_counts[s] }
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 three), so its per-severity cells link straight to the
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
- deps_row = "<tr><th>Dependency audit</th><td colspan=\"#{SEVERITY_ORDER.size}\">—</td>" \
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>"
@@ -1,3 +1,3 @@
1
1
  module Scryer
2
- VERSION = "1.2.0"
2
+ VERSION = "1.2.1"
3
3
  end
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.0
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: 2026-08-16 00:00:00.000000000 Z
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.5.11
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,