scryer 1.0.0 → 1.1.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 +253 -0
- data/README.md +459 -41
- data/lib/generators/scryer/USAGE +10 -2
- data/lib/generators/scryer/templates/scryer_initializer.rb +15 -0
- data/lib/scryer/ai_fix_suggester.rb +29 -9
- data/lib/scryer/ast.rb +26 -0
- data/lib/scryer/authorization_watcher.rb +156 -0
- data/lib/scryer/baseline.rb +75 -0
- data/lib/scryer/cli.rb +189 -3
- data/lib/scryer/finding.rb +6 -0
- data/lib/scryer/fix_verifier.rb +82 -0
- data/lib/scryer/minitest.rb +48 -0
- data/lib/scryer/performance_rules/inefficient_save_loop_rule.rb +32 -0
- data/lib/scryer/performance_rules/missing_pagination_rule.rb +1 -0
- data/lib/scryer/performance_rules/n_plus_one_query_rule.rb +1 -0
- data/lib/scryer/performance_rules/unbounded_table_scan_rule.rb +1 -0
- data/lib/scryer/report_renderer.rb +539 -46
- data/lib/scryer/rspec.rb +55 -0
- data/lib/scryer/rule.rb +22 -2
- data/lib/scryer/rules/action_cable_forgery_protection_rule.rb +3 -0
- data/lib/scryer/rules/active_storage_inline_disposition_rule.rb +3 -0
- data/lib/scryer/rules/active_storage_missing_content_type_validation_rule.rb +3 -0
- data/lib/scryer/rules/authentication_bypass_rule.rb +30 -7
- data/lib/scryer/rules/command_injection_rule.rb +3 -0
- data/lib/scryer/rules/consider_all_requests_local_rule.rb +51 -0
- data/lib/scryer/rules/cors_misconfiguration_rule.rb +51 -20
- data/lib/scryer/rules/csrf_protection_rule.rb +60 -11
- data/lib/scryer/rules/force_ssl_rule.rb +3 -0
- data/lib/scryer/rules/graphql_missing_query_limits_rule.rb +31 -0
- data/lib/scryer/rules/hardcoded_basic_auth_rule.rb +3 -0
- data/lib/scryer/rules/hardcoded_secret_key_base_rule.rb +3 -0
- data/lib/scryer/rules/hardcoded_secret_rule.rb +3 -0
- data/lib/scryer/rules/host_authorization_disabled_rule.rb +50 -0
- data/lib/scryer/rules/idor_rule.rb +63 -9
- data/lib/scryer/rules/insecure_cookie_serializer_rule.rb +3 -0
- data/lib/scryer/rules/job_raw_params_rule.rb +40 -7
- data/lib/scryer/rules/jwt_insecure_rule.rb +3 -0
- data/lib/scryer/rules/mass_assignment_rule.rb +32 -5
- data/lib/scryer/rules/missing_authorization_rule.rb +103 -0
- data/lib/scryer/rules/missing_policy_scope_rule.rb +134 -0
- data/lib/scryer/rules/open_redirect_rule.rb +3 -0
- data/lib/scryer/rules/path_traversal_rule.rb +22 -1
- data/lib/scryer/rules/security_headers_rule.rb +3 -0
- data/lib/scryer/rules/sql_injection_rule.rb +3 -0
- data/lib/scryer/rules/ssrf_rule.rb +67 -13
- data/lib/scryer/rules/unsafe_deserialization_rule.rb +3 -0
- data/lib/scryer/rules/verbose_production_log_level_rule.rb +53 -0
- data/lib/scryer/rules/weak_crypto_rule.rb +37 -2
- data/lib/scryer/rules/weak_session_cookie_rule.rb +3 -0
- data/lib/scryer/rules/xss_unsafe_html_rule.rb +41 -0
- data/lib/scryer/style_rules/frozen_string_literal_rule.rb +1 -0
- data/lib/scryer/version.rb +1 -1
- data/lib/scryer.rb +15 -0
- data/lib/tasks/scryer.rake +117 -4
- metadata +24 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b897d15fb27a51e0bce5737d3a03c6130ee398b6f17920ee7f065781333894d6
|
|
4
|
+
data.tar.gz: 91c26cdd7faa290f83595921aa9c017e9ed1a66cbdb000a8341307eb707312d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a50d223f834e04699a0f0f4a985a514b4e2fc8aad2638fe1950742fd57ee81a39314dddd2deacbfe750af5be36e923735705c2b09259d79bdb27c8567399ce0
|
|
7
|
+
data.tar.gz: '07786b335ebcbd4abc812c31f2e92255ef26019300a2bb14929976a7171d52e293d575850e5f4be0ea2d20f9541a9ca51041c167ab383f89ae45bf11380d8c1e'
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,259 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Format loosely follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [1.1.1] - 2026-08-14
|
|
9
|
+
|
|
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
|
|
14
|
+
every example are unchanged (still real, still exactly as scanned), only the identifying labels
|
|
15
|
+
are redacted. 1.1.0 is yanked from RubyGems for this reason; no code changed between 1.1.0 and
|
|
16
|
+
1.1.1.
|
|
17
|
+
|
|
18
|
+
## [1.1.0] - 2026-08-14
|
|
19
|
+
|
|
20
|
+
- `Scryer::AuthorizationWatcher` (`require "scryer/authorization_watcher"`, opt-in): a runtime
|
|
21
|
+
companion to the static `idor`/`missing_authorization`/`missing_policy_scope` rules. Registers
|
|
22
|
+
one `after_action` and checks Pundit's `pundit_policy_authorized?`/`pundit_policy_scoped?` and
|
|
23
|
+
CanCanCan's `@_authorized` (both real, verified-by-reading-the-actual-gem-source signals, not
|
|
24
|
+
guessed) to catch a write action (`create`/`update`/`destroy`, or any `POST`/`PUT`/`PATCH`/
|
|
25
|
+
`DELETE`) that completed successfully with neither library's authorization check actually
|
|
26
|
+
invoked during that request — a live, much lower-false-positive-risk signal than the static
|
|
27
|
+
rules, which can only ever see "no call is visible in this file's source," not whether it ran.
|
|
28
|
+
Silently a no-op on apps using neither library (checked per-request, not just at boot). Verified
|
|
29
|
+
end-to-end against real `ActionController::Base`/`ActionController::API` + real installed
|
|
30
|
+
`pundit`/`cancancan` gems (not mocked) — 7 scenarios (authorized, unauthorized,
|
|
31
|
+
GET-to-a-write-action-name, plain read action, both libraries, neither library present, an
|
|
32
|
+
API-only base class) all behave as designed.
|
|
33
|
+
- Accuracy benchmark (`benchmark/`): a hand-labeled corpus of `vulnerable`/`safe` Ruby/Rails
|
|
34
|
+
snippets per rule, run through the real `Rule#scan` path, reporting measured precision/recall/F1
|
|
35
|
+
per rule and in aggregate (`rake benchmark` / `ruby benchmark/run.rb`) — replacing "expect some
|
|
36
|
+
false positives, that's normal for this class of tool" with an actual number. Explicitly framed
|
|
37
|
+
as a synthetic, self-authored corpus, not an independent benchmark — see `benchmark/README.md`'s
|
|
38
|
+
honesty caveat. Directly caught a real bug while building it: `mass_assignment` never fired on
|
|
39
|
+
`@record.update(params[...])` (an instance-variable receiver) — only a bare constant or implicit
|
|
40
|
+
receiver was ever recognized as "likely a model" — even though that's at least as common a
|
|
41
|
+
real-world shape as `Model.new(...)`. Fixed (`IVAR_RECEIVER_METHODS`, scoped to `update`/
|
|
42
|
+
`update!`/`assign_attributes`/`attributes=` only, not `new`/`create`/`create!`, to avoid matching
|
|
43
|
+
`Hash#update`-style receivers) and verified against the full test suite and a real external app
|
|
44
|
+
with zero regressions. Now populated for all 36 registered rules, 180 hand-labeled samples total:
|
|
45
|
+
**82 TP / 3 FN / 1 FP / 94 TN — 98.8% precision, 96.5% recall, 97.6% F1** (`rake benchmark`). The
|
|
46
|
+
two rules scoring below 100% (`graphql_missing_query_limits`, `unbounded_table_scan`) are gaps
|
|
47
|
+
already disclosed in each rule's own top comment before this benchmark existed — this just
|
|
48
|
+
measured them for the first time instead of only asserting them.
|
|
49
|
+
- Test suite: Scryer's own rules now have automated coverage — `rake test` (Minitest, no new
|
|
50
|
+
runtime dependency) runs a bad/clean fixture pair against every one of the 36 registered rules,
|
|
51
|
+
plus a completeness check that fails if a new rule ships with no fixture. Previously this gem had
|
|
52
|
+
zero automated tests of its own; every rule change was verified by hand against ad hoc fixtures
|
|
53
|
+
and a real external Rails app each time. 37/37 passing, 0 rule bugs found while building the
|
|
54
|
+
fixtures — every rule's fixture behaved exactly as its own doc comment described.
|
|
55
|
+
- Testing: two new opt-in files let a host app assert its own scan stays clean as part of its
|
|
56
|
+
normal test suite instead of only via a separate `scryer` run — `require "scryer/rspec"`
|
|
57
|
+
(`have_no_critical_findings`, `have_no_findings_for(rule_id)` matchers) and
|
|
58
|
+
`require "scryer/minitest"` (`Scryer::MinitestAssertions#assert_no_critical_scryer_findings`/
|
|
59
|
+
`#assert_no_scryer_findings_for`). Neither loads automatically with the gem (RSpec/Minitest stay
|
|
60
|
+
out of the runtime dependency list). New `Scryer.scan(root:)` convenience method backs both,
|
|
61
|
+
reusing `c.dirs`/`c.skip_rules` from the host app's own configuration.
|
|
62
|
+
- `scryer verify --rule RULE_ID --file PATH`: a new CLI subcommand that re-parses one file and
|
|
63
|
+
re-runs one rule against it, independent of a full scan — confirms a specific fix actually
|
|
64
|
+
cleared the finding it targeted (exit 0 clean / 1 still firing), meant to run right after
|
|
65
|
+
applying a fix by hand or reviewing an AI-suggested one. `--list-rules` prints every known
|
|
66
|
+
rule_id.
|
|
67
|
+
- AI-verified remediation: when an `ai_client` is configured, every AI-rewritten `suggested_fix` is
|
|
68
|
+
now automatically re-checked — the rewritten line is substituted into an in-memory copy of the
|
|
69
|
+
real file (nothing written to disk) and the same rule is re-run against it. Each finding gets a
|
|
70
|
+
new `fix_verified` field (`true`/`false`/`nil`; JSON/HTML only — CSV/SARIF unchanged), and the
|
|
71
|
+
HTML report shows a green "AI fix verified" or red "AI fix NOT verified" line under the suggested
|
|
72
|
+
fix. Required extending the AI prompt to ask for a machine-parseable `AFTER:` code block as the
|
|
73
|
+
last thing in the model's reply (`Scryer::FixVerifier` parses it); shares its core re-scan logic
|
|
74
|
+
with `scryer verify` above.
|
|
75
|
+
- 3 new Rails config-audit rules, all file-scoped to avoid flagging Rails' own development/test
|
|
76
|
+
defaults: `consider_all_requests_local_production` (`config.consider_all_requests_local = true`
|
|
77
|
+
in `config/environments/production.rb` — shows full debug error pages in production; critical,
|
|
78
|
+
CWE-209), `verbose_production_log_level` (`config.log_level = :debug` in production — logs full
|
|
79
|
+
request params and SQL bind values; warning, CWE-532), `host_authorization_disabled`
|
|
80
|
+
(`config.hosts.clear` — disables Rails' Host-header allowlist entirely; any file, warning,
|
|
81
|
+
CWE-350). 31 security rules total now (up from 28), 36 registered rules overall.
|
|
82
|
+
- Generators: the generated `config/initializers/scryer.rb` now shows commented examples for
|
|
83
|
+
`c.skip_rules` and `c.ai_client`, not just `project_name`/`dirs`/`branch` — both were already
|
|
84
|
+
documented in USAGE but missing from the actual template. New `bin/rails scryer:ci` rake task —
|
|
85
|
+
a memorable shortcut for `scryer:report[json,sarif]`'s CI-sensible defaults.
|
|
86
|
+
- HTML report: every count in the Summary table and the "Warnings by type" table now links to the
|
|
87
|
+
matching findings, same click-to-drill-down as the OWASP coverage table and severity chart —
|
|
88
|
+
the Warnings-by-type rule-id column already linked to its rule group, but the count column next
|
|
89
|
+
to it didn't; Summary's category × severity cells (e.g. "Security × Critical") had no existing
|
|
90
|
+
anchor to link to at all, so each finding now carries a category tag (`security`/`performance`/
|
|
91
|
+
`style`, alongside the existing CWE/OWASP/confidence tags) and the search filter was extended to
|
|
92
|
+
match multiple space-separated terms (all required, not a literal multi-word phrase) so a cell
|
|
93
|
+
like that can link with a two-word filter (`security critical`) precisely. Renamed the
|
|
94
|
+
`.owasp-link` CSS class to `.filter-link` throughout, since it's no longer OWASP-specific.
|
|
95
|
+
- HTML report: the severity distribution chart's counts (Critical/Warning/Info, at the very top
|
|
96
|
+
next to the score badge) are now links to the matching section under Findings — same
|
|
97
|
+
click-to-drill-down as the OWASP coverage table's counts. The Findings filter box now also
|
|
98
|
+
applies to Top priorities (a non-matching item hides, same as a non-matching finding elsewhere
|
|
99
|
+
— previously Top priorities never changed at all while filtering), and any finding matched by
|
|
100
|
+
an active filter (typed or via a click-through link) gets a visible highlight, not just "still
|
|
101
|
+
shown" — the point of filtering/linking is to show *which* finding is the target, not just to
|
|
102
|
+
hide the rest.
|
|
103
|
+
- HTML report: each finding now shows its CWE ID, OWASP category, and confidence level as small
|
|
104
|
+
tags (previously this data existed in JSON/CSV/SARIF but was never actually displayed per-finding
|
|
105
|
+
in the HTML report, even though the OWASP coverage scorecard summarized it). The Findings search
|
|
106
|
+
box matches on these tags for free, and the OWASP coverage table's counts are now links that
|
|
107
|
+
jump to Findings with that exact category pre-filled into the search — the aggregate count and
|
|
108
|
+
the underlying findings are one click apart instead of two disconnected views.
|
|
109
|
+
- New **Security Score** — `ReportRenderer#security_score` combines every security + dependency
|
|
110
|
+
finding's severity and confidence into a single 0-100 number and a letter grade (A-F), via
|
|
111
|
+
exponential decay from 100 (not linear subtraction, so a handful of findings doesn't
|
|
112
|
+
hard-clamp every real app to 0). Shown in the console summary (`Security Score: 11/100 (F)`)
|
|
113
|
+
and as a badge + severity distribution chart at the very top of the HTML report. Deliberately
|
|
114
|
+
NOT normalized by app size — see the README's new "Security score" section for what that means
|
|
115
|
+
and doesn't mean. Performance/code-quality findings aren't part of it (this is a *security*
|
|
116
|
+
score).
|
|
117
|
+
- New **rules-clean-rate** — `ReportRenderer#rules_clean_rate` reports "N/M rules clean," a
|
|
118
|
+
rule-level pass rate (how many of the registered checks fired zero findings) distinct from the
|
|
119
|
+
finding-weighted security score — printed alongside it (`Checks: 20/33 rules clean (60.6%)`)
|
|
120
|
+
and in the HTML score panel.
|
|
121
|
+
- HTML report: leads with the score badge + severity chart, then Overview/Summary/OWASP
|
|
122
|
+
coverage, collapsed-by-default "Checks performed" reference tables, and the Findings section —
|
|
123
|
+
Top priorities first, then a new text search/filter box (rule/file/message, plain JS, no page
|
|
124
|
+
reload) above the severity-grouped, accordion-collapsed finding list.
|
|
125
|
+
- Removed the report footer's "heuristic static analysis, not full data-flow/taint analysis —
|
|
126
|
+
review every finding in its surrounding context" line — the caveat itself hasn't changed
|
|
127
|
+
(still true, still documented at length in the README and in individual rule comments), but
|
|
128
|
+
repeating it on every single generated report read as undermining confidence in the tool
|
|
129
|
+
rather than informing. The `Generated by Scryer vX (Ruby Y)` attribution stays.
|
|
130
|
+
- New **baseline mode** — `scryer --save-baseline PATH` snapshots every current finding
|
|
131
|
+
(security/performance/style/dependency) as a set of fingerprints; `scryer --baseline PATH`
|
|
132
|
+
on a later scan reports only findings new since that snapshot (plus how many were fixed),
|
|
133
|
+
so an app with real pre-existing security debt can gate CI on new issues without being forced
|
|
134
|
+
to fix everything on day one. Fingerprints are `rule_id`/`kind` + file/gem + offending source
|
|
135
|
+
text — deliberately NOT tied to line number, so an unrelated edit earlier in the same file
|
|
136
|
+
doesn't make an existing finding look simultaneously new and fixed. Rails/rake equivalent:
|
|
137
|
+
`rails 'scryer:save_baseline[PATH]'` to save, `SCRYER_BASELINE=PATH rails scryer:report` to
|
|
138
|
+
compare (an env var rather than another bracket-arg token, since rake's arg parsing was
|
|
139
|
+
already stretched thin). See `Scryer::Baseline` (`lib/scryer/baseline.rb`) and the README's
|
|
140
|
+
new "Baseline mode" section.
|
|
141
|
+
- Fixed a real, shared precision bug affecting `idor`, `authentication_bypass`, and
|
|
142
|
+
`csrf_protection_disabled`: all three checked whether a class was a controller via a class-name
|
|
143
|
+
extraction that only handled a plain `class Foo` — a namespaced class (`class
|
|
144
|
+
Admin::PostsController`, `class Api::V1::UsersController`) parses its name as a different Ripper
|
|
145
|
+
shape (`:const_path_ref`, not `:const_ref`), so the class name came back `nil` and the whole
|
|
146
|
+
class was silently never examined by any of the three rules. Fixed with a new shared
|
|
147
|
+
`Ast.class_name` helper (verified against `Ripper.sexp` for 2- and 3-level namespacing) and
|
|
148
|
+
re-verified against a real production Rails app ("acme-app" elsewhere in these docs), which uses
|
|
149
|
+
namespacing extensively:
|
|
150
|
+
`idor` findings 2 → 4, `csrf_protection_disabled` 0 → 3, all genuinely namespaced controllers
|
|
151
|
+
that were previously invisible to these checks.
|
|
152
|
+
- New `idor`-adjacent rules, broadening authorization coverage beyond IDOR's specific
|
|
153
|
+
find-with-no-guard pattern:
|
|
154
|
+
- `missing_authorization` — a controller's `create`/`update`/`destroy` action with no
|
|
155
|
+
authorization evidence anywhere in the class (catches write actions that don't call `.find`
|
|
156
|
+
at all, which `idor` can't see).
|
|
157
|
+
- `missing_policy_scope` — a Pundit-using controller's `index` action querying a model
|
|
158
|
+
directly instead of through `policy_scope` — the common "remembered `authorize`, forgot
|
|
159
|
+
`policy_scope`" gotcha, since `index` has no single record for `authorize` to check.
|
|
160
|
+
- Every security rule (all 28) now carries a **CWE ID**, an **OWASP Top 10 (2021) category**, and
|
|
161
|
+
a **confidence level** (`high`/`medium`/`low`, distinct from `severity` — see `Rule.confidence`
|
|
162
|
+
in `lib/scryer/rule.rb`). All three appear on every `Finding` and flow through every report
|
|
163
|
+
format (new JSON fields, new CSV columns, and in SARIF: `external/cwe/cwe-NN` + the OWASP
|
|
164
|
+
category as rule-level `tags`, plus `confidence`/`cwe`/`owasp_category` as per-result
|
|
165
|
+
`properties` and a combined severity+confidence `rank`). Performance/style rules get a
|
|
166
|
+
`confidence` too (no CWE/OWASP — that taxonomy is security-specific). This is Scryer's own
|
|
167
|
+
best-effort categorization for practitioner convenience, not an OWASP-endorsed or independently
|
|
168
|
+
audited mapping — see the README's "What Scryer detects" section for the full caveat. Brakeman
|
|
169
|
+
already tags CWE and reports a confidence level for its own warnings; this isn't a novel
|
|
170
|
+
capability, just a fuller version of something that idea already existed elsewhere (see the
|
|
171
|
+
comparison table's new footnote).
|
|
172
|
+
- New `ReportRenderer#owasp_coverage` — counts security findings per OWASP Top 10 category, shown
|
|
173
|
+
as a console summary block ("OWASP Top 10 (2021) coverage:") and a new HTML report section, a
|
|
174
|
+
direct byproduct of every security rule now carrying an `owasp_category`.
|
|
175
|
+
- New [`action.yml`](action.yml) — a reusable composite GitHub Action wrapping install + scan +
|
|
176
|
+
SARIF upload, correctly ordered so the upload still happens on runs where findings were found
|
|
177
|
+
(previously only documented as a manual YAML snippet in the README's CI/CD section, which still
|
|
178
|
+
works and is kept as the "full control" alternative).
|
|
179
|
+
- New `ReportRenderer#top_risks` — merges every severity-bearing finding from a scan (security,
|
|
180
|
+
performance, code quality, and dependencies) into one list, sorted by severity first and
|
|
181
|
+
category second, so a scan produces one ranked "what to fix first" answer instead of four
|
|
182
|
+
separate category counts. Shown as a "Top priorities" list in the CLI/rake console summary
|
|
183
|
+
(right after the finding-count box) and, in the HTML report, at the top of the Findings section
|
|
184
|
+
— inside it rather than as its own top-level section, so it reads as "here's what to look at
|
|
185
|
+
first within these findings" rather than a fifth thing to scroll past before the findings even
|
|
186
|
+
start. Pure aggregation of data every format already collects — no new detection logic, and
|
|
187
|
+
JSON/CSV/SARIF are unaffected (each is consumed by tooling that does its own sorting).
|
|
188
|
+
- HTML report: the "Checks performed" section's three rule-reference tables (Security/
|
|
189
|
+
Performance/Style — every rule that *can* fire, not what actually did) are now collapsed-by-
|
|
190
|
+
default accordions instead of always-expanded tables, matching the same pattern individual rule
|
|
191
|
+
groups under Findings already used. With 28 security rules alone, showing all three tables
|
|
192
|
+
expanded on load buried Findings and Top priorities under a wall of reference rows nobody asked
|
|
193
|
+
to see first.
|
|
194
|
+
- HTML report: "Duplicate code groups" moved to after "Dependency audit"/"Files that couldn't be
|
|
195
|
+
parsed", as the last section before the footer.
|
|
196
|
+
- Fixed: the "Dependency audit" section's "Expand all"/"Collapse all" controls did nothing —
|
|
197
|
+
dependency findings rendered as plain, always-expanded `.finding` divs, never actually wrapped
|
|
198
|
+
in the `.accordion` markup those buttons operate on. Each of the 24 dependency findings in a
|
|
199
|
+
typical scan is now its own collapsed-by-default accordion, same pattern as Findings/Checks
|
|
200
|
+
performed.
|
|
201
|
+
- Repositioned the README, gemspec, docs site, and llms.txt around this: Scryer's differentiator
|
|
202
|
+
isn't "one command instead of several tools," it's ranking risk *across* security, performance,
|
|
203
|
+
dependencies, and code quality — something none of RuboCop/Brakeman/bundler-audit do even
|
|
204
|
+
within their own domain, let alone across all four. The honest heuristic-vs-taint-analysis
|
|
205
|
+
framing versus Brakeman (and the `idor` false-positive caveat) carries over unchanged from the
|
|
206
|
+
1.0.0 comparison table — this is a reframing of what Scryer adds, not a new claim about
|
|
207
|
+
detection accuracy.
|
|
208
|
+
|
|
209
|
+
- False-positive hardening pass across every rule added in 1.0.0 (10 security rules) plus every
|
|
210
|
+
pre-1.0.0 rule (the original 9 security checks, 4 performance rules, `frozen_string_literal`):
|
|
211
|
+
each rule was tested against realistic *safe* Rails idioms (not just its original single "clean"
|
|
212
|
+
fixture) run through the actual scanner, not reasoned about in the abstract. Real false
|
|
213
|
+
positives found and fixed:
|
|
214
|
+
- `job_raw_params` — `params.dig(:x)` and `params.permit(...)` (Rails' own narrow/allowlisting
|
|
215
|
+
extraction idioms) were treated the same as forwarding the raw `params` hash; now recognized
|
|
216
|
+
as narrow, alongside the existing `params[:x]` exemption.
|
|
217
|
+
- `weak_crypto` — a same-line substring match on "password" false-positived on
|
|
218
|
+
`passwordless_token`-style identifiers and on a defensive comment explicitly disclaiming
|
|
219
|
+
password use (`# cache key, not a password hash`); both now excluded.
|
|
220
|
+
- `cors_misconfiguration` — checked wildcard-origin and `credentials: true` file-wide instead of
|
|
221
|
+
per `allow do...end` block, flagging a common legitimate pattern (a public wildcard-origin API
|
|
222
|
+
and a separate, properly-credentialed partner API in the same initializer). Now scoped per
|
|
223
|
+
block.
|
|
224
|
+
- `idor` — `after_action :verify_authorized`/`:verify_policy_scoped` (Pundit's own safety-net
|
|
225
|
+
callbacks) weren't recognized as guards since they're passed as callback-name arguments, not
|
|
226
|
+
called directly; also added CanCanCan's `load_and_authorize_resource`/`authorize_resource`.
|
|
227
|
+
- `ssrf` — a URL with a fixed, literal host and only a path segment derived from `params`
|
|
228
|
+
(`"https://api.example.com/users/#{params[:id]}"`) was flagged with the same severity/message
|
|
229
|
+
as a fully attacker-controlled URL; now downgraded to a warning with an accurate message
|
|
230
|
+
(still flagged — a dynamic path on a fixed host isn't zero-risk — just not overstated).
|
|
231
|
+
- `path_traversal` — `send_file`/`send_data`'s keyword options (`filename:`, `type:`,
|
|
232
|
+
`disposition:`, which only affect response headers, never the filesystem) were checked for
|
|
233
|
+
`params` the same as the actual path argument.
|
|
234
|
+
- `inefficient_save_loop` — added a `db/seeds.rb`/`db/seeds/**` exemption (a one-time setup
|
|
235
|
+
script, not a request-handling hot path) but the first version compared against the file path
|
|
236
|
+
without accounting for the `./` prefix `Scanner` actually produces, so the exemption silently
|
|
237
|
+
never matched; fixed and re-verified against a real `Scryer::Scanner` run, not a hand-built
|
|
238
|
+
`Rule` instance.
|
|
239
|
+
- `authentication_bypass` — an `only:`-scoped skip (a common, often-legitimate pattern for
|
|
240
|
+
public actions on an authenticated controller) got the same "this is reachable without login"
|
|
241
|
+
wording as an unscoped skip; message now distinguishes the two (detection unchanged — a scoped
|
|
242
|
+
skip is still worth a human glance, just not implied to be a bug on its own).
|
|
243
|
+
- Two disclosed-but-not-fixed gaps, documented in-code rather than silently left: `idor`'s
|
|
244
|
+
already-known imprecision is joined by `graphql_missing_query_limits` (misses limits set via
|
|
245
|
+
an `include`d module rather than the class body directly — real cross-file resolution, not a
|
|
246
|
+
same-file AST tweak) and `active_storage_inline_disposition` (doesn't cross-reference the
|
|
247
|
+
separate content-type-validation rule, since the realistic case spans two different files).
|
|
248
|
+
- Everything else audited (`sql_injection`, `mass_assignment`, `command_injection`,
|
|
249
|
+
`hardcoded_secret`, `unsafe_deserialization`, `xss_unsafe_html`, `csrf_protection`,
|
|
250
|
+
`open_redirect`, `force_ssl_disabled`, `weak_session_cookie`, `insecure_cookie_serializer`,
|
|
251
|
+
`hardcoded_basic_auth`, `security_headers_disabled`, `jwt_insecure_usage`,
|
|
252
|
+
`action_cable_forgery_protection_disabled`, `hardcoded_secret_key_base`,
|
|
253
|
+
`n_plus_one_query`, `missing_pagination`, `unbounded_table_scan`, `frozen_string_literal`)
|
|
254
|
+
was verified clean against the same battery of realistic safe-idiom fixtures — no changes
|
|
255
|
+
needed.
|
|
256
|
+
- Every fix was re-verified against its rule's original true-positive case to confirm nothing
|
|
257
|
+
was silenced along with the false positive.
|
|
258
|
+
|
|
6
259
|
## [1.0.0] - 2026-08-13
|
|
7
260
|
|
|
8
261
|
Seventeen new security rules, a SARIF report format, a Ruby end-of-life check, and a
|