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
data/README.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
# Scryer — Ruby & Rails
|
|
2
|
-
|
|
3
|
-
Scryer
|
|
4
|
-
|
|
5
|
-
security, performance,
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
# Scryer — Ruby & Rails Application Security & Risk Auditor
|
|
2
|
+
|
|
3
|
+
Scryer scans a Ruby/Rails codebase and answers a different question than a single-purpose linter
|
|
4
|
+
does: not just "what's wrong," but **what's actually worth fixing first**. It looks across
|
|
5
|
+
security vulnerabilities, performance problems, dependency risk, and code quality in one pass,
|
|
6
|
+
ranks what it finds by severity, and surfaces the handful of issues that matter most at the top of
|
|
7
|
+
every report — with a human-reviewable suggested fix for each one.
|
|
8
|
+
|
|
9
|
+
Brakeman is very good at finding security patterns in Rails apps — years of deep taint/data-flow
|
|
10
|
+
analysis that Scryer's heuristics don't try to match (see the honest comparison below). But a
|
|
11
|
+
security pattern match in isolation doesn't tell you whether it's the most urgent thing on your
|
|
12
|
+
plate this week, or the fifth-most: is that one SQL-injection-shaped call more or less pressing
|
|
13
|
+
than the vulnerable gem sitting in your `Gemfile.lock`, or the N+1 query burning production
|
|
14
|
+
database time right now? Answering that means looking at security, performance, dependencies, and
|
|
15
|
+
code quality *together*, not as four separate tool outputs to reconcile by hand. That's Scryer's
|
|
16
|
+
job:
|
|
16
17
|
|
|
17
18
|
```bash
|
|
18
19
|
gem install scryer
|
|
@@ -23,20 +24,46 @@ scryer
|
|
|
23
24
|
Scryer Audit — 236 files scanned
|
|
24
25
|
────────────────────────────────
|
|
25
26
|
|
|
26
|
-
Security
|
|
27
|
+
Security Score: 10/100 (F)
|
|
28
|
+
Checks: 23/36 rules clean (63.9%)
|
|
29
|
+
|
|
30
|
+
Security 27 findings
|
|
27
31
|
Performance 10 findings
|
|
28
32
|
Code Quality 248 findings
|
|
29
33
|
Dependencies 24 findings
|
|
30
34
|
────────────────────────────────
|
|
31
|
-
Total
|
|
35
|
+
Total 309 findings
|
|
36
|
+
|
|
37
|
+
Top priorities:
|
|
38
|
+
1. [critical] security — mass_assignment (app/helpers/api/v1/create_order_helper.rb:14)
|
|
39
|
+
2. [critical] security — mass_assignment (app/helpers/api/v1/create_order_helper.rb:45)
|
|
40
|
+
3. [critical] security — mass_assignment (app/helpers/api/v1/checkout/checkout_helper.rb:92)
|
|
41
|
+
4. [critical] security — sql_injection (app/controllers/concerns/billing_helper.rb:12)
|
|
42
|
+
5. [critical] security — mass_assignment (app/controllers/api/v1/orders_controller.rb:8)
|
|
43
|
+
|
|
44
|
+
OWASP Top 10 (2021) coverage:
|
|
45
|
+
A01:2021-Broken Access Control: 16 findings
|
|
46
|
+
A03:2021-Injection: 4 findings
|
|
47
|
+
A08:2021-Software and Data Integrity Failures: 4 findings
|
|
48
|
+
A02:2021-Cryptographic Failures: 1 finding
|
|
49
|
+
A04:2021-Insecure Design: 1 finding
|
|
50
|
+
A09:2021-Security Logging and Monitoring Failures: 1 finding
|
|
32
51
|
|
|
33
52
|
JSON report: tmp/scryer_report.json
|
|
34
53
|
HTML report: tmp/scryer_report.html
|
|
35
54
|
```
|
|
36
55
|
|
|
37
|
-
That's real output from a scan of a live 236-file Rails app
|
|
56
|
+
That's real output from a scan of a live 236-file Rails app (an internal production codebase we
|
|
57
|
+
call "acme-app" here — file/controller names above are anonymized, since we don't publish that
|
|
58
|
+
app's source; the finding counts, severities, rule IDs, and line numbers are exactly as scanned,
|
|
59
|
+
unedited) — not a mockup, and the harsh grade is real too (see
|
|
60
|
+
[Security score](#security-score) for what it does and doesn't mean). "Top priorities" is the same
|
|
61
|
+
severity ranking [`ReportRenderer#top_risks`](lib/scryer/report_renderer.rb) applies across *all*
|
|
62
|
+
categories — security, dependencies, performance, code quality — not just within each one; in the
|
|
63
|
+
HTML report it's at the top of the Findings section, ahead of the 309 individual findings
|
|
64
|
+
underneath it. See
|
|
38
65
|
[Scryer vs RuboCop vs Brakeman vs bundler-audit](#scryer-vs-rubocop-vs-brakeman-vs-bundler-audit)
|
|
39
|
-
below for exactly how
|
|
66
|
+
below for exactly how this differs from what those tools do.
|
|
40
67
|
|
|
41
68
|
### What Scryer detects
|
|
42
69
|
|
|
@@ -54,8 +81,15 @@ below for exactly how it stacks up against the tools it's meant to consolidate.
|
|
|
54
81
|
* Server-side request forgery (SSRF)
|
|
55
82
|
* Path traversal
|
|
56
83
|
* Insecure direct object references (IDOR) — the least precise check in the gem; see
|
|
57
|
-
[comparison table](#scryer-vs-rubocop-vs-brakeman-vs-bundler-audit) for the false-positive tradeoff
|
|
58
|
-
|
|
84
|
+
[comparison table](#scryer-vs-rubocop-vs-brakeman-vs-bundler-audit) for the false-positive tradeoff.
|
|
85
|
+
Correctly examines namespaced controllers too (`Admin::PostsController`,
|
|
86
|
+
`Api::V1::UsersController`) — an earlier version silently skipped any namespaced class here
|
|
87
|
+
* A `create`/`update`/`destroy` action with no authorization check anywhere in its controller
|
|
88
|
+
(broader than IDOR — catches write actions that don't call `.find` at all)
|
|
89
|
+
* A Pundit-using controller's `index` action querying a model directly instead of through
|
|
90
|
+
`policy_scope` (the common "remembered `authorize`, forgot `policy_scope`" gotcha)
|
|
91
|
+
* Authentication filters explicitly skipped (`skip_before_action`) — also correctly examines
|
|
92
|
+
namespaced controllers, same fix as IDOR above
|
|
59
93
|
* Rails security configuration: disabled HTTPS enforcement, session cookies missing the secure
|
|
60
94
|
flag, the Marshal cookie serializer, disabled default security headers (`X-Frame-Options`,
|
|
61
95
|
`X-Content-Type-Options`, Content-Security-Policy), Action Cable request forgery protection
|
|
@@ -67,6 +101,32 @@ below for exactly how it stacks up against the tools it's meant to consolidate.
|
|
|
67
101
|
* Background jobs (`perform_async`/`perform_later`/`perform_now`) passed raw `params` — risks
|
|
68
102
|
leaking request data into Sidekiq/ActiveJob logs, Redis, or the Sidekiq web UI
|
|
69
103
|
* GraphQL schemas with no query depth/complexity limit (`max_depth`/`max_complexity`)
|
|
104
|
+
* Rails config audit: `config.consider_all_requests_local = true` left on in
|
|
105
|
+
`config/environments/production.rb` (shows full debug error pages — backtrace, local variables,
|
|
106
|
+
request params — to anyone who triggers an exception), `config.log_level = :debug` in production
|
|
107
|
+
(logs full request params and SQL bind values), `config.hosts.clear` (disables the Host-header
|
|
108
|
+
allowlist that protects against DNS-rebinding/Host-header-injection). All three are Rails
|
|
109
|
+
defaults in development/test where they're completely normal — only flagged where they're
|
|
110
|
+
actually risky.
|
|
111
|
+
|
|
112
|
+
Every security finding also carries a **CWE ID**, an **OWASP Top 10 (2021) category**, and a
|
|
113
|
+
**confidence level** (`high`/`medium`/`low`) — all three appear in every report format (JSON, CSV,
|
|
114
|
+
HTML, SARIF) and in the OWASP Top 10 coverage scorecard (console summary + a dedicated HTML
|
|
115
|
+
section). Two things worth being precise about:
|
|
116
|
+
|
|
117
|
+
- **Confidence is a different axis from severity.** Severity is "how bad is this if real";
|
|
118
|
+
confidence is "how often does this specific rule's pattern-match actually reflect a real issue."
|
|
119
|
+
`idor` is a good example of the split: a real IDOR is serious (`warning` severity, arguably
|
|
120
|
+
understating it), but the rule's own heuristic — no visible authorization call anywhere in the
|
|
121
|
+
controller class — is the least precise in the gem, so it's tagged `low` confidence. Most rules
|
|
122
|
+
are `medium`; the narrowest, most literal-match rules (a hardcoded string literal, an explicit
|
|
123
|
+
`= false` config assignment) are `high`.
|
|
124
|
+
- **The CWE/OWASP mapping is Scryer's own best-effort categorization**, chosen for practitioner
|
|
125
|
+
convenience (a quick answer to "does our tooling cover OWASP category X" in a compliance
|
|
126
|
+
conversation) — it is not an OWASP-endorsed mapping and hasn't been independently audited. A
|
|
127
|
+
handful of the 31 assignments involved a real judgment call where a rule's issue doesn't map
|
|
128
|
+
cleanly onto exactly one OWASP category (e.g. `job_raw_params`, which is as much a data-exposure
|
|
129
|
+
concern as a logging one). Treat it as a useful pointer, not a certification.
|
|
70
130
|
|
|
71
131
|
**Code quality**
|
|
72
132
|
|
|
@@ -90,6 +150,14 @@ below for exactly how it stacks up against the tools it's meant to consolidate.
|
|
|
90
150
|
* Ruby version end-of-life (no more security patches published for it, for any issue)
|
|
91
151
|
* `config/master.key` present on disk with no matching `.gitignore` entry
|
|
92
152
|
|
|
153
|
+
**Runtime** (opt-in, needs a running app — see [Runtime query watcher](#runtime-query-watcher) and
|
|
154
|
+
[Runtime authorization watcher](#runtime-authorization-watcher))
|
|
155
|
+
|
|
156
|
+
* N+1 queries and unused eager loading, as they actually happen
|
|
157
|
+
* A write action that completed with no Pundit/CanCanCan authorization check actually invoked
|
|
158
|
+
during that request — a live, false-positive-resistant companion to the static `idor`/
|
|
159
|
+
`missing_authorization`/`missing_policy_scope` rules above
|
|
160
|
+
|
|
93
161
|
### Example findings
|
|
94
162
|
|
|
95
163
|
Real output — three lines of deliberately flawed Rails code, scanned with plain `scryer`:
|
|
@@ -152,6 +220,15 @@ Generate detailed JSON or self-contained HTML reports:
|
|
|
152
220
|
scryer -o report.json -o report.html
|
|
153
221
|
```
|
|
154
222
|
|
|
223
|
+
The HTML report leads with a [security score](#security-score) badge and a severity distribution
|
|
224
|
+
chart, then Overview/Summary/OWASP coverage, collapsed-by-default reference tables (every rule
|
|
225
|
+
that *can* fire, not a wall of always-expanded detail), and the Findings section itself — Top
|
|
226
|
+
priorities first, then a text filter box (rule/file/message, no page reload) above the full
|
|
227
|
+
severity-grouped, accordion-collapsed list. Each finding also shows its CWE ID, OWASP category, and
|
|
228
|
+
confidence level as small tags; clicking a count in the OWASP coverage table jumps to Findings with
|
|
229
|
+
that exact category pre-filled into the search box, so "how many `A01` findings are there" and
|
|
230
|
+
"which ones, specifically" are one click apart instead of two different views.
|
|
231
|
+
|
|
155
232
|
### Developer-friendly suggestions
|
|
156
233
|
|
|
157
234
|
Every finding includes a human-reviewable suggested fix. Scryer never automatically modifies your source code.
|
|
@@ -198,17 +275,104 @@ normal for this class of tool, not a bug. An already-guarded call can still get
|
|
|
198
275
|
rules don't trace surrounding conditionals — always review a finding in its surrounding context
|
|
199
276
|
before acting on it.
|
|
200
277
|
|
|
278
|
+
## Security score
|
|
279
|
+
|
|
280
|
+
Every scan produces a single 0-100 score plus a letter grade (A-F), shown in the console summary
|
|
281
|
+
and as a badge at the very top of the HTML report:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
Security Score: 11/100 (F)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
The formula (`ReportRenderer#security_score` in `lib/scryer/report_renderer.rb`): every security
|
|
288
|
+
and dependency finding costs points, weighted by both its severity *and* its confidence (a
|
|
289
|
+
`low`-confidence `idor` finding costs less than a `high`-confidence `sql_injection` finding at the
|
|
290
|
+
same severity), combined via exponential decay from 100 rather than linear subtraction — one
|
|
291
|
+
critical finding visibly moves the score (100 → ~86) without a handful of findings driving any
|
|
292
|
+
real app straight to a hard-clamped 0.
|
|
293
|
+
|
|
294
|
+
Two things worth being precise about before you treat this number as meaningful:
|
|
295
|
+
|
|
296
|
+
- **It's not normalized by app size.** A 10-file app and a 1,000-file app with the same finding
|
|
297
|
+
*density* will score very differently here — this score reflects a scan's absolute finding
|
|
298
|
+
exposure, not a rate. That makes it useful for tracking *one project's own trend* over time (did
|
|
299
|
+
the next scan score higher or lower), not for comparing two differently-sized codebases against
|
|
300
|
+
each other.
|
|
301
|
+
- **Performance and code-quality findings aren't part of it.** This is a *security* score — only
|
|
302
|
+
security and dependency findings count. A slow app with clean security findings still scores
|
|
303
|
+
well here; check the "Performance"/"Code Quality" rows in the summary box separately for that.
|
|
304
|
+
|
|
305
|
+
Alongside the score, every scan also reports a **rule-level pass rate** — "how many of Scryer's
|
|
306
|
+
registered checks fired zero findings":
|
|
307
|
+
|
|
308
|
+
```
|
|
309
|
+
Checks: 23/36 rules clean (63.9%)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
This is the closest thing Scryer has to Brakeman's "X checks, Y warnings" framing — a coverage
|
|
313
|
+
signal, not a risk signal. It's deliberately a *different* number from the security score, and the
|
|
314
|
+
two won't always agree: a codebase can have a high clean rate (few distinct rules ever fire) and
|
|
315
|
+
still a low score (the few that did fire were severe and high-confidence), or the reverse (many
|
|
316
|
+
different rules each firing once, none of them serious). `ReportRenderer#rules_clean_rate` computes
|
|
317
|
+
it from `Scryer::RuleSet.all` (every registered security/performance/style rule) against which
|
|
318
|
+
rule_ids actually appeared in this scan's findings — dependency checks aren't counted here since
|
|
319
|
+
they're not backed by a `Scryer::Rule` subclass. Report both; neither alone tells the whole story.
|
|
320
|
+
|
|
321
|
+
The 10/F above is a real score from the acme-app example, not a cherry-picked good result — see
|
|
322
|
+
[A note on how this gem was actually verified](#a-note-on-how-this-gem-was-actually-verified) for
|
|
323
|
+
why every example in this README is real output.
|
|
324
|
+
|
|
325
|
+
## Accuracy benchmark
|
|
326
|
+
|
|
327
|
+
Every rule in this gem is heuristic pattern-matching — stated throughout this README, but until now
|
|
328
|
+
never backed by a measured number, just "expect some false positives, that's normal for this class
|
|
329
|
+
of tool." [`benchmark/`](benchmark/) turns that into an actual precision/recall benchmark: a
|
|
330
|
+
hand-labeled corpus of small Ruby/Rails snippets per rule (`benchmark/corpus.rb`), each marked
|
|
331
|
+
`vulnerable` (should fire) or `safe` (should NOT fire — including deliberately close "near miss"
|
|
332
|
+
samples that resemble the vulnerable shape, not just obviously unrelated code), run through the
|
|
333
|
+
real `Scryer::Rule#scan` — the same code path an actual scan uses, not a simulation.
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
rake benchmark # or: ruby benchmark/run.rb
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Current numbers, across all 36 registered rules and 180 labeled samples:
|
|
340
|
+
|
|
341
|
+
| | TP | FN | FP | TN | Precision | Recall | F1 |
|
|
342
|
+
|---|---|---|---|---|---|---|---|
|
|
343
|
+
| **All 36 rules** | 82 | 3 | 1 | 94 | **98.8%** | **96.5%** | **97.6%** |
|
|
344
|
+
|
|
345
|
+
Two rules score below 100% — both are gaps this gem already disclosed in the rule's own top comment
|
|
346
|
+
before this benchmark existed, now actually measured instead of just asserted:
|
|
347
|
+
|
|
348
|
+
- **`graphql_missing_query_limits`** (50% precision, 50% recall on its 4 samples): misses a schema
|
|
349
|
+
that gets its query limits through a shared custom base class or an `include`d module — both
|
|
350
|
+
require cross-file resolution this per-file rule doesn't attempt.
|
|
351
|
+
- **`unbounded_table_scan`** (recall 66.7%): misses an unbounded scan once the query is assigned to
|
|
352
|
+
a variable before `.each` instead of chained directly (`orders = Order.where(...); orders.each`).
|
|
353
|
+
- **`mass_assignment`** (recall 66.7%): the one gap this benchmark actually *found*, not just
|
|
354
|
+
measured — see the CHANGELOG entry on the ivar-receiver fix. The corpus's other documented
|
|
355
|
+
false-negative sample for this rule (a namespaced `Admin::Order.create(...)` receiver) is still
|
|
356
|
+
an open, disclosed gap.
|
|
357
|
+
|
|
358
|
+
**Read `benchmark/README.md` before quoting any of these numbers** — this is a corpus we wrote
|
|
359
|
+
ourselves specifically to stress-test our own rules, not an independent third-party benchmark (the
|
|
360
|
+
way, say, the OWASP Benchmark project is for Java tools), and not real production code the way the
|
|
361
|
+
acme-app numbers used throughout this README are. Treat it as a lower bound on rigor (something
|
|
362
|
+
concrete was actually measured) and an upper bound on confidence (real-world false-positive/
|
|
363
|
+
false-negative rates on *your* codebase, with its own idioms and libraries, can and will differ).
|
|
364
|
+
|
|
201
365
|
## Scryer vs RuboCop vs Brakeman vs bundler-audit
|
|
202
366
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
367
|
+
Brakeman is the deeper, more mature tool for the security categories it's spent years on — real
|
|
368
|
+
taint/data-flow analysis, not heuristic pattern-matching (footnote [i]). RuboCop owns style/lint
|
|
369
|
+
conventions; Scryer stays out of that territory except for one narrow, deliberately-scoped check
|
|
370
|
+
(footnote [h]). Scryer isn't trying to out-analyze either of them at their own specialty. What it
|
|
371
|
+
does instead: look at security, performance, dependency, and code-quality findings *together* and
|
|
372
|
+
rank the result by severity, so "what should I fix first" has one answer instead of four separate
|
|
373
|
+
tool outputs (and four different severity scales) to reconcile by hand — see footnote [j]. If you
|
|
374
|
+
already run Brakeman for its deeper security analysis or RuboCop for style, keep doing that; Scryer
|
|
375
|
+
sits alongside them and adds the cross-category picture neither one (nor bundler-audit) produces.
|
|
212
376
|
|
|
213
377
|
| Capability | Scryer | RuboCop | Brakeman | bundler-audit |
|
|
214
378
|
|-------------------------------------------|:------:|:--------:|:--------:|:-------------:|
|
|
@@ -222,6 +386,13 @@ cover into one command and one report instead of several separate tools, configs
|
|
|
222
386
|
| JSON report | ✅ | ✅ | ✅ | Limited [d] |
|
|
223
387
|
| SARIF report (GitHub Code Scanning, etc.) | ✅ | ❌ | ✅ | ❌ |
|
|
224
388
|
| Human-reviewable fix suggestions | ✅ | Partial [e] | Partial [f] | Limited [g] |
|
|
389
|
+
| Cross-category risk ranking ("fix this first") | ✅ [j] | ❌ | ❌ | ❌ |
|
|
390
|
+
| CWE / OWASP Top 10 category tags | ✅ [k] | ❌ | ✅ [k] | ❌ |
|
|
391
|
+
| Confidence level per finding | ✅ | ❌ | ✅ | ❌ |
|
|
392
|
+
| Official GitHub Action | ✅ [l] | ❌ | ❌ | ❌ |
|
|
393
|
+
| Baseline / new-vs-existing-findings diffing | ✅ [m] | ❌ | Partial [m] | ❌ |
|
|
394
|
+
| Overall security score | ✅ | ❌ | ❌ | ❌ |
|
|
395
|
+
| Fix verified against an actual re-scan | ✅ [n] | ❌ | ❌ | ❌ |
|
|
225
396
|
| Single command covering all of the above | ✅ | ❌ | ❌ | ❌ |
|
|
226
397
|
|
|
227
398
|
- **[a]** `rubocop-performance` adds some Ruby/Rails performance cops, but nothing like N+1-query
|
|
@@ -246,13 +417,38 @@ cover into one command and one report instead of several separate tools, configs
|
|
|
246
417
|
checks especially (`idor` in particular has real false-positive risk — see
|
|
247
418
|
[What Scryer detects](#what-scryer-detects)), and it's why every finding says "review this,"
|
|
248
419
|
never "this is definitely a bug."
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
Scryer
|
|
420
|
+
- **[j]** Each tool ranks findings within its own domain at best (e.g. Brakeman's per-warning
|
|
421
|
+
confidence level). None of them combine security, performance, dependency, and code-quality
|
|
422
|
+
findings into one ranked list — that's a different question than any single tool is built to
|
|
423
|
+
answer. `ReportRenderer#top_risks` sorts every severity-bearing finding from a scan (across all
|
|
424
|
+
four categories) by severity, shown as "Top priorities" in the console summary and at the top of
|
|
425
|
+
the HTML report — see the example near the top of this README.
|
|
426
|
+
- **[k]** Not a Scryer-only capability — Brakeman's own warnings already include a CWE reference.
|
|
427
|
+
Scryer's version is a fuller mapping (every one of its 31 security rules carries both a CWE ID
|
|
428
|
+
and an OWASP Top 10 (2021) category, aggregated into an OWASP coverage scorecard in every
|
|
429
|
+
report), but the underlying idea isn't new; see
|
|
430
|
+
[What Scryer detects](#what-scryer-detects) for the honesty caveat on how this mapping was built
|
|
431
|
+
(Scryer's own best-effort categorization, not OWASP-audited).
|
|
432
|
+
- **[l]** [`action.yml`](action.yml) in this repo — `uses: ramlaxmanyadav/scryer@<version>` runs
|
|
433
|
+
the scan and uploads SARIF to GitHub Code Scanning in one step, instead of hand-writing the
|
|
434
|
+
workflow YAML in [CI/CD integration](#cicd-integration) yourself (that manual version still
|
|
435
|
+
works and is documented there too, for anyone who wants full control over the steps).
|
|
436
|
+
- **[m]** Brakeman has an ignore file (`brakeman -I`) that permanently silences specific warnings
|
|
437
|
+
by fingerprint — genuinely similar in mechanism to [Baseline mode](#baseline-mode)'s
|
|
438
|
+
fingerprinting, marked Partial rather than ✅ here because it's a permanent suppression list you
|
|
439
|
+
maintain by hand, not an explicit "show me only what's new since this snapshot, and tell me
|
|
440
|
+
what got fixed" diff against a saved baseline the way `scryer --baseline` reports both.
|
|
441
|
+
- **[n]** `scryer verify --rule RULE_ID --file PATH` re-runs one rule against one file to confirm a
|
|
442
|
+
fix actually cleared it, and — when an `ai_client` is configured — the same check runs
|
|
443
|
+
automatically on every AI-rewritten `suggested_fix` before the report is written (see
|
|
444
|
+
[Verifying a fix](#verifying-a-fix)). RuboCop's `-A` rewrites code directly rather than verifying
|
|
445
|
+
a *suggested* fix; Brakeman and bundler-audit don't rewrite or re-check at all.
|
|
446
|
+
|
|
447
|
+
Brakeman's security analysis and RuboCop's style checks are still worth running on their own —
|
|
448
|
+
Scryer doesn't try to replace either, and running it alongside them costs nothing (different tools,
|
|
449
|
+
different config files, no shared state). What Scryer adds is the view none of them produce alone:
|
|
450
|
+
one ranked list of what's actually most worth fixing, built from security, performance,
|
|
451
|
+
dependency, and code-quality findings together.
|
|
256
452
|
|
|
257
453
|
## Install
|
|
258
454
|
|
|
@@ -346,7 +542,43 @@ security, performance, and style findings (each mapped from `Scryer::Rule.rule_i
|
|
|
346
542
|
dependency findings (mapped from `kind`, located at `Gemfile.lock`) — so nothing behaves any
|
|
347
543
|
differently than it does in HTML/JSON/CSV.
|
|
348
544
|
|
|
349
|
-
|
|
545
|
+
### Option A: the bundled `action.yml`
|
|
546
|
+
|
|
547
|
+
[`action.yml`](action.yml) in this repo wraps the whole thing — install, scan, SARIF upload, and
|
|
548
|
+
correctly ordering all three so the upload still happens even when findings were reported (see the
|
|
549
|
+
`continue-on-error` explanation below for why that ordering matters):
|
|
550
|
+
|
|
551
|
+
```yaml
|
|
552
|
+
# .github/workflows/scryer.yml
|
|
553
|
+
name: Scryer
|
|
554
|
+
on: [push, pull_request]
|
|
555
|
+
|
|
556
|
+
jobs:
|
|
557
|
+
scryer:
|
|
558
|
+
runs-on: ubuntu-latest
|
|
559
|
+
permissions:
|
|
560
|
+
contents: read
|
|
561
|
+
security-events: write # required to upload SARIF
|
|
562
|
+
steps:
|
|
563
|
+
- uses: actions/checkout@v4
|
|
564
|
+
- uses: ramlaxmanyadav/scryer@v1.1.0
|
|
565
|
+
with:
|
|
566
|
+
ruby-version: "3.3"
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
All inputs are optional — `path` (default `.`), `scryer-version` (default: latest), `extra-args`
|
|
570
|
+
(e.g. `--skip idor --no-deps`), `fail-on-findings` (default `true`), `upload-sarif` (default
|
|
571
|
+
`true`). Outputs: `sarif-path`, `json-path`, `exit-code`. See [action.yml](action.yml) for the full
|
|
572
|
+
input/output reference.
|
|
573
|
+
|
|
574
|
+
`@v1.1.0` above needs a matching git tag pushed to GitHub before `uses:` can resolve it — replace
|
|
575
|
+
it with whatever version tag you actually push (or `@main` to always track the latest commit,
|
|
576
|
+
accepting that it can change under you).
|
|
577
|
+
|
|
578
|
+
### Option B: hand-written steps
|
|
579
|
+
|
|
580
|
+
For full control over the individual steps (a different Ruby setup, extra caching, a different
|
|
581
|
+
job structure):
|
|
350
582
|
|
|
351
583
|
```yaml
|
|
352
584
|
# .github/workflows/scryer.yml
|
|
@@ -376,7 +608,50 @@ jobs:
|
|
|
376
608
|
(see [Running a scan](#running-a-scan)), which would otherwise skip the upload step entirely on
|
|
377
609
|
the run where you most want to see the results. If you want the job to still fail the build overall
|
|
378
610
|
on findings, check `scryer`'s own exit code in a separate step, or drop `continue-on-error` and
|
|
379
|
-
accept that the SARIF upload only happens on clean runs.
|
|
611
|
+
accept that the SARIF upload only happens on clean runs. `action.yml` (Option A) handles this
|
|
612
|
+
ordering for you automatically.
|
|
613
|
+
|
|
614
|
+
### Rails apps: `bin/rails scryer:ci`
|
|
615
|
+
|
|
616
|
+
Inside a Rails app (rather than the standalone `scryer` executable above), `scryer:ci` is a
|
|
617
|
+
memorable shortcut for the same CI-sensible defaults — JSON + SARIF under `tmp/`, dependency audit
|
|
618
|
+
on, same `SCRYER_BASELINE` support and same fail-the-build-on-findings behavior as `scryer:report`
|
|
619
|
+
— without needing to remember the bracket-arg syntax:
|
|
620
|
+
|
|
621
|
+
```bash
|
|
622
|
+
bin/rails scryer:ci # same as bin/rails 'scryer:report[json,sarif]'
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## Verifying a fix
|
|
626
|
+
|
|
627
|
+
Two ways to confirm a specific finding is actually resolved, without waiting on (or paying for) a
|
|
628
|
+
full rescan:
|
|
629
|
+
|
|
630
|
+
**`scryer verify`** re-parses one file and re-runs one rule against it — meant to run right after
|
|
631
|
+
applying a fix by hand (or reviewing an AI-suggested one):
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
scryer verify --rule sql_injection --file app/models/user.rb
|
|
635
|
+
# scryer verify: sql_injection no longer fires on app/models/user.rb — fix verified. (exit 0)
|
|
636
|
+
# or: scryer verify: sql_injection still fires on app/models/user.rb (1 finding(s)): (exit 1)
|
|
637
|
+
# line 12: `where` is called with a string built via interpolation, ...
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
`--path ROOT` sets the project root `--file` is resolved against (default: current directory);
|
|
641
|
+
`--list-rules` prints every known `rule_id`. Deliberately narrow: this only answers "does the one
|
|
642
|
+
thing I targeted still fire," not "did this change introduce a different finding elsewhere" — a
|
|
643
|
+
normal `scryer` run (or `--baseline`) already answers that.
|
|
644
|
+
|
|
645
|
+
**AI-verified remediation** does the same check automatically when an `ai_client` is configured
|
|
646
|
+
(see [AI-assisted fix suggestions](#ai-assisted-fix-suggestions)) — every AI-rewritten
|
|
647
|
+
`suggested_fix` is re-scanned against an in-memory copy of the real file before the report is
|
|
648
|
+
written, and each finding gets a `fix_verified: true/false/nil` field (`true`: the rule no longer
|
|
649
|
+
fires with this fix applied; `false`: it was checked and still fires, or the rewritten line doesn't
|
|
650
|
+
even parse; `nil`: not attempted at all — no `ai_client` configured, or the AI's reply didn't
|
|
651
|
+
include a usable code block). The HTML report shows this as a green "AI fix verified" or red "AI
|
|
652
|
+
fix NOT verified" line under each finding's suggested fix. Nothing is ever written back to the real
|
|
653
|
+
file for this check — same "never auto-applied" rule as `suggested_fix` itself; this only confirms
|
|
654
|
+
whether the *suggestion* would work if you applied it.
|
|
380
655
|
|
|
381
656
|
## What gets scanned
|
|
382
657
|
|
|
@@ -420,6 +695,41 @@ without touching the initializer — useful for a one-off "does this go away wit
|
|
|
420
695
|
scryer --skip mass_assignment --skip weak_crypto
|
|
421
696
|
```
|
|
422
697
|
|
|
698
|
+
## Baseline mode
|
|
699
|
+
|
|
700
|
+
`--skip` silences a rule everywhere, permanently. Baseline mode is for the more common real-world
|
|
701
|
+
situation: an existing app has genuine pre-existing findings you're not going to fix today, but you
|
|
702
|
+
still want CI to fail on anything *new* from here on:
|
|
703
|
+
|
|
704
|
+
```bash
|
|
705
|
+
# Once: snapshot the current state
|
|
706
|
+
scryer --save-baseline tmp/scryer_baseline.json
|
|
707
|
+
|
|
708
|
+
# Every run after that: only new findings show up, count toward the exit code,
|
|
709
|
+
# and appear in -o reports — everything already in the baseline is suppressed
|
|
710
|
+
scryer --baseline tmp/scryer_baseline.json -o scryer.sarif
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
```
|
|
714
|
+
Baseline: tmp/scryer_baseline.json — showing new findings only (2 fixed since baseline).
|
|
715
|
+
|
|
716
|
+
Security Score: 96/100 (A)
|
|
717
|
+
|
|
718
|
+
Security 1 finding
|
|
719
|
+
...
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
Findings are matched by a **fingerprint** (`rule_id` + file + the offending source text), not
|
|
723
|
+
file:line — an unrelated edit earlier in the same file shifting every following line number
|
|
724
|
+
wouldn't otherwise make an existing finding look simultaneously "new" and "fixed" on every commit.
|
|
725
|
+
"Fixed since baseline" counts findings that were in the baseline but aren't in this scan anymore
|
|
726
|
+
(across every category — security, performance, style, dependencies); duplicate-code groups aren't
|
|
727
|
+
part of baseline mode (they don't fit the same fingerprint shape) and always show in full.
|
|
728
|
+
|
|
729
|
+
Rails/rake equivalents: `rails 'scryer:save_baseline[tmp/scryer_baseline.json]'` to save, and
|
|
730
|
+
`SCRYER_BASELINE=tmp/scryer_baseline.json rails scryer:report` to compare (an env var, since rake's
|
|
731
|
+
bracket-arg parsing is already stretched thin between format tokens, `nodeps`, and an output path).
|
|
732
|
+
|
|
423
733
|
## Runtime query watcher
|
|
424
734
|
|
|
425
735
|
Everything above is a one-shot static scan — it reads source, never boots Rails, and can't see
|
|
@@ -464,6 +774,50 @@ This is genuinely runtime-only: with no queries running, it finds nothing, and i
|
|
|
464
774
|
the static `tmp/scryer_report.html` output. Think of it as this gem's answer to "what actually
|
|
465
775
|
happened during this request", where the rest of Scryer answers "what does this code look like".
|
|
466
776
|
|
|
777
|
+
## Runtime authorization watcher
|
|
778
|
+
|
|
779
|
+
The static `idor`/`missing_authorization`/`missing_policy_scope` rules can only ever say "no call
|
|
780
|
+
to a known authorization method is visible anywhere in this controller's source" — which is exactly
|
|
781
|
+
as wrong as it sounds whenever the real check happens somewhere the static AST walk can't see (a
|
|
782
|
+
shared base controller, a concern, a class-level macro). `Scryer::AuthorizationWatcher` answers a
|
|
783
|
+
narrower but far more reliable question instead: for *this actual request*, did Pundit's
|
|
784
|
+
`authorize`/`policy_scope` or CanCanCan's `authorize!` genuinely get called? Both libraries already
|
|
785
|
+
track this themselves internally (for their own `verify_authorized`/`check_authorization` helpers)
|
|
786
|
+
— this watcher registers one more `after_action` alongside those and checks the same flags
|
|
787
|
+
(`Pundit::Authorization#pundit_policy_authorized?`/`#pundit_policy_scoped?`, CanCanCan's
|
|
788
|
+
`@_authorized` ivar — verified by reading both gems' actual source, not guessed).
|
|
789
|
+
|
|
790
|
+
```ruby
|
|
791
|
+
# config/initializers/scryer.rb
|
|
792
|
+
require "scryer/authorization_watcher"
|
|
793
|
+
Scryer::AuthorizationWatcher.enable!
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
No middleware to install and no per-request scope to open (unlike `QueryWatcher` above) — a Rails
|
|
797
|
+
controller instance is already fresh per request, so the `after_action` above is all `enable!`
|
|
798
|
+
needs. It flags a `create`/`update`/`destroy` action (or any `POST`/`PUT`/`PATCH`/`DELETE` request)
|
|
799
|
+
that completed successfully (status < 400) with neither flag set:
|
|
800
|
+
|
|
801
|
+
```ruby
|
|
802
|
+
Scryer::AuthorizationWatcher.findings
|
|
803
|
+
# => [#<struct Scryer::AuthorizationWatcher::Finding kind="runtime_missing_authorization",
|
|
804
|
+
# message="WidgetsController#update completed a PATCH request (status 200) with no
|
|
805
|
+
# authorization check actually invoked during it ...", controller="WidgetsController",
|
|
806
|
+
# action="update", method="PATCH", path="/widgets/1", suggested_fix="...">]
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
Two honesty points worth being precise about:
|
|
810
|
+
|
|
811
|
+
- **Pundit/CanCanCan-only, same scope as the static rules it complements.** With neither gem
|
|
812
|
+
loaded, `enable!` still runs, but every request is silently skipped — an app with fully custom,
|
|
813
|
+
non-object-level authorization (a single `before_action :require_admin!`) gets no findings and no
|
|
814
|
+
false-positive flood, rather than being flagged for a pattern this watcher has no way to
|
|
815
|
+
recognize as intentional.
|
|
816
|
+
- **Write actions only.** Read-scoping gaps (an unscoped `index` — see the static
|
|
817
|
+
`missing_policy_scope` rule) aren't covered here: verifying "was the returned data correctly
|
|
818
|
+
scoped" at runtime, rather than "was a method called," is a materially different and harder check
|
|
819
|
+
this class doesn't attempt.
|
|
820
|
+
|
|
467
821
|
## Dependency audit
|
|
468
822
|
|
|
469
823
|
`Scryer::DependencyAudit` checks `Gemfile.lock` for the same broad concerns
|
|
@@ -634,6 +988,12 @@ Scryer::AiFixSuggester.enhance!(finding) # one Finding, in place
|
|
|
634
988
|
Scryer::AiFixSuggester.enhance_result!(result) # every finding on a Scanner::Result, in place
|
|
635
989
|
```
|
|
636
990
|
|
|
991
|
+
Both `bin/rails scryer:report` and the `scryer` executable also pass `root:` so each rewritten fix
|
|
992
|
+
gets verified against a re-scan automatically (`finding.fix_verified` — see
|
|
993
|
+
[Verifying a fix](#verifying-a-fix) for what that means and how to trigger it manually with
|
|
994
|
+
`scryer verify`); calling `enhance!`/`enhance_result!` directly as shown above skips verification
|
|
995
|
+
unless you pass `root:` yourself.
|
|
996
|
+
|
|
637
997
|
**This sends code snippets to whatever endpoint you configure.** `code_snippet`, `message`, and the
|
|
638
998
|
file path are included in the prompt — the same privacy consideration as any third-party service:
|
|
639
999
|
don't point this at an endpoint you don't trust with your source, and be mindful this is a second
|
|
@@ -653,12 +1013,61 @@ in a host app for the full description (also in
|
|
|
653
1013
|
not you ever run this generator. The generator exists purely to give you an editable config file.
|
|
654
1014
|
- **Settings it exposes**: `c.project_name` (report header label), `c.dirs` (which top-level
|
|
655
1015
|
directories get scanned), `c.branch` (override the git branch label — see
|
|
656
|
-
[Branch reporting](#branch-reporting)).
|
|
657
|
-
|
|
1016
|
+
[Branch reporting](#branch-reporting)), `c.skip_rules` (silence specific rule_ids — see
|
|
1017
|
+
[Skipping rules](#skipping-rules)), `c.ai_client` (see
|
|
1018
|
+
[AI-assisted fix suggestions](#ai-assisted-fix-suggestions)). All are optional; the
|
|
1019
|
+
commented-out initializer works as-is with just `bundle install` + the generator.
|
|
658
1020
|
- **Re-running it**: standard Thor/Rails::Generators behavior — if
|
|
659
1021
|
`config/initializers/scryer.rb` already exists, you'll be prompted to overwrite, skip, or diff
|
|
660
1022
|
rather than have it silently clobbered.
|
|
661
1023
|
|
|
1024
|
+
## Testing Scryer results in your own test suite
|
|
1025
|
+
|
|
1026
|
+
Two opt-in files turn "this app's own scan stays clean" into a normal test-suite assertion, so a
|
|
1027
|
+
regression fails the suite the same way any other regression would, instead of only showing up the
|
|
1028
|
+
next time someone runs `scryer` by hand. Neither loads with the gem automatically (RSpec/Minitest
|
|
1029
|
+
are never Scryer runtime dependencies — see the gemspec) — require the one matching your test
|
|
1030
|
+
framework yourself:
|
|
1031
|
+
|
|
1032
|
+
```ruby
|
|
1033
|
+
# spec/spec_helper.rb (RSpec)
|
|
1034
|
+
require "scryer/rspec"
|
|
1035
|
+
|
|
1036
|
+
RSpec.describe "security" do
|
|
1037
|
+
it "has no critical findings" do
|
|
1038
|
+
expect(Scryer.scan(root: Rails.root.to_s)).to have_no_critical_findings
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
it "never reintroduces the mass-assignment bug fixed in PR #123" do
|
|
1042
|
+
expect(Scryer.scan(root: Rails.root.to_s)).to have_no_findings_for("mass_assignment")
|
|
1043
|
+
end
|
|
1044
|
+
end
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
```ruby
|
|
1048
|
+
# test/test_helper.rb (Minitest / ActiveSupport::TestCase)
|
|
1049
|
+
require "scryer/minitest"
|
|
1050
|
+
|
|
1051
|
+
class SecurityTest < ActiveSupport::TestCase
|
|
1052
|
+
include Scryer::MinitestAssertions
|
|
1053
|
+
|
|
1054
|
+
test "no critical findings" do
|
|
1055
|
+
assert_no_critical_scryer_findings(Scryer.scan(root: Rails.root.to_s))
|
|
1056
|
+
end
|
|
1057
|
+
end
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
`Scryer.scan(root:)` runs the same static scan `scryer:report`/the `scryer` executable do (using
|
|
1061
|
+
`c.dirs`/`c.skip_rules` from your initializer), without needing a report written to disk —
|
|
1062
|
+
`have_no_critical_findings`/`assert_no_critical_scryer_findings` only look at *security* findings
|
|
1063
|
+
(same scoping as the [security score](#security-score)); `have_no_findings_for`/
|
|
1064
|
+
`assert_no_scryer_findings_for` check a specific `rule_id` across all three static categories,
|
|
1065
|
+
for pinning a specific bug so it can't come back unnoticed. This talks to the live filesystem on
|
|
1066
|
+
every test run (a real `Ripper`-based scan, same cost as running `scryer` itself) — for a large app
|
|
1067
|
+
this is meaningfully slower than a typical unit test, so it's usually one dedicated test/spec file
|
|
1068
|
+
run occasionally (a nightly job, a pre-release check) rather than part of every `rspec`/`rails test`
|
|
1069
|
+
invocation.
|
|
1070
|
+
|
|
662
1071
|
## Extending it
|
|
663
1072
|
|
|
664
1073
|
Every rule is a small class extending `Scryer::Rule` — see `lib/scryer/rules/*.rb` (security),
|
|
@@ -667,6 +1076,15 @@ the pattern. A new rule file dropped into any of the three directories is picked
|
|
|
667
1076
|
(rules self-register via `Rule.inherited` — no manual wiring needed, just `self.category =
|
|
668
1077
|
"security"|"performance"|"style"`). `Scryer::Ast` has the tree-walking helpers used throughout.
|
|
669
1078
|
|
|
1079
|
+
Scryer's own rules have their own Minitest suite — `rake test` (from the repo root, not inside a
|
|
1080
|
+
host app) runs a bad/clean fixture pair against every registered rule (`test/rule_fixtures_test.rb`),
|
|
1081
|
+
plus a completeness check that fails loudly if a new rule ships with no fixture. Add an entry to
|
|
1082
|
+
`RuleFixturesTest::FIXTURES` for any new or changed rule — and, for a genuine false positive/
|
|
1083
|
+
negative found against real code (not just the regression pair), add it to
|
|
1084
|
+
[`benchmark/corpus.rb`](benchmark/corpus.rb) too (see [Accuracy benchmark](#accuracy-benchmark))
|
|
1085
|
+
with a `note:` explaining the real-world shape it represents, so it's measured going forward
|
|
1086
|
+
instead of just fixed once and forgotten.
|
|
1087
|
+
|
|
670
1088
|
## License
|
|
671
1089
|
|
|
672
1090
|
MIT licensed.
|
data/lib/generators/scryer/USAGE
CHANGED
|
@@ -28,13 +28,21 @@ Description:
|
|
|
28
28
|
useful. Leave unset to use the actual checked-out
|
|
29
29
|
branch.
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
initializer — see the README for
|
|
31
|
+
A few more things ship in this gem but aren't controlled by the
|
|
32
|
+
initializer — see the README for each:
|
|
33
33
|
|
|
34
34
|
- Scryer::QueryWatcher: an opt-in runtime instrumentation for N+1
|
|
35
35
|
queries and unused eager loading (README: "Runtime query watcher").
|
|
36
36
|
- `bin/rails scryer:audit_dependencies`: a Gemfile.lock vulnerability
|
|
37
37
|
+ insecure-source audit against OSV.dev (README: "Dependency audit").
|
|
38
|
+
- `bin/rails scryer:ci`: a CI-friendly shortcut for
|
|
39
|
+
`scryer:report[json,sarif]` — JSON + SARIF output, dependency audit
|
|
40
|
+
on, same SCRYER_BASELINE support (README: "CI/CD integration").
|
|
41
|
+
- `require "scryer/rspec"` / `require "scryer/minitest"`: opt-in
|
|
42
|
+
matchers/assertions (`have_no_critical_findings`,
|
|
43
|
+
`assert_no_critical_scryer_findings`, ...) for asserting this app's
|
|
44
|
+
own scan stays clean as part of its normal test suite (README:
|
|
45
|
+
"Testing Scryer results in your own test suite").
|
|
38
46
|
|
|
39
47
|
Example:
|
|
40
48
|
bin/rails generate scryer:install
|