fiber_audit 0.2.1 → 0.3.1

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.fiber-audit.example.yml +7 -8
  3. data/ARCHITECTURE.md +97 -673
  4. data/CHANGELOG.md +52 -0
  5. data/README.md +163 -86
  6. data/lib/fiber_audit/cli.rb +19 -5
  7. data/lib/fiber_audit/configuration.rb +31 -6
  8. data/lib/fiber_audit/operation_semantics.rb +172 -0
  9. data/lib/fiber_audit/operation_vocabulary.rb +2 -1
  10. data/lib/fiber_audit/runtime/active_operations.rb +39 -9
  11. data/lib/fiber_audit/runtime/boot.rb +4 -0
  12. data/lib/fiber_audit/runtime/environment.rb +58 -0
  13. data/lib/fiber_audit/runtime/execution_context.rb +55 -43
  14. data/lib/fiber_audit/runtime/lifecycle.rb +91 -50
  15. data/lib/fiber_audit/runtime/operation_liveness_monitor.rb +282 -0
  16. data/lib/fiber_audit/runtime/operation_liveness_policy.rb +47 -0
  17. data/lib/fiber_audit/runtime/probes/base.rb +27 -4
  18. data/lib/fiber_audit/runtime/probes/subprocess.rb +89 -8
  19. data/lib/fiber_audit/runtime/probes/thread_state.rb +0 -14
  20. data/lib/fiber_audit/runtime/scheduler_evidence_classifier.rb +60 -0
  21. data/lib/fiber_audit/runtime/scheduler_observer.rb +54 -18
  22. data/lib/fiber_audit/runtime/scheduler_snapshot.rb +95 -0
  23. data/lib/fiber_audit/runtime/watchdog.rb +54 -4
  24. data/lib/fiber_audit/runtime.rb +5 -0
  25. data/lib/fiber_audit/static/call_site_extractor.rb +1 -0
  26. data/lib/fiber_audit/static/rules/base.rb +19 -0
  27. data/lib/fiber_audit/static/rules/blocking_subprocess.rb +67 -10
  28. data/lib/fiber_audit/static/rules/direct_socket.rb +54 -19
  29. data/lib/fiber_audit/static/rules/io_select.rb +6 -6
  30. data/lib/fiber_audit/static/rules/net_http_in_request.rb +11 -8
  31. data/lib/fiber_audit/static/rules/synchronization.rb +13 -8
  32. data/lib/fiber_audit/static/rules/thread_current_state.rb +20 -30
  33. data/lib/fiber_audit/static/rules/thread_join.rb +9 -7
  34. data/lib/fiber_audit/version.rb +1 -1
  35. metadata +6 -1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,58 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.1 (2026-08-23)
