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 +4 -4
- data/CHANGELOG.md +165 -4
- data/README.md +74 -1027
- data/docs/architecture.md +268 -0
- data/docs/contributing.md +42 -0
- data/docs/fix-mode.md +364 -0
- data/docs/rails-integration.md +162 -0
- data/docs/rules.md +310 -0
- data/docs/usage.md +301 -0
- data/lib/scryer/ai_fix_suggester.rb +9 -3
- data/lib/scryer/cli.rb +524 -36
- data/lib/scryer/colorizer.rb +56 -0
- data/lib/scryer/dependency_fixer.rb +96 -0
- data/lib/scryer/fix_runner.rb +161 -0
- data/lib/scryer/fix_verifier.rb +97 -10
- data/lib/scryer/mechanical_fixer.rb +288 -0
- data/lib/scryer/scanner.rb +25 -12
- data/lib/scryer/version.rb +1 -1
- data/lib/scryer.rb +17 -3
- data/lib/tasks/scryer.rake +335 -21
- metadata +35 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 688af786218666d530e02b72ec75afbd2e64fb811cda54e7bb732531bb69af1d
|
|
4
|
+
data.tar.gz: ffb6e7eba883bfaa1165462db1278a59e2cbe97e0d16adbf19b44fe25ac27293
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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.
|