fiber_audit 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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.fiber-audit.example.yml +19 -0
  3. data/ARCHITECTURE.md +726 -0
  4. data/CHANGELOG.md +30 -0
  5. data/LICENSE +201 -0
  6. data/README.md +66 -8
  7. data/lib/fiber_audit/cli.rb +87 -2
  8. data/lib/fiber_audit/configuration.rb +106 -6
  9. data/lib/fiber_audit/errors.rb +2 -0
  10. data/lib/fiber_audit/operation_vocabulary.rb +42 -0
  11. data/lib/fiber_audit/reporters/text.rb +1 -1
  12. data/lib/fiber_audit/runtime/active_operations.rb +146 -0
  13. data/lib/fiber_audit/runtime/boot.rb +83 -0
  14. data/lib/fiber_audit/runtime/clock.rb +35 -0
  15. data/lib/fiber_audit/runtime/environment.rb +289 -0
  16. data/lib/fiber_audit/runtime/event.rb +86 -0
  17. data/lib/fiber_audit/runtime/execution_context.rb +89 -0
  18. data/lib/fiber_audit/runtime/heartbeat.rb +113 -0
  19. data/lib/fiber_audit/runtime/jsonl/schema.rb +312 -0
  20. data/lib/fiber_audit/runtime/jsonl/writer.rb +122 -0
  21. data/lib/fiber_audit/runtime/lifecycle.rb +343 -0
  22. data/lib/fiber_audit/runtime/limits.rb +102 -0
  23. data/lib/fiber_audit/runtime/location.rb +44 -0
  24. data/lib/fiber_audit/runtime/policy.rb +121 -0
  25. data/lib/fiber_audit/runtime/probes/base.rb +333 -0
  26. data/lib/fiber_audit/runtime/probes/http.rb +80 -0
  27. data/lib/fiber_audit/runtime/probes/io_select.rb +50 -0
  28. data/lib/fiber_audit/runtime/probes/registry.rb +156 -0
  29. data/lib/fiber_audit/runtime/probes/socket.rb +76 -0
  30. data/lib/fiber_audit/runtime/probes/subprocess.rb +83 -0
  31. data/lib/fiber_audit/runtime/probes/synchronization.rb +58 -0
  32. data/lib/fiber_audit/runtime/probes/thread_state.rb +39 -0
  33. data/lib/fiber_audit/runtime/probes/thread_wait.rb +25 -0
  34. data/lib/fiber_audit/runtime/rails_integration.rb +279 -0
  35. data/lib/fiber_audit/runtime/recorder.rb +333 -0
  36. data/lib/fiber_audit/runtime/redactor.rb +102 -0
  37. data/lib/fiber_audit/runtime/sampler.rb +26 -0
  38. data/lib/fiber_audit/runtime/scheduler_observer.rb +128 -0
  39. data/lib/fiber_audit/runtime/session.rb +112 -0
  40. data/lib/fiber_audit/runtime/supervisor.rb +114 -0
  41. data/lib/fiber_audit/runtime/validation.rb +68 -0
  42. data/lib/fiber_audit/runtime/watchdog.rb +479 -0
  43. data/lib/fiber_audit/runtime/watchdog_policy.rb +64 -0
  44. data/lib/fiber_audit/runtime.rb +37 -0
  45. data/lib/fiber_audit/static/rules/blocking_subprocess.rb +3 -8
  46. data/lib/fiber_audit/static/rules/direct_socket.rb +2 -3
  47. data/lib/fiber_audit/static/rules/io_select.rb +2 -4
  48. data/lib/fiber_audit/static/rules/net_http_in_request.rb +3 -5
  49. data/lib/fiber_audit/static/rules/synchronization.rb +2 -6
  50. data/lib/fiber_audit/static/rules/thread_current_state.rb +3 -2
  51. data/lib/fiber_audit/static/rules/thread_join.rb +3 -2
  52. data/lib/fiber_audit/version.rb +1 -1
  53. data/lib/fiber_audit.rb +1 -0
  54. metadata +38 -2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.2.0 (2026-08-12)
