necropsy 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +33 -0
- data/CHANGELOG.md +44 -0
- data/MEASUREMENTS.md +112 -0
- data/README.md +233 -16
- data/Rakefile +3 -1
- data/bench/README.md +92 -0
- data/bench/audit.rb +113 -0
- data/bench/audits/0.2.1/audit.json +501 -0
- data/bench/audits/0.2.1/audit.md +55 -0
- data/bench/audits/0.2.1/baseline_performance.yml +16 -0
- data/bench/audits/0.2.1/config.yml +42 -0
- data/bench/audits/0.2.1/review.yml +56 -0
- data/bench/audits/0.2.1/review_queue.yml +3651 -0
- data/bench/corpora/v1/README.md +30 -0
- data/bench/corpora/v1/labels.yml +37 -0
- data/bench/corpora/v1/manifest.yml +63 -0
- data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
- data/bench/corpora/v1/self.necropsy.yml +8 -0
- data/bench/corpora/v1/tools/debride.yml +42 -0
- data/bench/corpora/v1/tools/spoom.yml +12 -0
- data/bench/corpora/v1/tools/type_aware.yml +11 -0
- data/bench/golden/v1/candidate_union.json +21918 -0
- data/bench/golden/v1/metadata.json +12 -0
- data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
- data/bench/golden/v1/reports/plain_ruby.json +135 -0
- data/bench/golden/v1/reports/rails.json +176 -0
- data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
- data/bench/golden/v1/reports/self.json +3449 -0
- data/bench/review_queue.rb +35 -0
- data/bench/run.rb +31 -0
- data/bench/schema/candidate-union-v1.schema.json +70 -0
- data/docs/impv_implementation_matrix.md +179 -0
- data/docs/migrations/0.2.1.md +57 -0
- data/docs/migrations/0.3.0.md +207 -0
- data/docs/migrations/0.4.0.md +13 -0
- data/docs/necropsy_performance_adr.md +43 -0
- data/docs/necropsy_scope_decisions.md +58 -0
- data/docs/necropsy_type_facts_adr.md +22 -0
- data/gemfiles/prism_min.gemfile +9 -0
- data/gemfiles/prism_min.gemfile.lock +50 -0
- data/lib/necropsy/analyzer.rb +121 -2
- data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +81 -19
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +104 -10
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +74 -250
- data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
- data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
- data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
- data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
- data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
- data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +225 -20
- data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
- data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
- data/lib/necropsy/analyzers/static/cha.rb +33 -69
- data/lib/necropsy/analyzers/static/name_resolution.rb +151 -21
- data/lib/necropsy/analyzers/static/rta.rb +249 -25
- data/lib/necropsy/ast_scanner/call_recording.rb +275 -0
- data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
- data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
- data/lib/necropsy/ast_scanner/dsl_macros.rb +532 -0
- data/lib/necropsy/ast_scanner/method_definitions.rb +358 -0
- data/lib/necropsy/ast_scanner/references.rb +115 -0
- data/lib/necropsy/ast_scanner/ruby_semantics.rb +263 -0
- data/lib/necropsy/ast_scanner/traversal.rb +347 -0
- data/lib/necropsy/ast_scanner/value_definitions.rb +78 -0
- data/lib/necropsy/ast_scanner.rb +89 -564
- data/lib/necropsy/bench/candidate_union.rb +555 -0
- data/lib/necropsy/bench/claim_gate.rb +112 -0
- data/lib/necropsy/bench/evaluator.rb +356 -24
- data/lib/necropsy/bench/finding_facts.rb +152 -0
- data/lib/necropsy/bench/precision_gate.rb +144 -0
- data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
- data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
- data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
- data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
- data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
- data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
- data/lib/necropsy/bench/release_audit.rb +360 -0
- data/lib/necropsy/bench/report_normalizer.rb +140 -0
- data/lib/necropsy/bench/review_queue.rb +154 -0
- data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
- data/lib/necropsy/bench/seed_runner.rb +408 -0
- data/lib/necropsy/bounded_canonicalizer.rb +218 -0
- data/lib/necropsy/cache/scan_cache.rb +94 -18
- data/lib/necropsy/call_site_identity.rb +54 -0
- data/lib/necropsy/cli.rb +296 -53
- data/lib/necropsy/clock.rb +40 -0
- data/lib/necropsy/confidence/scorer.rb +181 -58
- data/lib/necropsy/configuration.rb +339 -18
- data/lib/necropsy/convention_rules.rb +138 -0
- data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
- data/lib/necropsy/definition_identity.rb +37 -0
- data/lib/necropsy/diagnostics.rb +342 -0
- data/lib/necropsy/embedded_ruby.rb +55 -0
- data/lib/necropsy/entry_points/plain.rb +125 -7
- data/lib/necropsy/entry_points/rails.rb +422 -105
- data/lib/necropsy/entry_points/test.rb +6 -1
- data/lib/necropsy/flow_interpreter.rb +460 -0
- data/lib/necropsy/graph/blocker_matching.rb +338 -0
- data/lib/necropsy/graph/call_graph.rb +1234 -62
- data/lib/necropsy/graph/definition_index.rb +149 -0
- data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
- data/lib/necropsy/graph/evidence_store.rb +213 -0
- data/lib/necropsy/graph/resolution_store.rb +497 -0
- data/lib/necropsy/graph_self_check.rb +79 -0
- data/lib/necropsy/guardrail/baseline.rb +359 -13
- data/lib/necropsy/guardrail/diff.rb +5 -2
- data/lib/necropsy/guardrail/quarantine.rb +101 -10
- data/lib/necropsy/load_graph.rb +206 -0
- data/lib/necropsy/models.rb +894 -21
- data/lib/necropsy/performance_profiler.rb +108 -0
- data/lib/necropsy/project.rb +361 -13
- data/lib/necropsy/reachability/engine.rb +80 -15
- data/lib/necropsy/reference_barrier.rb +458 -0
- data/lib/necropsy/report.rb +153 -14
- data/lib/necropsy/reporter.rb +441 -19
- data/lib/necropsy/runner.rb +258 -14
- data/lib/necropsy/runtime_feedback.rb +136 -0
- data/lib/necropsy/semantics_matrix.rb +153 -0
- data/lib/necropsy/trace_point_runtime.rb +19 -0
- data/lib/necropsy/type_facts.rb +53 -0
- data/lib/necropsy/version.rb +1 -1
- data/lib/necropsy/why_not_explanation.rb +436 -0
- data/lib/necropsy/why_not_renderer.rb +197 -0
- data/lib/necropsy/world_policy.rb +90 -0
- data/lib/necropsy.rb +39 -2
- data/schema/necropsy-report-v2.schema.json +366 -0
- data/script/measure.rb +20 -0
- metadata +106 -2
|
@@ -5,106 +5,133 @@ require 'date'
|
|
|
5
5
|
module Necropsy
|
|
6
6
|
module Confidence
|
|
7
7
|
class Scorer
|
|
8
|
-
|
|
8
|
+
RUBY_HOOKS = %w[
|
|
9
|
+
inherited included extended prepended method_added singleton_method_added
|
|
10
|
+
const_missing method_missing respond_to_missing?
|
|
11
|
+
].freeze
|
|
12
|
+
RUBY_PROTOCOLS = %w[
|
|
13
|
+
== eql? hash <=> to_s to_str to_a to_h to_proc inspect coerce
|
|
14
|
+
succ initialize_copy marshal_dump marshal_load
|
|
15
|
+
].freeze
|
|
16
|
+
BUILTIN_IMPLICIT_CALLERS = [
|
|
17
|
+
{
|
|
18
|
+
name_pattern: /^on_/,
|
|
19
|
+
owner_ancestors: ['RuboCop::Cop::Base'],
|
|
20
|
+
reason: 'RuboCop Commissioner dispatches on_* callbacks'
|
|
21
|
+
}
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
def initialize(graph:, reachability:, project:, clock: Clock.new)
|
|
9
25
|
@graph = graph
|
|
10
26
|
@reachability = reachability
|
|
11
27
|
@project = project
|
|
28
|
+
@clock = clock
|
|
29
|
+
@source_lines = {}
|
|
12
30
|
end
|
|
13
31
|
|
|
14
32
|
def findings
|
|
15
33
|
graph.method_nodes.filter_map do |node|
|
|
16
34
|
next if node.test
|
|
35
|
+
next unless graph.analyze_source?(node.file)
|
|
36
|
+
next if rails_generated?(node)
|
|
17
37
|
|
|
18
|
-
|
|
38
|
+
blockers = graph.matching_blockers(node)
|
|
39
|
+
classification = classification_for(node, blockers)
|
|
19
40
|
next unless classification
|
|
20
41
|
|
|
21
|
-
score, level, reasons = score_for(node, classification)
|
|
42
|
+
score, level, reasons, score_components = score_for(node, classification, blockers)
|
|
22
43
|
Finding.new(
|
|
23
44
|
node: node,
|
|
24
45
|
classification: classification,
|
|
25
46
|
confidence: level,
|
|
26
47
|
score: score,
|
|
48
|
+
score_components: score_components,
|
|
27
49
|
reasons: reasons,
|
|
28
|
-
evidences: graph.incoming_edges(node.
|
|
50
|
+
evidences: graph.incoming_edges(node.graph_id).flat_map(&:evidences) + graph.alive_evidences(node.graph_id),
|
|
51
|
+
blockers: blockers
|
|
29
52
|
)
|
|
30
53
|
end
|
|
31
54
|
end
|
|
32
55
|
|
|
33
56
|
private
|
|
34
57
|
|
|
35
|
-
attr_reader :graph, :reachability, :project
|
|
58
|
+
attr_reader :graph, :reachability, :project, :clock
|
|
36
59
|
|
|
37
|
-
def classification_for(node)
|
|
38
|
-
if
|
|
39
|
-
return :unused if graph.dynamic_enabled? && !graph.dynamic_alive?(node.id)
|
|
60
|
+
def classification_for(node, blockers)
|
|
61
|
+
return nil if graph.dynamic_alive?(node.graph_id)
|
|
40
62
|
|
|
63
|
+
if reachability.runtime_paths.key?(node.graph_id) || reachability.external_paths.key?(node.graph_id)
|
|
41
64
|
nil
|
|
42
|
-
elsif
|
|
65
|
+
elsif blockers.any?
|
|
66
|
+
:blocked
|
|
67
|
+
elsif reachability.test_paths.key?(node.graph_id)
|
|
43
68
|
:test_only_reachable
|
|
44
69
|
else
|
|
45
70
|
:unreachable
|
|
46
71
|
end
|
|
47
72
|
end
|
|
48
73
|
|
|
49
|
-
def score_for(node, classification)
|
|
74
|
+
def score_for(node, classification, blockers)
|
|
50
75
|
reasons = []
|
|
51
76
|
score = base_score(classification)
|
|
77
|
+
components = [score_component("base(#{classification})", score, 'Base classification score')]
|
|
78
|
+
|
|
79
|
+
if blockers.any?
|
|
80
|
+
components << score_component('matching_blocker', 0.0, 'Incomplete runtime dispatch matches this definition')
|
|
81
|
+
reasons << "Blocked by #{blockers.length} unresolved runtime dispatch#{'es' unless blockers.one?}."
|
|
82
|
+
end
|
|
52
83
|
|
|
53
|
-
if graph.uncertainties(node.
|
|
84
|
+
if graph.uncertainties(node.graph_id).any?
|
|
54
85
|
score -= 0.35
|
|
86
|
+
components << score_component('near_metaprogramming', -0.35, 'Unresolved metaprogramming nearby')
|
|
55
87
|
reasons << 'Lowered because this node is near unresolved metaprogramming.'
|
|
56
88
|
end
|
|
57
89
|
|
|
58
90
|
if graph.class_info(node.owner)&.dynamic
|
|
59
91
|
score -= 0.25
|
|
92
|
+
components << score_component('dynamic_owner', -0.25, 'Owner defines dynamic dispatch')
|
|
60
93
|
reasons << 'Lowered because the owner class defines dynamic dispatch.'
|
|
61
94
|
end
|
|
62
95
|
|
|
63
96
|
if generated_accessor?(node)
|
|
64
97
|
score -= 0.2
|
|
98
|
+
components << score_component('generated_accessor', -0.2, 'Generated by an accessor DSL')
|
|
65
99
|
reasons << 'Lowered because this node is generated by a Ruby accessor DSL.'
|
|
66
100
|
end
|
|
67
101
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
reasons << "
|
|
102
|
+
if (reason = implicit_call_context(node))
|
|
103
|
+
score -= 0.4
|
|
104
|
+
components << score_component('implicit_caller', -0.4, reason)
|
|
105
|
+
reasons << "Lowered because #{reason}."
|
|
72
106
|
end
|
|
73
107
|
|
|
74
|
-
|
|
75
|
-
if evidence_analyzers.length > 1
|
|
76
|
-
score += 0.08
|
|
77
|
-
reasons << 'Raised because multiple analyzers agree on incoming reachability evidence.'
|
|
78
|
-
end
|
|
108
|
+
add_runtime_unobserved_annotation(node, reasons, components)
|
|
79
109
|
|
|
80
|
-
|
|
81
|
-
score += 0.25
|
|
82
|
-
reasons << 'Static unreachable and absent from dynamic observations.'
|
|
83
|
-
end
|
|
110
|
+
add_quarantine_annotation(node, classification, reasons, components)
|
|
84
111
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
112
|
+
clamped_score = score.clamp(0.0, 1.0)
|
|
113
|
+
components << score_component('score_clamp', clamped_score - score, 'Clamped to the 0.0–1.0 range') if clamped_score != score
|
|
114
|
+
[clamped_score, level_for(clamped_score), reasons, components]
|
|
115
|
+
end
|
|
89
116
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
end
|
|
98
|
-
end
|
|
117
|
+
def score_component(name, value, details)
|
|
118
|
+
ScoreComponent.new(name: name, value: value.round(4), details: details)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def add_runtime_unobserved_annotation(node, reasons, components)
|
|
122
|
+
return unless graph.dynamic_observation?
|
|
123
|
+
return if graph.dynamic_alive?(node.graph_id)
|
|
99
124
|
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
components << score_component('runtime_unobserved', 0.0, 'Informational only; deadness is unchanged')
|
|
126
|
+
reasons << 'Runtime observations did not include this definition; classification and confidence are unchanged.'
|
|
102
127
|
end
|
|
103
128
|
|
|
104
129
|
def base_score(classification)
|
|
130
|
+
return 0.25 if classification == :blocked
|
|
131
|
+
|
|
105
132
|
case classification
|
|
106
133
|
when :unreachable
|
|
107
|
-
|
|
134
|
+
0.62
|
|
108
135
|
when :test_only_reachable
|
|
109
136
|
0.55
|
|
110
137
|
when :unused
|
|
@@ -118,6 +145,62 @@ module Necropsy
|
|
|
118
145
|
%i[attr_reader attr_writer attr_accessor struct_new data_define].include?(node.defined_via)
|
|
119
146
|
end
|
|
120
147
|
|
|
148
|
+
def rails_generated?(node)
|
|
149
|
+
node.defined_via.to_s.start_with?('rails_')
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def implicit_call_reason(node)
|
|
153
|
+
return "#{node.name} is a Ruby hook invoked by the VM" if RUBY_HOOKS.include?(node.name)
|
|
154
|
+
return "#{node.name} is a Ruby protocol method invoked implicitly" if RUBY_PROTOCOLS.include?(node.name)
|
|
155
|
+
|
|
156
|
+
rule = implicit_caller_rules.find do |candidate|
|
|
157
|
+
candidate[:name_pattern].match?(node.name) && owner_matches?(node.owner, candidate[:owner_ancestors])
|
|
158
|
+
end
|
|
159
|
+
return unless rule
|
|
160
|
+
|
|
161
|
+
rule[:reason] || 'the method matches a configured implicit caller'
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def implicit_call_context(node)
|
|
165
|
+
direct_reason = implicit_call_reason(node)
|
|
166
|
+
return direct_reason if direct_reason
|
|
167
|
+
|
|
168
|
+
root_id = implicit_paths[node.graph_id]
|
|
169
|
+
"it is reachable from implicitly invoked #{root_id}" if root_id
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def implicit_paths
|
|
173
|
+
@implicit_paths ||= begin
|
|
174
|
+
roots = graph.method_nodes.reject(&:test).select { |node| implicit_call_reason(node) }.map(&:graph_id)
|
|
175
|
+
paths = roots.to_h { |node_id| [node_id, node_id] }
|
|
176
|
+
queue = roots.dup
|
|
177
|
+
head = 0
|
|
178
|
+
|
|
179
|
+
while head < queue.length
|
|
180
|
+
node_id = queue.fetch(head)
|
|
181
|
+
head += 1
|
|
182
|
+
graph.edges_from(node_id).each_key do |callee_id|
|
|
183
|
+
next if paths.key?(callee_id)
|
|
184
|
+
|
|
185
|
+
paths[callee_id] = paths.fetch(node_id)
|
|
186
|
+
queue << callee_id
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
paths
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def implicit_caller_rules
|
|
194
|
+
BUILTIN_IMPLICIT_CALLERS + project.config.implicit_callers
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def owner_matches?(owner, ancestors)
|
|
198
|
+
return true if ancestors.empty?
|
|
199
|
+
return false unless owner
|
|
200
|
+
|
|
201
|
+
ancestors.any? { |ancestor| graph.owner_reachable_from_ancestor?(owner, ancestor) }
|
|
202
|
+
end
|
|
203
|
+
|
|
121
204
|
def level_for(score)
|
|
122
205
|
return :certain if score >= 0.9
|
|
123
206
|
return :high if score >= 0.7
|
|
@@ -126,31 +209,71 @@ module Necropsy
|
|
|
126
209
|
:low
|
|
127
210
|
end
|
|
128
211
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
value['days'] || value[:days] || value['observation_days'] || value[:observation_days]
|
|
134
|
-
end.map(&:to_i).max
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def quarantine_expired?(node)
|
|
138
|
-
since = quarantine_since(node)
|
|
139
|
-
return false unless since
|
|
212
|
+
def add_quarantine_annotation(node, classification, reasons, components)
|
|
213
|
+
status = quarantine_status(node, classification)
|
|
214
|
+
return unless status
|
|
140
215
|
|
|
141
|
-
|
|
216
|
+
case status
|
|
217
|
+
when :review_required
|
|
218
|
+
components << score_component(
|
|
219
|
+
'quarantine_review_required', 0.0, 'Quarantine expired; deadness is unchanged'
|
|
220
|
+
)
|
|
221
|
+
reasons << 'Quarantine annotation has expired and requires review; classification and confidence are unchanged.'
|
|
222
|
+
when :invalid
|
|
223
|
+
components << score_component(
|
|
224
|
+
'quarantine_invalid_date', 0.0, 'Invalid quarantine date; deadness is unchanged'
|
|
225
|
+
)
|
|
226
|
+
reasons << 'Quarantine annotation has an invalid since date; classification and confidence are unchanged.'
|
|
227
|
+
when :fingerprint_required
|
|
228
|
+
components << score_component(
|
|
229
|
+
'quarantine_fingerprint_required', 0.0, 'Legacy quarantine requires an exact physical fingerprint'
|
|
230
|
+
)
|
|
231
|
+
reasons << 'Quarantine annotation requires a physical fingerprint before it can apply to this definition.'
|
|
232
|
+
when :stale_fingerprint
|
|
233
|
+
components << score_component(
|
|
234
|
+
'quarantine_stale_fingerprint', 0.0, 'Quarantine fingerprint does not match this physical definition'
|
|
235
|
+
)
|
|
236
|
+
reasons << 'Quarantine annotation is stale or belongs to a different physical definition.'
|
|
237
|
+
end
|
|
142
238
|
end
|
|
143
239
|
|
|
144
|
-
def
|
|
240
|
+
def quarantine_status(node, classification)
|
|
145
241
|
path = File.join(project.root, node.file)
|
|
146
242
|
return nil unless File.exist?(path)
|
|
147
243
|
|
|
148
|
-
|
|
149
|
-
annotation =
|
|
150
|
-
return nil unless annotation
|
|
244
|
+
lines = @source_lines[path] ||= File.readlines(path, chomp: true)
|
|
245
|
+
annotation = node.line > 1 ? lines[node.line - 2] : nil
|
|
246
|
+
return nil unless annotation&.include?('necropsy:quarantine') && annotation.match?(/\bsince=/)
|
|
247
|
+
|
|
248
|
+
fingerprints = quarantine_fingerprints(annotation)
|
|
249
|
+
fingerprint_status = if fingerprints.nil?
|
|
250
|
+
:fingerprint_required
|
|
251
|
+
elsif fingerprints.include?(node.physical_fingerprint(classification))
|
|
252
|
+
:matched
|
|
253
|
+
else
|
|
254
|
+
:stale_fingerprint
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
raw_since = annotation[/\bsince=([^\s]+)/, 1]
|
|
258
|
+
return :invalid unless raw_since
|
|
259
|
+
|
|
260
|
+
since = Date.iso8601(raw_since)
|
|
261
|
+
return fingerprint_status unless fingerprint_status == :matched
|
|
262
|
+
|
|
263
|
+
since <= clock.date - project.config.quarantine_days ? :review_required : :active
|
|
264
|
+
rescue ArgumentError, TypeError
|
|
265
|
+
:invalid
|
|
266
|
+
rescue SystemCallError
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def quarantine_fingerprints(annotation)
|
|
271
|
+
single = annotation[/\bfingerprint=([0-9a-f]{64})(?:\s|$)/, 1]
|
|
272
|
+
multiple = annotation[/\bfingerprints=([0-9a-f]{64}(?:,[0-9a-f]{64})*)(?:\s|$)/, 1]
|
|
273
|
+
return [single] if single
|
|
274
|
+
return multiple.split(',') if multiple
|
|
275
|
+
return [] if annotation.match?(/\bfingerprints?=/)
|
|
151
276
|
|
|
152
|
-
Date.iso8601(annotation[/\bsince=([0-9-]+)/, 1])
|
|
153
|
-
rescue ArgumentError, SystemCallError
|
|
154
277
|
nil
|
|
155
278
|
end
|
|
156
279
|
end
|