scryer 1.2.1 → 1.3.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 +129 -10
- data/README.md +9 -4
- data/docs/architecture.md +45 -36
- data/docs/rails-integration.md +182 -1
- data/docs/rules.md +8 -1
- data/docs/usage.md +5 -2
- data/lib/generators/scryer/install_generator.rb +1 -0
- data/lib/generators/scryer/templates/scryer_initializer.rb +22 -0
- data/lib/scryer/ai_client.rb +1 -0
- data/lib/scryer/ai_fix_suggester.rb +1 -0
- data/lib/scryer/apm.rb +458 -0
- data/lib/scryer/ast.rb +53 -0
- data/lib/scryer/authorization_watcher.rb +1 -0
- data/lib/scryer/baseline.rb +1 -0
- data/lib/scryer/cache_extractor.rb +1 -0
- data/lib/scryer/cli.rb +19 -11
- data/lib/scryer/colorizer.rb +1 -0
- data/lib/scryer/dependency_audit.rb +1 -0
- data/lib/scryer/dependency_fixer.rb +1 -0
- data/lib/scryer/duplicate_detector.rb +1 -0
- data/lib/scryer/finding.rb +1 -0
- data/lib/scryer/fix_runner.rb +1 -0
- data/lib/scryer/fix_verifier.rb +1 -0
- data/lib/scryer/mechanical_fixer.rb +1 -0
- data/lib/scryer/method_extractor.rb +1 -0
- data/lib/scryer/minitest.rb +1 -0
- data/lib/scryer/performance_rules/inefficient_save_loop_rule.rb +1 -0
- data/lib/scryer/performance_rules/missing_pagination_rule.rb +1 -0
- data/lib/scryer/performance_rules/n_plus_one_query_rule.rb +2 -0
- data/lib/scryer/performance_rules/unbounded_table_scan_rule.rb +9 -1
- data/lib/scryer/query_extractor.rb +1 -0
- data/lib/scryer/query_watcher.rb +1 -0
- data/lib/scryer/railtie.rb +64 -4
- data/lib/scryer/report_renderer.rb +161 -99
- data/lib/scryer/rspec.rb +1 -0
- data/lib/scryer/rule.rb +15 -2
- data/lib/scryer/rule_set.rb +1 -0
- data/lib/scryer/rules/action_cable_forgery_protection_rule.rb +1 -0
- data/lib/scryer/rules/active_storage_inline_disposition_rule.rb +1 -0
- data/lib/scryer/rules/active_storage_missing_content_type_validation_rule.rb +1 -0
- data/lib/scryer/rules/authentication_bypass_rule.rb +1 -0
- data/lib/scryer/rules/command_injection_rule.rb +1 -0
- data/lib/scryer/rules/consider_all_requests_local_rule.rb +1 -0
- data/lib/scryer/rules/cors_misconfiguration_rule.rb +1 -0
- data/lib/scryer/rules/csrf_protection_rule.rb +1 -0
- data/lib/scryer/rules/dangerous_eval_rule.rb +117 -0
- data/lib/scryer/rules/force_ssl_rule.rb +1 -0
- data/lib/scryer/rules/graphql_missing_query_limits_rule.rb +1 -0
- data/lib/scryer/rules/hardcoded_basic_auth_rule.rb +1 -0
- data/lib/scryer/rules/hardcoded_secret_key_base_rule.rb +1 -0
- data/lib/scryer/rules/hardcoded_secret_rule.rb +1 -0
- data/lib/scryer/rules/host_authorization_disabled_rule.rb +1 -0
- data/lib/scryer/rules/idor_rule.rb +6 -14
- data/lib/scryer/rules/insecure_cookie_serializer_rule.rb +1 -0
- data/lib/scryer/rules/job_raw_params_rule.rb +1 -0
- data/lib/scryer/rules/jwt_insecure_rule.rb +1 -0
- data/lib/scryer/rules/mass_assignment_rule.rb +10 -16
- data/lib/scryer/rules/missing_authorization_rule.rb +1 -0
- data/lib/scryer/rules/missing_policy_scope_rule.rb +2 -9
- data/lib/scryer/rules/open_redirect_rule.rb +1 -0
- data/lib/scryer/rules/path_traversal_rule.rb +1 -0
- data/lib/scryer/rules/security_headers_rule.rb +1 -0
- data/lib/scryer/rules/sql_injection_rule.rb +1 -0
- data/lib/scryer/rules/ssrf_rule.rb +1 -0
- data/lib/scryer/rules/unsafe_deserialization_rule.rb +1 -0
- data/lib/scryer/rules/verbose_production_log_level_rule.rb +1 -0
- data/lib/scryer/rules/weak_crypto_rule.rb +1 -0
- data/lib/scryer/rules/weak_session_cookie_rule.rb +1 -0
- data/lib/scryer/rules/xss_unsafe_html_rule.rb +1 -0
- data/lib/scryer/scanner.rb +126 -7
- data/lib/scryer/style_rules/frozen_string_literal_rule.rb +1 -0
- data/lib/scryer/version.rb +2 -1
- data/lib/scryer.rb +49 -0
- data/lib/tasks/scryer.rake +8 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3d7873062d73beb49e505138b100a9507d676b2d5a13838ba382fedb9ac31a2e
|
|
4
|
+
data.tar.gz: 6f9168a62835d3aba9a583720fcfea45c5271ddb24d16773023a298566522ad0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 784555c1e3b96c43ec90f3151fc1beeb2abe90d1905dd897b4a40b33c44a63d00173a07705f44fbefbfb98c53342f328fffb57d1f2d5c196002ee30c3037b9c1
|
|
7
|
+
data.tar.gz: cf1e78fa3112a1ad8cf28a8686ec59a8ce5e221945ef0b0eb3d26a9ea5c8b943bc36e49547de6bf6e5770623c5e57f1377c603a8d50401585a88a00eedb0d3ad
|
data/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,126 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Format loosely follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
-
## [
|
|
6
|
+
## [1.3.0] - 2026-09-21
|
|
7
|
+
|
|
8
|
+
- **Added: `Scryer::APM`, runtime method-level tracing with New Relic and OpenTelemetry
|
|
9
|
+
integration** (see README's "Runtime method tracing (APM)" section in
|
|
10
|
+
`docs/rails-integration.md`). Wraps configured classes' own methods (`Module#prepend`, the same
|
|
11
|
+
technique `QueryWatcher` already uses — no `TracePoint`) and, when a supported provider's SDK is
|
|
12
|
+
loaded, creates a real nested span per call via *that SDK's own* public API: New Relic via
|
|
13
|
+
`NewRelic::Agent::Tracer.start_segment`, OpenTelemetry via
|
|
14
|
+
`OpenTelemetry.tracer_provider.tracer(...).start_span` with `OpenTelemetry::Context.attach`/
|
|
15
|
+
`.detach` for correct parent/child nesting. Only `instrumentation: :selective` is implemented
|
|
16
|
+
(`:discovery`/`:deep_trace` raise a clear "not implemented yet" error rather than silently
|
|
17
|
+
no-opping); only `provider: :new_relic` and `provider: :opentelemetry` are implemented (no
|
|
18
|
+
Datadog, no other vendor SDK). Zero new gem dependencies — both providers are soft-detected
|
|
19
|
+
(`defined?(NewRelic::Agent::Tracer)` / `defined?(OpenTelemetry::Trace)`), never required; a host
|
|
20
|
+
app adds whichever SDK it wants to its own Gemfile. 29 tests in `test/apm_test.rb` (nesting/
|
|
21
|
+
parent-child spans for both providers, exception handling, kwargs/blocks/visibility
|
|
22
|
+
preservation, inherited-method exclusion, duplicate-instrumentation guard, sampling, redaction,
|
|
23
|
+
thread isolation, New Relic segment creation + exporter-failure safety, OpenTelemetry span
|
|
24
|
+
creation + start-failure safety, Rack middleware, config flow-through) plus a new
|
|
25
|
+
`benchmark/apm_overhead.rb` with measured (not estimated) per-call overhead numbers — see the
|
|
26
|
+
doc section for actual figures and what they do/don't cover. `Scryer::Railtie` now also
|
|
27
|
+
auto-wires APM (`require`/`Scryer::APM.enable!`/middleware installation) whenever
|
|
28
|
+
`c.apm.enabled = true`, so that flag plus `c.apm.include` is genuinely sufficient in an
|
|
29
|
+
initializer — no separate manual wiring needed. This boot-time path is held to a stricter safety
|
|
30
|
+
bar than the rest of the public API: any failure while auto-enabling (a bad `include` entry, an
|
|
31
|
+
unimplemented `instrumentation` mode, a `require` failure) is caught — `rescue Exception`, not
|
|
32
|
+
just `StandardError`, deliberately, since a packaging-related `LoadError` is a `ScriptError` —
|
|
33
|
+
logged, and leaves APM cleanly disabled rather than ever failing app boot. Likewise, a single
|
|
34
|
+
method that fails to instrument (an internal error, not a config typo) no longer aborts the rest
|
|
35
|
+
of that class or `include` list. `Scryer::APM.enable!` called directly (console, tests) is
|
|
36
|
+
unchanged and still raises loudly for an unimplemented mode — only the automatic Railtie path
|
|
37
|
+
degrades gracefully. 5 more tests in `test/railtie_apm_test.rb` cover that boot-time path
|
|
38
|
+
directly (34 new tests total).
|
|
39
|
+
|
|
40
|
+
## [1.2.2] - 2026-09-12
|
|
41
|
+
|
|
42
|
+
- HTML report redesign: the top-of-report score panel is now a card (soft shadow, rounded
|
|
43
|
+
corners) instead of a flat box, the four grade badges are gradient-filled rounded squares
|
|
44
|
+
instead of plain circles, and the severity bars use a CSS grid for consistent label/track/count
|
|
45
|
+
alignment instead of flexbox eyeballing. Purely visual — no change to report content or data.
|
|
46
|
+
- Fixed: `Scryer::CLI#git` shelled out via backtick interpolation
|
|
47
|
+
(`` `git -C #{Shellwords.escape(root)} #{cmd}` ``) — flagged by Scryer's own `command_injection`
|
|
48
|
+
rule on a self-scan. Replaced with `Open3.capture3("git", "-C", root, *cmd)`, an argv array with
|
|
49
|
+
no shell involved at all, so there's nothing to escape or inject regardless of what `root`
|
|
50
|
+
contains. `cmd` callers now pass argv words instead of a pre-joined string.
|
|
51
|
+
- Added the `# frozen_string_literal: true` magic comment to every file under `lib/` that was
|
|
52
|
+
missing it (66 files) — this gem's own `frozen_string_literal` rule fired on itself.
|
|
53
|
+
- Fixed: `unbounded_table_scan` and `n_plus_one_query` false-positived on any bare
|
|
54
|
+
`Const.all`/`.where`/`.order`-shaped call, even when `Const` was a plain Ruby module/class with
|
|
55
|
+
no ActiveRecord ancestry (e.g. this gem's own `RuleSet.all.each`) — neither rule consulted
|
|
56
|
+
`known_models`/`known_non_models` at all. Both now gate their match through
|
|
57
|
+
`Ast.likely_model_name?`, same as `IdorRule` already did. `Scanner#collect_class_declarations`
|
|
58
|
+
also now records every `module X` declaration as a "definitely not a model" signal (a bare
|
|
59
|
+
module can never be `< ActiveRecord::Base`), alongside the existing no-superclass-class signal.
|
|
60
|
+
- **Breaking (report shape): one blended `Security Score` replaced with four independent
|
|
61
|
+
scores** — `security_score`, `performance_score`, `style_score`, and `dependency_score`
|
|
62
|
+
(`ReportRenderer#category_score` runs the same severity+confidence-weighted formula for each,
|
|
63
|
+
scoped to only that category's own findings; see `SCORE_SEVERITY_WEIGHT`/
|
|
64
|
+
`SCORE_CONFIDENCE_WEIGHT`). Previously `security_score` folded performance findings in at a
|
|
65
|
+
diluted weight and dropped style findings entirely, which meant a codebase could be
|
|
66
|
+
security-clean but drowning in N+1 queries and the console/HTML report would barely show it. Now
|
|
67
|
+
each category gets its own 0-100 score and letter grade, and none of them dilute or leak into
|
|
68
|
+
another: `scryer`'s console summary prints all four (`Security Score`/`Performance Score`/
|
|
69
|
+
`Style Score`/`Dependency Score`, each `N/100 (grade)`; `Dependency Score` shows
|
|
70
|
+
`skipped (--no-deps)`/`skipped (nodeps)` when the dependency audit didn't run), and the HTML
|
|
71
|
+
report's top panel shows four badges instead of one. `as_hash`/JSON output gains
|
|
72
|
+
`performance_score`/`style_score`/`dependency_score` keys alongside the existing
|
|
73
|
+
`security_score` (whose own meaning changed: it's now security findings only, no longer
|
|
74
|
+
security + dependency + diluted-performance combined). `rules_clean_rate` is unchanged (still one
|
|
75
|
+
aggregate rule-level pass rate across all three rule-backed categories together — a different,
|
|
76
|
+
complementary signal, not itself split per-category). Covered by a rewritten
|
|
77
|
+
`test/security_score_test.rb`, including a dedicated independence test proving a finding in one
|
|
78
|
+
category never affects another's score.
|
|
79
|
+
- Added `dangerous_eval` (new `Scryer::Rules::DangerousEvalRule`, CWE-95, critical severity): flags
|
|
80
|
+
`eval`/`instance_eval`/`class_eval`/`module_eval` called with anything other than a hardcoded
|
|
81
|
+
string literal with no interpolation — a real coverage gap (Scryer had no eval-injection check at
|
|
82
|
+
all), reported against a real `eval(singular_instance)` finding another scanner caught and Scryer
|
|
83
|
+
didn't. Deliberately broader than this gem's other injection rules (sql_injection/mass_assignment/
|
|
84
|
+
ssrf/path_traversal only fire when the argument traces back to `params` specifically): an
|
|
85
|
+
eval-family argument has essentially no legitimate reason to be anything but a literal the
|
|
86
|
+
developer wrote, so this fires on *any* non-literal argument — a local variable, an ivar, a method
|
|
87
|
+
call, an interpolated string — regardless of where the value actually came from. Confidence varies
|
|
88
|
+
the same way a taint-analysis scanner's own "Dangerous Eval" check varies confidence with its
|
|
89
|
+
taint engine (High vs Weak): `high` when `params` is directly visible in the argument's own expression (e.g.
|
|
90
|
+
`eval(params[:code])`), `medium` otherwise — the closest signal available without real data-flow
|
|
91
|
+
tracing. Severity stays `critical` either way regardless of confidence: the blast radius if the
|
|
92
|
+
argument is ever attacker-influenced doesn't depend on how sure Scryer is today, and `eval` is
|
|
93
|
+
never the *correct* way to do dynamic dispatch even when today's value happens to be safe
|
|
94
|
+
(`const_get`/`safe_constantize` do the same job with zero code-execution risk). A block-only call
|
|
95
|
+
(`obj.instance_eval { ... }`, no string argument) is never flagged. Covered by
|
|
96
|
+
`test/rule_fixtures_test.rb`, a new `test/dangerous_eval_rule_test.rb` (9 tests, including the
|
|
97
|
+
confidence split), and a new `benchmark/corpus.rb` entry (100%/100%/100% precision/recall/F1).
|
|
98
|
+
- Fixed: `mass_assignment`, `idor`, and `missing_policy_scope` false-positived on a plain Ruby
|
|
99
|
+
service/command object's constructor or class-level entry point (e.g. an internal
|
|
100
|
+
`Server.new(params).call` convention, or the equivalent `interactor`-gem-style
|
|
101
|
+
`SomeThingInteractor.call(params)`) — syntactically identical to `Order.new(params)` (a bare
|
|
102
|
+
constant, a `.new`/`.find`/`.where`-adjacent verb, a raw `params` argument), but none of these
|
|
103
|
+
calls write request data onto ActiveRecord attributes by themselves. Two fixes, in order of
|
|
104
|
+
precedence:
|
|
105
|
+
- **Project-wide model resolution** (`Scanner#call`, new): every scan now walks every
|
|
106
|
+
`class X < Y` declaration across every scanned file once, up front, and resolves which class
|
|
107
|
+
names actually descend from `ApplicationRecord`/`ActiveRecord::Base` (transitively — an
|
|
108
|
+
abstract per-shard base class or an STI subclass still resolves correctly), and which classes
|
|
109
|
+
are declared with *no* superclass at all or with a superclass matching `/\AApplication(?!Record\z)/`
|
|
110
|
+
(`ApplicationJob`, `ApplicationMailer`, a project's own `ApplicationService`, ...) — neither
|
|
111
|
+
shape an ActiveRecord model is ever declared with, so that's an unconditional "definitely not a
|
|
112
|
+
model" signal regardless of the class's name. This is the general fix: a service object with
|
|
113
|
+
*any* name is now excluded as long as it's declared somewhere in the scanned project, and a
|
|
114
|
+
real model is never wrongly excluded just because its name happens to look like a service
|
|
115
|
+
object (e.g. a model genuinely named `Command`). See `Ast.likely_model_name?` and
|
|
116
|
+
`Scanner#collect_class_declarations`/`#resolve_known_models`.
|
|
117
|
+
- **Naming-convention fallback** (unchanged from the original fix, only reached when a class
|
|
118
|
+
isn't declared anywhere Scryer scanned — a gem-provided constant, or outside `c.dirs`):
|
|
119
|
+
`Ast::NON_MODEL_RECEIVER_SUFFIXES` excludes constants ending in `Service`, `Server`,
|
|
120
|
+
`Interactor`, `Operation`, `Command`, or `UseCase`.
|
|
121
|
+
All three rules' previously-duplicated `NON_MODEL_RECEIVERS` exclusion lists are now one shared
|
|
122
|
+
`Ast::NON_MODEL_RECEIVER_NAMES`. Covered by new `test/mass_assignment_rule_test.rb` and
|
|
123
|
+
`test/scanner_known_models_test.rb` — no prior dedicated test file existed for `MassAssignmentRule`
|
|
124
|
+
at all, and no prior test exercised known-model resolution across files (inherently impossible to
|
|
125
|
+
test via a single-file `scan_with` call).
|
|
7
126
|
|
|
8
127
|
## [1.2.1] - 2026-09-04
|
|
9
128
|
|
|
@@ -357,10 +476,10 @@ All notable changes to this project are documented here. Format loosely follows
|
|
|
357
476
|
`properties` and a combined severity+confidence `rank`). Performance/style rules get a
|
|
358
477
|
`confidence` too (no CWE/OWASP — that taxonomy is security-specific). This is Scryer's own
|
|
359
478
|
best-effort categorization for practitioner convenience, not an OWASP-endorsed or independently
|
|
360
|
-
audited mapping — see the README's "What Scryer detects" section for the full caveat.
|
|
361
|
-
already
|
|
362
|
-
capability, just a fuller version of something that idea already existed
|
|
363
|
-
comparison table's new footnote).
|
|
479
|
+
audited mapping — see the README's "What Scryer detects" section for the full caveat.
|
|
480
|
+
Taint-analysis scanners already tag CWE and report a confidence level for their own warnings;
|
|
481
|
+
this isn't a novel capability, just a fuller version of something that idea already existed
|
|
482
|
+
elsewhere (see the comparison table's new footnote).
|
|
364
483
|
- New `ReportRenderer#owasp_coverage` — counts security findings per OWASP Top 10 category, shown
|
|
365
484
|
as a console summary block ("OWASP Top 10 (2021) coverage:") and a new HTML report section, a
|
|
366
485
|
direct byproduct of every security rule now carrying an `owasp_category`.
|
|
@@ -392,9 +511,9 @@ All notable changes to this project are documented here. Format loosely follows
|
|
|
392
511
|
performed.
|
|
393
512
|
- Repositioned the README, gemspec, docs site, and llms.txt around this: Scryer's differentiator
|
|
394
513
|
isn't "one command instead of several tools," it's ranking risk *across* security, performance,
|
|
395
|
-
dependencies, and code quality — something none of RuboCop/
|
|
396
|
-
within their own domain, let alone across all four. The honest heuristic-vs-taint-analysis
|
|
397
|
-
framing versus
|
|
514
|
+
dependencies, and code quality — something none of RuboCop/a taint-analysis scanner/bundler-audit
|
|
515
|
+
do even within their own domain, let alone across all four. The honest heuristic-vs-taint-analysis
|
|
516
|
+
framing versus taint-analysis scanners (and the `idor` false-positive caveat) carries over unchanged from the
|
|
398
517
|
1.0.0 comparison table — this is a reframing of what Scryer adds, not a new claim about
|
|
399
518
|
detection accuracy.
|
|
400
519
|
|
|
@@ -451,8 +570,8 @@ All notable changes to this project are documented here. Format loosely follows
|
|
|
451
570
|
## [1.0.0] - 2026-08-13
|
|
452
571
|
|
|
453
572
|
Seventeen new security rules, a SARIF report format, a Ruby end-of-life check, and a
|
|
454
|
-
credentials-exposure check — see the README's "Scryer vs RuboCop vs
|
|
455
|
-
section for how the new security coverage is positioned (heuristic pattern-matching, not taint
|
|
573
|
+
credentials-exposure check — see the README's "Scryer vs RuboCop vs a taint-analysis scanner vs
|
|
574
|
+
bundler-audit" section for how the new security coverage is positioned (heuristic pattern-matching, not taint
|
|
456
575
|
analysis; `idor` in particular carries real false-positive risk by nature of the problem).
|
|
457
576
|
|
|
458
577
|
- New security rules (all `category: "security"`, skippable individually via `--skip RULE_ID` /
|
data/README.md
CHANGED
|
@@ -58,8 +58,11 @@ HTML report: tmp/scryer_report.html
|
|
|
58
58
|
That's real output from a scan of a live 236-file Rails app (an internal production codebase we
|
|
59
59
|
call "acme-app" here — file/controller names above are anonymized, since we don't publish that
|
|
60
60
|
app's source; the finding counts, severities, rule IDs, and line numbers are exactly as scanned,
|
|
61
|
-
unedited) — not a mockup, and the harsh grade is real too
|
|
62
|
-
|
|
61
|
+
unedited) — not a mockup, and the harsh grade is real too. This capture predates the current
|
|
62
|
+
version, which reports four independent scores (Security/Performance/Style/Dependency, each with
|
|
63
|
+
its own grade) instead of the single `Security Score` line shown above — see
|
|
64
|
+
[Scores](docs/architecture.md#scores) for the current format and what each one does and doesn't
|
|
65
|
+
mean. "Top priorities" is the same
|
|
63
66
|
severity ranking [`ReportRenderer#top_risks`](lib/scryer/report_renderer.rb) applies across *all*
|
|
64
67
|
categories — security, dependencies, performance, code quality — not just within each one; in the
|
|
65
68
|
HTML report it's at the top of the Findings section, ahead of the 309 individual findings
|
|
@@ -73,7 +76,7 @@ below for exactly how this differs from what those tools do.
|
|
|
73
76
|
security vulnerabilities, performance heuristics, dependency risk, and code-quality issues in one
|
|
74
77
|
scan, ranked by severity across all four categories. See
|
|
75
78
|
[What Scryer detects](docs/rules.md#what-scryer-detects).
|
|
76
|
-
- **Is it Rails-specific?** Most of the
|
|
79
|
+
- **Is it Rails-specific?** Most of the 32 security rules target Rails conventions specifically
|
|
77
80
|
(controllers, `config/environments/*`, Active Storage, Action Cable, `params`), but the scanning
|
|
78
81
|
engine itself only needs Ruby source — no Rails app or database required to run it. See
|
|
79
82
|
[Designed for Ruby](docs/rules.md#designed-for-ruby).
|
|
@@ -117,7 +120,9 @@ each one longer-form than a single section here, cross-linked from wherever it's
|
|
|
117
120
|
performance numbers, the security model, false-positive handling, the accuracy benchmark, and
|
|
118
121
|
the full Scryer vs RuboCop vs Brakeman vs bundler-audit comparison.
|
|
119
122
|
- [**Rails Integration**](docs/rails-integration.md) — the runtime query/authorization watchers,
|
|
120
|
-
|
|
123
|
+
runtime method-level tracing (New Relic or OpenTelemetry, for finding what an APM tool's own
|
|
124
|
+
"Other"/"Application Code" time actually is), the `scryer:install` generator, and testing
|
|
125
|
+
Scryer's own results in your app's test suite.
|
|
121
126
|
- [**Contributing & Releasing**](docs/contributing.md) — how to add a new rule, and how this
|
|
122
127
|
gem's own release process works.
|
|
123
128
|
|
data/docs/architecture.md
CHANGED
|
@@ -24,7 +24,7 @@ no Rails/database boot.
|
|
|
24
24
|
|
|
25
25
|
Parsing is the only real cost, and it's linear in file count/size — there's no Rails boot, no
|
|
26
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 [
|
|
27
|
+
236-file production Rails app (the same one behind the [Scores](#scores) example
|
|
28
28
|
above) completes in about 2.5 seconds end to end on a laptop, `--no-deps` set. The one deliberately
|
|
29
29
|
slower path is the [dependency audit](./rules.md#dependency-audit) (on by default): it queries
|
|
30
30
|
[OSV.dev](https://osv.dev) once per unique gem in `Gemfile.lock`, so total time scales with distinct
|
|
@@ -91,55 +91,64 @@ normal for this class of tool, not a bug. An already-guarded call can still get
|
|
|
91
91
|
rules don't trace surrounding conditionals — always review a finding in its surrounding context
|
|
92
92
|
before acting on it.
|
|
93
93
|
|
|
94
|
-
##
|
|
94
|
+
## Scores
|
|
95
95
|
|
|
96
|
-
Every scan produces
|
|
97
|
-
and as
|
|
96
|
+
Every scan produces **four independent 0-100 scores**, each with its own letter grade (A-F) — one
|
|
97
|
+
per category — shown in the console summary and as four badges at the very top of the HTML report:
|
|
98
98
|
|
|
99
99
|
```
|
|
100
|
-
Security Score:
|
|
100
|
+
Security Score: 11/100 (F)
|
|
101
|
+
Performance Score: 62/100 (D)
|
|
102
|
+
Style Score: 100/100 (A)
|
|
103
|
+
Dependency Score: 88/100 (B)
|
|
101
104
|
```
|
|
102
105
|
|
|
103
|
-
The formula (`ReportRenderer#
|
|
104
|
-
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
106
|
+
The formula is the same for all four (`ReportRenderer#category_score` in
|
|
107
|
+
`lib/scryer/report_renderer.rb`, called once per category by `#security_score`/
|
|
108
|
+
`#performance_score`/`#style_score`/`#dependency_score`): every finding *in that category* costs
|
|
109
|
+
points, weighted by both its severity *and* its confidence (a `low`-confidence `idor` finding costs
|
|
110
|
+
less than a `high`-confidence `sql_injection` finding at the same severity), combined via
|
|
111
|
+
exponential decay from 100 rather than linear subtraction — one critical finding visibly moves that
|
|
112
|
+
category's score (100 → ~86) without a handful of findings driving it straight to a hard-clamped 0.
|
|
113
|
+
Crucially, the four scores never mix: a finding only ever affects the score for its own category, so
|
|
114
|
+
a codebase that's security-clean but drowning in N+1 queries gets an honest `Performance Score: F`
|
|
115
|
+
instead of that risk being invisible or diluted inside a single blended number. (Earlier versions of
|
|
116
|
+
this gem had exactly one combined score that folded performance findings in at a diluted weight and
|
|
117
|
+
dropped style findings entirely — that's what these four replaced.)
|
|
118
|
+
|
|
119
|
+
Two things worth being precise about before you treat these numbers as meaningful:
|
|
120
|
+
|
|
121
|
+
- **None of them are normalized by app size.** A 10-file app and a 1,000-file app with the same
|
|
122
|
+
finding *density* will score very differently here — each score reflects that category's absolute
|
|
123
|
+
finding exposure in this scan, not a rate. That makes them useful for tracking *one project's own
|
|
124
|
+
trend* over time (did the next scan score higher or lower, in this category), not for comparing
|
|
125
|
+
two differently-sized codebases against each other.
|
|
126
|
+
- **Duplicate-code groups have no score of their own.** A `DuplicateDetector::DuplicateGroup` isn't
|
|
127
|
+
even a `Finding` — no severity/confidence to weigh in the first place (see
|
|
128
|
+
`Scryer::DuplicateDetector`) — so it's excluded from all four; check the "Duplicate code" row in
|
|
129
|
+
the summary box directly for that. `Style Score` today is really just `frozen_string_literal`, the
|
|
130
|
+
one rule-based style check this gem has.
|
|
131
|
+
|
|
132
|
+
Alongside the four scores, every scan also reports one overall **rule-level pass rate** — "how many
|
|
133
|
+
of Scryer's registered checks fired zero findings":
|
|
128
134
|
|
|
129
135
|
```
|
|
130
136
|
Checks: 23/36 rules clean (63.9%)
|
|
131
137
|
```
|
|
132
138
|
|
|
133
139
|
This is the closest thing Scryer has to Brakeman's "X checks, Y warnings" framing — a coverage
|
|
134
|
-
signal, not a risk signal. It's deliberately a *different* number from the
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
signal, not a risk signal. It's deliberately a *different* number from the four scores above (and
|
|
141
|
+
spans all three rule-backed categories together, rather than being split per-category itself), and
|
|
142
|
+
they won't always agree: a codebase can have a high clean rate (few distinct rules ever fire) and
|
|
143
|
+
still a low score in one category (the few that did fire there were severe and high-confidence), or
|
|
144
|
+
the reverse (many different rules each firing once, none of them serious). `ReportRenderer#rules_clean_rate` computes
|
|
138
145
|
it from `Scryer::RuleSet.all` (every registered security/performance/style rule) against which
|
|
139
146
|
rule_ids actually appeared in this scan's findings — dependency checks aren't counted here since
|
|
140
147
|
they're not backed by a `Scryer::Rule` subclass. Report both; neither alone tells the whole story.
|
|
141
148
|
|
|
142
|
-
The
|
|
149
|
+
The Security Score above is illustrative of the format (four independent scores, one letter grade
|
|
150
|
+
each) rather than one specific captured scan — see the README's own top-of-page example for a real,
|
|
151
|
+
unmodified `Security Score: 10/100 (F)` from an actual live app, and
|
|
143
152
|
[A note on how this gem was actually verified](#a-note-on-how-this-gem-was-actually-verified) for
|
|
144
153
|
why every example in this README is real output.
|
|
145
154
|
|
|
@@ -245,7 +254,7 @@ sits alongside them and adds the cross-category picture neither one (nor bundler
|
|
|
245
254
|
four categories) by severity, shown as "Top priorities" in the console summary and at the top of
|
|
246
255
|
the HTML report — see the example near the top of this README.
|
|
247
256
|
- **[k]** Not a Scryer-only capability — Brakeman's own warnings already include a CWE reference.
|
|
248
|
-
Scryer's version is a fuller mapping (every one of its
|
|
257
|
+
Scryer's version is a fuller mapping (every one of its 32 security rules carries both a CWE ID
|
|
249
258
|
and an OWASP Top 10 (2021) category, aggregated into an OWASP coverage scorecard in every
|
|
250
259
|
report), but the underlying idea isn't new; see
|
|
251
260
|
[What Scryer detects](./rules.md#what-scryer-detects) for the honesty caveat on how this mapping was built
|
data/docs/rails-integration.md
CHANGED
|
@@ -91,6 +91,187 @@ Two honesty points worth being precise about:
|
|
|
91
91
|
this class doesn't attempt.
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
## Runtime method tracing (APM)
|
|
95
|
+
|
|
96
|
+
The problem `QueryWatcher` and `AuthorizationWatcher` don't touch: once a request lands inside
|
|
97
|
+
application code — a service object, a repository, a background-job method — most APM tools
|
|
98
|
+
(New Relic included) can only see it as one undifferentiated block of "Application Code" or
|
|
99
|
+
"Other" time. `Scryer::APM` answers "which method inside that block actually took the time" by
|
|
100
|
+
wrapping configured classes' own methods (the same `Module#prepend` technique `QueryWatcher` uses
|
|
101
|
+
on `ActiveRecord::QueryMethods`, not `TracePoint`) and, when a supported APM agent/SDK is loaded,
|
|
102
|
+
creating a real nested span per call via *that agent's own* public API — never an undocumented
|
|
103
|
+
endpoint or a separate exporter of Scryer's own. **Two providers are implemented:**
|
|
104
|
+
|
|
105
|
+
- **`:new_relic`** — a real nested segment per call via
|
|
106
|
+
`NewRelic::Agent::Tracer.start_segment`/`Segment#finish`, the same mechanism `add_method_tracer`
|
|
107
|
+
itself uses internally.
|
|
108
|
+
- **`:opentelemetry`** — a real nested span per call via
|
|
109
|
+
`OpenTelemetry.tracer_provider.tracer(...).start_span`, with
|
|
110
|
+
`OpenTelemetry::Context.attach`/`.detach` used to make each span the "current" one so the next
|
|
111
|
+
nested instrumented call picks it up as its parent automatically — the SDK's own documented
|
|
112
|
+
manual-span API (the one used whenever the block form, `Tracer#in_span`, doesn't fit).
|
|
113
|
+
|
|
114
|
+
Neither SDK is a hard dependency of this gem (see the gemspec: zero runtime deps beyond stdlib) —
|
|
115
|
+
both are soft-detected via `defined?(...)`. If you set `c.apm.provider = :opentelemetry`, add
|
|
116
|
+
`opentelemetry-sdk` (plus whatever exporter you want, e.g. `opentelemetry-exporter-otlp`) to your
|
|
117
|
+
own app's `Gemfile`, exactly the way you already add `newrelic_rpm` yourself to use `:new_relic`.
|
|
118
|
+
If the configured provider's SDK isn't loaded, spans are still traced (available via
|
|
119
|
+
`Scryer::APM.on_span`) but nothing is exported anywhere — a warning is logged once, the
|
|
120
|
+
instrumented methods keep working normally either way.
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# config/initializers/scryer.rb
|
|
124
|
+
Scryer.configure do |c|
|
|
125
|
+
c.apm.enabled = true
|
|
126
|
+
c.apm.provider = :opentelemetry # or :new_relic
|
|
127
|
+
c.apm.instrumentation = :selective # only mode implemented so far — see Limitations below
|
|
128
|
+
c.apm.include = %w[BookingService PaymentService InventoryService]
|
|
129
|
+
c.apm.exclude = %w[] # Scryer:: / ActiveRecord:: / ActionController:: / ActionDispatch:: / Rails::
|
|
130
|
+
# are always excluded regardless of this list
|
|
131
|
+
c.apm.sampling_rate = 0.10 # 0.0–1.0, decided once per request, not per span
|
|
132
|
+
c.apm.capture_exception_messages = false # class name only by default; see Redaction below
|
|
133
|
+
end
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
That's genuinely the whole thing — `c.apm.enabled = true` is sufficient by itself, no separate
|
|
137
|
+
`require`/`.enable!`/`middleware.use` calls needed. `Scryer::Railtie` (loaded automatically as
|
|
138
|
+
soon as the gem is in your `Gemfile`) checks `c.apm.enabled` in a Rails initializer that runs
|
|
139
|
+
*after* every file in `config/initializers/` — including this one — has already run, and does the
|
|
140
|
+
`require "scryer/apm"` / `Scryer::APM.enable!` / `Rails.application.config.middleware.use
|
|
141
|
+
Scryer::APM::Middleware` sequence for you (`Scryer::Railtie.maybe_enable_apm`, tested directly in
|
|
142
|
+
`test/railtie_apm_test.rb`) if and only if that flag is true. Leaving `c.apm.enabled` at its
|
|
143
|
+
default (`false`) — or omitting the `c.apm` block entirely — means none of this runs and there is
|
|
144
|
+
no measurable overhead, same as never having required this file at all.
|
|
145
|
+
|
|
146
|
+
**The one thing that genuinely can't be defaulted: `c.apm.include` must still be populated.**
|
|
147
|
+
There is no "trace every method automatically" mode — an empty `include` (the default) means
|
|
148
|
+
`enabled = true` turns the machinery on but instruments nothing, by design (see Production safety
|
|
149
|
+
below for why "instrument every method in the app" is deliberately not offered).
|
|
150
|
+
|
|
151
|
+
Outside a request (a Sidekiq job, a rake task), open a trace scope yourself the same way
|
|
152
|
+
`QueryWatcher.watch` works for queries:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
Scryer::APM.trace { SomeJob.new.perform }
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### What gets instrumented, precisely
|
|
159
|
+
|
|
160
|
+
`c.apm.include` takes class/module name strings. For each one, **only methods defined directly on
|
|
161
|
+
that class** (`instance_methods(false)` + private/protected equivalents) are wrapped — methods
|
|
162
|
+
*inherited* from a superclass or module are never touched, even if the named class itself has none
|
|
163
|
+
of its own. This is deliberate, not a current limitation: instrumenting inherited methods via a
|
|
164
|
+
bare class-name entry is exactly how `include: ["SomeModel"]` would end up silently wrapping
|
|
165
|
+
`ActiveRecord::Base` internals, which is the "sensitive framework internals" risk a production
|
|
166
|
+
instrumentation tool has to avoid by construction, not by asking users to get their `exclude` list
|
|
167
|
+
right. Method visibility (public/private/protected) is preserved exactly. Positional args, keyword
|
|
168
|
+
args, blocks, and return values all pass through `super` unchanged; exceptions of every class
|
|
169
|
+
(not just `StandardError`) are recorded on the span and always re-raised, never swallowed.
|
|
170
|
+
|
|
171
|
+
### What each span records
|
|
172
|
+
|
|
173
|
+
`class_name`, `method_name`, `file`/`line` (captured once when the method is wrapped, not per
|
|
174
|
+
call), `trace_id`/`span_id`/`parent_span_id` (nested calls get correct parent/child relationships
|
|
175
|
+
via a thread-local span stack — see the "nested calls" test in `test/apm_test.rb`), `duration_ms`,
|
|
176
|
+
`status` (`:ok`/`:error`), `exception_class`, `thread_id`. **Method argument values, local
|
|
177
|
+
variables, and return values are never recorded, under any configuration** — there is no opt-in
|
|
178
|
+
that changes this. `exception_message` is `nil` unless `capture_exception_messages: true`, and even
|
|
179
|
+
then passes through a best-effort (not exhaustive — see Limitations) redaction pass that masks
|
|
180
|
+
long token/hash-shaped substrings and email addresses before being kept.
|
|
181
|
+
|
|
182
|
+
### Production safety
|
|
183
|
+
|
|
184
|
+
- **`instrumentation: :off`** installs no hooks at all — measured overhead is within noise of not
|
|
185
|
+
requiring the file (see `benchmark/apm_overhead.rb`).
|
|
186
|
+
- **Sampling is per-trace, not per-span**: the decision is made once when `Scryer::APM.trace` opens
|
|
187
|
+
(or on the first instrumented call if a request somehow reaches one without a scope), so a
|
|
188
|
+
sampled request gets a complete, connected trace rather than a random scatter of orphaned spans.
|
|
189
|
+
- **Provider failures never break the traced method.** New Relic's `start_segment`/`finish`/
|
|
190
|
+
`notice_error` and OpenTelemetry's `start_span`/`finish`/`record_exception` are each wrapped in
|
|
191
|
+
their own `rescue StandardError` — see `test_new_relic_segment_start_failure_does_not_break_the_traced_method`
|
|
192
|
+
and `test_open_telemetry_span_start_failure_does_not_break_the_traced_method` in
|
|
193
|
+
`test/apm_test.rb`, which stub a failing `Tracer.start_segment`/`Tracer#start_span` and confirm
|
|
194
|
+
the real method's return value still comes through unaffected.
|
|
195
|
+
- **No custom export queue, batching, or retry/backoff exists in this module, and none is needed**:
|
|
196
|
+
spans are handed directly to the already-async, already-batched, already-retried agent/SDK
|
|
197
|
+
transport via its own span API — Scryer::APM never talks to a network endpoint itself. The one
|
|
198
|
+
thing this means: if your configured provider isn't loaded but you keep `Scryer::APM.on_span`
|
|
199
|
+
wired to your own sink, *that* sink is your responsibility to make non-blocking/bounded — nothing
|
|
200
|
+
here queues or batches on your behalf.
|
|
201
|
+
- **Calling `.instrument_by_name`/`.instrument_method` more than once for the same class/method is a
|
|
202
|
+
no-op** (a process-wide dedupe guard), so re-running an initializer (e.g., Rails' `to_prepare` in
|
|
203
|
+
development) can't double-wrap a method into two nested spans for one call.
|
|
204
|
+
- **A misconfiguration can never fail app boot, even in production.** This is the specific
|
|
205
|
+
guarantee `test/railtie_apm_test.rb` exists to prove, not just assert: a bad class name in
|
|
206
|
+
`include`, `instrumentation` set to an unimplemented mode, or any other failure while
|
|
207
|
+
`Scryer::Railtie` auto-enables APM (see "That's genuinely the whole thing" above) is caught —
|
|
208
|
+
broadly, `rescue Exception` rather than just `StandardError`, since a `require` failure from a
|
|
209
|
+
packaging issue would raise `LoadError`, a `ScriptError` — logged once, and leaves APM cleanly
|
|
210
|
+
disabled for that process; the rest of the app boots and runs normally. One method failing to
|
|
211
|
+
instrument (an internal error, not a bad config value) is scoped even tighter: only that method
|
|
212
|
+
is skipped, not the rest of its class or the rest of `include`. **This graceful-degradation bar
|
|
213
|
+
is specific to the automatic Railtie path** — `Scryer::APM.enable!` called directly (a console, a
|
|
214
|
+
test, a non-Rails script) still raises immediately for an unimplemented mode, which is the
|
|
215
|
+
correct behavior when a developer is watching and wants fast feedback rather than a silently
|
|
216
|
+
disabled feature.
|
|
217
|
+
|
|
218
|
+
### Measured overhead
|
|
219
|
+
|
|
220
|
+
From `benchmark/apm_overhead.rb` (Apple silicon dev machine, Ruby 3.4.8, 200,000 calls per
|
|
221
|
+
scenario — re-run this yourself before trusting these numbers for capacity planning on different
|
|
222
|
+
hardware):
|
|
223
|
+
|
|
224
|
+
| scenario | ns/call | vs. uninstrumented |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| baseline (no Scryer::APM at all) | ~28 ns | 1.0x |
|
|
227
|
+
| `instrumentation: :off` | ~28 ns | ~1.0x (no measurable difference) |
|
|
228
|
+
| `:selective`, inside a trace, `sampling_rate: 0.0` | ~172 ns | ~6.3x |
|
|
229
|
+
| `:selective`, inside a trace, `sampling_rate: 1.0` (full span) | ~1771 ns | ~64x |
|
|
230
|
+
|
|
231
|
+
The `:off` vs. unsampled-but-wrapped gap (~6x) is the fixed cost of the `prepend`/`define_method`
|
|
232
|
+
dispatch layer itself, before any span is even considered — this is the floor cost paid by every
|
|
233
|
+
call to a wrapped method regardless of sampling, which is why `include` should name specific
|
|
234
|
+
service/business-logic classes, not broad namespaces. Full span creation (~1771 ns, ~1.8 µs) is
|
|
235
|
+
still small in absolute terms next to typical service-method bodies (a DB query alone is usually
|
|
236
|
+
1–20+ ms), but at high call volume with `sampling_rate: 1.0` it is not free — this is exactly what
|
|
237
|
+
`sampling_rate` exists to bound. Retained-memory cost: 50,000 held `Span` objects (worst case — a
|
|
238
|
+
consumer that doesn't drain them) measured ~53 MB RSS growth, roughly ~1 KB/span.
|
|
239
|
+
|
|
240
|
+
### Limitations — implemented vs. not yet
|
|
241
|
+
|
|
242
|
+
**Implemented and tested** (see `test/apm_test.rb`, 29 examples, and `test/railtie_apm_test.rb`, 5
|
|
243
|
+
more): `:off` and `:selective` modes; nested parent/child spans; exception recording + re-raise;
|
|
244
|
+
keyword args/blocks/return values; method visibility preservation; inherited-method exclusion;
|
|
245
|
+
duplicate-instrumentation guard; per-class `exclude`; trace-scoped sampling; thread isolation
|
|
246
|
+
(concurrent requests on different threads never share trace/span state); New Relic segment
|
|
247
|
+
creation, error notification, and exporter-failure safety; OpenTelemetry span creation
|
|
248
|
+
(with correct parent/child nesting via `Context.attach`/`.detach`), exception recording, and
|
|
249
|
+
start-failure safety; the Rack middleware; reading config through `Scryer.configure`;
|
|
250
|
+
`c.apm.enabled = true` alone (no manual `require`/`.enable!`/`middleware.use`) genuinely
|
|
251
|
+
triggering tracing via the Railtie; a bad `include` entry or an unimplemented mode failing to
|
|
252
|
+
crash boot (see "Production safety" above); a single method's instrumentation failure not taking
|
|
253
|
+
out the rest of its class.
|
|
254
|
+
|
|
255
|
+
**Not implemented** — raises a clear `ArgumentError` naming this doc rather than silently
|
|
256
|
+
no-opping if configured:
|
|
257
|
+
- `instrumentation: :discovery` and `:deep_trace` modes (low-overhead profiling to *find* expensive
|
|
258
|
+
methods automatically, and extra detail captured only for slow/failed requests, respectively).
|
|
259
|
+
- Any provider other than `:new_relic` and `:opentelemetry` — no Datadog, no vendor-specific SDK
|
|
260
|
+
beyond those two.
|
|
261
|
+
- Singleton (class) method instrumentation — only instance methods.
|
|
262
|
+
- Distributed trace-context propagation across a process boundary (an outgoing HTTP call to
|
|
263
|
+
another service, or an inbound request that already carries a W3C `traceparent` header). This
|
|
264
|
+
module's own `trace_id`/`span_id` fields are local-process only. The `:opentelemetry` provider's
|
|
265
|
+
*spans* do still nest correctly with each other within one process via `Context.attach`/`.detach`
|
|
266
|
+
— what's missing is reading/writing the `traceparent` header itself to connect that local trace
|
|
267
|
+
to one already in progress elsewhere.
|
|
268
|
+
|
|
269
|
+
**Known caveat, not a bug**: `Scryer::APM.disable!` (test-only) clears configuration/dedupe state
|
|
270
|
+
but cannot un-`prepend` an already-instrumented class — Ruby has no supported way to do that. Tests
|
|
271
|
+
that need a clean slate instrument a freshly-defined, uniquely-named class per example rather than
|
|
272
|
+
reusing one across examples; a host app calling `.enable!` more than once in a running process
|
|
273
|
+
should expect the same constraint.
|
|
274
|
+
|
|
94
275
|
## Generators
|
|
95
276
|
|
|
96
277
|
Scryer ships one generator: `scryer:install`. Run `bin/rails generate scryer:install --help`
|
|
@@ -152,7 +333,7 @@ end
|
|
|
152
333
|
`Scryer.scan(root:)` runs the same static scan `scryer:report`/the `scryer` executable do (using
|
|
153
334
|
`c.dirs`/`c.skip_rules` from your initializer), without needing a report written to disk —
|
|
154
335
|
`have_no_critical_findings`/`assert_no_critical_scryer_findings` only look at *security* findings
|
|
155
|
-
(same scoping as the [
|
|
336
|
+
(same scoping as the [Security Score](./architecture.md#scores)); `have_no_findings_for`/
|
|
156
337
|
`assert_no_scryer_findings_for` check a specific `rule_id` across all three static categories,
|
|
157
338
|
for pinning a specific bug so it can't come back unnoticed. This talks to the live filesystem on
|
|
158
339
|
every test run (a real `Ripper`-based scan, same cost as running `scryer` itself) — for a large app
|
data/docs/rules.md
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
* SQL injection
|
|
10
10
|
* Mass assignment
|
|
11
11
|
* Command injection
|
|
12
|
+
* Dangerous `eval` (`eval`/`instance_eval`/`class_eval`/`module_eval` called with anything other
|
|
13
|
+
than a hardcoded string literal) — broader than most other checks here: it fires on any
|
|
14
|
+
non-literal argument, not just one tracing back to `params`, since there's essentially no
|
|
15
|
+
legitimate reason for an eval-family argument to be anything else. Always `critical` severity;
|
|
16
|
+
confidence is `high` when `params` is directly visible in the argument, `medium` otherwise (the
|
|
17
|
+
closest signal available without real data-flow tracing — same axis Brakeman's own "Dangerous
|
|
18
|
+
Eval" check varies High/Weak confidence on)
|
|
12
19
|
* Hardcoded secrets (and hardcoded HTTP Basic Auth credentials)
|
|
13
20
|
* Unsafe deserialization
|
|
14
21
|
* XSS-prone HTML
|
|
@@ -157,7 +164,7 @@ Generate detailed JSON or self-contained HTML reports:
|
|
|
157
164
|
scryer -o report.json -o report.html
|
|
158
165
|
```
|
|
159
166
|
|
|
160
|
-
The HTML report leads with
|
|
167
|
+
The HTML report leads with four [score](./architecture.md#scores) badges and a severity distribution
|
|
161
168
|
chart, then Overview/Summary/OWASP coverage, collapsed-by-default reference tables (every rule
|
|
162
169
|
that *can* fire, not a wall of always-expanded detail), and the Findings section itself — Top
|
|
163
170
|
priorities first, then a text filter box (rule/file/message, no page reload) above the full
|
data/docs/usage.md
CHANGED
|
@@ -61,7 +61,7 @@ example — followed by where each report was written.
|
|
|
61
61
|
|
|
62
62
|
**Color.** The summary box, top priorities, and `scryer fix`/`scryer verify` output are colored
|
|
63
63
|
automatically at a real terminal — severity labels (`critical`/`warning`/`info` → red/yellow/cyan),
|
|
64
|
-
|
|
64
|
+
each score's letter grade (A/B green, C yellow, D/F red), fixed/skipped status lines, and
|
|
65
65
|
so on. Off automatically whenever it would be wrong to color: piped/redirected output (e.g. `-o -`
|
|
66
66
|
to a file, or piped to `less` without `-R`), [`NO_COLOR`](https://no-color.org) set, or
|
|
67
67
|
`TERM=dumb`. `--color`/`--no-color` force it either way regardless of any of that — e.g. `scryer
|
|
@@ -282,7 +282,10 @@ scryer --baseline tmp/scryer_baseline.json -o scryer.sarif
|
|
|
282
282
|
```
|
|
283
283
|
Baseline: tmp/scryer_baseline.json — showing new findings only (2 fixed since baseline).
|
|
284
284
|
|
|
285
|
-
Security Score:
|
|
285
|
+
Security Score: 96/100 (A)
|
|
286
|
+
Performance Score: 90/100 (A)
|
|
287
|
+
Style Score: 100/100 (A)
|
|
288
|
+
Dependency Score: 88/100 (B)
|
|
286
289
|
|
|
287
290
|
Security 1 finding
|
|
288
291
|
...
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
# Configuration for the scryer gem (static code analysis: security,
|
|
2
3
|
# duplicate code, performance heuristics). Run `bin/rails scryer:report`
|
|
3
4
|
# to scan this app and generate tmp/scryer_report.{json,html} locally.
|
|
@@ -41,3 +42,24 @@ end
|
|
|
41
42
|
# Scryer::QueryWatcher.enable!
|
|
42
43
|
# Rails.application.config.middleware.use Scryer::QueryWatcher::Middleware
|
|
43
44
|
# end
|
|
45
|
+
|
|
46
|
+
# Runtime method-level tracing (see README's "Runtime method tracing (APM)")
|
|
47
|
+
# — finds which method inside a request is actually responsible for time
|
|
48
|
+
# an APM tool would otherwise lump into "Other"/"Application Code". Providers
|
|
49
|
+
# :new_relic and :opentelemetry (each soft-detected; no hard dependency added
|
|
50
|
+
# by enabling this — add whichever SDK you pick to your own Gemfile) and
|
|
51
|
+
# instrumentation :selective (named classes only) are implemented so far.
|
|
52
|
+
# `c.apm.enabled = true` below is genuinely sufficient on its own —
|
|
53
|
+
# Scryer::Railtie wires up the require/.enable!/middleware for you once this
|
|
54
|
+
# flag is true, after this file has already run. Safe to leave on in
|
|
55
|
+
# production — leaving `include` empty (or `enabled` false) has no
|
|
56
|
+
# measurable overhead, and sampling_rate bounds the cost of the classes you
|
|
57
|
+
# do name:
|
|
58
|
+
#
|
|
59
|
+
# Scryer.configure do |c|
|
|
60
|
+
# c.apm.enabled = true
|
|
61
|
+
# c.apm.provider = :opentelemetry # or :new_relic
|
|
62
|
+
# c.apm.instrumentation = :selective
|
|
63
|
+
# c.apm.include = %w[] # e.g. %w[BookingService PaymentService] — required, see README
|
|
64
|
+
# c.apm.sampling_rate = 0.10
|
|
65
|
+
# end
|
data/lib/scryer/ai_client.rb
CHANGED