6
+
7
+ ### Added
8
+
9
+ - Foundational runtime event, session, safety-policy, redaction, and JSONL
10
+ contracts for the observational runtime auditor.
11
+ - An explicit, bounded, thread-safe runtime session recorder with injected
12
+ clocks and sampling, exact drop accounting, crash-tolerant JSONL writes, and
13
+ fail-open safety.
14
+ - `fiber-audit runtime -- COMMAND` with strict activation settings, shell-free
15
+ process supervision, signal forwarding, status preservation, process-local
16
+ JSONL sessions, and fork-safe recorder rebinding.
17
+ - An opt-in scheduler watchdog with explicit disabled/absent/active/unsupported
18
+ states, scheduler-owned monotonic heartbeats, one bounded start/completion pair
19
+ per stall, privacy-safe project frame evidence, and a process-local active
20
+ operation registry.
21
+ - Targeted FA1001–FA1007 runtime probes with monotonic timing, conservative
22
+ project callsites, active-operation overlap, strict recursion protection, late
23
+ standard-library installation, and no captured commands, URLs, addresses,
24
+ payloads, exception data, or thread-local values.
25
+ - Rails runtime execution-context detection with bounded, PID-aware fiber-local
26
+ context stack and process-local Rails integration. Hooks into Rack middleware,
27
+ ActionController, ActiveJob, and ActionCable boundaries with inert wrappers
28
+ after deactivation or fork. Snapshots context at probe observation start and
29
+ propagates through active operations and events while preserving JSONL schema
30
+ 1.0, privacy requirements, and lifecycle ownership. Static/runtime correlation
31
+ remains future work.
32
+
3
33
  ## 0.1.0 (2026-08-02)
4
34
 
5
35
  First static-only release of FiberAudit. Requires Ruby 3.3 or newer.
data/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
data/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # FiberAudit
2
2
 
3
3
  FiberAudit audits Ruby and Rails code for operations that can block the thread
4
- running a Fiber scheduler. Version 0.1.0 performs static analysis only.
4
+ running a Fiber scheduler. Version 0.2.0 includes static analysis and an explicit,
5
+ observational runtime audit.
5
6
 
6
- > **Static-only disclaimer:** This is a static-only audit. PASS cannot be
7
- > granted without runtime coverage.
7
+ > **Safety disclaimer:** FiberAudit does not prove that an application is
8
+ > fiber-safe. Static findings are hypotheses, and absence of runtime events does
9
+ > not establish complete coverage. FiberAudit does not emit unconditional `PASS`.
8
10
 
9
11
  ## Requirements and installation
10
12
 
11
- FiberAudit v0.1.0 supports Ruby 3.3 and 3.4.
13
+ FiberAudit v0.2.0 supports Ruby 3.3 and 3.4.
12
14
 
13
15
  ```sh
14
16
  gem install fiber_audit
@@ -35,6 +37,8 @@ file exists.
35
37
  ```text
36
38
  fiber-audit static [--format text|json] [--config PATH] [--out PATH]
37
39
  [--min-severity LEVEL] [--no-color]
40
+ fiber-audit runtime [--config PATH] [--out DIRECTORY]
41
+ [--sampling-rate RATE] [--no-fail-open] -- COMMAND [ARGUMENTS...]
38
42
  fiber-audit list-rules
39
43
  fiber-audit explain FA1001
40
44
  fiber-audit version
@@ -58,6 +62,48 @@ Explicit `--format` always wins.
58
62
 
59
63
  Use `fiber-audit explain <RULE_ID>` for exact targets and remediation.
60
64
 
