rubocop-metz 0.5.2
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.
Potentially problematic release.
This version of rubocop-metz might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/metz_scan/analyzers/contextual_node_walker.rb +82 -0
- data/lib/metz_scan/analyzers/cross_package_reference_metadata.rb +36 -0
- data/lib/metz_scan/analyzers/cross_package_reference_set.rb +33 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/ambient_context_collector.rb +54 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/ambient_context_reference.rb +37 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/current_attribute_collector.rb +73 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/reference_set.rb +37 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/thread_current_collector.rb +76 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure/triage.rb +109 -0
- data/lib/metz_scan/analyzers/implicit_context_pressure.rb +139 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/finding.rb +18 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/metadata.rb +23 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/root_kind.rb +58 -0
- data/lib/metz_scan/analyzers/inheritance_descendants/triage.rb +30 -0
- data/lib/metz_scan/analyzers/inheritance_descendants.rb +137 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/finding.rb +20 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/metadata.rb +40 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/namespace.rb +41 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/reference_collector.rb +50 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/reference_set.rb +12 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/shared_namespace_triage/exception_declaration.rb +117 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure/shared_namespace_triage.rb +69 -0
- data/lib/metz_scan/analyzers/namespace_leak_pressure.rb +139 -0
- data/lib/metz_scan/analyzers/occurrence.rb +28 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/finding.rb +20 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/metadata.rb +27 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/package_map.rb +11 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/reference_collector.rb +38 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/reference_set.rb +12 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure/shared_dependency_triage.rb +124 -0
- data/lib/metz_scan/analyzers/package_dependency_pressure.rb +139 -0
- data/lib/metz_scan/analyzers/package_map.rb +81 -0
- data/lib/metz_scan/analyzers/project_analyzer_triage.rb +20 -0
- data/lib/metz_scan/analyzers/repeated_branching/branch_site_collector.rb +138 -0
- data/lib/metz_scan/analyzers/repeated_branching/branch_value.rb +27 -0
- data/lib/metz_scan/analyzers/repeated_branching/contextual_node_walker.rb +11 -0
- data/lib/metz_scan/analyzers/repeated_branching/decision_subject.rb +46 -0
- data/lib/metz_scan/analyzers/repeated_branching/predicate_chain.rb +58 -0
- data/lib/metz_scan/analyzers/repeated_branching/triage.rb +28 -0
- data/lib/metz_scan/analyzers/repeated_branching.rb +135 -0
- data/lib/metz_scan/analyzers/repeated_query_criteria/triage.rb +103 -0
- data/lib/metz_scan/analyzers/repeated_query_criteria.rb +285 -0
- data/lib/metz_scan/analyzers/ruby_file_enumerator.rb +37 -0
- data/lib/metz_scan/analyzers/service_soup/service_call_pattern.rb +53 -0
- data/lib/metz_scan/analyzers/service_soup/setup_workflow_triage.rb +48 -0
- data/lib/metz_scan/analyzers/service_soup/workflow_collector.rb +130 -0
- data/lib/metz_scan/analyzers/service_soup.rb +128 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/family_builder.rb +69 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/finding.rb +30 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/metadata.rb +74 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/method_body_facts.rb +100 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure/triage.rb +106 -0
- data/lib/metz_scan/analyzers/subclass_override_pressure.rb +133 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/artifact_writer.rb +51 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/baseline_delta.rb +231 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/collaborators.rb +76 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/markdown_renderer.rb +353 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/readiness_catalog.rb +123 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/summary.rb +333 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner/target_set.rb +139 -0
- data/lib/metz_scan/calibration/project_analyzer_evidence_runner.rb +64 -0
- data/lib/metz_scan/calibration/rubydex_drift_formatter.rb +91 -0
- data/lib/metz_scan/cli.rb +120 -0
- data/lib/metz_scan/commands/explain.rb +128 -0
- data/lib/metz_scan/commands/project_analyzers.rb +105 -0
- data/lib/metz_scan/commands/report.rb +115 -0
- data/lib/metz_scan/commands/rules.rb +92 -0
- data/lib/metz_scan/commands/scan/auto_fix.rb +138 -0
- data/lib/metz_scan/commands/scan/compliance_scorecard.rb +124 -0
- data/lib/metz_scan/commands/scan/github_annotations_renderer.rb +55 -0
- data/lib/metz_scan/commands/scan/offense_extractor.rb +42 -0
- data/lib/metz_scan/commands/scan/project_analyzer_breakdown.rb +55 -0
- data/lib/metz_scan/commands/scan/project_analyzer_metadata.rb +78 -0
- data/lib/metz_scan/commands/scan/project_analyzer_offenses.rb +85 -0
- data/lib/metz_scan/commands/scan/project_analyzer_runner.rb +148 -0
- data/lib/metz_scan/commands/scan/project_analyzer_summary_aggregate_formatter.rb +74 -0
- data/lib/metz_scan/commands/scan/project_analyzer_summary_breakdown_formatter.rb +46 -0
- data/lib/metz_scan/commands/scan/project_analyzer_triage_formatter.rb +40 -0
- data/lib/metz_scan/commands/scan/project_analyzer_triage_priority.rb +38 -0
- data/lib/metz_scan/commands/scan/project_config_scope.rb +231 -0
- data/lib/metz_scan/commands/scan/runner.rb +199 -0
- data/lib/metz_scan/commands/scan/sarif_renderer.rb +103 -0
- data/lib/metz_scan/commands/scan/sarif_rule_descriptors.rb +82 -0
- data/lib/metz_scan/commands/scan/sarif_severity.rb +27 -0
- data/lib/metz_scan/commands/scan/target_ruby_version.rb +56 -0
- data/lib/metz_scan/commands/scan/text_renderer.rb +145 -0
- data/lib/metz_scan/commands/scan.rb +201 -0
- data/lib/metz_scan/project_index/null_backend.rb +35 -0
- data/lib/metz_scan/project_index/rubydex_backend/file_discovery.rb +33 -0
- data/lib/metz_scan/project_index/rubydex_backend/location_formatting.rb +36 -0
- data/lib/metz_scan/project_index/rubydex_backend/method_declarations.rb +118 -0
- data/lib/metz_scan/project_index/rubydex_backend.rb +133 -0
- data/lib/metz_scan/project_index.rb +109 -0
- data/lib/metz_scan/version.rb +5 -0
- data/lib/metz_scan.rb +5 -0
- metadata +185 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module MetzScan
|
|
8
|
+
module Calibration
|
|
9
|
+
module ProjectAnalyzerEvidenceRunner
|
|
10
|
+
class ArtifactWriter
|
|
11
|
+
def initialize(summary:, output_dir:, run_id:)
|
|
12
|
+
@summary = summary
|
|
13
|
+
@output_dir = output_dir
|
|
14
|
+
@run_id = run_id || "project-analyzer-evidence-#{Time.now.utc.strftime('%Y%m%d-%H%M%S')}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
FileUtils.mkdir_p(run_dir)
|
|
19
|
+
write_json
|
|
20
|
+
write_markdown
|
|
21
|
+
artifacts
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :summary, :output_dir, :run_id
|
|
27
|
+
|
|
28
|
+
def write_json
|
|
29
|
+
File.write(artifacts.fetch("json"), "#{JSON.pretty_generate(persisted_summary)}\n")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def write_markdown
|
|
33
|
+
File.write(artifacts.fetch("markdown"), MarkdownRenderer.new(persisted_summary).call)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def persisted_summary
|
|
37
|
+
summary.merge("artifacts" => artifacts)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def artifacts
|
|
41
|
+
{ "directory" => run_dir, "json" => File.join(run_dir, "summary.json"),
|
|
42
|
+
"markdown" => File.join(run_dir, "summary.md") }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def run_dir
|
|
46
|
+
File.join(File.expand_path(output_dir), run_id)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module MetzScan
|
|
7
|
+
module Calibration
|
|
8
|
+
module ProjectAnalyzerEvidenceRunner
|
|
9
|
+
class BaselineDelta
|
|
10
|
+
COUNT_KEYS = %w[finding_count offense_count].freeze
|
|
11
|
+
private_constant :COUNT_KEYS
|
|
12
|
+
|
|
13
|
+
def initialize(summary:, baseline_file:)
|
|
14
|
+
@summary = summary
|
|
15
|
+
@baseline = BaselineFile.load(baseline_file)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
validate_scope!
|
|
20
|
+
delta_payload
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
attr_reader :summary, :baseline
|
|
26
|
+
|
|
27
|
+
def delta_payload
|
|
28
|
+
{ "baseline" => baseline.identity,
|
|
29
|
+
"finding_count" => metric_delta("finding_count", current_summary, baseline_summary),
|
|
30
|
+
"offense_count" => metric_delta("offense_count", current_summary, baseline_summary),
|
|
31
|
+
"rules" => rule_deltas,
|
|
32
|
+
"breakdowns" => breakdown_deltas }.compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def validate_scope!
|
|
36
|
+
BaselineFile.scope_keys.each { |key| validate_scope_value!(key) if baseline_scope.key?(key) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate_scope_value!(key)
|
|
40
|
+
return if normalized_scope_value(key, baseline_scope[key]) == normalized_scope_value(key, summary[key])
|
|
41
|
+
|
|
42
|
+
raise Error, "baseline scope mismatch for #{key}: expected #{baseline_scope[key].inspect}, " \
|
|
43
|
+
"got #{summary[key].inspect}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def normalized_scope_value(key, value)
|
|
47
|
+
case key
|
|
48
|
+
when "targets_file" then value && File.expand_path(value.to_s)
|
|
49
|
+
when "analyzer_filter" then Array(value).map(&:to_s).sort
|
|
50
|
+
else value
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def rule_deltas
|
|
55
|
+
all_rule_names.map { |name| rule_delta_for(name) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def rule_delta_for(name)
|
|
59
|
+
{ "cop_name" => name,
|
|
60
|
+
"finding_count" => rule_metric_delta(name, "finding_count"),
|
|
61
|
+
"offense_count" => rule_metric_delta(name, "offense_count") }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def rule_metric_delta(name, key)
|
|
65
|
+
metric_delta(key, current_rules.fetch(name, {}), baseline_rules.fetch(name, {}))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def all_rule_names
|
|
69
|
+
(current_rules.keys | baseline_rules.keys).sort
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def current_rules
|
|
73
|
+
@current_rules ||= rules_by_name(current_summary)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def baseline_rules
|
|
77
|
+
@baseline_rules ||= rules_by_name(baseline_summary)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def rules_by_name(source)
|
|
81
|
+
Array(rules_for(source)).to_h { |rule| [rule.fetch("cop_name"), rule] }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def rules_for(source)
|
|
85
|
+
BaselineFile.rules_for(source)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def breakdown_deltas
|
|
89
|
+
{ "confidence" => breakdown_delta_for("confidence"),
|
|
90
|
+
"triage_severity" => breakdown_delta_for("triage_severity"),
|
|
91
|
+
"metadata" => metadata_breakdown_deltas }.compact
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def metadata_breakdown_deltas
|
|
95
|
+
category_deltas = breakdown_delta_for("project_analyzer_category", metadata: true)
|
|
96
|
+
{ "project_analyzer_category" => category_deltas } unless category_deltas.empty?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def breakdown_delta_for(key, metadata: false)
|
|
100
|
+
current = breakdown_counts(current_summary, key, metadata: metadata)
|
|
101
|
+
baseline = breakdown_counts(baseline_summary, key, metadata: metadata)
|
|
102
|
+
|
|
103
|
+
(current.keys | baseline.keys).sort.map do |value|
|
|
104
|
+
{ "value" => value, "finding_count" => count_delta(current.fetch(value, 0), baseline.fetch(value, 0)) }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def breakdown_counts(source, key, metadata:)
|
|
109
|
+
values = metadata ? source.dig("breakdowns", "metadata", key) : source.dig("breakdowns", key)
|
|
110
|
+
Array(values).to_h { |entry| [entry.fetch("value"), entry.fetch("finding_count").to_i] }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def metric_delta(key, current, baseline)
|
|
114
|
+
count_delta(current.fetch(key, 0), baseline.fetch(key, 0))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def count_delta(current, baseline)
|
|
118
|
+
current = current.to_i
|
|
119
|
+
baseline = baseline.to_i
|
|
120
|
+
{ "baseline" => baseline, "current" => current, "delta" => current - baseline }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def current_summary = summary
|
|
124
|
+
|
|
125
|
+
def baseline_summary
|
|
126
|
+
baseline.summary
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def baseline_scope
|
|
130
|
+
baseline.scope
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
class BaselineFile
|
|
135
|
+
SCOPE_KEYS = %w[targets_file default_output analyzer_filter].freeze
|
|
136
|
+
private_constant :SCOPE_KEYS
|
|
137
|
+
|
|
138
|
+
def self.load(path)
|
|
139
|
+
new(path: path)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.from_summary(summary, label:, generated_from:, targets_file: nil)
|
|
143
|
+
document = document_for(summary, label: label, generated_from: generated_from, targets_file: targets_file)
|
|
144
|
+
new(document: document)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def self.scope_keys = SCOPE_KEYS
|
|
148
|
+
|
|
149
|
+
def self.rules_for(source)
|
|
150
|
+
source["rules"] || source.dig("project_analyzers", "rules") || []
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def initialize(path: nil, document: nil)
|
|
154
|
+
@path = path && File.expand_path(path)
|
|
155
|
+
@document = document
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
attr_reader :path
|
|
159
|
+
|
|
160
|
+
def identity
|
|
161
|
+
{ "file" => path, "label" => document["label"], "generated_from" => document["generated_from"] }.compact
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def to_h
|
|
165
|
+
document
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def to_yaml
|
|
169
|
+
YAML.dump(to_h)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def summary
|
|
173
|
+
document.fetch("summary", document)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def scope
|
|
177
|
+
document.fetch("scope", {})
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class << self
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def document_for(summary, label:, generated_from:, targets_file:)
|
|
184
|
+
{ "label" => label, "generated_from" => generated_from,
|
|
185
|
+
"scope" => scope_for(summary, targets_file: targets_file),
|
|
186
|
+
"summary" => compact_summary_for(summary) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def scope_for(summary, targets_file:)
|
|
190
|
+
{ "targets_file" => targets_file || summary["targets_file"],
|
|
191
|
+
"default_output" => summary.fetch("default_output"),
|
|
192
|
+
"analyzer_filter" => summary.fetch("analyzer_filter", []) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def compact_summary_for(summary)
|
|
196
|
+
{ "finding_count" => summary.fetch("finding_count"),
|
|
197
|
+
"offense_count" => summary.fetch("offense_count"),
|
|
198
|
+
"rules" => compact_rules_for(summary),
|
|
199
|
+
"breakdowns" => summary.fetch("breakdowns", {}) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def compact_rules_for(summary)
|
|
203
|
+
rules_for(summary).map { |rule| compact_rule_for(rule) }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def compact_rule_for(rule)
|
|
207
|
+
{ "cop_name" => rule.fetch("cop_name"),
|
|
208
|
+
"finding_count" => rule.fetch("finding_count"),
|
|
209
|
+
"offense_count" => rule.fetch("offense_count") }
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def document
|
|
216
|
+
@document ||= load_document
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def load_document
|
|
220
|
+
JSON.parse(contents)
|
|
221
|
+
rescue JSON::ParserError
|
|
222
|
+
YAML.safe_load(contents, aliases: false)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def contents
|
|
226
|
+
@contents ||= File.read(path)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../project_index"
|
|
4
|
+
require_relative "../../commands/scan/project_analyzer_runner"
|
|
5
|
+
|
|
6
|
+
module MetzScan
|
|
7
|
+
module Calibration
|
|
8
|
+
module ProjectAnalyzerEvidenceRunner
|
|
9
|
+
class AnalyzerSelection
|
|
10
|
+
def initialize(rule_ids)
|
|
11
|
+
@rule_ids = Array(rule_ids).compact
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
return nil if rule_ids.empty?
|
|
16
|
+
|
|
17
|
+
rule_ids.map { |rule_id| analyzer_for(rule_id) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :rule_ids
|
|
23
|
+
|
|
24
|
+
def analyzer_for(rule_id)
|
|
25
|
+
analyzer = analyzers_by_rule_id[rule_id]
|
|
26
|
+
raise Error, "unknown project analyzer: #{rule_id}" unless analyzer
|
|
27
|
+
|
|
28
|
+
analyzer
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def analyzers_by_rule_id
|
|
32
|
+
Commands::Scan::ProjectAnalyzerRunner::ANALYZERS.to_h { |analyzer| [analyzer::RULE_ID, analyzer] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class FindingRunner
|
|
37
|
+
def initialize(analyzers: nil)
|
|
38
|
+
@analyzers = analyzers
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def call(paths, index:, default_output:)
|
|
42
|
+
return project_findings(paths, index: index, default_output: default_output) unless analyzers
|
|
43
|
+
|
|
44
|
+
selected = selected_analyzers(default_output)
|
|
45
|
+
filter_default_output(run_selected_analyzers(selected, paths, index), default_output)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
attr_reader :analyzers
|
|
51
|
+
|
|
52
|
+
def project_findings(paths, index:, default_output:)
|
|
53
|
+
Commands::Scan::ProjectAnalyzerRunner.project_findings_for(
|
|
54
|
+
paths, index: index, default_output: default_output
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def selected_analyzers(default_output)
|
|
59
|
+
return analyzers unless default_output
|
|
60
|
+
|
|
61
|
+
analyzers.select { |analyzer| Commands::Scan::ProjectAnalyzerRunner.default_output_analyzer?(analyzer) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run_selected_analyzers(selected, paths, index)
|
|
65
|
+
selected.flat_map { |analyzer| analyzer.new(paths: paths, index: index).call }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def filter_default_output(findings, default_output)
|
|
69
|
+
return findings unless default_output
|
|
70
|
+
|
|
71
|
+
findings.select { |finding| Commands::Scan::ProjectAnalyzerRunner.default_output_finding?(finding) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MetzScan
|
|
4
|
+
module Calibration
|
|
5
|
+
module ProjectAnalyzerEvidenceRunner
|
|
6
|
+
class MarkdownRenderer
|
|
7
|
+
def initialize(summary)
|
|
8
|
+
@summary = summary
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call
|
|
12
|
+
"#{sections.join("\n")}\n"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :summary
|
|
18
|
+
|
|
19
|
+
def sections
|
|
20
|
+
section_renderers.flat_map { |renderer| renderer.new(summary).call }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def section_renderers
|
|
24
|
+
[HeaderMarkdown, TargetsMarkdown, RulesMarkdown, ReadinessMarkdown,
|
|
25
|
+
NotableFindingsMarkdown, BaselineDeltaMarkdown, BreakdownsMarkdown]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module MarkdownTableCells
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def cell(value)
|
|
33
|
+
value.to_s.gsub("|", "\\|")
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class HeaderMarkdown
|
|
38
|
+
def initialize(summary)
|
|
39
|
+
@summary = summary
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def call
|
|
43
|
+
["# Project analyzer evidence", "", *metadata_lines, "", *count_lines, ""]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
attr_reader :summary
|
|
49
|
+
|
|
50
|
+
def metadata_lines
|
|
51
|
+
["Generated: #{summary.fetch('generated_at')}",
|
|
52
|
+
"Default output filter: #{summary.fetch('default_output')}",
|
|
53
|
+
"Analyzer filter: #{analyzer_filter_label}", *targets_file_lines,
|
|
54
|
+
"Fixture root: `#{summary.fetch('fixture_root')}`"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def count_lines
|
|
58
|
+
["Findings: #{summary.fetch('finding_count')}", "Offenses: #{summary.fetch('offense_count')}"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def analyzer_filter_label
|
|
62
|
+
analyzer_names = summary.fetch("analyzer_filter", [])
|
|
63
|
+
analyzer_names.empty? ? "none" : analyzer_names.join(", ")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def targets_file_lines
|
|
67
|
+
targets_file = summary["targets_file"]
|
|
68
|
+
targets_file ? ["Targets file: `#{targets_file}`"] : []
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class TargetsMarkdown
|
|
73
|
+
def initialize(summary)
|
|
74
|
+
@summary = summary
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def call
|
|
78
|
+
["## Targets", "", header, *rows, ""]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
attr_reader :summary
|
|
84
|
+
|
|
85
|
+
def header
|
|
86
|
+
"| Target | Revision | Scan paths | Backend | Findings | Offenses |\n" \
|
|
87
|
+
"| --- | --- | --- | --- | ---: | ---: |"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def rows
|
|
91
|
+
summary.fetch("targets").map { |target| row(target) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def row(target)
|
|
95
|
+
"| #{target.fetch('name')} | #{revision_label(target)} | #{scan_paths_label(target)} | " \
|
|
96
|
+
"#{target.fetch('index').fetch('backend')} | #{target.fetch('finding_count')} | " \
|
|
97
|
+
"#{target.fetch('offense_count')} |"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def revision_label(target)
|
|
101
|
+
target.fetch("git").fetch("revision", "unknown")[0, 12]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def scan_paths_label(target)
|
|
105
|
+
target.fetch("scan_paths").map { |path| "`#{path}`" }.join("<br>")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class RulesMarkdown
|
|
110
|
+
def initialize(summary)
|
|
111
|
+
@summary = summary
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def call
|
|
115
|
+
return ["## Rules", "", "No project-analyzer findings."] if rules.empty?
|
|
116
|
+
|
|
117
|
+
["## Rules", "", header, *rows]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
attr_reader :summary
|
|
123
|
+
|
|
124
|
+
def rules
|
|
125
|
+
summary.fetch("project_analyzers").fetch("rules", [])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def header
|
|
129
|
+
"| Rule | Findings | Offenses | Status | Confidence | Severity |\n" \
|
|
130
|
+
"| --- | ---: | ---: | --- | --- | --- |"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def rows
|
|
134
|
+
rules.map { |rule| row(rule) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def row(rule)
|
|
138
|
+
"| #{rule.fetch('cop_name')} | #{rule.fetch('finding_count')} | #{rule.fetch('offense_count')} | " \
|
|
139
|
+
"#{rule.fetch('status', '?')} | #{rule.fetch('confidence', '?')} | " \
|
|
140
|
+
"#{rule.fetch('triage_severity', '?')} |"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class ReadinessMarkdown
|
|
145
|
+
include MarkdownTableCells
|
|
146
|
+
|
|
147
|
+
def initialize(summary)
|
|
148
|
+
@summary = summary
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def call
|
|
152
|
+
return [] if readiness.empty?
|
|
153
|
+
|
|
154
|
+
["", "## Readiness", "", header, *rows, ""]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
attr_reader :summary
|
|
160
|
+
|
|
161
|
+
def readiness
|
|
162
|
+
summary.fetch("project_analyzers").fetch("readiness", [])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def header
|
|
166
|
+
"| Rule | Disposition | Evidence | Next | Not next |\n" \
|
|
167
|
+
"| --- | --- | --- | --- | --- |"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def rows
|
|
171
|
+
readiness.map { |entry| row(entry) }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def row(entry)
|
|
175
|
+
"| #{cell(entry.fetch('rule_id'))} | #{cell(entry.fetch('disposition'))} | " \
|
|
176
|
+
"#{cell(entry.fetch('evidence'))} | #{cell(entry.fetch('next'))} | " \
|
|
177
|
+
"#{cell(entry.fetch('not_next'))} |"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
class NotableFindingsMarkdown
|
|
182
|
+
include MarkdownTableCells
|
|
183
|
+
|
|
184
|
+
def initialize(summary)
|
|
185
|
+
@notable_findings = summary.fetch("notable_findings", [])
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def call
|
|
189
|
+
return [] if notable_findings.empty?
|
|
190
|
+
|
|
191
|
+
["", "## Notable Findings", "", header, *rows, ""]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
attr_reader :notable_findings
|
|
197
|
+
|
|
198
|
+
def header
|
|
199
|
+
"| Target | Rule | Confidence | Severity | Category | Location | Message |\n" \
|
|
200
|
+
"| --- | --- | --- | --- | --- | --- | --- |"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def rows
|
|
204
|
+
notable_findings.map { |finding| row(finding) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def row(finding)
|
|
208
|
+
"| #{cell(finding.fetch('target'))} | #{cell(finding.fetch('rule_id'))} | " \
|
|
209
|
+
"#{cell(finding.fetch('confidence', '?'))} | #{cell(finding.fetch('triage_severity', '?'))} | " \
|
|
210
|
+
"#{cell(finding.fetch('category', '?'))} | #{cell(location_label(finding))} | " \
|
|
211
|
+
"#{cell(finding.fetch('message'))} |"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def location_label(finding)
|
|
215
|
+
occurrence = finding.fetch("occurrence", {})
|
|
216
|
+
[occurrence["path"], occurrence["line"]].compact.join(":")
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
class BreakdownsMarkdown
|
|
221
|
+
def initialize(summary)
|
|
222
|
+
@breakdowns = summary.fetch("breakdowns", {})
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def call
|
|
226
|
+
return [] if breakdowns.empty?
|
|
227
|
+
|
|
228
|
+
["", "## Breakdowns", "", *subsections.flatten]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
private
|
|
232
|
+
|
|
233
|
+
attr_reader :breakdowns
|
|
234
|
+
|
|
235
|
+
def subsections
|
|
236
|
+
[table("Confidence", breakdowns["confidence"]),
|
|
237
|
+
table("Severity", breakdowns["triage_severity"]),
|
|
238
|
+
*metadata_tables].compact
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def metadata_tables
|
|
242
|
+
breakdowns.fetch("metadata", {}).map { |key, values| table("Metadata: `#{key}`", values) }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def table(title, values)
|
|
246
|
+
return if values.to_a.empty?
|
|
247
|
+
|
|
248
|
+
["### #{title}", "", "| Value | Findings |", "| --- | ---: |",
|
|
249
|
+
*values.map { |value| row(value) }, ""]
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def row(value)
|
|
253
|
+
"| #{value.fetch('value')} | #{value.fetch('finding_count')} |"
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
class BaselineDeltaMarkdown
|
|
258
|
+
include MarkdownTableCells
|
|
259
|
+
|
|
260
|
+
def initialize(summary)
|
|
261
|
+
@delta = summary["baseline_delta"]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def call
|
|
265
|
+
return [] unless delta
|
|
266
|
+
|
|
267
|
+
header_lines + totals_lines + detail_lines
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
private
|
|
271
|
+
|
|
272
|
+
attr_reader :delta
|
|
273
|
+
|
|
274
|
+
def baseline_label
|
|
275
|
+
baseline = delta.fetch("baseline")
|
|
276
|
+
label = baseline["label"] ? "#{baseline['label']} " : ""
|
|
277
|
+
"#{label}`#{baseline.fetch('file')}`"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def header_lines
|
|
281
|
+
["", "## Baseline Deltas", "", "Baseline: #{baseline_label}", ""]
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def totals_lines
|
|
285
|
+
[totals_header, total_row("Findings", delta.fetch("finding_count")),
|
|
286
|
+
total_row("Offenses", delta.fetch("offense_count")), ""]
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def detail_lines
|
|
290
|
+
analyzer_table + confidence_table + severity_table + category_table
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def analyzer_table
|
|
294
|
+
table("Analyzer", "Rule", analyzer_rows)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def confidence_table
|
|
298
|
+
table("Confidence", "Value", breakdown_rows(delta.dig("breakdowns", "confidence")))
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def severity_table
|
|
302
|
+
table("Severity", "Value", breakdown_rows(delta.dig("breakdowns", "triage_severity")))
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def category_table
|
|
306
|
+
table("Category", "Value", breakdown_rows(delta.dig("breakdowns", "metadata",
|
|
307
|
+
"project_analyzer_category")))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def totals_header
|
|
311
|
+
"| Metric | Baseline | Current | Delta |\n| --- | ---: | ---: | ---: |"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def total_row(label, count)
|
|
315
|
+
"| #{label} | #{count.fetch('baseline')} | #{count.fetch('current')} | #{signed(count)} |"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def analyzer_rows
|
|
319
|
+
delta.fetch("rules", []).map do |rule|
|
|
320
|
+
["#{cell(rule.fetch('cop_name'))} findings", rule.fetch("finding_count")]
|
|
321
|
+
end + delta.fetch("rules", []).map do |rule|
|
|
322
|
+
["#{cell(rule.fetch('cop_name'))} offenses", rule.fetch("offense_count")]
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def breakdown_rows(entries)
|
|
327
|
+
Array(entries).map { |entry| [cell(entry.fetch("value")), entry.fetch("finding_count")] }
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def table(title, value_header, rows)
|
|
331
|
+
changed = rows.select { |_value, count| count.fetch("delta").nonzero? }
|
|
332
|
+
return ["### #{title}", "", "No #{title.downcase} count changes.", ""] if changed.empty?
|
|
333
|
+
|
|
334
|
+
["### #{title}", "", "| #{value_header} | Baseline | Current | Delta |",
|
|
335
|
+
"| --- | ---: | ---: | ---: |",
|
|
336
|
+
*changed.map { |value, count| row(value, count) }, ""]
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def row(value, count)
|
|
340
|
+
"| #{value} | #{count.fetch('baseline')} | #{count.fetch('current')} | #{signed(count)} |"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def signed(count)
|
|
344
|
+
delta_value = count.fetch("delta")
|
|
345
|
+
delta_value.positive? ? "+#{delta_value}" : delta_value.to_s
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
private_constant :MarkdownTableCells, :HeaderMarkdown, :TargetsMarkdown, :RulesMarkdown
|
|
350
|
+
private_constant :ReadinessMarkdown, :NotableFindingsMarkdown, :BaselineDeltaMarkdown, :BreakdownsMarkdown
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|