Most Rails teams run two or three separate tools to feel reasonably confident about their codebase: Brakeman for security taint analysis, RuboCop for style, maybe bundler-audit for dependency CVEs. Each one is good at its job. None of them tells you, across all of that output, what to actually fix first. Scryer is the tool I built to answer that one question.
What Scryer actually does
Scryer scans security, performance, dependency, and code-quality issues in a single pass, then produces a ranked “Top priorities” list that spans all four categories — something none of the individual tools produce on their own. Install and run:
gem install scryerscryerAgainst a real 236-file production Rails app, that looks like:
Scryer Audit — 236 files scanned────────────────────────────────Security Score: 11/100 (F)Performance Score: 62/100 (D)Style Score: 100/100 (A)Dependency Score: 88/100 (B)Checks: 23/36 rules clean (63.9%)
Top priorities: 1. [critical] security — mass_assignment (app/helpers/api/v1/create_order_helper.rb:14) ...JSON report: tmp/scryer_report.jsonHTML report: tmp/scryer_report.htmlFor a Rails app specifically:
# Gemfilegroup :development do gem "scryer"endbundle installbin/rails generate scryer:installbin/rails scryer:reportThe generator writes exactly one file — config/initializers/scryer.rb — and nothing else. No routes, no migrations, no controllers.
Heuristic pattern-matching, not taint analysis — on purpose
Scryer parses every file once with Ruby’s stdlib Ripper, walks the resulting syntax tree, and runs each check (a Scryer::Rule subclass) against the shapes it finds. That’s deliberately not data-flow/taint analysis like Brakeman does — there’s no tracing of whether a value flowing into eval actually originated from user input. The tradeoff buys something concrete: zero runtime dependencies beyond Ruby’s own standard library, no Rails boot, no database connection, and a full scan of that 236-file app in about 2.5 seconds.
Note
Scryer and Brakeman aren’t competing for the same job. Brakeman’s taint analysis catches things pattern-matching can’t; Scryer’s cross-category ranking and single-command workflow cover ground Brakeman doesn’t attempt. I run both.
What it actually finds
A typical finding looks like this:
def create @invoice = Invoice.create(params[:invoice])end[CRITICAL] mass_assignment — app/controllers/invoices_controller.rb:3`create` receives `params` (or a subscript of it) directly, with no `.permit(...)` call.fix: Wrap the params in a strong-parameters method, e.g. `create(order_params)` with`def order_params; params.require(:order).permit(:status, :total); end`Across 32 security rules, every finding carries a CWE ID, an OWASP Top 10 (2021) category, a severity, and — importantly — a separate confidence level, because not every rule is equally precise. IDOR detection, for instance, is explicitly the least precise rule in the set; it’s flagged as such rather than pretending otherwise.
The rule set also goes further than most static scanners bother to: CORS misconfiguration (wildcard origin plus credentials: true), insecure JWT handling (alg: none, hardcoded secrets), GraphQL queries with no depth or complexity limits, and background jobs that get handed raw params instead of sanitized arguments.
Scoring that doesn’t pretend bigger apps are automatically worse
The four scores (Security, Performance, Style, Dependency) are computed independently via exponential decay from 100, weighted by severity and confidence — one critical finding alone moves a clean score from 100 down to roughly 86. They’re deliberately not normalized by codebase size, so they’re useful for tracking one project’s trend over time, not for comparing a 50-file app against a 5,000-file one.
A separate, differently-scoped number — Checks: 23/36 rules clean — reports how many of the 36 registered rules fired zero findings at all. That’s a coverage signal, not a risk signal, and the two are kept deliberately distinct.
Measuring its own accuracy
Rather than asserting precision, Scryer ships a hand-labeled benchmark corpus (benchmark/corpus.rb) of vulnerable and safe Ruby/Rails snippets — including deliberate near-misses — and runs every rule against it:
rake benchmarkCurrent numbers across all 36 rules and 180 labeled samples: 98.8% precision, 96.5% recall, 97.6% F1. Two rules score below 100% on recall — graphql_missing_query_limits misses cross-file resolution through a shared base class, and unbounded_table_scan misses a scan once the query result is assigned to a variable before .each. Both gaps are documented, not hidden.
Warning
This is a self-authored corpus built to stress-test Scryer’s own rules — it’s a lower bound on rigor compared to an independent benchmark like OWASP Benchmark, even if it’s a reasonable upper bound on confidence for the rules it covers.
Runtime checks, not just static ones
Two opt-in runtime watchers extend Scryer past static analysis entirely:
Query Watcher catches N+1 queries and unused eager-loading as they happen in a running app, using ActiveSupport::Notifications plus Module#prepend — a different mechanism from Bullet, not a replacement for it.
if Rails.env.development? || Rails.env.test? require "scryer/query_watcher" Scryer::QueryWatcher.enable! Rails.application.config.middleware.use Scryer::QueryWatcher::MiddlewareendAuthorization Watcher flags write requests (POST/PUT/PATCH/DELETE) that complete successfully without Pundit’s authorize/policy_scope or CanCanCan’s authorize! ever being invoked — the kind of gap that’s easy to introduce with one missing before_action and easy to miss in review.
Fixing findings, not just listing them
scryer fix can apply fixes directly. A small set of rules — frozen_string_literal, SQL injection (replacing string interpolation with a bind parameter), and a handful of Rails security config flips — are fixed mechanically with no AI involved. Everything else, Scryer will attempt via whatever LLM client you configure (any provider — the interface is just a callable that takes a prompt and returns a string), and then re-scans to verify the fix actually cleared the finding before keeping it:
scryer fix -r ./scryer_config.rb --dry-runscryer fix -r ./scryer_config.rbFixed: mass_assignment — app/controllers/orders_controller.rb:8 Switches to strong parameters so only explicitly permitted attributes reach `Order.new`.Re-scanning to verify every applied fix...Verified: all 2 applied fix(es) confirmed clean on a full re-scan.Nothing gets left in a “probably fixed” state — it’s either verified clean on re-scan or it isn’t applied.
CI integration
SARIF output feeds straight into GitHub Code Scanning. The bundled GitHub Action is a two-line add:
- uses: actions/checkout@v4- uses: ramlaxmanyadav/scryer@v1.2.0 with: ruby-version: '3.3'For teams that want to track findings over time rather than re-litigating the whole codebase on every PR, baseline mode diffs against a saved snapshot and matches findings by rule + file + the offending source text itself — not file/line — so an unrelated formatting change three lines up doesn’t produce a false “new finding.”
Scryer is MIT-licensed and available as a gem today. If you’re running Brakeman and RuboCop separately and still manually triaging which of forty findings to look at first, that gap is exactly what it’s for.
For the full rule reference, architecture notes, and CI setup guides, see the detailed Scryer documentation.