65
+ ## Runtime audit
66
+
67
+ The explicit runtime command observes only a command supplied after `--`; it
68
+ never executes source fragments discovered by static analysis:
69
+
70
+ ```sh
71
+ fiber-audit runtime -- bundle exec rspec
72
+ ```
73
+
74
+ Each observed Ruby process writes a separate owner-only JSONL session under
75
+ `tmp/fiber-audit-runtime` by default. Targeted probes observe the operations
76
+ represented by FA1001–FA1007: subprocess calls, thread waits, synchronization,
77
+ thread-local access, explicit select, direct sockets, and synchronous HTTP.
78
+ Events contain canonical operation names, monotonic duration, and a conservative
79
+ project-relative callsite. They never contain commands, URLs, addresses, ports,
80
+ headers, payloads, responses, exception data, or thread-local keys and values.
81
+ Libraries such as Open3, Monitor, Socket, Net::HTTP, and OpenURI may be loaded
82
+ after runtime boot; FiberAudit rescans only these known targets after `require`.
83
+
84
+ Rails execution contexts (`:request`, `:middleware`, `:job`, `:websocket`) are
85
+ captured automatically when Rails integration is active. A bounded, PID-aware
86
+ fiber-local context stack tracks the current execution context during probe
87
+ observations. Rails boundaries are wrapped via prepend hooks that become inert
88
+ after deactivation or fork, preserving application semantics without interfering
89
+ with normal Rails operation. The integration supports late loading: hooks are
90
+ installed when Rails components become available, even after runtime boot.
91
+ Static/runtime correlation remains future work.
92
+
93
+ The scheduler watchdog records one bounded start/completion pair when its
94
+ scheduler-owned heartbeat stops progressing past the configured threshold.
95
+ Scheduler-friendly waits should continue heartbeats. A session also records an
96
+ explicit watchdog state:
97
+
98
+ - `watchdog_active` — a heartbeat ran under an installed scheduler;
99
+ - `watchdog_absent` — no scheduler was observed;
100
+ - `watchdog_unsupported` — a scheduler could not safely host the heartbeat;
101
+ - `watchdog_disabled` — watchdog policy disabled observation.
102
+
103
+ Absent or unsupported monitoring, a clean session, and absence of stall events
104
+ are **not** proof of fiber safety. Native work that holds Ruby's GVL can also
105
+ prevent the watchdog thread from running until that work returns.
106
+
61
107
  ## Configuration
62
108
 
63
109
  Copy `.fiber-audit.example.yml` to `.fiber-audit.yml` in the project root.
@@ -85,6 +131,18 @@ rules:
85
131
  report:
86
132
  formats: [text, json]
87
133
  min_severity: low
