scryer 1.1.1 → 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: b897d15fb27a51e0bce5737d3a03c6130ee398b6f17920ee7f065781333894d6
4
- data.tar.gz: 91c26cdd7faa290f83595921aa9c017e9ed1a66cbdb000a8341307eb707312d5
3
+ metadata.gz: 47762d5ce6faa0ec795a2f4966f61dee19625e4dd803e0219a0f47440c1d9892
4
+ data.tar.gz: 9d892e071867b5ff6e2023bd0dfa0f1e98c4b7730b4652ad9d1a5bdaf204eb6b
5
5
  SHA512:
6
- metadata.gz: 0a50d223f834e04699a0f0f4a985a514b4e2fc8aad2638fe1950742fd57ee81a39314dddd2deacbfe750af5be36e923735705c2b09259d79bdb27c8567399ce0
7
- data.tar.gz: '07786b335ebcbd4abc812c31f2e92255ef26019300a2bb14929976a7171d52e293d575850e5f4be0ea2d20f9541a9ca51041c167ab383f89ae45bf11380d8c1e'
6
+ metadata.gz: 51b48a7aad2906b43d3d86a0a82a19275c7c6fb19860ec23da7671f765648d47ca29454e88d9ad959b4b8870ba91d1ed5b5590c0651306f8560889baa3acee0f
7
+ data.tar.gz: a8d456858bca651e77ceb583304645f5f69ae827c6669c09efb9caf88eb707de72f66177faea7f935415095b72874c282c762b4a5db78252be10a90155708aa6
data/CHANGELOG.md CHANGED
@@ -5,12 +5,204 @@ 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
+
39
+ ## [1.2.0] - 2026-08-16
40
+
41
+ - Added `c.detect_duplicates` (`Scryer::Configuration`, default `true`): duplicate-code detection
42
+ (method/query/cache-key similarity across models, controllers, helpers, and concerns) previously
43
+ ran unconditionally with no way to turn it off, since it isn't a `Scryer::Rule` and has no
44
+ `rule_id` for `skip_rules` to address. Set `c.detect_duplicates = false` in
45
+ `config/initializers/scryer.rb` to disable it project-wide, or override for a single run with
46
+ the standalone executable's new `--no-duplicates` flag or the rake task's new `noduplicates`
47
+ bracket token (`rails 'scryer:report[html,noduplicates]'`, alongside the existing `nodeps`) —
48
+ either can only turn detection off, never back on over an already-`false` config. `scryer
49
+ verify`/`scryer fix`/the fix-verification re-scan now always skip this pass internally too (none
50
+ of them ever read `duplicate_groups`), a small unconditional performance improvement independent
51
+ of this new config. Covered by new tests in `test/scanner_test.rb` and `test/cli_scan_test.rb`.
52
+ - The standalone `scryer`/`scryer fix` executable no longer requires `-r`/`--require` to pick up
53
+ an `ai_client` — if it's omitted entirely, `config/initializers/scryer.rb` under `--path` is now
54
+ auto-required when it exists (the same file a Rails app booted against the project would already
55
+ autoload). This was the single most common cause of "AI is configured but nothing gets fixed"
56
+ seen in real usage: forgetting `-r` silently left `ai_client` `nil`, indistinguishable from no
57
+ client being configured at all. An explicit `-r` always wins and is never overridden; a one-line
58
+ notice is printed whenever auto-discovery fires so it's never a silent behavior switch. Covered
59
+ by new tests in `test/cli_fix_test.rb`.
60
+ - `scryer fix`/`rails scryer:fix`'s interactive per-finding review prompt is now a numbered menu
61
+ (`1) Yes 2) Skip 3) Yes to all remaining 4) Cancel`) instead of `[y]es/[n]o/[a]ll/[s]kip`
62
+ letters — numeric choices only, and a blank or unrecognized answer re-prompts instead of
63
+ defaulting to "no." New: option 4, "Cancel," stops the review immediately — every remaining
64
+ candidate is marked skipped without even asking the AI client or mechanical fixer for a rewrite
65
+ (previously "skip all remaining" still ran an AI call for every not-yet-reviewed finding before
66
+ discarding the result). `Scryer::FixRunner.apply`'s `confirm:` callback can now return the symbol
67
+ `:cancel` to trigger this; a new `:cancelled` status is yielded to the progress block. `--yes`
68
+ (`SCRYER_FIX_YES=1`) remains the one-shot non-interactive way to fix everything in a single
69
+ command. Covered by new tests in `test/cli_fix_test.rb` and `test/fix_runner_test.rb`.
70
+ - Added `scryer fix --deps` (`SCRYER_FIX_DEPS=1 rails scryer:fix`): the 24-ish dependency findings
71
+ from `scryer --audit-deps`/`rails scryer:audit_dependencies` were previously report-only — nothing
72
+ in `scryer fix` ever touched them, since a `DependencyAudit::Finding` has no `.line`/`.file`/
73
+ `.rule_id` for `FixRunner`/`MechanicalFixer`/`FixVerifier` to target. New `Scryer::DependencyFixer`
74
+ runs `bundle update GEM --conservative` once per distinct vulnerable gem that has a published
75
+ patched version, then re-queries OSV.dev for that gem to confirm the bump actually cleared every
76
+ advisory before reporting it fixed. Gems with no fixed version yet, insecure `git://`/`http://`
77
+ sources, a past-EOL Ruby version, and an exposed `config/master.key` are always left for manual
78
+ review — none of those are a gem-version bump. Supports `--dry-run`. Covered by
79
+ `test/dependency_fixer_test.rb` and `test/cli_fix_deps_test.rb`.
80
+ - `scryer fix`/`rails scryer:fix` now try the configured `ai_client` *before* `Scryer::MechanicalFixer`
81
+ for every rule, including the six the mechanical fixer can already handle on its own
82
+ (`frozen_string_literal`, `sql_injection`'s sole-argument case, and the four config-flip rules) —
83
+ previously the mechanical fixer always won when it applied, even with an `ai_client` configured.
84
+ The mechanical fixer is now the fallback: it only runs when AI isn't configured, declined to
85
+ produce anything usable, raised, or its rewrite didn't verify. Same verify-then-write safety gate
86
+ either way. Fixed `FixVerifier.apply_line_replacement` to restore a line's original indentation
87
+ when an AI reply's `AFTER:` block drops it (confirmed against a real AI-generated fix in
88
+ production that came back flush against the left margin instead of matching the original 8-space
89
+ indent) — not a correctness bug, but a fix that silently strips indentation looks nothing like
90
+ something a developer would actually commit. Covered by new tests in `test/fix_runner_test.rb`.
91
+ - Fixed: `scryer fix`/`rails scryer:fix` silently swallowed an `ai_client` that raised an exception
92
+ (a bad API key, a network timeout, a malformed response) — it looked identical to "no ai_client
93
+ configured at all," both showing up as a generic `Skipped (needs manual review)` with the rule's
94
+ own default `suggested_fix` text, no way to tell which one actually happened. `AiFixSuggester.enhance!`
95
+ now accepts an `on_error:` callback (the failure still never propagates or breaks the run);
96
+ `FixRunner.apply` surfaces it as a new `:ai_error` status distinct from `:skipped`, and both
97
+ `scryer fix` and `rails scryer:fix` now print the actual exception class and message
98
+ (`Skipped (AI client error): mass_assignment — ...` / `RuntimeError: 401 Unauthorized`) instead
99
+ of leaving you to guess. Covered by new tests in `test/fix_runner_test.rb` and `test/cli_fix_test.rb`.
100
+ - `scryer verify` no longer requires both `--rule` and `--file` — omit either or both to broaden
101
+ scope: `--file PATH` alone checks every rule against that one file; `--rule ID` alone checks the
102
+ whole project for that one rule; neither flag checks the whole project against every rule (same
103
+ security/performance/style scope `--baseline` uses; duplicate-code groups excluded for the same
104
+ reason `--baseline` excludes them — they don't fit the same single-finding shape). The original
105
+ `--rule ID --file PATH` case is unchanged. The three broader cases print grouped by severity
106
+ (critical first) with each finding's message truncated to a scannable length, instead of a wall
107
+ of full-length messages — full untruncated detail is what a real `scryer -o report.json`/
108
+ `report.html` is for. Covered by `test/cli_verify_test.rb` (10 tests).
109
+ - Colored console output: the summary box, top priorities, and `scryer fix`/`scryer verify` output
110
+ are colored automatically at a real terminal (severity labels, the security score's letter
111
+ grade, fixed/skipped status lines) via a new hand-rolled `Scryer::Colorizer` — no new gem
112
+ dependency, consistent with the zero-runtime-dependency design. Off automatically when it would
113
+ be wrong to color (piped/redirected output, [`NO_COLOR`](https://no-color.org) set, `TERM=dumb`);
114
+ `--color`/`--no-color` (CLI) or `SCRYER_COLOR`/`SCRYER_NO_COLOR` (rake) force it either way,
115
+ taking precedence over `NO_COLOR`/auto-detection (an explicit per-invocation flag is more
116
+ specific than a session-wide env var — the same precedence ripgrep/eslint use). Covered by a new
117
+ `test/colorizer_test.rb` (13 tests).
118
+ - New `Scryer::MechanicalFixer`: deterministic, no-AI fixes for `frozen_string_literal`,
119
+ `sql_injection` (sole-argument case only), `force_ssl_disabled`, `insecure_cookie_serializer`,
120
+ `weak_session_cookie`, and `security_headers_disabled` (plain-assignment case only) — the narrow
121
+ set of rules where there's exactly one correct rewrite, so `scryer fix`/`rails scryer:fix` no
122
+ longer require an `ai_client` to fix these. A mechanical fixer always wins over the AI path when
123
+ one applies; falls through to `ai_client` (or manual review) otherwise. Flows through the exact
124
+ same verify-then-write pipeline as an AI-generated fix — nothing here is trusted more than an
125
+ LLM's guess would be. Fixes correctly strip quote marks directly hugging a `#{...}` interpolation
126
+ (the common `"id = '#{x}'"` manual-SQL-quoting style) rather than leaving them around the new `?`
127
+ placeholder — naively swapping just the interpolation would produce `where("id = '?'", x)`, which
128
+ double-quotes the bound value and silently breaks the query while still looking "verified" (the
129
+ rule only checks for interpolation, not query correctness) — covered by a dedicated regression
130
+ test. `scryer fix`'s usage error is now only raised when there's genuinely nothing an invocation
131
+ could fix (no `ai_client` AND nothing matched has a built-in fixer). Covered by
132
+ `test/mechanical_fixer_test.rb` (13 tests) plus updated coverage in `test/fix_runner_test.rb` and
133
+ `test/cli_fix_test.rb`.
134
+ - Fix mode: `scryer fix` (and `rails scryer:fix`) — the third leg of scan → fix → verify. Requires
135
+ an `ai_client`; asks it for a rewrite of every qualifying finding and **writes to the real file**
136
+ the ones `Scryer::FixVerifier` independently confirms actually clear the finding (re-parses the
137
+ file with that one line replaced and re-runs the flagged rule against it) — the same safety gate
138
+ AI-verified remediation already used for `fix_verified`, just now acting on it instead of only
139
+ reporting it. Anything not verified is left alone and listed as needing manual review, same as a
140
+ normal report. `--dry-run` previews without writing; `--rule`/`--file` scope to specific findings.
141
+ New `Scryer::FixRunner` module holds the apply/re-verify loop shared by both the CLI and rake
142
+ task (multiple fixes in one file are applied highest-line-number-first, so an earlier fix
143
+ expanding into several lines can't invalidate a not-yet-processed finding's line number earlier
144
+ in the same file); a final whole-project re-scan after every write confirms nothing regressed.
145
+ Covered by a new `test/fix_runner_test.rb` (dry-run never writes, real run writes only verified
146
+ fixes and skips the rest, the line-shift-ordering scenario specifically, and the re-verify step)
147
+ — 5 new tests, all passing alongside the existing suite.
148
+ - Fixed: `scryer fix`'s final re-scan verify step was silently skipped whenever every candidate
149
+ fixed successfully (it only ran when something was left for manual review) — the CLI never
150
+ actually confirmed its own writes on the common, all-clean path. `rails scryer:fix` never had
151
+ this bug. Caught by new CLI-level tests (`test/cli_fix_test.rb`) exercising `Scryer::CLI#run_fix`
152
+ directly, which the original `FixRunner`-only tests didn't cover.
153
+ - `scryer fix`/`rails scryer:fix`: pick specific findings by number when `--rule`/`--file` still
154
+ leave more than one candidate — `--list` (`SCRYER_FIX_LIST=1` for the rake task) prints a stable,
155
+ numbered candidate list without calling the AI client or writing anything; `--number 2` /
156
+ `--number 1,3` (`SCRYER_FIX_NUMBERS=...`) fixes only those positions.
157
+ - Fixed: `FixVerifier` marked a fix as "not verified" whenever the same rule fired *anywhere else in
158
+ the file*, instead of checking whether the specific offending code it targeted was gone — so a
159
+ file with two separate findings of the same rule (e.g. two `sql_injection` occurrences) could
160
+ never have either one verified, even when both individual fixes were completely correct. Now
161
+ matched by rule_id + `code_snippet`, the same identity `Baseline` fingerprints already use, so
162
+ each finding verifies independently of any others sharing its rule_id. Found by dry-running
163
+ `scryer fix --rule sql_injection` against a real app with two such findings in one file; covered
164
+ by a new regression test in `test/fix_runner_test.rb`.
165
+ - `scryer fix`/`rails scryer:fix` now print a live progress line for each finding as it's resolved
166
+ ("Fixed: ...", "Would fix: ...", "Skipped (needs manual review): ...") instead of only the final
167
+ summary once every candidate is done — each `Fixed`/`Would fix` line includes a short 1-2
168
+ sentence explanation of the change, pulled from the AI's own reply (`Scryer::FixRunner.explain`)
169
+ rather than only a bare rule_id/file/line. The final summary and re-scan verify step are
170
+ unchanged.
171
+ - `frozen_string_literal` is now opt-in for an unscoped `scryer fix`/`rails scryer:fix` sweep (no
172
+ `--rule`/rule_id given) — it's a cosmetic, `info`-severity finding that would otherwise touch
173
+ nearly every file in a project. At a real terminal it asks once, up front; non-interactively
174
+ (CI, piped stdin) it's excluded with a one-line notice, discoverable via
175
+ `--rule frozen_string_literal` / `SCRYER_FIX_YES=1`. `Scryer::MechanicalFixer.fix_frozen_string_literal`
176
+ also now actually analyzes the file before recommending the fix — it declines (falls back to
177
+ manual review) if it finds a plausible in-place string mutation (a bang-method or `<<` on a
178
+ string literal, or on a local variable assigned one earlier in the file), since freezing a file's
179
+ literals would otherwise raise `FrozenError` at runtime while still looking "verified" by
180
+ Scryer's own narrow check (it only looks for the magic comment's absence, not query/mutation
181
+ correctness).
182
+ - `scryer fix`/`rails scryer:fix`: per-finding interactive review. At a real terminal (not
183
+ `--dry-run`/`--yes`/`SCRYER_FIX_YES`), every independently-verified fix — rule, message,
184
+ explanation, and the actual `AFTER:` rewrite — is shown and confirmed one at a time before being
185
+ written, instead of silently applying everything that verified clean: `[y]es` / `[n]o` / `[a]ll
186
+ remaining` / `[s]kip all remaining`. Non-interactive runs keep applying everything automatically,
187
+ same as before this existed. `Scryer::FixRunner.apply` gained an optional `confirm:` callback
188
+ (model-layer, testable without a real terminal); the CLI/rake task supply the actual prompt. This
189
+ replaces the old upfront "pick which numbered findings to fix" prompt that used to appear
190
+ automatically in an interactive terminal with more than one match and no `--number` — the new
191
+ per-finding review covers the same need with more information (you see the actual diff) and less
192
+ friction (no numbers to type); `--number`/`--list` remain for scripted/non-interactive scoping.
193
+ - Fixed: every `Scryer::MechanicalFixer` rewrite was built from `finding.code_snippet`, which
194
+ `Ast.source_line` deliberately `.strip`s for display in a report — so a mechanically-fixed line
195
+ silently lost its original indentation on every real write (not a syntax break, since Ruby
196
+ doesn't require indentation, but a real diff-correctness bug no prior test caught, since they all
197
+ used `assert_includes`, which doesn't care about leading whitespace). Fixers now read the actual
198
+ on-disk line via a new `raw_line` helper; `security_headers_disabled`'s comment-out rewrite also
199
+ now preserves the original line's indentation instead of jumping to column 0. Covered by a new
200
+ regression test in `test/fix_runner_test.rb` that checks the exact written line, not just a
201
+ substring.
202
+
8
203
  ## [1.1.1] - 2026-08-14
9
204
 
10
- - Docs-only release: 1.1.0's bundled README.md/CHANGELOG.md/benchmark/README.md referenced the
11
- real internal Rails app this gem was verified against by an identifying name and used its actual
12
- controller/file paths in example output. Replaced with an anonymized placeholder ("acme-app") and
13
- generic file/controller names — the finding counts, severities, rule IDs, and line numbers in
205
+ - Docs-only, generic file/controller names the finding counts, severities, rule IDs, and line numbers in
14
206
  every example are unchanged (still real, still exactly as scanned), only the identifying labels
15
207
  are redacted. 1.1.0 is yanked from RubyGems for this reason; no code changed between 1.1.0 and
16
208
  1.1.1.