6
+
7
+ ### Added
8
+
9
+ - Added a lifecycle-owned operation-liveness monitor with strict policy transport,
10
+ explicit active/disabled/unsupported states, and bounded long-active
11
+ start/completion evidence independent of scheduler-heartbeat stalls.
12
+ - Added shared operation semantics and five Boolean/nil scheduler-classification
13
+ measurements to targeted operations, watchdog overlaps, and long-active events.
14
+ - Expanded the bounded scheduler semantic matrix for Process wait variants,
15
+ coordination hooks, localhost address resolution, and Ruby 4 IO-close
16
+ interruption; added a local measurement-only runtime probe benchmark.
17
+
18
+ ### Changed
19
+
20
+ - Scheduler snapshot failures now remain `nil`/unknown in JSONL schema 1.0 rather
21
+ than asserting false state.
22
+ - FA1006 keeps rule/operation identity while distinguishing socket allocation,
23
+ endpoint setup, local connection, and unknown subclass constructor semantics.
24
+ - FA1004 now reports medium severity without execution-context escalation and
25
+ describes possible shared-state exposure without claiming request leakage.
26
+ - Documented the tested support contract as CRuby 3.3, 3.4, and 4.0 on Ubuntu
27
+ Linux; the gem installation requirement remains Ruby 3.3 or newer.
28
+
29
+ ### Fixed
30
+
31
+ - Corrected scheduler snapshots to preserve the actual `Fiber#blocking?` Boolean
32
+ instead of converting both blocking and non-blocking Fibers to true.
33
+
34
+ ## 0.3.0 (2026-08-12)
35
+
36
+ ### Changed
37
+
38
+ - Reframed static rules as scheduler-cooperation requirements. Advisory rules no
39
+ longer escalate solely because they appear in request-like execution contexts.
40
+ - Split FA1001 under its existing rule ID into subprocess creation, replacement,
41
+ waiting, detach, and stream lifecycle semantics; added Process `spawn`, `exec`,
42
+ `wait`, `wait2`, `waitpid`, `waitpid2`, and `Process::Status.wait` coverage.
43
+ - Corrected FA1004 to report only true Thread variables. `Thread.current[]` and
44
+ `Thread.current[]=` are Fiber-local and are no longer static or runtime findings.
45
+ - Propagated Rails/runtime execution context into child Fibers through immutable
46
+ Ruby Fiber storage, with explicit current-Fiber `clear!`/`reset!` behavior.
47
+ - Added scheduler presence, blocking-Fiber state, and optional scheduler-hook
48
+ capability measurements to targeted operation events.
49
+ - Added bounded `scheduler_stall_operation_overlap` JSONL 1.0 events. They record
50
+ temporal overlap between a watchdog stall and active operations, not causality.
51
+ - Reconciled scheduler observer state only after `Fiber.set_scheduler` accepts a
52
+ change, so rejected replacement attempts preserve the previous observation.
53
+ - Added Ruby 4.0 to the required CI matrix and executable local scheduler-semantic
54
+ reproductions under `script/scheduler-semantics`, including Ruby 4's
55
+ `fiber_interrupt` scheduler protocol hook.
56
+
5
57
  ## 0.2.1 (2026-08-12)
6
58
 
7
59
  ### Fixed
data/README.md CHANGED
@@ -1,22 +1,27 @@
1
1
  # FiberAudit
2
2
 
3
- FiberAudit audits Ruby and Rails code for operations that can block the thread
4
- running a Fiber scheduler. Version 0.2.1 includes static analysis and an explicit,
5
- observational runtime audit.
3
+ FiberAudit audits Ruby and Rails code for operations that require cooperation
4
+ from a Fiber scheduler. It provides Rubydex/Prism-backed static hypotheses and
5
+ an explicitly activated observational runtime recorder.
6
6
 
7
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
+ > fiber-safe. Runtime observations prove execution, not scheduler harm; temporal
9
+ > overlap and long-active duration do not prove causality or deadlock. Absence of
10
+ > events does not establish execution coverage. FiberAudit never emits an
11
+ > unconditional `PASS`.
10
12
 
11
13
  ## Requirements and installation
12
14
 
13
- FiberAudit v0.2.1 supports Ruby 3.3 and 3.4.
15
+ The gem requires Ruby 3.3 or newer. CI tests CRuby 3.3, 3.4, and 4.0 on Ubuntu
16
+ Linux. Other Ruby engines and operating systems are not currently part of the
17
+ tested support contract. Native Rubydex packages must be available for the
18
+ selected platform.
14
19
 
15
20
  ```sh
16
21
  gem install fiber_audit
17
22
  ```
18
23
 
19
- Or add it to a bundle and run `bundle install`:
24
+ Or add it to a bundle:
20
25
 
21
26
  ```ruby
22
27
  gem "fiber_audit", require: false
@@ -24,15 +29,15 @@ gem "fiber_audit", require: false
24
29
 
25
30
  ## Quick start
26
31
 
27
- Run from a project directory or any directory beneath it:
32
+ Run from a project directory or beneath it:
28
33
 
29
34
  ```sh
30
35
  fiber-audit static