134
+
135
+ runtime:
136
+ redaction:
137
+ mode: strict
138
+ sampling:
139
+ rate: 0.1
140
+ watchdog:
141
+ enabled: true
142
+ heartbeat_interval_ms: 25
143
+ stall_threshold_ms: 100
144
+ max_frames: 20
145
+ fail_open: true
88
146
  ```
89
147
 
90
148
  `--min-severity` overrides `report.min_severity` for one run. Severity ordering
@@ -132,7 +190,7 @@ Missing reasons and invalid configuration return exit code 2.
132
190
  - `PASS_WITH_WARNINGS` — only low or informational findings.
133
191
  - `NO_FINDINGS` — no findings at the configured threshold.
134
192
 
135
- FiberAudit never emits unconditional `PASS` in v0.1.0.
193
+ FiberAudit never emits unconditional `PASS` in v0.2.0.
136
194
 
137
195
  ## Exit codes
138
196
 
@@ -141,10 +199,10 @@ FiberAudit never emits unconditional `PASS` in v0.1.0.
141
199
  | 0 | No active finding at or above the configured threshold |
142
200
  | 1 | One or more active findings at or above the threshold |
143
201
  | 2 | Invalid options, configuration, analysis, or report output |
144
- | 3 | Reserved; never emitted by v0.1.0 |
202
+ | 3 | Reserved; never emitted by v0.2.0 |
145
203
 
146
204
  Source parse errors are included in report data while analysis continues on
147
205
  other files.
148
206
 
149
- See [ARCHITECTURE.md](ARCHITECTURE.md) for implementation boundaries and the
150
- future runtime-analysis architecture.
207
+ See [ARCHITECTURE.md](ARCHITECTURE.md) for implementation boundaries, runtime
208
+ architecture, and explicitly deferred correlation work.
@@ -9,6 +9,8 @@ require_relative 'audit'
9
9
  require_relative 'findings/severity'
10
10
  require_relative 'reporters/text'
11
11
  require_relative 'reporters/json'
12
+ require_relative 'runtime/environment'
13
+ require_relative 'runtime/supervisor'
12
14
  require_relative 'static/rules/built_ins'
13
15
 
14
16
  module FiberAudit
@@ -23,6 +25,8 @@ module FiberAudit
23
25
  case command
24
26
  when 'static'
25
27
  run_static(args, stdout: stdout, stderr: stderr, cwd: cwd)
28
+ when 'runtime'
29
+ run_runtime(args, stdout: stdout, cwd: cwd)
26
30
  when 'list-rules'
27
31
  reject_arguments!(args)
28
32
  list_rules(stdout)
@@ -53,12 +57,13 @@ module FiberAudit
53
57
 
54
58
  Commands:
55
59
  static Run static fiber-compatibility analysis
60
+ runtime Observe an explicitly supplied Ruby command
56
61
  list-rules List all registered static rules
57
62
  explain ID Explain a specific rule
58
63
  version Print version
59
64
  help Show this help
60
65
 
61
- Run `fiber-audit static --help` for analysis options.
66
+ Run `fiber-audit static --help` or `fiber-audit runtime --help` for options.
62
67
  HELP
63
68
  end
64
69
 
@@ -77,6 +82,84 @@ module FiberAudit
77
82
  reportable_findings?(result.findings, configuration.min_severity) ? 1 : 0
78
83
  end
79
84
 
85
+ def run_runtime(argv, stdout:, cwd:)
86
+ options, command, parser = parse_runtime_options(argv)
87
+ if options[:help]
88
+ stdout.puts parser
89
+ return 0
90
+ end
91
+
92
+ project = Project.detect(start_path: cwd)
93
+ configuration = load_configuration(project, options[:config])
94
+ policy = runtime_policy_with_overrides(configuration.runtime_policy, options)
95
+ output_directory = runtime_output_directory(options[:out], project)
96
+ settings = Runtime::Environment.build(
97
+ policy: policy,
98
+ output_directory: output_directory,
99
+ project_root: project.root
100
+ )
101
+ environment = Runtime::Environment.child_environment(
102
+ settings: settings,
103
+ watchdog_policy: configuration.runtime_watchdog_policy,
104
+ probes_enabled: true
105
+ )
106
+ Runtime::Supervisor.new(
107
+ command: command,
108
+ environment: environment,
109
+ cwd: project.invocation_path
110
+ ).run
111
+ end
112
+
113
+ def parse_runtime_options(argv)
114
+ options = { config: nil, out: nil, sampling_rate: nil, no_fail_open: false, help: false }
115
+ parser = OptionParser.new do |opts|
116
+ opts.banner = 'Usage: fiber-audit runtime [options] -- COMMAND [ARGUMENTS...]'
117
+ opts.on('--config PATH', 'Path to configuration file') { |value| options[:config] = value }
118
+ opts.on('--out DIRECTORY', 'Directory for runtime JSONL sessions') { |value| options[:out] = value }
119
+ opts.on('--sampling-rate RATE', Float, 'Override runtime sampling rate') do |value|
120
+ options[:sampling_rate] = value
121
+ end
122
+ opts.on('--no-fail-open', 'Fail the Ruby child if runtime recording fails') { options[:no_fail_open] = true }
123
+ opts.on('-h', '--help', 'Show runtime options') { options[:help] = true }
124
+ end
125
+
126
+ separator = argv.index('--')
127
+ option_arguments = separator ? argv.take(separator) : argv.dup
128
+ command = separator ? argv.drop(separator + 1) : []
129
+ parser.parse!(option_arguments)
130
+ unless options[:help]
131
+ raise OptionParser::MissingArgument, 'runtime command separator -- is required' unless separator
132
+ raise OptionParser::MissingArgument, 'runtime command is required after --' if command.empty?
133
+ end
134
+ reject_arguments!(option_arguments)
135
+ [options, command, parser]
136
+ end
137
+
138
+ def runtime_policy_with_overrides(policy, options)
139
+ values = policy.to_h
140
+ values[:sampling_rate] = options[:sampling_rate] unless options[:sampling_rate].nil?
141
+ values[:fail_open] = false if options[:no_fail_open]
142
+ Runtime::Policy.new(**values)
143
+ rescue RuntimeContractError => e
144
+ raise ConfigurationError, "runtime command policy is invalid: #{e.message}"
145
+ end
146
+
147
+ def runtime_output_directory(override, project)
148
+ path = if override
149
+ File.expand_path(override, project.invocation_path)
150
+ else
151
+ File.join(project.root, 'tmp', 'fiber-audit-runtime')
152
+ end
153
+ Runtime::Environment.prepare_output_directory(path)
154
+ end
155
+
156
+ def load_configuration(project, override)
157
+ config_path = project.config_path(override)
158
+ raise ConfigurationError, "configuration file does not exist: #{config_path}" if override && !File.file?(config_path)
159
+
160
+ Configuration.load(config_path)
161
+ end
162
+
80
163
  def analyze_project(options, cwd, stderr)
81
164
  project = Project.detect(start_path: cwd)
82
165
  stderr.puts unknown_project_note(project) unless project.known?
@@ -117,7 +200,9 @@ module FiberAudit
117
200
  rules_config: configuration.rules_config,
118
201
  report_formats: configuration.report_formats,
119
202
  min_severity: severity,
120
- suppressions_path: configuration.suppressions_path
203
+ suppressions_path: configuration.suppressions_path,
204
+ runtime_policy: configuration.runtime_policy,
205
+ runtime_watchdog_policy: configuration.runtime_watchdog_policy
121
206
  )
122
207
  end
123
208
 
@@ -4,13 +4,34 @@ require 'yaml'
4
4
  require 'pathname'
5
5
  require_relative 'errors'
6
6
  require_relative 'findings/severity'
7
+ require_relative 'runtime/policy'
8
+ require_relative 'runtime/watchdog_policy'
7
9
 
8
10
  module FiberAudit
11
+ # rubocop:disable Metrics/ClassLength
9
12
  class Configuration
10
- KNOWN_TOP_LEVEL_KEYS = %w[static rules report].freeze
13
+ KNOWN_TOP_LEVEL_KEYS = %w[static rules report runtime].freeze
11
14
  KNOWN_STATIC_KEYS = %w[include exclude suppressions_path].freeze
12
15
  KNOWN_REPORT_KEYS = %w[formats min_severity].freeze
13
16
  KNOWN_RULE_KEYS = %w[enabled severity].freeze
17
+ KNOWN_RUNTIME_KEYS = %w[redaction sampling overhead watchdog fail_open].freeze
18
+ KNOWN_REDACTION_KEYS = %w[mode].freeze
19
+ KNOWN_SAMPLING_KEYS = %w[rate].freeze
20
+ KNOWN_OVERHEAD_KEYS = %w[
21
+ max_events_per_second max_events_per_session max_record_bytes max_session_bytes
22
+ ].freeze
23
+ KNOWN_WATCHDOG_KEYS = %w[
24
+ enabled heartbeat_interval_ms stall_threshold_ms max_frames
25
+ ].freeze
26
+ RUNTIME_POLICY_PATHS = {
27
+ 'redaction' => 'runtime.redaction.mode',
28
+ 'sampling_rate' => 'runtime.sampling.rate',
29
+ 'max_events_per_second' => 'runtime.overhead.max_events_per_second',
30
+ 'max_events_per_session' => 'runtime.overhead.max_events_per_session',
31
+ 'max_record_bytes' => 'runtime.overhead.max_record_bytes',
32
+ 'max_session_bytes' => 'runtime.overhead.max_session_bytes',
33
+ 'fail_open' => 'runtime.fail_open'
34
+ }.freeze
14
35
  VALID_FORMATS = %w[text json].freeze
15
36
 
16
37
  DEFAULT_STATIC_INCLUDE = %w[
@@ -28,7 +49,8 @@ module FiberAudit
28
49
  ].freeze
29
50
 
30
51
  attr_reader :static_include, :static_exclude, :rules_config,
31
- :report_formats, :min_severity, :suppressions_path
52
+ :report_formats, :min_severity, :suppressions_path,
53
+ :runtime_policy, :runtime_watchdog_policy
32
54
 
33
55
  def initialize(
34
56
  static_include: DEFAULT_STATIC_INCLUDE,
@@ -36,11 +58,14 @@ module FiberAudit
36
58
  rules_config: {},
37
59
  report_formats: %w[text],
38
60
  min_severity: :low,
39
- suppressions_path: nil
61
+ suppressions_path: nil,
62
+ runtime_policy: Runtime::Policy.new,
63
+ runtime_watchdog_policy: Runtime::WatchdogPolicy.new
40
64
  )
41
65
  validate_types!(
42
66
  static_include, static_exclude, rules_config,
43
- report_formats, min_severity, suppressions_path
67
+ report_formats, min_severity, suppressions_path, runtime_policy,
68
+ runtime_watchdog_policy
44
69
  )
45
70
 
46
71
  @static_include = static_include
@@ -49,6 +74,8 @@ module FiberAudit
49
74
  @report_formats = report_formats
50
75
  @min_severity = coerce_severity(min_severity, 'report.min_severity')
51
76
  @suppressions_path = suppressions_path
77
+ @runtime_policy = runtime_policy
78
+ @runtime_watchdog_policy = runtime_watchdog_policy
52
79
  end
53
80
 
54
81
  def rule_enabled?(rule_id)
@@ -76,6 +103,7 @@ module FiberAudit
76
103
  static = yaml['static'] || {}
77
104
  rules = yaml['rules'] || {}
78
105
  report = yaml['report'] || {}
106
+ runtime = yaml['runtime'] || {}
79
107
 
80
108
  new(
81
109
  static_include: static['include'] || DEFAULT_STATIC_INCLUDE,
@@ -83,7 +111,9 @@ module FiberAudit
83
111
  rules_config: rules,
84
112
  report_formats: report['formats'] || %w[text],
85
113
  min_severity: report.fetch('min_severity', :low),
86
- suppressions_path: static['suppressions_path']
114
+ suppressions_path: static['suppressions_path'],
115
+ runtime_policy: runtime_policy_from(runtime),
116
+ runtime_watchdog_policy: runtime_watchdog_policy_from(runtime)
87
117
  )
88
118
  end
89
119
 
@@ -115,6 +145,8 @@ module FiberAudit
115
145
  check_unknown_keys(report, KNOWN_REPORT_KEYS, 'report')
116
146
  end
117
147
 
148
+ validate_runtime_structure!(yaml['runtime']) if yaml.key?('runtime')
149
+
118
150
  return unless yaml.key?('rules')
119
151
 
120
152
  rules = yaml['rules']
@@ -124,6 +156,64 @@ module FiberAudit
124
156
  "rules must be a mapping, got #{rules.class}"
125
157
  end
126
158
 
159
+ def validate_runtime_structure!(runtime)
160
+ unless runtime.is_a?(Hash)
161
+ raise ConfigurationError,
162
+ "runtime must be a mapping, got #{runtime.class}"
163
+ end
164
+ check_unknown_keys(runtime, KNOWN_RUNTIME_KEYS, 'runtime')
165
+ validate_runtime_mapping!(runtime, 'redaction', KNOWN_REDACTION_KEYS)
166
+ validate_runtime_mapping!(runtime, 'sampling', KNOWN_SAMPLING_KEYS)
167
+ validate_runtime_mapping!(runtime, 'overhead', KNOWN_OVERHEAD_KEYS)
168
+ validate_runtime_mapping!(runtime, 'watchdog', KNOWN_WATCHDOG_KEYS)
169
+ end
170
+
171
+ def validate_runtime_mapping!(runtime, key, allowed)
172
+ return unless runtime.key?(key)
173
+
174
+ value = runtime[key]
175
+ unless value.is_a?(Hash)
176
+ raise ConfigurationError,
177
+ "runtime.#{key} must be a mapping, got #{value.class}"
178
+ end
179
+ check_unknown_keys(value, allowed, "runtime.#{key}")
180
+ end
181
+
182
+ def runtime_policy_from(runtime)
183
+ defaults = Runtime::Policy::DEFAULTS
184
+ redaction = runtime.fetch('redaction', {})
185
+ sampling = runtime.fetch('sampling', {})
186
+ overhead = runtime.fetch('overhead', {})
187
+ Runtime::Policy.new(
188
+ redaction: redaction.fetch('mode', defaults[:redaction]),
189
+ sampling_rate: sampling.fetch('rate', defaults[:sampling_rate]),
190
+ max_events_per_second: overhead.fetch('max_events_per_second', defaults[:max_events_per_second]),
191
+ max_events_per_session: overhead.fetch('max_events_per_session', defaults[:max_events_per_session]),
192
+ max_record_bytes: overhead.fetch('max_record_bytes', defaults[:max_record_bytes]),
193
+ max_session_bytes: overhead.fetch('max_session_bytes', defaults[:max_session_bytes]),
194
+ fail_open: runtime.fetch('fail_open', defaults[:fail_open])
195
+ )
196
+ rescue RuntimeContractError => e
197
+ field = e.message.split.first
198
+ path = RUNTIME_POLICY_PATHS.fetch(field, 'runtime')
199
+ raise ConfigurationError, "#{path} is invalid: #{e.message}"
200
+ end
201
+
202
+ def runtime_watchdog_policy_from(runtime)
203
+ values = runtime.fetch('watchdog', {})
204
+ defaults = Runtime::WatchdogPolicy::DEFAULTS
205
+ Runtime::WatchdogPolicy.new(
206
+ enabled: values.fetch('enabled', defaults[:enabled]),
207
+ heartbeat_interval_ms: values.fetch('heartbeat_interval_ms', defaults[:heartbeat_interval_ms]),
208
+ stall_threshold_ms: values.fetch('stall_threshold_ms', defaults[:stall_threshold_ms]),
209
+ max_frames: values.fetch('max_frames', defaults[:max_frames])
210
+ )
211
+ rescue RuntimeContractError => e
212
+ field = e.message.split.first
213
+ path = field == 'enabled' ? 'runtime.watchdog.enabled' : "runtime.watchdog.#{field}"
214
+ raise ConfigurationError, "#{path} is invalid: #{e.message}"
215
+ end
216
+
127
217
  def check_unknown_keys(hash, allowed, path)
128
218
  unknown = hash.keys - allowed
129
219
  return if unknown.empty?
@@ -139,7 +229,7 @@ module FiberAudit
139
229
 
140
230
  def validate_types!(
141
231
  include_patterns, exclude_patterns, rules,
142
- formats, _severity, suppressions
232
+ formats, _severity, suppressions, runtime_policy, watchdog_policy
143
233
  )
144
234
  unless include_patterns.is_a?(Array) &&
145
235
  include_patterns.all?(String)
@@ -158,6 +248,15 @@ module FiberAudit
158
248
  validate_rules!(rules)
159
249
  validate_report_formats!(formats)
160
250
 
251
+ unless runtime_policy.is_a?(Runtime::Policy)
252
+ raise ConfigurationError,
253
+ 'runtime_policy must be a FiberAudit::Runtime::Policy'
254
+ end
255
+ unless watchdog_policy.is_a?(Runtime::WatchdogPolicy)
256
+ raise ConfigurationError,
257
+ 'runtime_watchdog_policy must be a FiberAudit::Runtime::WatchdogPolicy'
258
+ end
259
+
161
260
  return if suppressions.nil? || suppressions.is_a?(String)
162
261
 
163
262
  raise ConfigurationError,
@@ -233,4 +332,5 @@ module FiberAudit
233
332
  "#{path} is not a valid severity: #{value.inspect}"
234
333
  end
235
334
  end
335
+ # rubocop:enable Metrics/ClassLength
236
336
  end