scryer 0.1.0 → 0.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 +47 -0
- data/LICENSE.txt +21 -0
- data/README.md +235 -46
- data/lib/scryer/ai_fix_suggester.rb +8 -6
- data/lib/scryer/cli.rb +51 -16
- data/lib/scryer/report_renderer.rb +13 -6
- data/lib/scryer/scanner.rb +4 -1
- data/lib/scryer/style_rules/frozen_string_literal_rule.rb +50 -0
- data/lib/scryer/version.rb +1 -1
- data/lib/scryer.rb +1 -0
- data/lib/tasks/scryer.rake +70 -26
- metadata +26 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1c8d4523447f9bdd1ea796fd27177efa18d6f726be6764e08aac1b18e05482b
|
|
4
|
+
data.tar.gz: de971058c3bfd42ad8ed9eabb6b633b305d1d10dc4971ea50e606fdefd573067
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 516425ec399996c7d20d20d6e8e222f188bf227cf911c401cdf514c084de22adcb9d7d15d2629cbb4cf79d0f3ef1f939058e64d24d14f0b3a3239d3922289270
|
|
7
|
+
data.tar.gz: 8c5efc1769526f6d8ed9b335a53129ac8f461156719e846d00f30e1ceae340f9b774bd83c89814689351b87c1200fb28fb5c76f4420bb4d86a296042717254d8
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. Format loosely follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
|
+
|
|
6
|
+
## [0.2.0] - 2026-08-11
|
|
7
|
+
|
|
8
|
+
- New `style` rule category (`Scryer::RuleSet` category `"style"`, alongside `security` and
|
|
9
|
+
`performance`): a deliberately narrow first check, `frozen_string_literal`, flags Ruby files
|
|
10
|
+
missing the `# frozen_string_literal: true` magic comment (info severity). Findings appear
|
|
11
|
+
everywhere other findings do (JSON/HTML/CSV reports, AI-assisted fix suggestions, `--skip`), and
|
|
12
|
+
are counted under "Code Quality" in the summary box alongside duplicate-code groups. This
|
|
13
|
+
remains the *only* style/lint check Scryer performs — see the README's Scryer-vs-RuboCop
|
|
14
|
+
comparison for why the rest of that territory is intentionally left to RuboCop.
|
|
15
|
+
- Fixed: `bin/rails scryer:report` now fails (non-zero exit) when security or dependency findings
|
|
16
|
+
are present, matching `scryer` (the standalone executable) and `scryer:audit_dependencies`. Previously
|
|
17
|
+
it always exited 0 regardless of findings, so it couldn't actually gate a CI job the way the
|
|
18
|
+
README described.
|
|
19
|
+
- Added `LICENSE.txt` (MIT) — declared via `spec.license` since 0.1.0 but not previously shipped
|
|
20
|
+
as an actual file.
|
|
21
|
+
- `spec.homepage` now points to the live docs site (https://ramlaxmanyadav.github.io/scryer/)
|
|
22
|
+
instead of the GitHub repo — the repo is still one click away via `source_code_uri` in
|
|
23
|
+
`spec.metadata`.
|
|
24
|
+
|
|
25
|
+
## [0.1.0] - 2026-08-10
|
|
26
|
+
|
|
27
|
+
Initial release.
|
|
28
|
+
|
|
29
|
+
- Static security scan (`Ripper`-based, no Rails/Bundler needed to run it): SQL injection, mass
|
|
30
|
+
assignment, command injection, hardcoded secrets, unsafe deserialization, XSS-prone unescaped
|
|
31
|
+
HTML, CSRF gaps, weak cryptography, open redirects.
|
|
32
|
+
- Performance heuristics: N+1 queries, missing pagination, inefficient per-record saves, unbounded
|
|
33
|
+
full-table iteration.
|
|
34
|
+
- Duplicate/near-duplicate code detection via token-normalized similarity.
|
|
35
|
+
- Dependency audit against `Gemfile.lock` — known-vulnerable gem versions (live OSV.dev query) and
|
|
36
|
+
insecure git/http sources — runs by default on every scan (`--no-deps`/`nodeps` to opt out).
|
|
37
|
+
- Rule skipping: `c.skip_rules` / `--skip RULE_ID` to silence a specific check without editing it.
|
|
38
|
+
- One-off single-gem vulnerability lookup: `scryer --check-gem NAME[:VERSION]`.
|
|
39
|
+
- JSON, self-contained HTML, and CSV report formats.
|
|
40
|
+
- Optional, provider-agnostic AI-assisted fix suggestions (`Scryer::AiFixSuggester` +
|
|
41
|
+
`Scryer::AiClient`) — bring any LLM via a `#call(prompt)`/`#complete(prompt)` interface.
|
|
42
|
+
- Opt-in runtime query watcher (`Scryer::QueryWatcher`) for N+1s and unused eager loading during
|
|
43
|
+
actual request handling.
|
|
44
|
+
- `scryer` standalone executable and `scryer:report`/`scryer:audit_dependencies` Rails rake tasks.
|
|
45
|
+
|
|
46
|
+
[0.2.0]: https://github.com/ramlaxmanyadav/scryer/releases/tag/v0.2.0
|
|
47
|
+
[0.1.0]: https://github.com/ramlaxmanyadav/scryer/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ram Laxman Yadav
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -1,38 +1,168 @@
|
|
|
1
|
-
# Scryer
|
|
1
|
+
# Scryer — Ruby & Rails Code Security Auditor
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Scryer is a Ruby static code analysis and security auditing tool. It analyzes Ruby and Rails
|
|
4
|
+
projects, audits `Gemfile.lock` dependencies for known vulnerabilities, identifies potential
|
|
5
|
+
security, performance, and code-quality issues, and gives you an actionable, human-reviewable
|
|
6
|
+
suggestion for fixing each one.
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
Most Rails teams already run several separate tools to cover code quality and security: RuboCop
|
|
9
|
+
for style, Brakeman for security, bundler-audit for dependency CVEs, Reek for code smells, plus
|
|
10
|
+
whatever custom scripts glue their outputs together in CI — different gems, different config
|
|
11
|
+
files, different report formats, different CI steps to maintain.
|
|
12
|
+
|
|
13
|
+
**Scryer's job is to be the one audit command that covers all of it** — security vulnerabilities,
|
|
14
|
+
performance problems, duplicate/smelly code, and dependency vulnerabilities — in a single scan
|
|
15
|
+
with a single report:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install scryer
|
|
19
|
+
scryer
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Scryer Audit — 236 files scanned
|
|
24
|
+
────────────────────────────────
|
|
25
|
+
|
|
26
|
+
Security 8 findings
|
|
27
|
+
Performance 10 findings
|
|
28
|
+
Code Quality 248 findings
|
|
29
|
+
Dependencies 24 findings
|
|
30
|
+
────────────────────────────────
|
|
31
|
+
Total 290 findings
|
|
32
|
+
|
|
33
|
+
JSON report: tmp/scryer_report.json
|
|
34
|
+
HTML report: tmp/scryer_report.html
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
That's real output from a scan of a live 236-file Rails app — not a mockup. See
|
|
38
|
+
[Scryer vs RuboCop vs Brakeman vs bundler-audit](#scryer-vs-rubocop-vs-brakeman-vs-bundler-audit)
|
|
39
|
+
below for exactly how it stacks up against the tools it's meant to consolidate.
|
|
40
|
+
|
|
41
|
+
### What Scryer detects
|
|
42
|
+
|
|
43
|
+
**Security**
|
|
44
|
+
|
|
45
|
+
* SQL injection
|
|
46
|
+
* Mass assignment
|
|
47
|
+
* Command injection
|
|
48
|
+
* Hardcoded secrets
|
|
49
|
+
* Unsafe deserialization
|
|
50
|
+
* XSS-prone HTML
|
|
51
|
+
* CSRF gaps
|
|
52
|
+
* Weak cryptography
|
|
53
|
+
* Open redirects
|
|
54
|
+
|
|
55
|
+
**Code quality**
|
|
56
|
+
|
|
57
|
+
* Near-duplicate code
|
|
58
|
+
* Repeated logic
|
|
59
|
+
* Potentially problematic code patterns
|
|
60
|
+
* Missing `frozen_string_literal` magic comment (the one deliberate, narrow style check — see
|
|
61
|
+
[comparison table](#scryer-vs-rubocop-vs-brakeman-vs-bundler-audit) for why not more)
|
|
62
|
+
|
|
63
|
+
**Performance**
|
|
64
|
+
|
|
65
|
+
* N+1 queries
|
|
66
|
+
* Missing pagination
|
|
67
|
+
* Inefficient per-record saves
|
|
68
|
+
* Unbounded full-table iteration
|
|
69
|
+
|
|
70
|
+
**Dependencies**
|
|
71
|
+
|
|
72
|
+
* Known dependency vulnerabilities via OSV.dev
|
|
73
|
+
* Insecure gem sources
|
|
74
|
+
|
|
75
|
+
### Example findings
|
|
76
|
+
|
|
77
|
+
Real output — three lines of deliberately flawed Rails code, scanned with plain `scryer`:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
def create
|
|
81
|
+
@invoice = Invoice.create(params[:invoice])
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
[CRITICAL] mass_assignment — app/controllers/invoices_controller.rb:3
|
|
87
|
+
`create` receives `params` (or a subscript of it) directly, with no `.permit(...)` call — every
|
|
88
|
+
attribute in the request can be set, including ones the form/API was never meant to expose (e.g.
|
|
89
|
+
`admin`, `role_id`).
|
|
90
|
+
|
|
91
|
+
fix: Wrap the params in a strong-parameters method, e.g. `create(order_params)` with
|
|
92
|
+
`def order_params; params.require(:order).permit(:status, :total); end` — only the explicitly
|
|
93
|
+
permitted keys get through.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
Invoice.where("customer_name = '#{name}'")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
[CRITICAL] sql_injection — app/controllers/invoices_controller.rb:9
|
|
102
|
+
`where` is called with a string built via interpolation, which lets user-controlled input change
|
|
103
|
+
the SQL executed.
|
|
104
|
+
|
|
105
|
+
fix: Use a parameterized form instead, e.g. `where("column = ?", value)` or the hash form
|
|
106
|
+
`where(column: value)` — both let Active Record escape the value safely instead of interpolating
|
|
107
|
+
it directly into SQL.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
@invoices = Invoice.where(status: "open")
|
|
112
|
+
@invoices.each { |invoice| invoice.account.name }
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
[WARNING] n_plus_one_query — app/controllers/invoices_controller.rb:14
|
|
117
|
+
`invoice.account` is called inside a loop — if `account` is an association, this issues a separate
|
|
118
|
+
query per iteration instead of one batched query (a classic N+1).
|
|
119
|
+
|
|
120
|
+
fix: Eager-load the association on the base query before the loop, e.g.
|
|
121
|
+
`invoices = Model.includes(:account).where(...)` (or add `:account` to an existing `.includes(...)`
|
|
122
|
+
call), so Rails fetches it in one extra query instead of one per record.
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Every finding follows this shape, whichever category it's in: exactly where the issue is, why it
|
|
126
|
+
matters, and a fix written against your actual code — not a generic paragraph you have to
|
|
127
|
+
translate into your own file.
|
|
128
|
+
|
|
129
|
+
### Reports
|
|
130
|
+
|
|
131
|
+
Generate detailed JSON or self-contained HTML reports:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
scryer -o report.json -o report.html
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Developer-friendly suggestions
|
|
138
|
+
|
|
139
|
+
Every finding includes a human-reviewable suggested fix. Scryer never automatically modifies your source code.
|
|
140
|
+
|
|
141
|
+
### Runtime analysis
|
|
142
|
+
|
|
143
|
+
Scryer can optionally monitor ActiveRecord queries at runtime to detect N+1 queries and unused eager loading.
|
|
144
|
+
|
|
145
|
+
### Designed for Ruby
|
|
146
|
+
|
|
147
|
+
Scryer uses Ruby's standard-library `Ripper` parser, allowing source analysis without requiring Rails or Bundler to run the static scan itself.
|
|
14
148
|
|
|
15
149
|
Several more things live alongside the static scan, each documented in its own section below:
|
|
16
150
|
|
|
17
151
|
- **Skipping rules** ([Skipping rules](#skipping-rules)): silence a specific rule by `rule_id`
|
|
18
152
|
(config-wide via `c.skip_rules`, or one-off via `--skip`) without editing or deleting it.
|
|
19
|
-
- A **
|
|
20
|
-
|
|
21
|
-
broad goal as [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
[OSV.dev](https://osv.dev) for known-vulnerable gem versions and insecure sources — the same
|
|
25
|
-
broad goal as [bundler-audit](https://github.com/rubysec/bundler-audit), built independently on a
|
|
26
|
-
different data source.
|
|
153
|
+
- A **dependency audit** ([Dependency audit](#dependency-audit)), on by default, that checks
|
|
154
|
+
`Gemfile.lock` against [OSV.dev](https://osv.dev) for known-vulnerable gem versions and insecure
|
|
155
|
+
sources — the same broad goal as [bundler-audit](https://github.com/rubysec/bundler-audit), built
|
|
156
|
+
independently on a different data source. Pass `--no-deps` (or the `nodeps` rake arg) for a fast,
|
|
157
|
+
fully offline run instead.
|
|
27
158
|
- **AI-assisted fix suggestions** ([AI-assisted fix suggestions](#ai-assisted-fix-suggestions)),
|
|
28
159
|
optional, provider-agnostic: rewrites each finding's `suggested_fix` against its actual code
|
|
29
160
|
using an LLM you configure — any LLM, not a specific vendor.
|
|
30
161
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
from the optional AI enrichment below — either way, it's text for a human to read and act on.
|
|
162
|
+
**Nothing is auto-applied.** A security or performance fix needs a human's judgment about the
|
|
163
|
+
surrounding code; this gem's job is to point at the issue and explain it clearly, not to rewrite
|
|
164
|
+
your files. This holds whether `suggested_fix` came from a rule's static template or from the
|
|
165
|
+
optional AI enrichment below — either way, it's text for a human to read and act on.
|
|
36
166
|
|
|
37
167
|
## A note on how this gem was actually verified
|
|
38
168
|
|
|
@@ -50,6 +180,50 @@ normal for this class of tool, not a bug. An already-guarded call can still get
|
|
|
50
180
|
rules don't trace surrounding conditionals — always review a finding in its surrounding context
|
|
51
181
|
before acting on it.
|
|
52
182
|
|
|
183
|
+
## Scryer vs RuboCop vs Brakeman vs bundler-audit
|
|
184
|
+
|
|
185
|
+
Scryer isn't trying to out-lint RuboCop or out-analyze Brakeman — where an existing tool
|
|
186
|
+
specializes, it's still worth running on its own; style/lint conventions are almost entirely
|
|
187
|
+
RuboCop's job, and Scryer stays out of that territory except for one narrow, deliberately-scoped
|
|
188
|
+
check (see footnote below). What Scryer actually replaces is *stitching several of these together
|
|
189
|
+
yourself*: it covers categories none of the others do alone (performance heuristics,
|
|
190
|
+
duplicate-code detection, runtime query analysis), and folds the categories they *do* cover into
|
|
191
|
+
one command and one report instead of several separate tools, configs, and CI steps.
|
|
192
|
+
|
|
193
|
+
| Capability | Scryer | RuboCop | Brakeman | bundler-audit |
|
|
194
|
+
|-------------------------------------------|:------:|:--------:|:--------:|:-------------:|
|
|
195
|
+
| Style/lint conventions | Partial [h] | ✅ | ❌ | ❌ |
|
|
196
|
+
| Rails security scanning | ✅ | ❌ | ✅ | ❌ |
|
|
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
|
+
| Human-reviewable fix suggestions | ✅ | Partial [e] | Partial [f] | Limited [g] |
|
|
204
|
+
| Single command covering all of the above | ✅ | ❌ | ❌ | ❌ |
|
|
205
|
+
|
|
206
|
+
- **[a]** `rubocop-performance` adds some Ruby/Rails performance cops, but nothing like N+1-query
|
|
207
|
+
or missing-pagination detection.
|
|
208
|
+
- **[b]** A handful of cops catch exact-duplicate patterns (e.g. `Lint/DuplicateMethods`) — no
|
|
209
|
+
near-duplicate/similarity detection across methods.
|
|
210
|
+
- **[c]** RuboCop's built-in HTML formatter is a plain findings list, not an interactive report.
|
|
211
|
+
- **[d]** bundler-audit's output is primarily console text; no first-class JSON formatter.
|
|
212
|
+
- **[e]** RuboCop's `-A` auto-corrects many style violations directly — an automatic rewrite, not
|
|
213
|
+
a human-reviewable explanation, and only for auto-correctable cops.
|
|
214
|
+
- **[f]** Brakeman's warnings describe the issue and a confidence level, not a concrete
|
|
215
|
+
before/after code fix.
|
|
216
|
+
- **[g]** bundler-audit names the patched version to upgrade to; no code-level remediation (it
|
|
217
|
+
doesn't operate on your code at all, only `Gemfile.lock`).
|
|
218
|
+
- **[h]** One check only: a missing `# frozen_string_literal: true` magic comment
|
|
219
|
+
(`frozen_string_literal`, info severity). Everything else in RuboCop's style/lint domain —
|
|
220
|
+
naming, layout, quote style, line length, and hundreds more — is intentionally out of scope; see
|
|
221
|
+
[Skipping rules](#skipping-rules) if you don't want even this one.
|
|
222
|
+
|
|
223
|
+
If you already run RuboCop for style, keep it — Scryer isn't a replacement for it. If you're
|
|
224
|
+
currently running Brakeman + bundler-audit + a duplicate-code linter as three separate steps,
|
|
225
|
+
Scryer is the "run one thing instead" option.
|
|
226
|
+
|
|
53
227
|
## Install
|
|
54
228
|
|
|
55
229
|
Scryer is a development-time analysis tool, not something a running production process needs —
|
|
@@ -80,13 +254,16 @@ scanned, the branch label).
|
|
|
80
254
|
bin/rails scryer:report # writes tmp/scryer_report.{json,html}
|
|
81
255
|
```
|
|
82
256
|
|
|
83
|
-
By default this writes both `tmp/scryer_report.json` and `tmp/scryer_report.html
|
|
84
|
-
|
|
85
|
-
|
|
257
|
+
By default this writes both `tmp/scryer_report.json` and `tmp/scryer_report.html`, and includes a
|
|
258
|
+
dependency audit against `Gemfile.lock` (needs network — see [Dependency audit](#dependency-audit);
|
|
259
|
+
pass the `nodeps` arg for a fast, fully offline run instead). Format and output path are
|
|
260
|
+
configurable via task args — any mix of `json`, `html`, `csv` plus at most one path (quote the
|
|
261
|
+
whole thing so your shell doesn't eat the brackets/commas, e.g.
|
|
86
262
|
`bin/rails 'scryer:report[json,doc/security_report.json]'`). Rake splits bracket args on every
|
|
87
263
|
comma, so each format is its own item in the list rather than one comma-joined string
|
|
88
|
-
(`scryer:report[json,html]` is two args, not `"json,html"` as one). At most one non-format
|
|
89
|
-
is accepted per call; giving two raises an error rather than guessing which one
|
|
264
|
+
(`scryer:report[json,html]` is two args, not `"json,html"` as one). At most one non-format,
|
|
265
|
+
non-`nodeps` token is accepted per call; giving two raises an error rather than guessing which one
|
|
266
|
+
you meant.
|
|
90
267
|
|
|
91
268
|
Outside a Rails app (or in CI, or anywhere you don't want a rake task), the gem also ships a
|
|
92
269
|
`scryer` executable — same idea as `brakeman -o report.json`, with `-o` repeatable and format
|
|
@@ -97,13 +274,17 @@ scryer # scans ., writes tmp/scryer_report.{j
|
|
|
97
274
|
scryer -o report.json # just one file, exact path, format from extension
|
|
98
275
|
scryer -o report.json -o report.html # as many outputs as you like, one -o each
|
|
99
276
|
scryer -p /path/to/app -o /tmp/out.html # -p sets the root to scan (default: cwd)
|
|
277
|
+
scryer --no-deps # skip the dependency audit for a fast offline run
|
|
100
278
|
scryer --help # full option list (--project-name, --branch, --version, ...)
|
|
101
279
|
```
|
|
102
280
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
281
|
+
Console output is a summary box across every category — see the box in the intro above for a real
|
|
282
|
+
example — followed by where each report was written.
|
|
283
|
+
|
|
284
|
+
It exits `0` when the scan is clean and `1` when there's at least one security or dependency
|
|
285
|
+
finding, so `scryer -o report.json` can gate a CI job the same way `brakeman -o report.json` does.
|
|
286
|
+
A `2` exit means a usage error (bad flag, unrecognized output extension) rather than anything
|
|
287
|
+
about the scan itself.
|
|
107
288
|
|
|
108
289
|
Or use the scanning engine directly as a library:
|
|
109
290
|
|
|
@@ -119,7 +300,7 @@ of checks that ran, a breakdown of warnings by type, every finding in detail, an
|
|
|
119
300
|
groups — laid out similarly to a Brakeman report. `tmp/scryer_report.json` has the same data in
|
|
120
301
|
machine-readable form, for feeding into your own dashboard or CI gate. A third format, `.csv`
|
|
121
302
|
(`scryer -o report.csv` / `rails 'scryer:report[csv]'`), is a flat one-row-per-finding table
|
|
122
|
-
(security + performance findings, plus dependency findings
|
|
303
|
+
(security + performance findings, plus dependency findings unless `--no-deps`/`nodeps` was used) —
|
|
123
304
|
handy for dropping into a spreadsheet or importing into a ticketing tool. It skips duplicate-code
|
|
124
305
|
groups, which don't reduce to a single actionable row.
|
|
125
306
|
|
|
@@ -222,12 +403,14 @@ library, so it works the same whether or not this happens to run under Bundler.
|
|
|
222
403
|
source was read or copied to build this.
|
|
223
404
|
|
|
224
405
|
```bash
|
|
225
|
-
bin/rails scryer:audit_dependencies # inside a Rails app
|
|
406
|
+
bin/rails scryer:audit_dependencies # inside a Rails app — dependency audit only, no static scan
|
|
226
407
|
```
|
|
227
408
|
|
|
228
|
-
This is the one part of Scryer that needs a live network connection (to reach OSV.dev)
|
|
229
|
-
|
|
230
|
-
|
|
409
|
+
This is the one part of Scryer that needs a live network connection (to reach OSV.dev), and it
|
|
410
|
+
runs automatically as part of every `scryer`/`scryer:report` scan — pass `--no-deps` (CLI) or the
|
|
411
|
+
`nodeps` arg (rake) for a fast, fully offline run instead. `scryer:audit_dependencies` above (and
|
|
412
|
+
`scryer --audit-deps` below) are for when you want *only* the dependency audit, the same way
|
|
413
|
+
`bundle-audit check` is a separate, standalone command from your test suite. Either way it exits
|
|
231
414
|
non-zero if anything is found, so it can gate CI.
|
|
232
415
|
|
|
233
416
|
Two checks run, and either can be called on its own as a library:
|
|
@@ -248,9 +431,11 @@ Scryer::DependencyAudit.vulnerable_gems(Rails.root.to_s) # needs network (OSV.
|
|
|
248
431
|
and the fixed version(s) to upgrade to.
|
|
249
432
|
|
|
250
433
|
**From the `scryer` executable** (outside a Rails app, or in CI): `scryer --audit-deps` runs the
|
|
251
|
-
same two checks standalone, same output/exit-code behavior as the rake task.
|
|
252
|
-
folds them into the normal `-o` report
|
|
253
|
-
*and* dependency findings together
|
|
434
|
+
same two checks standalone (no static scan), same output/exit-code behavior as the rake task above.
|
|
435
|
+
Plain `scryer` already folds them into the normal `-o` report — one HTML/JSON/CSV file covering
|
|
436
|
+
static findings *and* dependency findings together — so `--audit-deps` is only for when you want
|
|
437
|
+
dependency findings *without* the static scan. And for a single gem, without touching
|
|
438
|
+
`Gemfile.lock` at all:
|
|
254
439
|
|
|
255
440
|
```bash
|
|
256
441
|
scryer --check-gem rack # every advisory ever filed against rack, any version
|
|
@@ -360,8 +545,12 @@ in a host app for the full description (also in
|
|
|
360
545
|
|
|
361
546
|
## Extending it
|
|
362
547
|
|
|
363
|
-
Every rule is a small class extending `Scryer::Rule` — see `lib/scryer/rules/*.rb`
|
|
364
|
-
(
|
|
365
|
-
rule file dropped into
|
|
366
|
-
`Rule.inherited` — no manual wiring needed
|
|
367
|
-
helpers used throughout.
|
|
548
|
+
Every rule is a small class extending `Scryer::Rule` — see `lib/scryer/rules/*.rb` (security),
|
|
549
|
+
`lib/scryer/performance_rules/*.rb` (performance), and `lib/scryer/style_rules/*.rb` (style) for
|
|
550
|
+
the pattern. A new rule file dropped into any of the three directories is picked up automatically
|
|
551
|
+
(rules self-register via `Rule.inherited` — no manual wiring needed, just `self.category =
|
|
552
|
+
"security"|"performance"|"style"`). `Scryer::Ast` has the tree-walking helpers used throughout.
|
|
553
|
+
|
|
554
|
+
## License
|
|
555
|
+
|
|
556
|
+
MIT licensed.
|
|
@@ -35,13 +35,15 @@ module Scryer
|
|
|
35
35
|
finding
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
# Enhances every security/performance finding on a
|
|
39
|
-
# place. Runs across a small thread pool
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
38
|
+
# Enhances every security/performance/style finding on a
|
|
39
|
+
# Scanner::Result in place. Runs across a small thread pool
|
|
40
|
+
# (network-bound work, same pattern as
|
|
41
|
+
# DependencyAudit.vulnerable_gems) so a large finding count doesn't
|
|
42
|
+
# mean one-request-at-a-time. No-op if no client is configured —
|
|
43
|
+
# callers don't need to check first.
|
|
43
44
|
def enhance_result!(result, client: Scryer.configuration.ai_client, concurrency: 4)
|
|
44
|
-
enhance_many!(result.security_findings + result.performance_findings
|
|
45
|
+
enhance_many!(result.security_findings + result.performance_findings + result.style_findings,
|
|
46
|
+
client: client, concurrency: concurrency)
|
|
45
47
|
result
|
|
46
48
|
end
|
|
47
49
|
|
data/lib/scryer/cli.rb
CHANGED
|
@@ -33,10 +33,16 @@ module Scryer
|
|
|
33
33
|
|
|
34
34
|
result = Scanner.new(root: root, dirs: Scryer.configuration.dirs, skip_rules: skip_rules).call
|
|
35
35
|
|
|
36
|
+
# Dependency auditing (OSV.dev) runs by default — a single `scryer`
|
|
37
|
+
# invocation is meant to cover the same ground as RuboCop + Brakeman +
|
|
38
|
+
# bundler-audit + Reek run separately, and that story isn't true if
|
|
39
|
+
# the dependency half is silently skipped unless you remember a flag.
|
|
40
|
+
# `--no-deps` opts back out for a fast, fully offline run (e.g. no
|
|
41
|
+
# network in this environment, or you only want the static scan).
|
|
42
|
+
ran_deps = !options[:no_deps]
|
|
36
43
|
dependency_findings = []
|
|
37
|
-
if
|
|
38
|
-
@stdout.puts "Scryer:
|
|
39
|
-
@stdout.puts "Scryer: querying OSV.dev for known vulnerabilities (needs network)..."
|
|
44
|
+
if ran_deps
|
|
45
|
+
@stdout.puts "Scryer: querying OSV.dev for known-vulnerable gems (needs network)..."
|
|
40
46
|
dependency_findings = DependencyAudit.insecure_sources(root) + DependencyAudit.vulnerable_gems(root)
|
|
41
47
|
end
|
|
42
48
|
|
|
@@ -58,12 +64,7 @@ module Scryer
|
|
|
58
64
|
outputs = options[:outputs].empty? ? default_outputs(root) : options[:outputs]
|
|
59
65
|
outputs.each { |path| write_report(renderer, path) }
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
"#{result.security_findings.size} security findings, " \
|
|
63
|
-
"#{result.performance_findings.size} performance findings, " \
|
|
64
|
-
"#{result.duplicate_groups.size} duplicate groups" \
|
|
65
|
-
"#{options[:include_deps] ? ", #{dependency_findings.size} dependency findings" : ""}."
|
|
66
|
-
@stdout.puts "Report written to #{outputs.join(', ')}"
|
|
67
|
+
print_summary(result: result, dependency_findings: dependency_findings, ran_deps: ran_deps, outputs: outputs)
|
|
67
68
|
|
|
68
69
|
result.security_findings.empty? && dependency_findings.empty? ? 0 : 1
|
|
69
70
|
rescue UsageError => e
|
|
@@ -95,6 +96,42 @@ module Scryer
|
|
|
95
96
|
total.positive? ? 1 : 0
|
|
96
97
|
end
|
|
97
98
|
|
|
99
|
+
# The "one audit command" summary — a single scan's worth of every
|
|
100
|
+
# category Scryer covers (security, performance, duplicate/smelly code,
|
|
101
|
+
# dependencies), the same categories usually split across RuboCop +
|
|
102
|
+
# Brakeman + bundler-audit + Reek, side by side in one box.
|
|
103
|
+
def print_summary(result:, dependency_findings:, ran_deps:, outputs:)
|
|
104
|
+
# "Code Quality" is the umbrella label for both duplicate-code groups
|
|
105
|
+
# and rule-based style findings (e.g. frozen_string_literal) — two
|
|
106
|
+
# different detectors, same broad concern, one row in the box.
|
|
107
|
+
code_quality = result.duplicate_groups.size + result.style_findings.size
|
|
108
|
+
deps_count = ran_deps ? dependency_findings.size : nil
|
|
109
|
+
total = result.security_findings.size + result.performance_findings.size + code_quality + (deps_count || 0)
|
|
110
|
+
|
|
111
|
+
rows = [
|
|
112
|
+
["Security", result.security_findings.size],
|
|
113
|
+
["Performance", result.performance_findings.size],
|
|
114
|
+
["Code Quality", code_quality],
|
|
115
|
+
["Dependencies", deps_count]
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
divider = "─" * 32
|
|
119
|
+
@stdout.puts ""
|
|
120
|
+
@stdout.puts "Scryer Audit — #{result.files_scanned} files scanned"
|
|
121
|
+
@stdout.puts divider
|
|
122
|
+
@stdout.puts ""
|
|
123
|
+
rows.each { |label, count| @stdout.puts summary_row(label, count) }
|
|
124
|
+
@stdout.puts divider
|
|
125
|
+
@stdout.puts summary_row("Total", total)
|
|
126
|
+
@stdout.puts ""
|
|
127
|
+
outputs.each { |path| @stdout.puts "#{format_for(path).upcase} report: #{path}" }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def summary_row(label, count)
|
|
131
|
+
value = count.nil? ? "skipped (--no-deps)" : "#{count} finding#{"s" unless count == 1}"
|
|
132
|
+
"#{label.ljust(14)}#{value.rjust(20)}"
|
|
133
|
+
end
|
|
134
|
+
|
|
98
135
|
# One-off OSV.dev lookup for a single gem — no Gemfile.lock, no scan,
|
|
99
136
|
# no other network calls. `spec` is "name" or "name:version" (colon
|
|
100
137
|
# rather than a second CLI arg, so this stays a single -o-style flag).
|
|
@@ -128,13 +165,11 @@ module Scryer
|
|
|
128
165
|
"insecure git/http sources (offline), instead of running the normal static scan. " \
|
|
129
166
|
"Exits non-zero if anything is found, so this can gate CI the same way " \
|
|
130
167
|
"`bundle-audit check` does.") { options[:audit_deps] = true }
|
|
131
|
-
opts.on("--
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"together — and, with an AI client configured, AI-rewritten suggested fixes for " \
|
|
137
|
-
"all of them.") { options[:include_deps] = true }
|
|
168
|
+
opts.on("--no-deps",
|
|
169
|
+
"Skip the dependency audit (OSV.dev vulnerable gems + insecure git/http sources) " \
|
|
170
|
+
"that otherwise runs as part of every normal scan. Use this for a fast, fully " \
|
|
171
|
+
"offline run — e.g. no network available, or you only want the static " \
|
|
172
|
+
"findings.") { options[:no_deps] = true }
|
|
138
173
|
opts.on("--skip RULE_ID",
|
|
139
174
|
"Skip a rule by rule_id (repeatable) — e.g. a known false positive on this " \
|
|
140
175
|
"codebase. Adds to c.skip_rules for this run only; doesn't affect other " \
|
|
@@ -44,6 +44,7 @@ module Scryer
|
|
|
44
44
|
"parse_errors" => @result.parse_errors.map { |pe| { "file" => pe[:file], "error" => pe[:error] } },
|
|
45
45
|
"security_findings" => @result.security_findings.map(&:to_h),
|
|
46
46
|
"performance_findings" => @result.performance_findings.map(&:to_h),
|
|
47
|
+
"style_findings" => @result.style_findings.map(&:to_h),
|
|
47
48
|
"duplicate_groups" => @result.duplicate_groups.map { |g| duplicate_group_hash(g) },
|
|
48
49
|
"dependency_findings" => @dependency_findings.map(&:to_h)
|
|
49
50
|
}
|
|
@@ -66,6 +67,7 @@ module Scryer
|
|
|
66
67
|
rows = [CSV_HEADERS]
|
|
67
68
|
h["security_findings"].each { |f| rows << static_csv_row(f) }
|
|
68
69
|
h["performance_findings"].each { |f| rows << static_csv_row(f) }
|
|
70
|
+
h["style_findings"].each { |f| rows << static_csv_row(f) }
|
|
69
71
|
h["dependency_findings"].each { |f| rows << dependency_csv_row(f) }
|
|
70
72
|
|
|
71
73
|
rows.map { |row| row.map { |field| csv_field(field) }.join(",") }.join("\n")
|
|
@@ -75,7 +77,8 @@ module Scryer
|
|
|
75
77
|
h = as_hash
|
|
76
78
|
security = h["security_findings"]
|
|
77
79
|
performance = h["performance_findings"]
|
|
78
|
-
|
|
80
|
+
style = h["style_findings"]
|
|
81
|
+
all_findings = security + performance + style
|
|
79
82
|
by_severity = all_findings.group_by { |f| f["severity"] }
|
|
80
83
|
duplicate_groups = h["duplicate_groups"]
|
|
81
84
|
dependency_findings = h["dependency_findings"]
|
|
@@ -105,7 +108,7 @@ module Scryer
|
|
|
105
108
|
|
|
106
109
|
<section id="summary">
|
|
107
110
|
<h2>Summary</h2>
|
|
108
|
-
#{render_summary_table(security, performance, duplicate_groups, dependency_findings)}
|
|
111
|
+
#{render_summary_table(security, performance, style, duplicate_groups, dependency_findings)}
|
|
109
112
|
</section>
|
|
110
113
|
|
|
111
114
|
<section id="checks-performed">
|
|
@@ -185,19 +188,21 @@ module Scryer
|
|
|
185
188
|
"<table class=\"kv\">#{body}</table>"
|
|
186
189
|
end
|
|
187
190
|
|
|
188
|
-
def render_summary_table(security, performance, duplicate_groups, dependency_findings)
|
|
191
|
+
def render_summary_table(security, performance, style, duplicate_groups, dependency_findings)
|
|
189
192
|
sec_counts = count_by_severity(security)
|
|
190
193
|
perf_counts = count_by_severity(performance)
|
|
191
|
-
|
|
194
|
+
style_counts = count_by_severity(style)
|
|
195
|
+
total_counts = SEVERITY_ORDER.each_with_object({}) { |s, acc| acc[s] = sec_counts[s] + perf_counts[s] + style_counts[s] }
|
|
192
196
|
|
|
193
197
|
header = "<tr><th>Category</th>" + SEVERITY_ORDER.map { |s| "<th>#{SEVERITY_LABELS[s]}</th>" }.join + "<th>Total</th></tr>"
|
|
194
198
|
sec_row = summary_row("Security", sec_counts)
|
|
195
199
|
perf_row = summary_row("Performance", perf_counts)
|
|
200
|
+
style_row = summary_row("Style", style_counts)
|
|
196
201
|
total_row = summary_row("Total", total_counts, css_class: "total")
|
|
197
202
|
dup_row = "<tr><th>Duplicate code</th><td colspan=\"#{SEVERITY_ORDER.size}\">—</td><td>#{duplicate_groups.size} group(s)</td></tr>"
|
|
198
203
|
deps_row = "<tr><th>Dependency audit</th><td colspan=\"#{SEVERITY_ORDER.size}\">—</td><td>#{dependency_findings.size} finding(s)</td></tr>"
|
|
199
204
|
|
|
200
|
-
"<table class=\"summary\">#{header}#{sec_row}#{perf_row}#{dup_row}#{deps_row}#{total_row}</table>"
|
|
205
|
+
"<table class=\"summary\">#{header}#{sec_row}#{perf_row}#{style_row}#{dup_row}#{deps_row}#{total_row}</table>"
|
|
201
206
|
end
|
|
202
207
|
|
|
203
208
|
def summary_row(label, counts, css_class: nil)
|
|
@@ -221,9 +226,11 @@ module Scryer
|
|
|
221
226
|
def render_checks_performed
|
|
222
227
|
security_rules = rules_by_category["security"] || []
|
|
223
228
|
performance_rules = rules_by_category["performance"] || []
|
|
229
|
+
style_rules = rules_by_category["style"] || []
|
|
224
230
|
|
|
225
231
|
"<h3>Security (#{security_rules.size})</h3>#{checks_table(security_rules)}" \
|
|
226
|
-
"<h3>Performance (#{performance_rules.size})</h3>#{checks_table(performance_rules)}"
|
|
232
|
+
"<h3>Performance (#{performance_rules.size})</h3>#{checks_table(performance_rules)}" \
|
|
233
|
+
"<h3>Style (#{style_rules.size})</h3>#{checks_table(style_rules)}"
|
|
227
234
|
end
|
|
228
235
|
|
|
229
236
|
def checks_table(rules)
|
data/lib/scryer/scanner.rb
CHANGED
|
@@ -26,7 +26,7 @@ module Scryer
|
|
|
26
26
|
QUERY_SIMILARITY_THRESHOLD = 0.7
|
|
27
27
|
CACHE_SIMILARITY_THRESHOLD = 0.7
|
|
28
28
|
|
|
29
|
-
Result = Struct.new(:security_findings, :performance_findings, :duplicate_groups, :files_scanned, :parse_errors, keyword_init: true)
|
|
29
|
+
Result = Struct.new(:security_findings, :performance_findings, :style_findings, :duplicate_groups, :files_scanned, :parse_errors, keyword_init: true)
|
|
30
30
|
|
|
31
31
|
# `skip_rules` silences specific checks by rule_id (e.g. a known false
|
|
32
32
|
# positive on this codebase) without editing/removing the rule itself —
|
|
@@ -44,6 +44,7 @@ module Scryer
|
|
|
44
44
|
all_cache_calls = []
|
|
45
45
|
security_findings = []
|
|
46
46
|
performance_findings = []
|
|
47
|
+
style_findings = []
|
|
47
48
|
parse_errors = []
|
|
48
49
|
|
|
49
50
|
files.each do |abs_path|
|
|
@@ -69,6 +70,7 @@ module Scryer
|
|
|
69
70
|
case rule_class.category
|
|
70
71
|
when "security" then security_findings
|
|
71
72
|
when "performance" then performance_findings
|
|
73
|
+
when "style" then style_findings
|
|
72
74
|
end
|
|
73
75
|
next unless bucket
|
|
74
76
|
|
|
@@ -97,6 +99,7 @@ module Scryer
|
|
|
97
99
|
Result.new(
|
|
98
100
|
security_findings: security_findings,
|
|
99
101
|
performance_findings: performance_findings,
|
|
102
|
+
style_findings: style_findings,
|
|
100
103
|
duplicate_groups: duplicate_groups,
|
|
101
104
|
files_scanned: files.size,
|
|
102
105
|
parse_errors: parse_errors
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Scryer
|
|
2
|
+
module Rules
|
|
3
|
+
# Flags Ruby files with no `# frozen_string_literal: true` magic comment
|
|
4
|
+
# — the one deliberate, narrow style check Scryer makes (see the
|
|
5
|
+
# README's Scryer-vs-RuboCop comparison: everything else in
|
|
6
|
+
# style/lint conventions is intentionally left to RuboCop). Checked on
|
|
7
|
+
# raw source rather than the parsed sexp — a magic comment is lexical,
|
|
8
|
+
# not part of the AST — so this only needs the file's leading lines,
|
|
9
|
+
# not Ripper.sexp.
|
|
10
|
+
class FrozenStringLiteralRule < Rule
|
|
11
|
+
self.rule_id = "frozen_string_literal"
|
|
12
|
+
self.category = "style"
|
|
13
|
+
self.default_severity = "info"
|
|
14
|
+
self.title = "Missing `frozen_string_literal` magic comment"
|
|
15
|
+
|
|
16
|
+
MAGIC_COMMENT = /\A#\s*frozen_string_literal:\s*(true|false)\s*\z/i.freeze
|
|
17
|
+
|
|
18
|
+
def scan
|
|
19
|
+
return [] if source.strip.empty?
|
|
20
|
+
return [] if leading_comment_lines.any? { |line| MAGIC_COMMENT.match?(line.strip) }
|
|
21
|
+
|
|
22
|
+
[
|
|
23
|
+
finding(
|
|
24
|
+
line: 1,
|
|
25
|
+
message: "This file has no `# frozen_string_literal: true` magic comment — every " \
|
|
26
|
+
"string literal allocates a new String object at runtime instead of reusing " \
|
|
27
|
+
"a single frozen one.",
|
|
28
|
+
suggested_fix: "Add `# frozen_string_literal: true` as the first line of the file " \
|
|
29
|
+
"(after a shebang line, if any) — a cheap, safe default in modern " \
|
|
30
|
+
"Ruby. If a specific literal needs to stay mutable, call `.dup` on it " \
|
|
31
|
+
"explicitly at that call site."
|
|
32
|
+
)
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# The lines Ruby itself considers when looking for a magic comment:
|
|
39
|
+
# starting from the top of the file, skip a shebang line, then take
|
|
40
|
+
# every line up to (not including) the first line that isn't blank or
|
|
41
|
+
# a comment.
|
|
42
|
+
def leading_comment_lines
|
|
43
|
+
lines = source.each_line.to_a
|
|
44
|
+
lines.shift if lines.first&.start_with?("#!")
|
|
45
|
+
|
|
46
|
+
lines.take_while { |line| line.strip.empty? || line.lstrip.start_with?("#") }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/scryer/version.rb
CHANGED
data/lib/scryer.rb
CHANGED
|
@@ -15,6 +15,7 @@ require "scryer/ai_fix_suggester"
|
|
|
15
15
|
|
|
16
16
|
Dir[File.join(__dir__, "scryer", "rules", "*.rb")].sort.each { |f| require f }
|
|
17
17
|
Dir[File.join(__dir__, "scryer", "performance_rules", "*.rb")].sort.each { |f| require f }
|
|
18
|
+
Dir[File.join(__dir__, "scryer", "style_rules", "*.rb")].sort.each { |f| require f }
|
|
18
19
|
|
|
19
20
|
module Scryer
|
|
20
21
|
class Configuration
|
data/lib/tasks/scryer.rake
CHANGED
|
@@ -9,32 +9,35 @@ require "fileutils"
|
|
|
9
9
|
# Ruby nil-or-empty checks only.
|
|
10
10
|
namespace :scryer do
|
|
11
11
|
desc "Run Scryer and write a report. Args are any mix of json/html/csv (which formats " \
|
|
12
|
-
"to write — default json,html), the token '
|
|
13
|
-
"vulnerable gems + insecure git/http sources
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
12
|
+
"to write — default json,html), the token 'nodeps' (skip the dependency audit — OSV.dev " \
|
|
13
|
+
"vulnerable gems + insecure git/http sources — that otherwise runs by default; use for a " \
|
|
14
|
+
"fast, fully offline run), plus at most one path (a directory when writing more than one " \
|
|
15
|
+
"format, or an exact file for a single format). Rake splits bracket args on every comma, " \
|
|
16
|
+
"so pass each token as its own item rather than one comma-joined string. " \
|
|
17
|
+
"e.g. rails scryer:report, rails 'scryer:report[html]', " \
|
|
18
18
|
"rails 'scryer:report[json,doc/security.json]', rails 'scryer:report[json,html]', " \
|
|
19
|
-
"rails 'scryer:report[html,
|
|
19
|
+
"rails 'scryer:report[html,nodeps]', rails 'scryer:report[csv]'"
|
|
20
20
|
task :report, [:format] do |_, args|
|
|
21
21
|
root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
|
|
22
22
|
dirs = Scryer.configuration.dirs
|
|
23
23
|
|
|
24
24
|
# Ignore the declared :format name and read every positional value Rake
|
|
25
25
|
# was given (args.to_a) — the whole point is accepting a variable number
|
|
26
|
-
# of format/
|
|
27
|
-
formats, path_arg,
|
|
26
|
+
# of format/nodeps tokens plus one path, which a single named param can't do.
|
|
27
|
+
formats, path_arg, run_deps = parse_report_args(args.to_a)
|
|
28
28
|
|
|
29
29
|
skip_rules = Scryer.configuration.skip_rules
|
|
30
30
|
puts "Scryer: skipping #{skip_rules.join(', ')}." if skip_rules.any?
|
|
31
31
|
|
|
32
32
|
result = Scryer::Scanner.new(root: root, dirs: dirs, skip_rules: skip_rules).call
|
|
33
33
|
|
|
34
|
+
# Dependency auditing (OSV.dev) runs by default — a single scryer:report
|
|
35
|
+
# run is meant to cover the same ground as RuboCop + Brakeman +
|
|
36
|
+
# bundler-audit + Reek run separately. Pass the 'nodeps' token for a
|
|
37
|
+
# fast, fully offline run instead.
|
|
34
38
|
dependency_findings = []
|
|
35
|
-
if
|
|
36
|
-
puts "Scryer:
|
|
37
|
-
puts "Scryer: querying OSV.dev for known vulnerabilities (needs network)..."
|
|
39
|
+
if run_deps
|
|
40
|
+
puts "Scryer: querying OSV.dev for known-vulnerable gems (needs network)..."
|
|
38
41
|
dependency_findings = Scryer::DependencyAudit.insecure_sources(root) + Scryer::DependencyAudit.vulnerable_gems(root)
|
|
39
42
|
end
|
|
40
43
|
|
|
@@ -65,12 +68,17 @@ namespace :scryer do
|
|
|
65
68
|
File.write(path, content)
|
|
66
69
|
end
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
ScryerTasks.print_summary(result: result, dependency_findings: dependency_findings, ran_deps: run_deps, paths: paths)
|
|
72
|
+
|
|
73
|
+
# Same gate as the `scryer` executable and scryer:audit_dependencies
|
|
74
|
+
# below: fail the task (and so the CI job running it) on any security or
|
|
75
|
+
# dependency finding, so `bin/rails scryer:report` gates a build the
|
|
76
|
+
# same way `brakeman` or `bundle-audit check` would. Performance/style
|
|
77
|
+
# findings are advisory only and never fail the task.
|
|
78
|
+
if result.security_findings.any? || dependency_findings.any?
|
|
79
|
+
abort("Scryer: found #{result.security_findings.size} security finding(s) and " \
|
|
80
|
+
"#{dependency_findings.size} dependency finding(s).")
|
|
81
|
+
end
|
|
74
82
|
end
|
|
75
83
|
|
|
76
84
|
desc "Check Gemfile.lock for known-vulnerable gem versions (via OSV.dev — needs network) " \
|
|
@@ -98,17 +106,17 @@ namespace :scryer do
|
|
|
98
106
|
|
|
99
107
|
VALID_FORMATS = %w[json html csv].freeze
|
|
100
108
|
EXTENSION_FOR_FORMAT = { "json" => "json", "html" => "html", "csv" => "csv" }.freeze
|
|
101
|
-
|
|
109
|
+
NO_DEPS_TOKEN = "nodeps".freeze
|
|
102
110
|
|
|
103
111
|
# tokens is every bracket arg Rake was given, e.g. %w[json doc/security.json] or
|
|
104
|
-
# %w[json html] or %w[html
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
112
|
+
# %w[json html] or %w[html nodeps] or []. Returns [formats, path_arg,
|
|
113
|
+
# run_deps] — any token matching a known format is a format, the literal
|
|
114
|
+
# "nodeps" token opts *out* of the dependency audit that otherwise runs by
|
|
115
|
+
# default, and at most one other token is allowed, which is the path.
|
|
108
116
|
def parse_report_args(tokens)
|
|
109
117
|
tokens = tokens.map { |t| blank_to_nil(t) }.compact
|
|
110
|
-
|
|
111
|
-
tokens = tokens.reject { |t| t.downcase ==
|
|
118
|
+
run_deps = tokens.none? { |t| t.downcase == NO_DEPS_TOKEN }
|
|
119
|
+
tokens = tokens.reject { |t| t.downcase == NO_DEPS_TOKEN }
|
|
112
120
|
|
|
113
121
|
format_tokens, other_tokens = tokens.partition { |t| VALID_FORMATS.include?(t.downcase) }
|
|
114
122
|
|
|
@@ -119,7 +127,7 @@ namespace :scryer do
|
|
|
119
127
|
formats = format_tokens.map(&:downcase).uniq
|
|
120
128
|
formats = %w[json html] if formats.empty?
|
|
121
129
|
|
|
122
|
-
[formats, other_tokens.first,
|
|
130
|
+
[formats, other_tokens.first, run_deps]
|
|
123
131
|
end
|
|
124
132
|
|
|
125
133
|
# No path given: default filenames under tmp/. A path given with a single
|
|
@@ -169,4 +177,40 @@ module ScryerTasks
|
|
|
169
177
|
rescue StandardError
|
|
170
178
|
nil
|
|
171
179
|
end
|
|
180
|
+
|
|
181
|
+
# The "one audit command" summary — a single scan's worth of every
|
|
182
|
+
# category Scryer covers (security, performance, duplicate/smelly code,
|
|
183
|
+
# dependencies), the same categories usually split across RuboCop +
|
|
184
|
+
# Brakeman + bundler-audit + Reek, side by side in one box.
|
|
185
|
+
def print_summary(result:, dependency_findings:, ran_deps:, paths:)
|
|
186
|
+
# "Code Quality" is the umbrella label for both duplicate-code groups
|
|
187
|
+
# and rule-based style findings (e.g. frozen_string_literal) — two
|
|
188
|
+
# different detectors, same broad concern, one row in the box.
|
|
189
|
+
code_quality = result.duplicate_groups.size + result.style_findings.size
|
|
190
|
+
deps_count = ran_deps ? dependency_findings.size : nil
|
|
191
|
+
total = result.security_findings.size + result.performance_findings.size + code_quality + (deps_count || 0)
|
|
192
|
+
|
|
193
|
+
rows = [
|
|
194
|
+
["Security", result.security_findings.size],
|
|
195
|
+
["Performance", result.performance_findings.size],
|
|
196
|
+
["Code Quality", code_quality],
|
|
197
|
+
["Dependencies", deps_count]
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
divider = "─" * 32
|
|
201
|
+
puts ""
|
|
202
|
+
puts "Scryer Audit — #{result.files_scanned} files scanned"
|
|
203
|
+
puts divider
|
|
204
|
+
puts ""
|
|
205
|
+
rows.each { |label, count| puts summary_row(label, count) }
|
|
206
|
+
puts divider
|
|
207
|
+
puts summary_row("Total", total)
|
|
208
|
+
puts ""
|
|
209
|
+
paths.each { |format, path| puts "#{format.upcase} report: #{path}" }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def summary_row(label, count)
|
|
213
|
+
value = count.nil? ? "skipped (nodeps)" : "#{count} finding#{"s" unless count == 1}"
|
|
214
|
+
"#{label.ljust(14)}#{value.rjust(20)}"
|
|
215
|
+
end
|
|
172
216
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scryer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ram Laxman Yadav
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -25,23 +25,26 @@ dependencies:
|
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '13.0'
|
|
27
27
|
description: |
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
One scan, one report: security, performance, duplicate-code, and dependency audits for Ruby
|
|
29
|
+
and Rails — the ground Brakeman, bundler-audit, Reek, and custom glue scripts usually split
|
|
30
|
+
between them, covered by a single `scryer` command. (Style/lint is RuboCop's job — Scryer
|
|
31
|
+
doesn't touch that.)
|
|
32
|
+
|
|
33
|
+
Detects SQL injection, mass assignment, hardcoded secrets, XSS, weak crypto, and more; N+1
|
|
34
|
+
queries, missing pagination, and other performance heuristics; near-duplicate code; and
|
|
35
|
+
known-vulnerable gems via a live OSV.dev dependency audit — on by default, every run.
|
|
36
|
+
|
|
37
|
+
Every finding includes a human-reviewable suggested fix — never auto-applied, optionally
|
|
38
|
+
rewritten against your actual code by any LLM you configure. Reports in JSON, self-contained
|
|
39
|
+
HTML, or CSV. Zero runtime dependencies beyond Ruby's own stdlib.
|
|
39
40
|
email:
|
|
40
41
|
executables:
|
|
41
42
|
- scryer
|
|
42
43
|
extensions: []
|
|
43
44
|
extra_rdoc_files: []
|
|
44
45
|
files:
|
|
46
|
+
- CHANGELOG.md
|
|
47
|
+
- LICENSE.txt
|
|
45
48
|
- README.md
|
|
46
49
|
- exe/scryer
|
|
47
50
|
- lib/generators/scryer/USAGE
|
|
@@ -77,12 +80,18 @@ files:
|
|
|
77
80
|
- lib/scryer/rules/weak_crypto_rule.rb
|
|
78
81
|
- lib/scryer/rules/xss_unsafe_html_rule.rb
|
|
79
82
|
- lib/scryer/scanner.rb
|
|
83
|
+
- lib/scryer/style_rules/frozen_string_literal_rule.rb
|
|
80
84
|
- lib/scryer/version.rb
|
|
81
85
|
- lib/tasks/scryer.rake
|
|
82
|
-
homepage: https://github.
|
|
86
|
+
homepage: https://ramlaxmanyadav.github.io/scryer/
|
|
83
87
|
licenses:
|
|
84
88
|
- MIT
|
|
85
|
-
metadata:
|
|
89
|
+
metadata:
|
|
90
|
+
source_code_uri: https://github.com/ramlaxmanyadav/scryer
|
|
91
|
+
documentation_uri: https://ramlaxmanyadav.github.io/scryer/
|
|
92
|
+
changelog_uri: https://github.com/ramlaxmanyadav/scryer/blob/main/CHANGELOG.md
|
|
93
|
+
bug_tracker_uri: https://github.com/ramlaxmanyadav/scryer/issues
|
|
94
|
+
rubygems_mfa_required: 'true'
|
|
86
95
|
post_install_message:
|
|
87
96
|
rdoc_options: []
|
|
88
97
|
require_paths:
|
|
@@ -101,6 +110,6 @@ requirements: []
|
|
|
101
110
|
rubygems_version: 3.5.11
|
|
102
111
|
signing_key:
|
|
103
112
|
specification_version: 4
|
|
104
|
-
summary:
|
|
105
|
-
|
|
113
|
+
summary: Ruby/Rails security & quality auditor — one command instead of Brakeman +
|
|
114
|
+
bundler-audit + Reek.
|
|
106
115
|
test_files: []
|