36
+ fiber-audit runtime -- bundle exec rspec
31
37
  ```
32
38
 
33
- FiberAudit walks upward to find the nearest `Gemfile`, `gems.rb`, or
34
- `config/application.rb`. It loads `.fiber-audit.yml` from that root when the
35
- file exists.
39
+ FiberAudit walks upward to the nearest `Gemfile`, `gems.rb`, or
40
+ `config/application.rb` and loads `.fiber-audit.yml` from that root when present.
36
41
 
37
42
  ```text
38
43
  fiber-audit static [--format text|json] [--config PATH] [--out PATH]
@@ -44,82 +49,135 @@ fiber-audit explain FA1001
44
49
  fiber-audit version
45
50
  ```
46
51
 
47
- Output defaults to text on a TTY and JSON when piped. `--out PATH` defaults to
48
- JSON, writes only the report to that file, and prints a one-line confirmation.
49
- Explicit `--format` always wins.
52
+ Static output defaults to text on a TTY and JSON when piped. `--out PATH`
53
+ defaults to JSON, writes only the report to that file, and prints a one-line
54
+ confirmation. Explicit `--format` always wins.
50
55
 
51
56
  ## Shipped rules
52
57
 
53
58
  | ID | Detects | Default severity |
54
59
  |---|---|---|
55
- | FA1001 | Blocking subprocess operations | high |
56
- | FA1002 | `Thread#join` and `Thread#value` | high |
57
- | FA1003 | Thread-oriented synchronization | medium |
58
- | FA1004 | Thread-local state access | high/medium |
59
- | FA1005 | Explicit `IO.select`/`Kernel.select` | medium |
60
- | FA1006 | Direct socket construction | medium |
61
- | FA1007 | Synchronous HTTP in request-like contexts | high |
60
+ | FA1001 | Subprocess creation, replacement, waiting, and streams | info/medium |
61
+ | FA1002 | Thread-wait scheduler coordination | low |
62
+ | FA1003 | Synchronization scheduler coordination | low/info |
63
+ | FA1004 | Thread-variable access shared across Fibers on one Thread | medium |
64
+ | FA1005 | `IO.select` scheduler capability requirement | medium |
65
+ | FA1006 | Socket allocation and constructor endpoint semantics | low |
66
+ | FA1007 | HTTP scheduler cooperation in request-like contexts | medium |
67
+
68
+ FA1004 reports true `thread_variable_get/set` access. It does not retain keys or
69
+ values and does not claim that request-sensitive leakage occurred. FA1006 keeps
70
+ stable `<Class>.new` operation identities while distinguishing inventory-only
71
+ allocation, address resolution/network endpoint setup, local connection, and
72
+ unknown IPSocket-subclass construction.
62
73
 
63
74
  Use `fiber-audit explain <RULE_ID>` for exact targets and remediation.
64
75
 
65
76
  ## Runtime audit
66
77
 
67
- The explicit runtime command observes only a command supplied after `--`; it
68
- never executes source fragments discovered by static analysis:
78
+ The runtime command observes only the command supplied after `--`; static
79
+ analysis never executes discovered source fragments. Each observed Ruby process
80
+ writes a separate owner-only JSONL schema `1.0` session under
81
+ `tmp/fiber-audit-runtime` by default.
69
82
 
