fiber_audit 0.1.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 +7 -0
- data/.fiber-audit.example.yml +33 -0
- data/CHANGELOG.md +32 -0
- data/README.md +150 -0
- data/bin/fiber-audit +5 -0
- data/lib/fiber_audit/audit.rb +288 -0
- data/lib/fiber_audit/cli.rb +245 -0
- data/lib/fiber_audit/configuration.rb +236 -0
- data/lib/fiber_audit/correlation/fingerprint.rb +26 -0
- data/lib/fiber_audit/errors.rb +8 -0
- data/lib/fiber_audit/execution_context.rb +47 -0
- data/lib/fiber_audit/findings/collection.rb +58 -0
- data/lib/fiber_audit/findings/confidence.rb +19 -0
- data/lib/fiber_audit/findings/evidence.rb +13 -0
- data/lib/fiber_audit/findings/finding.rb +76 -0
- data/lib/fiber_audit/findings/location.rb +9 -0
- data/lib/fiber_audit/findings/severity.rb +19 -0
- data/lib/fiber_audit/project.rb +96 -0
- data/lib/fiber_audit/reporters/base.rb +12 -0
- data/lib/fiber_audit/reporters/json.rb +34 -0
- data/lib/fiber_audit/reporters/schema.rb +574 -0
- data/lib/fiber_audit/reporters/text.rb +179 -0
- data/lib/fiber_audit/static/call_site.rb +71 -0
- data/lib/fiber_audit/static/call_site_extractor.rb +524 -0
- data/lib/fiber_audit/static/execution_context_resolver.rb +266 -0
- data/lib/fiber_audit/static/rules/base.rb +185 -0
- data/lib/fiber_audit/static/rules/blocking_subprocess.rb +94 -0
- data/lib/fiber_audit/static/rules/built_ins.rb +36 -0
- data/lib/fiber_audit/static/rules/direct_socket.rb +112 -0
- data/lib/fiber_audit/static/rules/io_select.rb +104 -0
- data/lib/fiber_audit/static/rules/net_http_in_request.rb +116 -0
- data/lib/fiber_audit/static/rules/registry.rb +123 -0
- data/lib/fiber_audit/static/rules/synchronization.rb +124 -0
- data/lib/fiber_audit/static/rules/thread_current_state.rb +113 -0
- data/lib/fiber_audit/static/rules/thread_join.rb +96 -0
- data/lib/fiber_audit/static/semantic_index.rb +300 -0
- data/lib/fiber_audit/suppressions/parser.rb +146 -0
- data/lib/fiber_audit/suppressions/store.rb +63 -0
- data/lib/fiber_audit/version.rb +5 -0
- data/lib/fiber_audit.rb +40 -0
- metadata +108 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'rubydex'
|
|
5
|
+
|
|
6
|
+
module FiberAudit
|
|
7
|
+
module Static
|
|
8
|
+
# Semantic index backed by Rubydex 0.2.9.
|
|
9
|
+
#
|
|
10
|
+
# === Line/column conventions (FiberAudit standard)
|
|
11
|
+
# * Lines are **one-based** (1, 2, 3, ...).
|
|
12
|
+
# * Columns are **zero-based** (0, 1, 2, ...).
|
|
13
|
+
#
|
|
14
|
+
# Rubydex 0.2.9 reports start_line as **zero-based** and start_column as
|
|
15
|
+
# zero-based. The +normalize_location+ helper adds 1 to start_line so that
|
|
16
|
+
# all returned locations use FiberAudit's one-based line convention.
|
|
17
|
+
class SemanticIndex
|
|
18
|
+
# Data types returned by public seams.
|
|
19
|
+
Declaration = Data.define(:name, :kind, :path, :line)
|
|
20
|
+
Reference = Data.define(:name, :path, :line, :column, :context)
|
|
21
|
+
Constant = Data.define(:name, :path, :line)
|
|
22
|
+
RubydexGap = Data.define(:method, :reason)
|
|
23
|
+
|
|
24
|
+
attr_reader :root, :gaps
|
|
25
|
+
|
|
26
|
+
def initialize(root:)
|
|
27
|
+
@root = Pathname.new(root).expand_path.cleanpath
|
|
28
|
+
@graph = nil
|
|
29
|
+
@gaps = []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Build (or rebuild) the index. Clears and deduplicates gaps on every call.
|
|
33
|
+
def build
|
|
34
|
+
@gaps = []
|
|
35
|
+
@graph = ::Rubydex::Graph.new
|
|
36
|
+
@graph.workspace_path = @root.to_s
|
|
37
|
+
@graph.index_workspace
|
|
38
|
+
@graph.resolve
|
|
39
|
+
record_gaps
|
|
40
|
+
self
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
@gaps << RubydexGap.new(method: 'build', reason: e.message)
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns Array[Declaration] for classes/modules/methods in the workspace.
|
|
47
|
+
# Returns one Declaration per workspace definition site so that reopened
|
|
48
|
+
# classes expose both (or more) definition sites.
|
|
49
|
+
def declarations
|
|
50
|
+
return [] unless @graph
|
|
51
|
+
|
|
52
|
+
@graph.declarations.select { |d| workspace_declaration?(d) }.filter_map do |decl|
|
|
53
|
+
kind = declaration_kind(decl)
|
|
54
|
+
workspace_definition_locations(decl).filter_map do |loc|
|
|
55
|
+
path, line = normalize_location(loc)
|
|
56
|
+
next unless path
|
|
57
|
+
|
|
58
|
+
Declaration.new(name: decl.name, kind: kind, path: path, line: line)
|
|
59
|
+
end
|
|
60
|
+
end.flatten
|
|
61
|
+
rescue StandardError
|
|
62
|
+
[]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Resolves a constant name to a Constant object.
|
|
66
|
+
# Returns nil if the constant cannot be resolved or is outside the workspace.
|
|
67
|
+
def resolve_constant(name, nesting:)
|
|
68
|
+
return nil unless @graph
|
|
69
|
+
|
|
70
|
+
result = @graph.resolve_constant(name.to_s, nesting || [])
|
|
71
|
+
return nil unless result
|
|
72
|
+
|
|
73
|
+
loc = first_workspace_location(result)
|
|
74
|
+
return nil unless loc
|
|
75
|
+
|
|
76
|
+
path, line = normalize_location(loc)
|
|
77
|
+
Constant.new(name: result.name, path: path, line: line)
|
|
78
|
+
rescue StandardError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns Array[String] of ancestor class/module names.
|
|
83
|
+
# Uses Rubydex's +ancestors+ method which considers ALL class/module
|
|
84
|
+
# declarations (including external/framework ancestors) while the target
|
|
85
|
+
# declaration itself remains workspace-owned (via find_declaration).
|
|
86
|
+
def ancestors_of(name)
|
|
87
|
+
return [] unless @graph
|
|
88
|
+
|
|
89
|
+
decl = find_declaration(name)
|
|
90
|
+
return [] unless decl
|
|
91
|
+
return [] unless decl.respond_to?(:ancestors)
|
|
92
|
+
|
|
93
|
+
ancestors = decl.ancestors
|
|
94
|
+
return [] unless ancestors
|
|
95
|
+
|
|
96
|
+
ancestors.select { |ancestor| class_or_module?(ancestor) && ancestor.name != name }
|
|
97
|
+
.map(&:name)
|
|
98
|
+
rescue StandardError
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns Array[String] of descendant class/module names.
|
|
103
|
+
# Gracefully returns [] if Rubydex cannot compute descendants.
|
|
104
|
+
def descendants_of(name)
|
|
105
|
+
return [] unless @graph
|
|
106
|
+
|
|
107
|
+
decl = find_declaration(name)
|
|
108
|
+
return [] unless decl
|
|
109
|
+
return [] unless decl.respond_to?(:descendants)
|
|
110
|
+
|
|
111
|
+
descendants = decl.descendants
|
|
112
|
+
return [] unless descendants
|
|
113
|
+
|
|
114
|
+
descendants.map(&:name).uniq - [name]
|
|
115
|
+
rescue StandardError
|
|
116
|
+
[]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Returns Array[Reference] for references to a given constant.
|
|
120
|
+
# Filters to workspace and rescues non-file URIs per reference.
|
|
121
|
+
def references_to(name)
|
|
122
|
+
return [] unless @graph
|
|
123
|
+
|
|
124
|
+
@graph.constant_references.filter_map do |ref|
|
|
125
|
+
next unless ref.respond_to?(:declaration) && ref.declaration&.name == name
|
|
126
|
+
|
|
127
|
+
loc = ref.location
|
|
128
|
+
next unless workspace_location?(loc)
|
|
129
|
+
|
|
130
|
+
path, line, column = normalize_location_full(loc)
|
|
131
|
+
Reference.new(name: name, path: path, line: line, column: column, context: nil)
|
|
132
|
+
rescue StandardError
|
|
133
|
+
nil
|
|
134
|
+
end
|
|
135
|
+
rescue StandardError
|
|
136
|
+
[]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
# ---- Pathname-based workspace containment ----
|
|
142
|
+
|
|
143
|
+
# Check if a file path is within the workspace using Pathname ancestry.
|
|
144
|
+
# This avoids string-prefix false positives like /workspace matching /workspace-other.
|
|
145
|
+
def workspace_path?(file_path)
|
|
146
|
+
return false unless file_path && !file_path.empty?
|
|
147
|
+
|
|
148
|
+
path_obj = Pathname.new(file_path)
|
|
149
|
+
path_obj == @root || path_obj.ascend.any? { |ancestor| ancestor == @root }
|
|
150
|
+
rescue ArgumentError, TypeError
|
|
151
|
+
false
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check if a Rubydex location points to a file within the workspace.
|
|
155
|
+
# Rescues non-file URIs (e.g. gem:// URIs) per location.
|
|
156
|
+
def workspace_location?(loc)
|
|
157
|
+
return false unless loc
|
|
158
|
+
|
|
159
|
+
file_path = location_file_path(loc)
|
|
160
|
+
return false unless file_path
|
|
161
|
+
|
|
162
|
+
workspace_path?(file_path)
|
|
163
|
+
rescue StandardError
|
|
164
|
+
false
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def workspace_declaration?(decl)
|
|
168
|
+
return false unless decl.respond_to?(:definitions)
|
|
169
|
+
|
|
170
|
+
decl.definitions.any? do |defn|
|
|
171
|
+
workspace_location?(defn.location)
|
|
172
|
+
rescue StandardError
|
|
173
|
+
false
|
|
174
|
+
end
|
|
175
|
+
rescue StandardError
|
|
176
|
+
false
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# ---- Definition site enumeration ----
|
|
180
|
+
|
|
181
|
+
# Returns all workspace-owned locations from a declaration's definitions.
|
|
182
|
+
# This ensures reopened classes expose all definition sites.
|
|
183
|
+
def workspace_definition_locations(decl)
|
|
184
|
+
return [] unless decl.respond_to?(:definitions)
|
|
185
|
+
|
|
186
|
+
decl.definitions.filter_map do |d|
|
|
187
|
+
d.location if workspace_location?(d.location)
|
|
188
|
+
rescue StandardError
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
rescue StandardError
|
|
192
|
+
[]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Returns the first workspace-owned location from a declaration's definitions.
|
|
196
|
+
# Used for constant resolution where only the primary site is needed.
|
|
197
|
+
def first_workspace_location(decl)
|
|
198
|
+
return nil unless decl.respond_to?(:definitions)
|
|
199
|
+
|
|
200
|
+
defn = decl.definitions.find do |d|
|
|
201
|
+
workspace_location?(d.location)
|
|
202
|
+
rescue StandardError
|
|
203
|
+
false
|
|
204
|
+
end
|
|
205
|
+
defn&.location
|
|
206
|
+
rescue StandardError
|
|
207
|
+
nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# ---- Location normalization (FiberAudit: 1-based lines, 0-based columns) ----
|
|
211
|
+
|
|
212
|
+
# Extracts the file system path from a rubydex location.
|
|
213
|
+
# Uses to_file_path which raises NotFileUriError for non-file URIs.
|
|
214
|
+
def location_file_path(loc)
|
|
215
|
+
loc.to_file_path
|
|
216
|
+
rescue ::Rubydex::Location::NotFileUriError, StandardError
|
|
217
|
+
nil
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Normalize a rubydex location to [path, line] for Declaration/Constant.
|
|
221
|
+
# Rubydex 0.2.9 reports start_line as zero-based; we add 1 for the
|
|
222
|
+
# FiberAudit one-based line convention.
|
|
223
|
+
def normalize_location(loc)
|
|
224
|
+
path = location_file_path(loc)
|
|
225
|
+
[path, loc.start_line + 1]
|
|
226
|
+
rescue StandardError
|
|
227
|
+
[nil, nil]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Normalize a rubydex location to [path, line, column] for Reference.
|
|
231
|
+
# Rubydex 0.2.9 reports start_line as zero-based; we add 1 for the
|
|
232
|
+
# FiberAudit one-based line convention. Columns remain zero-based.
|
|
233
|
+
def normalize_location_full(loc)
|
|
234
|
+
path = location_file_path(loc)
|
|
235
|
+
[path, loc.start_line + 1, loc.start_column]
|
|
236
|
+
rescue StandardError
|
|
237
|
+
[nil, nil, nil]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def declaration_kind(decl)
|
|
241
|
+
case decl
|
|
242
|
+
when defined?(::Rubydex::Class) && ::Rubydex::Class
|
|
243
|
+
:class
|
|
244
|
+
when defined?(::Rubydex::Module) && ::Rubydex::Module
|
|
245
|
+
:module
|
|
246
|
+
when defined?(::Rubydex::Method) && ::Rubydex::Method
|
|
247
|
+
:method
|
|
248
|
+
else
|
|
249
|
+
:unknown
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def class_or_module?(decl)
|
|
254
|
+
return false unless decl
|
|
255
|
+
|
|
256
|
+
(defined?(::Rubydex::Class) && decl.is_a?(::Rubydex::Class)) ||
|
|
257
|
+
(defined?(::Rubydex::Module) && decl.is_a?(::Rubydex::Module))
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def find_declaration(name)
|
|
261
|
+
@graph.declarations.find do |d|
|
|
262
|
+
workspace_declaration?(d) && d.name == name.to_s
|
|
263
|
+
end
|
|
264
|
+
rescue StandardError
|
|
265
|
+
nil
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# ---- Gap management ----
|
|
269
|
+
|
|
270
|
+
# Clears and deduplicates gaps on each build.
|
|
271
|
+
# Uses RubydexGap Data objects (not hashes).
|
|
272
|
+
def record_gaps
|
|
273
|
+
known_gaps = [
|
|
274
|
+
RubydexGap.new(
|
|
275
|
+
method: 'method_name_from_reference',
|
|
276
|
+
reason: 'Rubydex::MethodReference only exposes receiver and location, not the method name being called'
|
|
277
|
+
),
|
|
278
|
+
RubydexGap.new(
|
|
279
|
+
method: 'ancestors_external',
|
|
280
|
+
reason: 'Rubydex ancestors may not include all external ancestors without explicit dependency indexing'
|
|
281
|
+
),
|
|
282
|
+
RubydexGap.new(
|
|
283
|
+
method: 'call_site_extraction',
|
|
284
|
+
reason: 'Rubydex tracks method references without names or call context; Prism AST parsing needed'
|
|
285
|
+
),
|
|
286
|
+
RubydexGap.new(
|
|
287
|
+
method: 'dynamic_methods',
|
|
288
|
+
reason: 'Rubydex may not fully track methods defined via define_method, method_missing, or other metaprogramming'
|
|
289
|
+
),
|
|
290
|
+
RubydexGap.new(
|
|
291
|
+
method: 'def_delegator_tracking',
|
|
292
|
+
reason: 'Forwardable def_delegator creates method aliases that Rubydex may not fully resolve to their target'
|
|
293
|
+
)
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
@gaps = (known_gaps + @gaps).uniq
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'prism'
|
|
5
|
+
require_relative '../errors'
|
|
6
|
+
|
|
7
|
+
module FiberAudit
|
|
8
|
+
module Suppressions
|
|
9
|
+
InlineSuppression = Data.define(
|
|
10
|
+
:rule_id, :reason, :path, :start_line, :end_line
|
|
11
|
+
)
|
|
12
|
+
YamlSuppression = Data.define(:rule, :symbol, :operation, :reason)
|
|
13
|
+
|
|
14
|
+
class Parser
|
|
15
|
+
class << self
|
|
16
|
+
# Parses inline suppressions from a file's content using Prism
|
|
17
|
+
# for comment detection.
|
|
18
|
+
# Directives are recognized ONLY from actual Prism comments -
|
|
19
|
+
# never from raw text scans. This ensures directive text in
|
|
20
|
+
# strings, heredocs, and regex literals cannot start or end
|
|
21
|
+
# suppressions.
|
|
22
|
+
def parse_inline(path, content)
|
|
23
|
+
result = Prism.parse(content)
|
|
24
|
+
return [] if result.errors.any?
|
|
25
|
+
|
|
26
|
+
lines = content.lines
|
|
27
|
+
disables = []
|
|
28
|
+
enables = []
|
|
29
|
+
|
|
30
|
+
# Only examine actual comments from Prism
|
|
31
|
+
result.comments.each do |comment|
|
|
32
|
+
text = comment.location.slice
|
|
33
|
+
line_num = comment.location.start_line
|
|
34
|
+
|
|
35
|
+
if (match = text.match(
|
|
36
|
+
/fiber-audit:disable\s+(FA\d+)/
|
|
37
|
+
))
|
|
38
|
+
rule_id = match[1]
|
|
39
|
+
reason = extract_reason(text, path, line_num)
|
|
40
|
+
is_block = block_comment?(comment, lines)
|
|
41
|
+
disables << {
|
|
42
|
+
rule_id: rule_id,
|
|
43
|
+
reason: reason,
|
|
44
|
+
line: line_num,
|
|
45
|
+
block: is_block
|
|
46
|
+
}
|
|
47
|
+
elsif (match = text.match(
|
|
48
|
+
/fiber-audit:enable\s+(FA\d+)/
|
|
49
|
+
))
|
|
50
|
+
enables << {
|
|
51
|
+
rule_id: match[1],
|
|
52
|
+
line: line_num
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
build_suppressions(disables, enables, lines, path)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Parse YAML suppressions file
|
|
61
|
+
def parse_yaml(path)
|
|
62
|
+
return [] unless path && File.exist?(path)
|
|
63
|
+
|
|
64
|
+
yaml = YAML.safe_load_file(path) || {}
|
|
65
|
+
raw_suppressions = yaml['suppressions'] || []
|
|
66
|
+
|
|
67
|
+
raw_suppressions.map do |entry|
|
|
68
|
+
rule = entry['rule']
|
|
69
|
+
reason = entry['reason']
|
|
70
|
+
|
|
71
|
+
if reason.nil? || reason.strip.empty?
|
|
72
|
+
raise FiberAudit::ConfigurationError,
|
|
73
|
+
"YAML suppression for rule #{rule} at #{path} " \
|
|
74
|
+
"missing 'reason'"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
YamlSuppression.new(
|
|
78
|
+
rule: rule,
|
|
79
|
+
symbol: entry['symbol'],
|
|
80
|
+
operation: entry['operation'],
|
|
81
|
+
reason: reason.strip
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# Extract and validate reason from comment text
|
|
89
|
+
def extract_reason(text, path, line_number)
|
|
90
|
+
match = text.match(/--\s+(.+)$/)
|
|
91
|
+
if match.nil? || match[1].strip.empty?
|
|
92
|
+
raise FiberAudit::ConfigurationError,
|
|
93
|
+
"Inline suppression at #{path}:#{line_number} " \
|
|
94
|
+
'missing reason (use -- <reason>)'
|
|
95
|
+
end
|
|
96
|
+
match[1].strip
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Determine if comment is a block comment (only whitespace
|
|
100
|
+
# before #) vs a trailing comment (has code before #)
|
|
101
|
+
def block_comment?(comment, lines)
|
|
102
|
+
line = lines[comment.location.start_line - 1]
|
|
103
|
+
prefix = line[0, comment.location.start_column]
|
|
104
|
+
prefix.nil? || prefix.strip.empty?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Build InlineSuppression objects, matching enables to disables
|
|
108
|
+
def build_suppressions(disables, enables, lines, path)
|
|
109
|
+
used_enable_lines = []
|
|
110
|
+
|
|
111
|
+
disables.map do |d|
|
|
112
|
+
end_line = if d[:block]
|
|
113
|
+
enable = find_matching_enable(
|
|
114
|
+
d, enables, used_enable_lines
|
|
115
|
+
)
|
|
116
|
+
if enable
|
|
117
|
+
used_enable_lines << enable[:line]
|
|
118
|
+
enable[:line]
|
|
119
|
+
else
|
|
120
|
+
lines.size
|
|
121
|
+
end
|
|
122
|
+
else
|
|
123
|
+
d[:line]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
InlineSuppression.new(
|
|
127
|
+
rule_id: d[:rule_id],
|
|
128
|
+
reason: d[:reason],
|
|
129
|
+
path: path,
|
|
130
|
+
start_line: d[:line],
|
|
131
|
+
end_line: end_line
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def find_matching_enable(disable, enables, used_lines)
|
|
137
|
+
enables.find do |e|
|
|
138
|
+
e[:rule_id] == disable[:rule_id] &&
|
|
139
|
+
e[:line] > disable[:line] &&
|
|
140
|
+
!used_lines.include?(e[:line])
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'parser'
|
|
4
|
+
|
|
5
|
+
module FiberAudit
|
|
6
|
+
module Suppressions
|
|
7
|
+
class Store
|
|
8
|
+
attr_reader :inline_suppressions, :yaml_suppressions
|
|
9
|
+
|
|
10
|
+
def initialize(inline_suppressions: [], yaml_suppressions: [])
|
|
11
|
+
@inline_suppressions = inline_suppressions
|
|
12
|
+
@yaml_suppressions = yaml_suppressions
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Returns [active_findings, suppressed_findings]
|
|
16
|
+
def apply(findings)
|
|
17
|
+
suppressed = []
|
|
18
|
+
active = []
|
|
19
|
+
|
|
20
|
+
findings.each do |finding|
|
|
21
|
+
if suppressed?(finding)
|
|
22
|
+
suppressed << finding
|
|
23
|
+
else
|
|
24
|
+
active << finding
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
[active, suppressed]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def suppressed?(finding)
|
|
34
|
+
return true if yaml_suppressed?(finding)
|
|
35
|
+
return true if inline_suppressed?(finding)
|
|
36
|
+
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def yaml_suppressed?(finding)
|
|
41
|
+
@yaml_suppressions.any? do |sup|
|
|
42
|
+
next false unless sup.rule == finding.rule_id
|
|
43
|
+
next false if sup.symbol && sup.symbol != finding.symbol
|
|
44
|
+
next false if sup.operation && sup.operation != finding.operation
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def inline_suppressed?(finding)
|
|
51
|
+
return false unless finding.location
|
|
52
|
+
|
|
53
|
+
path = finding.location.path
|
|
54
|
+
line = finding.location.line
|
|
55
|
+
|
|
56
|
+
@inline_suppressions.any? do |sup|
|
|
57
|
+
sup.path == path && line >= sup.start_line && line <= sup.end_line &&
|
|
58
|
+
sup.rule_id == finding.rule_id
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/fiber_audit.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'fiber_audit/version'
|
|
4
|
+
require_relative 'fiber_audit/errors'
|
|
5
|
+
require_relative 'fiber_audit/findings/severity'
|
|
6
|
+
require_relative 'fiber_audit/findings/confidence'
|
|
7
|
+
require_relative 'fiber_audit/findings/location'
|
|
8
|
+
require_relative 'fiber_audit/findings/evidence'
|
|
9
|
+
require_relative 'fiber_audit/correlation/fingerprint'
|
|
10
|
+
require_relative 'fiber_audit/findings/finding'
|
|
11
|
+
require_relative 'fiber_audit/findings/collection'
|
|
12
|
+
require_relative 'fiber_audit/configuration'
|
|
13
|
+
require_relative 'fiber_audit/suppressions/parser'
|
|
14
|
+
require_relative 'fiber_audit/suppressions/store'
|
|
15
|
+
require_relative 'fiber_audit/execution_context'
|
|
16
|
+
require_relative 'fiber_audit/static/semantic_index'
|
|
17
|
+
require_relative 'fiber_audit/static/call_site'
|
|
18
|
+
require_relative 'fiber_audit/static/call_site_extractor'
|
|
19
|
+
require_relative 'fiber_audit/static/execution_context_resolver'
|
|
20
|
+
require_relative 'fiber_audit/static/rules/base'
|
|
21
|
+
require_relative 'fiber_audit/static/rules/registry'
|
|
22
|
+
require_relative 'fiber_audit/static/rules/blocking_subprocess'
|
|
23
|
+
require_relative 'fiber_audit/static/rules/thread_join'
|
|
24
|
+
require_relative 'fiber_audit/static/rules/synchronization'
|
|
25
|
+
require_relative 'fiber_audit/static/rules/thread_current_state'
|
|
26
|
+
require_relative 'fiber_audit/static/rules/io_select'
|
|
27
|
+
require_relative 'fiber_audit/static/rules/direct_socket'
|
|
28
|
+
require_relative 'fiber_audit/static/rules/net_http_in_request'
|
|
29
|
+
require_relative 'fiber_audit/static/rules/built_ins'
|
|
30
|
+
require_relative 'fiber_audit/reporters/base'
|
|
31
|
+
require_relative 'fiber_audit/reporters/schema'
|
|
32
|
+
require_relative 'fiber_audit/reporters/text'
|
|
33
|
+
require_relative 'fiber_audit/reporters/json'
|
|
34
|
+
require_relative 'fiber_audit/project'
|
|
35
|
+
require_relative 'fiber_audit/audit'
|
|
36
|
+
require_relative 'fiber_audit/cli'
|
|
37
|
+
|
|
38
|
+
module FiberAudit
|
|
39
|
+
# Public entry point — expanded as work packages are implemented
|
|
40
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fiber_audit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- FiberAudit Contributors
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rubydex
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: 0.2.0
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: 0.2.0
|
|
40
|
+
executables:
|
|
41
|
+
- fiber-audit
|
|
42
|
+
extensions: []
|
|
43
|
+
extra_rdoc_files: []
|
|
44
|
+
files:
|
|
45
|
+
- ".fiber-audit.example.yml"
|
|
46
|
+
- CHANGELOG.md
|
|
47
|
+
- README.md
|
|
48
|
+
- bin/fiber-audit
|
|
49
|
+
- lib/fiber_audit.rb
|
|
50
|
+
- lib/fiber_audit/audit.rb
|
|
51
|
+
- lib/fiber_audit/cli.rb
|
|
52
|
+
- lib/fiber_audit/configuration.rb
|
|
53
|
+
- lib/fiber_audit/correlation/fingerprint.rb
|
|
54
|
+
- lib/fiber_audit/errors.rb
|
|
55
|
+
- lib/fiber_audit/execution_context.rb
|
|
56
|
+
- lib/fiber_audit/findings/collection.rb
|
|
57
|
+
- lib/fiber_audit/findings/confidence.rb
|
|
58
|
+
- lib/fiber_audit/findings/evidence.rb
|
|
59
|
+
- lib/fiber_audit/findings/finding.rb
|
|
60
|
+
- lib/fiber_audit/findings/location.rb
|
|
61
|
+
- lib/fiber_audit/findings/severity.rb
|
|
62
|
+
- lib/fiber_audit/project.rb
|
|
63
|
+
- lib/fiber_audit/reporters/base.rb
|
|
64
|
+
- lib/fiber_audit/reporters/json.rb
|
|
65
|
+
- lib/fiber_audit/reporters/schema.rb
|
|
66
|
+
- lib/fiber_audit/reporters/text.rb
|
|
67
|
+
- lib/fiber_audit/static/call_site.rb
|
|
68
|
+
- lib/fiber_audit/static/call_site_extractor.rb
|
|
69
|
+
- lib/fiber_audit/static/execution_context_resolver.rb
|
|
70
|
+
- lib/fiber_audit/static/rules/base.rb
|
|
71
|
+
- lib/fiber_audit/static/rules/blocking_subprocess.rb
|
|
72
|
+
- lib/fiber_audit/static/rules/built_ins.rb
|
|
73
|
+
- lib/fiber_audit/static/rules/direct_socket.rb
|
|
74
|
+
- lib/fiber_audit/static/rules/io_select.rb
|
|
75
|
+
- lib/fiber_audit/static/rules/net_http_in_request.rb
|
|
76
|
+
- lib/fiber_audit/static/rules/registry.rb
|
|
77
|
+
- lib/fiber_audit/static/rules/synchronization.rb
|
|
78
|
+
- lib/fiber_audit/static/rules/thread_current_state.rb
|
|
79
|
+
- lib/fiber_audit/static/rules/thread_join.rb
|
|
80
|
+
- lib/fiber_audit/static/semantic_index.rb
|
|
81
|
+
- lib/fiber_audit/suppressions/parser.rb
|
|
82
|
+
- lib/fiber_audit/suppressions/store.rb
|
|
83
|
+
- lib/fiber_audit/version.rb
|
|
84
|
+
homepage: https://github.com/vdombr/fiber_audit
|
|
85
|
+
licenses:
|
|
86
|
+
- MIT
|
|
87
|
+
metadata:
|
|
88
|
+
rubygems_mfa_required: 'true'
|
|
89
|
+
source_code_uri: https://github.com/vdombr/fiber_audit
|
|
90
|
+
changelog_uri: https://github.com/vdombr/fiber_audit/blob/main/CHANGELOG.md
|
|
91
|
+
rdoc_options: []
|
|
92
|
+
require_paths:
|
|
93
|
+
- lib
|
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: 3.3.0
|
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
requirements: []
|
|
105
|
+
rubygems_version: 3.6.9
|
|
106
|
+
specification_version: 4
|
|
107
|
+
summary: Static fiber-scheduler compatibility auditor for Ruby and Rails applications
|
|
108
|
+
test_files: []
|