scryer 1.1.1 → 1.2.0

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