70
- ```sh
71
- fiber-audit runtime -- bundle exec rspec
83
+ Targeted probes cover the operations represented by FA1001–FA1007. Events retain
84
+ canonical operation names, monotonic duration, conservative project-relative
85
+ callsites, execution context, ephemeral Thread/Fiber identities, and allowlisted
86
+ scalar measurements. They never retain commands, command arguments, URLs,
87
+ addresses, hosts, ports, headers, payloads, responses, return values, exception
88
+ messages, environment secrets, or thread-variable keys and values.
89
+
90
+ Libraries such as Open3, Monitor, Socket, Net::HTTP, and OpenURI may load after
91
+ runtime boot; FiberAudit rescans only known targets after `require`. Runtime
92
+ wrappers remain inert outside explicit activation and after deactivation or fork.
93
+
94
+ ### Scheduler snapshots and operation classification
95
+
96
+ A targeted operation snapshots scheduler state at its start. Measurement failure
97
+ is represented as `nil` (unknown), never coerced to false:
98
+
99
+ ```text
100
+ scheduler_present: true | false | nil
101
+ fiber_blocking: true | false | nil
102
+ scheduler_io_select_supported: true | false | nil
103
+ scheduler_process_wait_supported: true | false | nil
104
+ scheduler_address_resolve_supported: true | false | nil
105
+ ```
106
+
107
+ Operation-specific evidence adds five Boolean/nil measurements:
108
+
109
+ ```text
110
+ operation_wait_possible
111
+ operation_inventory_only
112
+ operation_scheduler_capability_required
113
+ operation_scheduler_capability_supported
114
+ operation_scheduler_cooperation_available
72
115
  ```
73
116
 
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:
117
+ `operation_scheduler_cooperation_available: true` means the captured scheduler
118
+ and Fiber mode were compatible and, for an optional capability, the captured
119
+ hook was supported. Required core coordination hooks such as `block` and
120
+ `kernel_sleep` are inferred from known scheduler presence rather than measured
121
+ separately. This does not prove that the operation cooperated or completed
122
+ without delay. `nil` remains unknown or not applicable.
123
+
124
+ ### Scheduler watchdog
125
+
126
+ The watchdog records one bounded start/completion pair when a scheduler-owned
127
+ heartbeat stops progressing past its threshold. Scheduler-friendly waits should
128
+ continue heartbeats. State events are:
97
129
 
98
130
  - `watchdog_active` — a heartbeat ran under an installed scheduler;
99
131
  - `watchdog_absent` — no scheduler was observed;
100
- - `watchdog_unsupported` — a scheduler could not safely host the heartbeat;
132
+ - `watchdog_unsupported` — the scheduler could not safely host observation;
101
133
  - `watchdog_disabled` — watchdog policy disabled observation.
102
134
 
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.
135
+ Bounded `scheduler_stall_operation_overlap` events associate active operation
136
+ sequences with a stall on the same Thread. They establish temporal overlap, not
137
+ causality. Native work retaining Ruby's GVL can prevent the watchdog Thread from
138
+ running until the work returns.
139
+
140
+ ### Long-active operation monitor
141
+
142
+ The independent operation-liveness monitor polls the bounded active-operation
143
+ registry. By default it polls every 100 ms and emits after an observed operation
144
+ remains active for strictly more than 1 second. All targeted operation types are
145
+ eligible, but entries beyond the snapshot bound can remain unobserved under
146
+ registry pressure; truncation is explicit and absence is not a coverage claim:
147
+
148
+ - `operation_liveness_active`;
149
+ - `operation_liveness_disabled`;
150
+ - `operation_liveness_unsupported`;
151
+ - `operation_long_active_started`;
152
+ - `operation_long_active_completed`.
153
+
154
+ Ordinary registry removal closes a pair with `operation_finished: true`.
155
+ Shutdown closes an open pair with `operation_finished: false`; that value does
156
+ not mean the application operation failed. Long-active evidence means only that
157
+ a targeted operation remained registered across the threshold. It is not a
158
+ scheduler stall, proven deadlock, or proof of scheduler harm.
159
+
160
+ Watchdog, liveness-state, overlap, and long-active events bypass random sampling
161
+ but still consume recorder rate, event, record-size, and session-size budgets.
162
+ Snapshot and per-poll truncation, drops, unsupported states, internal errors, and
163
+ incomplete sessions remain visible. A successful `exec` may intentionally leave
164
+ a session without `session_end`.
165
+
166
+ Rails execution contexts (`request`, `middleware`, `job`, and `websocket`) are
167
+ captured when Rails integration is active. Bounded immutable Fiber storage
168
+ propagates logical context to child Fibers without retaining request data.
169
+ Combined static/runtime reporting remains future work.
106
170
 
107
171
  ## Configuration
108
172
 
109
- Copy `.fiber-audit.example.yml` to `.fiber-audit.yml` in the project root.
110
- Paths and globs are rooted at the detected project. An explicit `--config`
111
- path is resolved from the directory where the command was invoked.
173
+ Copy `.fiber-audit.example.yml` to `.fiber-audit.yml`. Paths and globs are rooted
174
+ at the detected project; explicit `--config` is resolved from the invocation
175
+ directory.
112
176
 
113
177
  ```yaml
