scryer 1.0.0 → 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 +414 -0
- data/README.md +114 -649
- 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/generators/scryer/USAGE +10 -2
- data/lib/generators/scryer/templates/scryer_initializer.rb +15 -0
- data/lib/scryer/ai_fix_suggester.rb +37 -11
- 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 +688 -14
- data/lib/scryer/colorizer.rb +56 -0
- data/lib/scryer/dependency_fixer.rb +96 -0
- data/lib/scryer/finding.rb +6 -0
- data/lib/scryer/fix_runner.rb +161 -0
- data/lib/scryer/fix_verifier.rb +169 -0
- data/lib/scryer/mechanical_fixer.rb +288 -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/scanner.rb +25 -12
- data/lib/scryer/style_rules/frozen_string_literal_rule.rb +1 -0
- data/lib/scryer/version.rb +1 -1
- data/lib/scryer.rb +30 -1
- data/lib/tasks/scryer.rake +447 -20
- metadata +52 -12
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# Architecture, Performance & Security Model
|
|
2
|
+
|
|
3
|
+
[← Back to Scryer — Ruby on Rails Security Auditor](../README.md)
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
Ruby's stdlib `Ripper` parses each file into an S-expression tree (see
|
|
8
|
+
[lib/scryer/ast.rb](../lib/scryer/ast.rb) for the small set of tree-walking helpers every rule uses).
|
|
9
|
+
Each check is a self-registering `Scryer::Rule` subclass
|
|
10
|
+
([lib/scryer/rule.rb](../lib/scryer/rule.rb)/[lib/scryer/rule_set.rb](../lib/scryer/rule_set.rb)) that
|
|
11
|
+
walks that tree looking for one specific shape — a call to a known-dangerous method, a
|
|
12
|
+
Rails-config assignment, a missing guard — and emits a `Scryer::Finding` when it matches.
|
|
13
|
+
`Scryer::Scanner` finds every Ruby file under the configured directories, parses each once, and
|
|
14
|
+
runs every registered rule against the resulting tree; `Scryer::ReportRenderer` merges the results
|
|
15
|
+
(plus a separate `Scryer::DependencyAudit` pass over `Gemfile.lock`) into JSON/HTML/CSV/SARIF and
|
|
16
|
+
computes the cross-category "Top priorities" ranking and security score. This is heuristic
|
|
17
|
+
pattern-matching on syntax, not [taint/data-flow
|
|
18
|
+
analysis](https://ramlaxmanyadav.github.io/scryer/taint-analysis-vs-heuristic-pattern-matching.html)
|
|
19
|
+
— no attempt is made to trace whether a specific value actually flows from an untrusted source to
|
|
20
|
+
a dangerous sink, which is exactly what makes it possible to run with zero runtime dependencies and
|
|
21
|
+
no Rails/database boot.
|
|
22
|
+
|
|
23
|
+
## Performance
|
|
24
|
+
|
|
25
|
+
Parsing is the only real cost, and it's linear in file count/size — there's no Rails boot, no
|
|
26
|
+
database connection, and (with `--no-deps`) no network call at all. A real, unscoped scan of a
|
|
27
|
+
236-file production Rails app (the same one behind the [Security score](#security-score) example
|
|
28
|
+
above) completes in about 2.5 seconds end to end on a laptop, `--no-deps` set. The one deliberately
|
|
29
|
+
slower path is the [dependency audit](./rules.md#dependency-audit) (on by default): it queries
|
|
30
|
+
[OSV.dev](https://osv.dev) once per unique gem in `Gemfile.lock`, so total time scales with distinct
|
|
31
|
+
dependency count and network latency, not codebase size — skip it with `--no-deps` (or `nodeps`)
|
|
32
|
+
for a fast, fully offline run when that matters more than dependency coverage on a given run (e.g.
|
|
33
|
+
a pre-commit hook, versus a full CI job).
|
|
34
|
+
|
|
35
|
+
## Security model
|
|
36
|
+
|
|
37
|
+
- **Static analysis only, by default.** The core scan reads source files and never executes your
|
|
38
|
+
code. The [runtime query watcher](./rails-integration.md#runtime-query-watcher) and [runtime authorization
|
|
39
|
+
watcher](./rails-integration.md#runtime-authorization-watcher) are the only things that hook into a running
|
|
40
|
+
process, and both are opt-in (you register them explicitly in an initializer) — a plain `scryer`
|
|
41
|
+
scan does neither.
|
|
42
|
+
- **No network calls except two, both skippable/opt-in.** The [dependency audit](./rules.md#dependency-audit)
|
|
43
|
+
queries OSV.dev (skip with `--no-deps`); [AI-assisted fix
|
|
44
|
+
suggestions](./fix-mode.md#ai-assisted-fix-suggestions) call whatever LLM endpoint you explicitly configure via
|
|
45
|
+
`c.ai_client` (nothing is called with no client configured, and no code snippet leaves your
|
|
46
|
+
machine unless you set one up).
|
|
47
|
+
- **Nothing is auto-applied without independent re-verification.** Every finding's `suggested_fix`
|
|
48
|
+
is just text until you act on it — except `scryer fix` ([Fix mode](./fix-mode.md#fix-mode)), the one command
|
|
49
|
+
that writes to disk, and even there only after re-parsing the file with that exact rewrite
|
|
50
|
+
applied and re-running the rule against it to confirm it actually clears the finding it targeted.
|
|
51
|
+
- **Zero runtime dependencies** beyond Ruby's own stdlib (`ripper`, `json`, `digest`,
|
|
52
|
+
`securerandom`, `set`, `net/http`, `uri`, `date`) — no transitive gem supply-chain surface to
|
|
53
|
+
audit beyond Ruby itself.
|
|
54
|
+
- **Safe to run against untrusted source.** Because it's a syntax-level static parse (never
|
|
55
|
+
`eval`, never `require`s the scanned code, never boots the target app), running Scryer against a
|
|
56
|
+
codebase you don't fully trust doesn't execute anything in it.
|
|
57
|
+
|
|
58
|
+
## False-positive handling
|
|
59
|
+
|
|
60
|
+
Every finding carries a `confidence` (`high`/`medium`/`low`, distinct from severity) — Scryer's own
|
|
61
|
+
best-effort assessment of how likely that specific match is a real issue, not a false positive. The
|
|
62
|
+
`idor` rule is explicitly the least precise check in the gem (see the [comparison
|
|
63
|
+
table](#scryer-vs-rubocop-vs-brakeman-vs-bundler-audit) footnotes) — real false-positive risk on
|
|
64
|
+
admin-gated or genuinely-global-model controllers; several other rules' false-positive risks and
|
|
65
|
+
mitigations are documented alongside them in [What Scryer
|
|
66
|
+
detects](./rules.md#what-scryer-detects). When a specific rule doesn't fit your codebase:
|
|
67
|
+
|
|
68
|
+
- **Silence it entirely**: `--skip RULE_ID` (repeatable) for a one-off run, or `c.skip_rules` in
|
|
69
|
+
the initializer for every run — see [Skipping rules](./usage.md#skipping-rules).
|
|
70
|
+
- **Suppress only pre-existing findings, not new ones**: [Baseline mode](./usage.md#baseline-mode) snapshots
|
|
71
|
+
the current findings and only reports/gates on what's new since then — the practical way to adopt
|
|
72
|
+
Scryer on a codebase with real pre-existing security debt without either fixing everything on day
|
|
73
|
+
one or turning the gate off entirely.
|
|
74
|
+
- **Confirm a specific fix actually worked**: `scryer verify` ([Verifying a
|
|
75
|
+
fix](./fix-mode.md#verifying-a-fix)) re-runs one rule against one file in isolation, independent of a full
|
|
76
|
+
scan.
|
|
77
|
+
|
|
78
|
+
## A note on how this gem was actually verified
|
|
79
|
+
|
|
80
|
+
Unlike most hand-written Rails work, this gem's core scanning engine was genuinely executed
|
|
81
|
+
during development — `Ripper` needs no Rails or bundler to run, so every rule here was tested
|
|
82
|
+
against real vulnerable-code and clean/fixed-code fixtures, not just written and hoped for. Where
|
|
83
|
+
a rule produced a false positive during that testing (an actual one did — `BCrypt::Password.create`
|
|
84
|
+
initially tripped the mass-assignment rule meant for `Model.create(params)`), it was caught and
|
|
85
|
+
fixed before being considered done. That's a meaningfully higher bar of confidence than most of
|
|
86
|
+
this account's Rails-app work, which can't be executed in the sandbox it was built in.
|
|
87
|
+
|
|
88
|
+
That said: this is a heuristic tool, not full data-flow/taint analysis (that's what a mature tool
|
|
89
|
+
like Brakeman spends years building). Expect some false positives and false negatives — that's
|
|
90
|
+
normal for this class of tool, not a bug. An already-guarded call can still get flagged since
|
|
91
|
+
rules don't trace surrounding conditionals — always review a finding in its surrounding context
|
|
92
|
+
before acting on it.
|
|
93
|
+
|
|
94
|
+
## Security score
|
|
95
|
+
|
|
96
|
+
Every scan produces a single 0-100 score plus a letter grade (A-F), shown in the console summary
|
|
97
|
+
and as a badge at the very top of the HTML report:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Security Score: 11/100 (F)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The formula (`ReportRenderer#security_score` in `lib/scryer/report_renderer.rb`): every security
|
|
104
|
+
and dependency finding costs points, weighted by both its severity *and* its confidence (a
|
|
105
|
+
`low`-confidence `idor` finding costs less than a `high`-confidence `sql_injection` finding at the
|
|
106
|
+
same severity), combined via exponential decay from 100 rather than linear subtraction — one
|
|
107
|
+
critical finding visibly moves the score (100 → ~86) without a handful of findings driving any
|
|
108
|
+
real app straight to a hard-clamped 0.
|
|
109
|
+
|
|
110
|
+
Two things worth being precise about before you treat this number as meaningful:
|
|
111
|
+
|
|
112
|
+
- **It's not normalized by app size.** A 10-file app and a 1,000-file app with the same finding
|
|
113
|
+
*density* will score very differently here — this score reflects a scan's absolute finding
|
|
114
|
+
exposure, not a rate. That makes it useful for tracking *one project's own trend* over time (did
|
|
115
|
+
the next scan score higher or lower), not for comparing two differently-sized codebases against
|
|
116
|
+
each other.
|
|
117
|
+
- **Performance and code-quality findings aren't part of it.** This is a *security* score — only
|
|
118
|
+
security and dependency findings count. A slow app with clean security findings still scores
|
|
119
|
+
well here; check the "Performance"/"Code Quality" rows in the summary box separately for that.
|
|
120
|
+
|
|
121
|
+
Alongside the score, every scan also reports a **rule-level pass rate** — "how many of Scryer's
|
|
122
|
+
registered checks fired zero findings":
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Checks: 23/36 rules clean (63.9%)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This is the closest thing Scryer has to Brakeman's "X checks, Y warnings" framing — a coverage
|
|
129
|
+
signal, not a risk signal. It's deliberately a *different* number from the security score, and the
|
|
130
|
+
two won't always agree: a codebase can have a high clean rate (few distinct rules ever fire) and
|
|
131
|
+
still a low score (the few that did fire were severe and high-confidence), or the reverse (many
|
|
132
|
+
different rules each firing once, none of them serious). `ReportRenderer#rules_clean_rate` computes
|
|
133
|
+
it from `Scryer::RuleSet.all` (every registered security/performance/style rule) against which
|
|
134
|
+
rule_ids actually appeared in this scan's findings — dependency checks aren't counted here since
|
|
135
|
+
they're not backed by a `Scryer::Rule` subclass. Report both; neither alone tells the whole story.
|
|
136
|
+
|
|
137
|
+
The 10/F above is a real score from the acme-app example, not a cherry-picked good result — see
|
|
138
|
+
[A note on how this gem was actually verified](#a-note-on-how-this-gem-was-actually-verified) for
|
|
139
|
+
why every example in this README is real output.
|
|
140
|
+
|
|
141
|
+
## Accuracy benchmark
|
|
142
|
+
|
|
143
|
+
Every rule in this gem is heuristic pattern-matching — stated throughout this README, but until now
|
|
144
|
+
never backed by a measured number, just "expect some false positives, that's normal for this class
|
|
145
|
+
of tool." [`benchmark/`](../benchmark/) turns that into an actual precision/recall benchmark: a
|
|
146
|
+
hand-labeled corpus of small Ruby/Rails snippets per rule (`benchmark/corpus.rb`), each marked
|
|
147
|
+
`vulnerable` (should fire) or `safe` (should NOT fire — including deliberately close "near miss"
|
|
148
|
+
samples that resemble the vulnerable shape, not just obviously unrelated code), run through the
|
|
149
|
+
real `Scryer::Rule#scan` — the same code path an actual scan uses, not a simulation.
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
rake benchmark # or: ruby benchmark/run.rb
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Current numbers, across all 36 registered rules and 180 labeled samples:
|
|
156
|
+
|
|
157
|
+
| | TP | FN | FP | TN | Precision | Recall | F1 |
|
|
158
|
+
|---|---|---|---|---|---|---|---|
|
|
159
|
+
| **All 36 rules** | 82 | 3 | 1 | 94 | **98.8%** | **96.5%** | **97.6%** |
|
|
160
|
+
|
|
161
|
+
Two rules score below 100% — both are gaps this gem already disclosed in the rule's own top comment
|
|
162
|
+
before this benchmark existed, now actually measured instead of just asserted:
|
|
163
|
+
|
|
164
|
+
- **`graphql_missing_query_limits`** (50% precision, 50% recall on its 4 samples): misses a schema
|
|
165
|
+
that gets its query limits through a shared custom base class or an `include`d module — both
|
|
166
|
+
require cross-file resolution this per-file rule doesn't attempt.
|
|
167
|
+
- **`unbounded_table_scan`** (recall 66.7%): misses an unbounded scan once the query is assigned to
|
|
168
|
+
a variable before `.each` instead of chained directly (`orders = Order.where(...); orders.each`).
|
|
169
|
+
- **`mass_assignment`** (recall 66.7%): the one gap this benchmark actually *found*, not just
|
|
170
|
+
measured — see the CHANGELOG entry on the ivar-receiver fix. The corpus's other documented
|
|
171
|
+
false-negative sample for this rule (a namespaced `Admin::Order.create(...)` receiver) is still
|
|
172
|
+
an open, disclosed gap.
|
|
173
|
+
|
|
174
|
+
**Read `benchmark/README.md` before quoting any of these numbers** — this is a corpus we wrote
|
|
175
|
+
ourselves specifically to stress-test our own rules, not an independent third-party benchmark (the
|
|
176
|
+
way, say, the OWASP Benchmark project is for Java tools), and not real production code the way the
|
|
177
|
+
acme-app numbers used throughout this README are. Treat it as a lower bound on rigor (something
|
|
178
|
+
concrete was actually measured) and an upper bound on confidence (real-world false-positive/
|
|
179
|
+
false-negative rates on *your* codebase, with its own idioms and libraries, can and will differ).
|
|
180
|
+
|
|
181
|
+
## Scryer vs RuboCop vs Brakeman vs bundler-audit
|
|
182
|
+
|
|
183
|
+
Brakeman is the deeper, more mature tool for the security categories it's spent years on — real
|
|
184
|
+
taint/data-flow analysis, not heuristic pattern-matching (footnote [i]). RuboCop owns style/lint
|
|
185
|
+
conventions; Scryer stays out of that territory except for one narrow, deliberately-scoped check
|
|
186
|
+
(footnote [h]). Scryer isn't trying to out-analyze either of them at their own specialty. What it
|
|
187
|
+
does instead: look at security, performance, dependency, and code-quality findings *together* and
|
|
188
|
+
rank the result by severity, so "what should I fix first" has one answer instead of four separate
|
|
189
|
+
tool outputs (and four different severity scales) to reconcile by hand — see footnote [j]. If you
|
|
190
|
+
already run Brakeman for its deeper security analysis or RuboCop for style, keep doing that; Scryer
|
|
191
|
+
sits alongside them and adds the cross-category picture neither one (nor bundler-audit) produces.
|
|
192
|
+
|
|
193
|
+
| Capability | Scryer | RuboCop | Brakeman | bundler-audit |
|
|
194
|
+
|-------------------------------------------|:------:|:--------:|:--------:|:-------------:|
|
|
195
|
+
| Style/lint conventions | Partial [h] | ✅ | ❌ | ❌ |
|
|
196
|
+
| Rails security scanning | ✅ [i] | ❌ | ✅ | ❌ |
|
|
197
|
+
| Performance heuristics | ✅ | Partial [a] | ❌ | ❌ |
|
|
198
|
+
| Duplicate/similar code detection | ✅ | Partial [b] | ❌ | ❌ |
|
|
199
|
+
| Dependency vulnerability scanning | ✅ | ❌ | ❌ | ✅ |
|
|
200
|
+
| Runtime query analysis (N+1 in production) | ✅ | ❌ | ❌ | ❌ |
|
|
201
|
+
| HTML report | ✅ | Partial [c] | ✅ | ❌ |
|
|
202
|
+
| JSON report | ✅ | ✅ | ✅ | Limited [d] |
|
|
203
|
+
| SARIF report (GitHub Code Scanning, etc.) | ✅ | ❌ | ✅ | ❌ |
|
|
204
|
+
| Human-reviewable fix suggestions | ✅ | Partial [e] | Partial [f] | Limited [g] |
|
|
205
|
+
| Cross-category risk ranking ("fix this first") | ✅ [j] | ❌ | ❌ | ❌ |
|
|
206
|
+
| CWE / OWASP Top 10 category tags | ✅ [k] | ❌ | ✅ [k] | ❌ |
|
|
207
|
+
| Confidence level per finding | ✅ | ❌ | ✅ | ❌ |
|
|
208
|
+
| Official GitHub Action | ✅ [l] | ❌ | ❌ | ❌ |
|
|
209
|
+
| Baseline / new-vs-existing-findings diffing | ✅ [m] | ❌ | Partial [m] | ❌ |
|
|
210
|
+
| Overall security score | ✅ | ❌ | ❌ | ❌ |
|
|
211
|
+
| Fix verified against an actual re-scan | ✅ [n] | ❌ | ❌ | ❌ |
|
|
212
|
+
| Single command covering all of the above | ✅ | ❌ | ❌ | ❌ |
|
|
213
|
+
|
|
214
|
+
- **[a]** `rubocop-performance` adds some Ruby/Rails performance cops, but nothing like N+1-query
|
|
215
|
+
or missing-pagination detection.
|
|
216
|
+
- **[b]** A handful of cops catch exact-duplicate patterns (e.g. `Lint/DuplicateMethods`) — no
|
|
217
|
+
near-duplicate/similarity detection across methods.
|
|
218
|
+
- **[c]** RuboCop's built-in HTML formatter is a plain findings list, not an interactive report.
|
|
219
|
+
- **[d]** bundler-audit's output is primarily console text; no first-class JSON formatter.
|
|
220
|
+
- **[e]** RuboCop's `-A` auto-corrects many style violations directly — an automatic rewrite, not
|
|
221
|
+
a human-reviewable explanation, and only for auto-correctable cops.
|
|
222
|
+
- **[f]** Brakeman's warnings describe the issue and a confidence level, not a concrete
|
|
223
|
+
before/after code fix.
|
|
224
|
+
- **[g]** bundler-audit names the patched version to upgrade to; no code-level remediation (it
|
|
225
|
+
doesn't operate on your code at all, only `Gemfile.lock`).
|
|
226
|
+
- **[h]** One check only: a missing `# frozen_string_literal: true` magic comment
|
|
227
|
+
(`frozen_string_literal`, info severity). Everything else in RuboCop's style/lint domain —
|
|
228
|
+
naming, layout, quote style, line length, and hundreds more — is intentionally out of scope; see
|
|
229
|
+
[Skipping rules](./usage.md#skipping-rules) if you don't want even this one.
|
|
230
|
+
- **[i]** Heuristic pattern-matching, not taint/data-flow analysis — Brakeman traces whether user
|
|
231
|
+
input can actually *reach* a sink; Scryer checks whether a known-dangerous call shape and a
|
|
232
|
+
`params` reference appear in the same expression. That's a real precision gap on the harder
|
|
233
|
+
checks especially (`idor` in particular has real false-positive risk — see
|
|
234
|
+
[What Scryer detects](./rules.md#what-scryer-detects)), and it's why every finding says "review this,"
|
|
235
|
+
never "this is definitely a bug."
|
|
236
|
+
- **[j]** Each tool ranks findings within its own domain at best (e.g. Brakeman's per-warning
|
|
237
|
+
confidence level). None of them combine security, performance, dependency, and code-quality
|
|
238
|
+
findings into one ranked list — that's a different question than any single tool is built to
|
|
239
|
+
answer. `ReportRenderer#top_risks` sorts every severity-bearing finding from a scan (across all
|
|
240
|
+
four categories) by severity, shown as "Top priorities" in the console summary and at the top of
|
|
241
|
+
the HTML report — see the example near the top of this README.
|
|
242
|
+
- **[k]** Not a Scryer-only capability — Brakeman's own warnings already include a CWE reference.
|
|
243
|
+
Scryer's version is a fuller mapping (every one of its 31 security rules carries both a CWE ID
|
|
244
|
+
and an OWASP Top 10 (2021) category, aggregated into an OWASP coverage scorecard in every
|
|
245
|
+
report), but the underlying idea isn't new; see
|
|
246
|
+
[What Scryer detects](./rules.md#what-scryer-detects) for the honesty caveat on how this mapping was built
|
|
247
|
+
(Scryer's own best-effort categorization, not OWASP-audited).
|
|
248
|
+
- **[l]** [`action.yml`](../action.yml) in this repo — `uses: ramlaxmanyadav/scryer@<version>` runs
|
|
249
|
+
the scan and uploads SARIF to GitHub Code Scanning in one step, instead of hand-writing the
|
|
250
|
+
workflow YAML in [CI/CD integration](./usage.md#cicd-integration) yourself (that manual version still
|
|
251
|
+
works and is documented there too, for anyone who wants full control over the steps).
|
|
252
|
+
- **[m]** Brakeman has an ignore file (`brakeman -I`) that permanently silences specific warnings
|
|
253
|
+
by fingerprint — genuinely similar in mechanism to [Baseline mode](./usage.md#baseline-mode)'s
|
|
254
|
+
fingerprinting, marked Partial rather than ✅ here because it's a permanent suppression list you
|
|
255
|
+
maintain by hand, not an explicit "show me only what's new since this snapshot, and tell me
|
|
256
|
+
what got fixed" diff against a saved baseline the way `scryer --baseline` reports both.
|
|
257
|
+
- **[n]** `scryer verify --rule RULE_ID --file PATH` re-runs one rule against one file to confirm a
|
|
258
|
+
fix actually cleared it, and — when an `ai_client` is configured — the same check runs
|
|
259
|
+
automatically on every AI-rewritten `suggested_fix` before the report is written (see
|
|
260
|
+
[Verifying a fix](./fix-mode.md#verifying-a-fix)). RuboCop's `-A` rewrites code directly rather than verifying
|
|
261
|
+
a *suggested* fix; Brakeman and bundler-audit don't rewrite or re-check at all.
|
|
262
|
+
|
|
263
|
+
Brakeman's security analysis and RuboCop's style checks are still worth running on their own —
|
|
264
|
+
Scryer doesn't try to replace either, and running it alongside them costs nothing (different tools,
|
|
265
|
+
different config files, no shared state). What Scryer adds is the view none of them produce alone:
|
|
266
|
+
one ranked list of what's actually most worth fixing, built from security, performance,
|
|
267
|
+
dependency, and code-quality findings together.
|
|
268
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Contributing & Releasing
|
|
2
|
+
|
|
3
|
+
[← Back to Scryer — Ruby on Rails Security Auditor](../README.md)
|
|
4
|
+
|
|
5
|
+
## Extending it
|
|
6
|
+
|
|
7
|
+
Every rule is a small class extending `Scryer::Rule` — see `lib/scryer/rules/*.rb` (security),
|
|
8
|
+
`lib/scryer/performance_rules/*.rb` (performance), and `lib/scryer/style_rules/*.rb` (style) for
|
|
9
|
+
the pattern. A new rule file dropped into any of the three directories is picked up automatically
|
|
10
|
+
(rules self-register via `Rule.inherited` — no manual wiring needed, just `self.category =
|
|
11
|
+
"security"|"performance"|"style"`). `Scryer::Ast` has the tree-walking helpers used throughout.
|
|
12
|
+
|
|
13
|
+
Scryer's own rules have their own Minitest suite — `rake test` (from the repo root, not inside a
|
|
14
|
+
host app) runs a bad/clean fixture pair against every registered rule (`test/rule_fixtures_test.rb`),
|
|
15
|
+
plus a completeness check that fails loudly if a new rule ships with no fixture. Add an entry to
|
|
16
|
+
`RuleFixturesTest::FIXTURES` for any new or changed rule — and, for a genuine false positive/
|
|
17
|
+
negative found against real code (not just the regression pair), add it to
|
|
18
|
+
[`benchmark/corpus.rb`](../benchmark/corpus.rb) too (see [Accuracy benchmark](./architecture.md#accuracy-benchmark))
|
|
19
|
+
with a `note:` explaining the real-world shape it represents, so it's measured going forward
|
|
20
|
+
instead of just fixed once and forgotten.
|
|
21
|
+
|
|
22
|
+
## Releasing
|
|
23
|
+
|
|
24
|
+
Two GitHub Actions workflows handle this repo's own CI/CD (separate from `action.yml`, which is
|
|
25
|
+
what *users* of Scryer drop into their own app's CI):
|
|
26
|
+
|
|
27
|
+
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) — runs `rake test` + `rake benchmark` +
|
|
28
|
+
a syntax sweep + a `gem build` sanity check on every push/PR to `main`, across every Ruby version
|
|
29
|
+
`required_ruby_version` claims to support (2.7 through 3.3).
|
|
30
|
+
- [`.github/workflows/release.yml`](.github/workflows/release.yml) — pushing a tag matching `v*`
|
|
31
|
+
re-runs the test suite, then publishes to RubyGems via
|
|
32
|
+
[Trusted Publishing](https://guides.rubygems.org/trusted-publishing/) (OIDC — no API key stored
|
|
33
|
+
as a GitHub secret, nothing to leak or rotate).
|
|
34
|
+
|
|
35
|
+
Trusted Publishing needs a one-time setup on rubygems.org before the release workflow can actually
|
|
36
|
+
publish anything (this can't be done from the repo itself — it's a rubygems.org account action):
|
|
37
|
+
sign in, go to the scryer gem's page → **Trusted Publishers** → add one with owner
|
|
38
|
+
`ramlaxmanyadav`, repository `scryer`, workflow filename `release.yml`, no environment. After that,
|
|
39
|
+
`git tag vX.Y.Z && git push origin vX.Y.Z` is the entire release process — bump
|
|
40
|
+
`lib/scryer/version.rb`, update `CHANGELOG.md`, commit, tag, push the tag, and the workflow does
|
|
41
|
+
the rest.
|
|
42
|
+
|