114
178
  static:
115
- include:
116
- - app/**/*.rb
117
- - lib/**/*.rb
118
- - config/**/*.rb
119
- exclude:
120
- - vendor/**/*
121
- - tmp/**/*
122
- - db/schema.rb
179
+ include: [app/**/*.rb, lib/**/*.rb, config/**/*.rb]
180
+ exclude: [vendor/**/*, tmp/**/*, db/schema.rb]
123
181
  suppressions_path: .fiber-audit-suppressions.yml
124
182
 
125
183
  rules:
@@ -137,40 +195,45 @@ runtime:
137
195
  mode: strict
138
196
  sampling:
139
197
  rate: 0.1
198
+ overhead:
199
+ max_events_per_second: 100
200
+ max_events_per_session: 10000
201
+ max_record_bytes: 16384
202
+ max_session_bytes: 10485760
140
203
  watchdog:
141
204
  enabled: true
142
205
  heartbeat_interval_ms: 25
143
206
  stall_threshold_ms: 100
144
207
  max_frames: 20
208
+ operation_liveness:
209
+ enabled: true
210
+ poll_interval_ms: 100
211
+ long_active_threshold_ms: 1000
145
212
  fail_open: true
146
213
  ```
147
214
 
148
- `--min-severity` overrides `report.min_severity` for one run. Severity ordering
149
- is `critical`, `high`, `medium`, `low`, `info`; findings below the threshold
150
- are omitted and do not affect the exit code. The default `low` threshold keeps
151
- informational findings silent.
215
+ Configuration is strict: unknown sections or keys, invalid types, and values
216
+ outside policy bounds return exit code 2. `--min-severity` overrides
217
+ `report.min_severity` for one static run. Severity ordering is `critical`,
218
+ `high`, `medium`, `low`, `info`; the default `low` threshold omits informational
219
+ findings.
152
220
 
153
221
  ## Suppressions
154
222
 
155
223
  Every suppression requires a non-empty reason. Directive-looking text inside
156
224
  strings, heredocs, or regular expressions is ignored.
157
225
 
158
- Suppress one line:
159
-
160
226
  ```ruby
161
227
  system(command) # fiber-audit:disable FA1001 -- trusted maintenance command
162
228
  ```
163
229
 
164
- Suppress a block:
165
-
166
230
  ```ruby
167
231
  # fiber-audit:disable FA1003 -- protected legacy boundary
168
232
  mutex.synchronize { update_record }
169
233
  # fiber-audit:enable FA1003
170
234
  ```
171
235
 
172
- A separate YAML file can suppress by rule and optionally by symbol or
173
- operation. Point `static.suppressions_path` at the file:
236
+ YAML suppressions can match rule and optionally symbol or operation:
174
237
 
175
238
  ```yaml
176
239
  suppressions:
@@ -180,29 +243,43 @@ suppressions:
180
243
  reason: isolated worker process with an external timeout
181
244
  ```
182
245
 
183
- Missing reasons and invalid configuration return exit code 2.
184
-
185
- ## Static statuses
246
+ ## Static statuses and exit codes
186
247
 
187
248
  - `FAIL` — at least one critical or high finding.
188
- - `REVIEW` — a medium finding, or a non-informational low/unknown-confidence
189
- finding.
249
+ - `REVIEW` — a medium finding, or non-informational low/unknown-confidence risk.
190
250
  - `PASS_WITH_WARNINGS` — only low or informational findings.
191
251
  - `NO_FINDINGS` — no findings at the configured threshold.
192
252
 
193
- FiberAudit never emits unconditional `PASS` in v0.2.1.
194
-
195
- ## Exit codes
253
+ FiberAudit never emits unconditional `PASS`.
196
254
 
197
255
  | Code | Meaning |
198
256
  |---|---|
199
257
  | 0 | No active finding at or above the configured threshold |
200
258
  | 1 | One or more active findings at or above the threshold |
201
259
  | 2 | Invalid options, configuration, analysis, or report output |
202
- | 3 | Reserved; never emitted by v0.2.1 |
260
+ | 3 | Reserved; not emitted |
261
+
262
+ Source parse errors remain report data while analysis continues on other files.
263
+
264
+ ## Development and semantic verification
265
+
266
+ ```sh
267
+ bundle exec rspec
268
+ bundle exec rubocop
269
+ bundle exec ruby script/scheduler-semantics
270
+ FIBER_AUDIT_BENCH_ITERATIONS=2000 bundle exec ruby benchmark/runtime_probe_overhead.rb
271
+ gem build fiber_audit.gemspec
272
+ bundle exec rake release:sanity
273
+ ```
274
+
275
+ The semantic script uses bounded local Threads, pipes, localhost resolution, and
276
+ child processes. CI exercises it through the spec suite on tested CRuby versions.
277
+ It verifies only scheduler capabilities consumed by FiberAudit; it is not a full
278
+ scheduler-conformance suite.
203
279
 
204
- Source parse errors are included in report data while analysis continues on
205
- other files.
280
+ The benchmark reports absent, installed/inactive, active sampling-zero, and
281
+ active sampling-one workloads in isolated subprocesses. It is diagnostic only:
282
+ there is no host-dependent CI timing threshold.
206
283
 
207
- See [ARCHITECTURE.md](ARCHITECTURE.md) for implementation boundaries, runtime
208
- architecture, and explicitly deferred correlation work.
284
+ See [ARCHITECTURE.md](ARCHITECTURE.md) for implementation boundaries and the
285
+ runtime truthfulness, privacy, lifecycle, and schema contracts.
@@ -101,6 +101,7 @@ module FiberAudit
101
101
  environment = Runtime::Environment.child_environment(
102
102
  settings: settings,
103
103
  watchdog_policy: configuration.runtime_watchdog_policy,
104
+ operation_liveness_policy: configuration.runtime_operation_liveness_policy,
104
105
  probes_enabled: true
105
106
  )
106
107
  Runtime::Supervisor.new(
@@ -202,7 +203,8 @@ module FiberAudit
202
203
  min_severity: severity,
203
204
  suppressions_path: configuration.suppressions_path,
204
205
  runtime_policy: configuration.runtime_policy,
205
- runtime_watchdog_policy: configuration.runtime_watchdog_policy
206
+ runtime_watchdog_policy: configuration.runtime_watchdog_policy,
207
+ runtime_operation_liveness_policy: configuration.runtime_operation_liveness_policy
206
208
  )
207
209
  end
208
210
 
@@ -274,12 +276,25 @@ module FiberAudit
274
276
  stdout.puts "Description: #{rule_class.description}"
275
277
  stdout.puts 'Targets:'
276
278
  rule_targets(rule_class).each { |target| stdout.puts " - #{target}" }
277
- stdout.puts "Remediation: #{rule_class.const_get(:REMEDIATION)}"
279
+ print_rule_remediations(rule_class, stdout)
278
280
  0
279
281
  end
280
282
 
283
+ def print_rule_remediations(rule_class, stdout)
284
+ values = if rule_class.const_defined?(:REMEDIATION, false)
285
+ [rule_class.const_get(:REMEDIATION)]
286
+ elsif rule_class.const_defined?(:CATEGORY_METADATA, false)
287
+ rule_class.const_get(:CATEGORY_METADATA).values.map { |metadata| metadata.fetch(:remediation) }.uniq
288
+ else
289
+ []
290
+ end
291
+ values.each_with_index do |remediation, index|
292
+ stdout.puts "#{index.zero? ? 'Remediation:' : ' '} #{remediation}"
293
+ end
294
+ end
295
+
281
296
  def rule_title(rule_class)
282
- return 'Blocking subprocess call' if rule_class.id == 'FA1001'
297
+ return 'Subprocess lifecycle operations' if rule_class.id == 'FA1001'
283
298
 
284
299
  %i[TITLE RULE_TITLE].each do |name|
285
300
  return rule_class.const_get(name) if rule_class.const_defined?(name, false)
@@ -296,8 +311,7 @@ module FiberAudit
296
311
  when 'FA1003'
297
312
  expand_target_map(rule_class::TARGETS, '#')
298
313
  when 'FA1004'
299
- rule_class::THREAD_VARIABLE_METHODS.map { |method| "Thread##{method}" } +
300
- rule_class::INDEX_METHODS.map { |method| "Thread.current.#{method}" }
314
+ rule_class::THREAD_VARIABLE_METHODS.map { |method| "Thread##{method}" }
301
315
  when 'FA1005'
302
316
  expand_target_map(rule_class::TARGETS.transform_values { |method| [method] }, '.')
303
317
  when 'FA1006'
@@ -6,6 +6,7 @@ require_relative 'errors'
6
6
  require_relative 'findings/severity'
7
7
  require_relative 'runtime/policy'
8
8
  require_relative 'runtime/watchdog_policy'
9
+ require_relative 'runtime/operation_liveness_policy'
9
10
 
10
11
  module FiberAudit
11
12
  # rubocop:disable Metrics/ClassLength
@@ -14,7 +15,8 @@ module FiberAudit
14
15
  KNOWN_STATIC_KEYS = %w[include exclude suppressions_path].freeze
15
16
  KNOWN_REPORT_KEYS = %w[formats min_severity].freeze
16
17
  KNOWN_RULE_KEYS = %w[enabled severity].freeze
17
- KNOWN_RUNTIME_KEYS = %w[redaction sampling overhead watchdog fail_open].freeze
18
+ KNOWN_RUNTIME_KEYS = %w[redaction sampling overhead watchdog operation_liveness fail_open].freeze
19
+ KNOWN_OPERATION_LIVENESS_KEYS = %w[enabled poll_interval_ms long_active_threshold_ms].freeze
18
20
  KNOWN_REDACTION_KEYS = %w[mode].freeze
19
21
  KNOWN_SAMPLING_KEYS = %w[rate].freeze
20
22
  KNOWN_OVERHEAD_KEYS = %w[
@@ -50,7 +52,8 @@ module FiberAudit
50
52
 
51
53
  attr_reader :static_include, :static_exclude, :rules_config,
52
54
  :report_formats, :min_severity, :suppressions_path,
53
- :runtime_policy, :runtime_watchdog_policy
55
+ :runtime_policy, :runtime_watchdog_policy,
56
+ :runtime_operation_liveness_policy
54
57
 
55
58
  def initialize(
56
59
  static_include: DEFAULT_STATIC_INCLUDE,
@@ -60,12 +63,13 @@ module FiberAudit
60
63
  min_severity: :low,
61
64
  suppressions_path: nil,
62
65
  runtime_policy: Runtime::Policy.new,
63
- runtime_watchdog_policy: Runtime::WatchdogPolicy.new
66
+ runtime_watchdog_policy: Runtime::WatchdogPolicy.new,
67
+ runtime_operation_liveness_policy: Runtime::OperationLivenessPolicy.new
64
68
  )
65
69
  validate_types!(
66
70
  static_include, static_exclude, rules_config,
67
71
  report_formats, min_severity, suppressions_path, runtime_policy,
68
- runtime_watchdog_policy
72
+ runtime_watchdog_policy, runtime_operation_liveness_policy
69
73
  )
70
74
 
71
75
  @static_include = static_include
@@ -76,6 +80,7 @@ module FiberAudit
76
80
  @suppressions_path = suppressions_path
77
81
  @runtime_policy = runtime_policy
78
82
  @runtime_watchdog_policy = runtime_watchdog_policy
83
+ @runtime_operation_liveness_policy = runtime_operation_liveness_policy
79
84
  end
80
85
 
81
86
  def rule_enabled?(rule_id)
@@ -113,7 +118,8 @@ module FiberAudit
113
118
  min_severity: report.fetch('min_severity', :low),
114
119
  suppressions_path: static['suppressions_path'],
115
120
  runtime_policy: runtime_policy_from(runtime),
116
- runtime_watchdog_policy: runtime_watchdog_policy_from(runtime)
121
+ runtime_watchdog_policy: runtime_watchdog_policy_from(runtime),
122
+ runtime_operation_liveness_policy: runtime_operation_liveness_policy_from(runtime)
117
123
  )
118
124
  end
119
125
 
@@ -166,6 +172,7 @@ module FiberAudit
166
172
  validate_runtime_mapping!(runtime, 'sampling', KNOWN_SAMPLING_KEYS)
167
173
  validate_runtime_mapping!(runtime, 'overhead', KNOWN_OVERHEAD_KEYS)
168
174
  validate_runtime_mapping!(runtime, 'watchdog', KNOWN_WATCHDOG_KEYS)
175
+ validate_runtime_mapping!(runtime, 'operation_liveness', KNOWN_OPERATION_LIVENESS_KEYS)
169
176
  end
170
177
 
171
178
  def validate_runtime_mapping!(runtime, key, allowed)
@@ -214,6 +221,19 @@ module FiberAudit
214
221
  raise ConfigurationError, "#{path} is invalid: #{e.message}"
215
222
  end
216
223
 
224
+ def runtime_operation_liveness_policy_from(runtime)
225
+ values = runtime.fetch('operation_liveness', {})
226
+ defaults = Runtime::OperationLivenessPolicy::DEFAULTS
227
+ Runtime::OperationLivenessPolicy.new(
228
+ enabled: values.fetch('enabled', defaults[:enabled]),
229
+ poll_interval_ms: values.fetch('poll_interval_ms', defaults[:poll_interval_ms]),
230
+ long_active_threshold_ms: values.fetch('long_active_threshold_ms', defaults[:long_active_threshold_ms])
231
+ )
232
+ rescue RuntimeContractError => e
233
+ field = e.message.split.first
234
+ raise ConfigurationError, "runtime.operation_liveness.#{field} is invalid: #{e.message}"
235
+ end
236
+
217
237
  def check_unknown_keys(hash, allowed, path)
218
238
  unknown = hash.keys - allowed
219
239
  return if unknown.empty?
@@ -229,7 +249,8 @@ module FiberAudit
229
249
 
230
250
  def validate_types!(
231
251
  include_patterns, exclude_patterns, rules,
232
- formats, _severity, suppressions, runtime_policy, watchdog_policy
252
+ formats, _severity, suppressions, runtime_policy, watchdog_policy,
253
+ operation_liveness_policy
233
254
  )
234
255
  unless include_patterns.is_a?(Array) &&
235
256
  include_patterns.all?(String)
@@ -256,6 +277,10 @@ module FiberAudit
256
277
  raise ConfigurationError,
257
278
  'runtime_watchdog_policy must be a FiberAudit::Runtime::WatchdogPolicy'
258
279
  end
280
+ unless operation_liveness_policy.is_a?(Runtime::OperationLivenessPolicy)
281
+ raise ConfigurationError,
282
+ 'runtime_operation_liveness_policy must be a FiberAudit::Runtime::OperationLivenessPolicy'
283
+ end
259
284
 
260
285
  return if suppressions.nil? || suppressions.is_a?(String)
261
286