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,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../commands/scan/project_analyzer_runner"
|
|
4
|
+
|
|
5
|
+
module MetzScan
|
|
6
|
+
module Calibration
|
|
7
|
+
module ProjectAnalyzerEvidenceRunner
|
|
8
|
+
class ReadinessCatalog
|
|
9
|
+
NOTES = {
|
|
10
|
+
"MetzProject/ServiceSoup" => {
|
|
11
|
+
"evidence" => "Medium design-pressure findings are calibrated; setup orchestration is downranked.",
|
|
12
|
+
"next" => "Monitor future calibration for setup noise or new service-call shapes.",
|
|
13
|
+
"not_next" => "Do not reopen promotion unless default-output evidence regresses."
|
|
14
|
+
},
|
|
15
|
+
"MetzProject/RepeatedBranching" => {
|
|
16
|
+
"evidence" => "Repeated branch tables are calibrated; generic subjects are lower-confidence.",
|
|
17
|
+
"next" => "Monitor generic-subject calibration and branch-subject readability.",
|
|
18
|
+
"not_next" => "Do not expand branch parsing without sparse fixture-backed evidence."
|
|
19
|
+
},
|
|
20
|
+
"MetzProject/DeepInheritanceTree" => {
|
|
21
|
+
"evidence" => "Useful inheritance spread evidence remains dependent on the optional project index.",
|
|
22
|
+
"next" => "Use opt-in calibration to watch grouped output and broad-root downranking.",
|
|
23
|
+
"not_next" => "Do not make default-output eligible while findings depend on optional indexing."
|
|
24
|
+
},
|
|
25
|
+
"MetzProject/PackageDependencyPressure" => {
|
|
26
|
+
"evidence" => "Expanded active fixtures show 47 findings: 5 medium package boundaries and " \
|
|
27
|
+
"42 low shared dependencies; consolidated medium review splits into " \
|
|
28
|
+
"3 useful prompts and 2 needs-context framework or extension surfaces.",
|
|
29
|
+
"next" => "Keep parked unless future generic work can separate cross-cutting logging pressure " \
|
|
30
|
+
"from framework roots and public extension registries.",
|
|
31
|
+
"not_next" => "Do not implement classifier behavior, promote, retune thresholds, downrank " \
|
|
32
|
+
"framework-root prompts, or add app-specific suppressions from this sample."
|
|
33
|
+
},
|
|
34
|
+
"MetzProject/NamespaceLeakPressure" => {
|
|
35
|
+
"evidence" => "Expanded active fixtures show 49 findings: 12 medium namespace-boundary prompts " \
|
|
36
|
+
"and 37 low shared namespaces; consolidated medium review mixes useful domain " \
|
|
37
|
+
"and security prompts with public extension or facade surfaces.",
|
|
38
|
+
"next" => "Keep parked unless future generic work separates namespace-boundary prompts from " \
|
|
39
|
+
"public extension, facade, payment, reporting, and renderer APIs.",
|
|
40
|
+
"not_next" => "Do not implement classifier behavior, promote, add app-specific suppressions, " \
|
|
41
|
+
"downrank payment namespaces, or retune thresholds from this narrow sample."
|
|
42
|
+
},
|
|
43
|
+
"MetzProject/ImplicitContextPressure" => {
|
|
44
|
+
"evidence" => "Expanded active fixtures show 12 findings dominated by mechanical framework state: " \
|
|
45
|
+
"9 mechanical, 1 useful execution-identity prompt, and 2 needs-context identity prompts.",
|
|
46
|
+
"next" => "Keep candidate-only and use broader samples to separate boundary setup from ambient identity " \
|
|
47
|
+
"design pressure.",
|
|
48
|
+
"not_next" => "Do not add suppressions, global-access forms, promotion, or default-output eligibility " \
|
|
49
|
+
"from this sample."
|
|
50
|
+
},
|
|
51
|
+
"MetzProject/RepeatedQueryCriteria" => {
|
|
52
|
+
"evidence" => "Expanded active fixtures show 16 findings: 13 useful prompts and 3 mechanical lookups.",
|
|
53
|
+
"next" => "Keep separating membership-table lookups from business-named and lifecycle lookup concepts.",
|
|
54
|
+
"not_next" => "Do not add more query forms, dynamic receivers, association receivers, SQL strings, " \
|
|
55
|
+
"joins, merges, Arel, single-key finders, bang finders, exists?, or take."
|
|
56
|
+
},
|
|
57
|
+
"MetzProject/SubclassOverridePressure" => {
|
|
58
|
+
"evidence" => "Expanded active fixtures show 148 findings: 93 low broad-root and " \
|
|
59
|
+
"55 medium manual-review findings across abstract hook, cooperative, and " \
|
|
60
|
+
"replacement override families.",
|
|
61
|
+
"next" => "Keep parked unless future generic work separates design-pressure hook contracts " \
|
|
62
|
+
"from deliberate extension protocols and setup conventions.",
|
|
63
|
+
"not_next" => "Do not implement classifier behavior, promote, make default-output eligible, " \
|
|
64
|
+
"suppress medium categories, or retune thresholds."
|
|
65
|
+
}
|
|
66
|
+
}.freeze
|
|
67
|
+
|
|
68
|
+
def initialize(analyzer_names: [], analyzers: Commands::Scan::ProjectAnalyzerRunner::ANALYZERS)
|
|
69
|
+
@analyzer_names = Array(analyzer_names)
|
|
70
|
+
@analyzers = analyzers
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def to_a
|
|
74
|
+
selected_analyzers.filter_map { |analyzer| readiness_entry(analyzer) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
attr_reader :analyzer_names, :analyzers
|
|
80
|
+
|
|
81
|
+
def selected_analyzers
|
|
82
|
+
return analyzers if analyzer_names.empty?
|
|
83
|
+
|
|
84
|
+
analyzers.select { |analyzer| analyzer_names.include?(rule_id_for(analyzer)) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def readiness_entry(analyzer)
|
|
88
|
+
note = NOTES[rule_id_for(analyzer)]
|
|
89
|
+
return unless note
|
|
90
|
+
|
|
91
|
+
analyzer_fields(analyzer).merge(note)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def analyzer_fields(analyzer)
|
|
95
|
+
status = analyzer::PROJECT_ANALYZER_STATUS
|
|
96
|
+
default_output = Commands::Scan::ProjectAnalyzerRunner.default_output_analyzer?(analyzer)
|
|
97
|
+
|
|
98
|
+
{ "rule_id" => rule_id_for(analyzer), "status" => status, "default_output" => default_output,
|
|
99
|
+
"disposition" => disposition_for(status, default_output) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def rule_id_for(analyzer)
|
|
103
|
+
analyzer::RULE_ID
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def disposition_for(status, default_output)
|
|
107
|
+
"#{status_label(status)}; #{default_output_label(default_output)}."
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def status_label(status)
|
|
111
|
+
return "Candidate-only" if status == "candidate"
|
|
112
|
+
return "Validated opt-in" if status == "validated"
|
|
113
|
+
|
|
114
|
+
status.to_s
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def default_output_label(default_output)
|
|
118
|
+
default_output ? "default-output eligible" : "not default-output eligible"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
require_relative "../../project_index"
|
|
7
|
+
require_relative "../../commands/scan/project_analyzer_metadata"
|
|
8
|
+
require_relative "../../commands/scan/project_analyzer_offenses"
|
|
9
|
+
require_relative "../../commands/scan/project_analyzer_runner"
|
|
10
|
+
require_relative "readiness_catalog"
|
|
11
|
+
|
|
12
|
+
module MetzScan
|
|
13
|
+
module Calibration
|
|
14
|
+
module ProjectAnalyzerEvidenceRunner
|
|
15
|
+
class Summary
|
|
16
|
+
def initialize(options)
|
|
17
|
+
@options = options
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
target_runs = targets.map { |target| target_run_for(target).call }
|
|
22
|
+
SummaryPayload.new(summary_payload_options(target_runs)).to_h
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :options
|
|
28
|
+
|
|
29
|
+
def target_run_for(target)
|
|
30
|
+
TargetRun.new(target_run_options(target))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def target_run_options(target)
|
|
34
|
+
options.slice(:target_set, :default_output, :index_builder, :finding_runner).merge(target: target)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def summary_payload_options(target_runs)
|
|
38
|
+
{ target_runs: target_runs, default_output: default_output, fixture_root: fixture_root,
|
|
39
|
+
analyzer_names: analyzer_names, targets_file: targets_file, baseline_file: baseline_file }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def targets = options.fetch(:targets)
|
|
43
|
+
|
|
44
|
+
def default_output = options.fetch(:default_output)
|
|
45
|
+
|
|
46
|
+
def fixture_root = options.fetch(:fixture_root)
|
|
47
|
+
|
|
48
|
+
def analyzer_names = options.fetch(:analyzer_names)
|
|
49
|
+
|
|
50
|
+
def targets_file = options.fetch(:targets_file)
|
|
51
|
+
|
|
52
|
+
def baseline_file = options[:baseline_file]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class TargetRun
|
|
56
|
+
def initialize(options)
|
|
57
|
+
@options = options
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
attr_reader :findings, :offenses
|
|
61
|
+
|
|
62
|
+
def call
|
|
63
|
+
@findings = project_findings
|
|
64
|
+
@offenses = Commands::Scan::ProjectAnalyzerOffenses.build(findings).offenses
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def metadata
|
|
69
|
+
identity_metadata.merge(result_metadata)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
attr_reader :options
|
|
75
|
+
|
|
76
|
+
def target = options.fetch(:target)
|
|
77
|
+
|
|
78
|
+
def target_set = options.fetch(:target_set)
|
|
79
|
+
|
|
80
|
+
def default_output = options.fetch(:default_output)
|
|
81
|
+
|
|
82
|
+
def finding_runner = options.fetch(:finding_runner)
|
|
83
|
+
|
|
84
|
+
def scan_paths
|
|
85
|
+
@scan_paths ||= target_set.scan_paths_for(target)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def index
|
|
89
|
+
@index ||= index_for(options.fetch(:index_builder))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def index_for(index_builder)
|
|
93
|
+
return NoScanIndex.new if scan_paths.empty?
|
|
94
|
+
|
|
95
|
+
index_builder.call(scan_paths)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def identity_metadata
|
|
99
|
+
{ "name" => File.basename(target), "root" => target, "scan_paths" => scan_paths,
|
|
100
|
+
"git" => GitMetadata.new(target).to_h, "index" => IndexMetadata.new(index).to_h }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def result_metadata
|
|
104
|
+
{ "project_analyzers" => project_analyzer_summary, "finding_count" => findings.size,
|
|
105
|
+
"offense_count" => offenses.size, "breakdowns" => breakdowns,
|
|
106
|
+
"notable_findings" => notable_findings }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def project_findings
|
|
110
|
+
return [] if scan_paths.empty?
|
|
111
|
+
|
|
112
|
+
finding_runner.call(scan_paths, index: index, default_output: default_output)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def project_analyzer_summary
|
|
116
|
+
Commands::Scan::ProjectAnalyzerMetadata.summary(findings, offenses)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def breakdowns
|
|
120
|
+
Commands::Scan::ProjectAnalyzerBreakdown.new(findings).to_h
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def notable_findings
|
|
124
|
+
NotableFindings.new(findings, target_name: File.basename(target)).to_a
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class SummaryPayload
|
|
129
|
+
def initialize(options)
|
|
130
|
+
@options = options
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def to_h
|
|
134
|
+
summary = identity_fields.merge(target_fields).merge(project_fields)
|
|
135
|
+
baseline_file ? summary.merge("baseline_delta" => baseline_delta_for(summary)) : summary
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
attr_reader :options
|
|
141
|
+
|
|
142
|
+
def target_runs = options.fetch(:target_runs)
|
|
143
|
+
|
|
144
|
+
def default_output = options.fetch(:default_output)
|
|
145
|
+
|
|
146
|
+
def fixture_root = options.fetch(:fixture_root)
|
|
147
|
+
|
|
148
|
+
def analyzer_names = options.fetch(:analyzer_names)
|
|
149
|
+
|
|
150
|
+
def targets_file = options.fetch(:targets_file)
|
|
151
|
+
|
|
152
|
+
def baseline_file = options[:baseline_file]
|
|
153
|
+
|
|
154
|
+
def identity_fields
|
|
155
|
+
{ "generated_at" => Time.now.utc.iso8601, "default_output" => default_output,
|
|
156
|
+
"fixture_root" => fixture_root, "analyzer_filter" => analyzer_names,
|
|
157
|
+
"targets_file" => targets_file }.compact
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def target_fields
|
|
161
|
+
{ "targets" => target_runs.map(&:metadata) }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def project_fields
|
|
165
|
+
{ "project_analyzers" => project_analyzer_summary_with_readiness, "finding_count" => findings.size,
|
|
166
|
+
"offense_count" => offenses.size, "breakdowns" => breakdowns,
|
|
167
|
+
"notable_findings" => notable_findings }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def findings = target_runs.flat_map(&:findings)
|
|
171
|
+
|
|
172
|
+
def offenses = target_runs.flat_map(&:offenses)
|
|
173
|
+
|
|
174
|
+
def notable_findings = target_runs.flat_map { |target_run| target_run.metadata.fetch("notable_findings") }
|
|
175
|
+
|
|
176
|
+
def project_analyzer_summary
|
|
177
|
+
Commands::Scan::ProjectAnalyzerMetadata.summary(findings, offenses)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def project_analyzer_summary_with_readiness
|
|
181
|
+
project_analyzer_summary.merge("readiness" => readiness_entries)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def readiness_entries
|
|
185
|
+
ReadinessCatalog.new(analyzer_names: analyzer_names).to_a
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def breakdowns
|
|
189
|
+
Commands::Scan::ProjectAnalyzerBreakdown.new(findings).to_h
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def baseline_delta_for(summary)
|
|
193
|
+
BaselineDelta.new(summary: summary, baseline_file: baseline_file).to_h
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
class NotableFindings
|
|
198
|
+
INCLUDED_CONFIDENCES = %w[high medium].freeze
|
|
199
|
+
LIMIT = 20
|
|
200
|
+
private_constant :INCLUDED_CONFIDENCES, :LIMIT
|
|
201
|
+
|
|
202
|
+
def initialize(findings, target_name:)
|
|
203
|
+
@findings = findings
|
|
204
|
+
@target_name = target_name
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def to_a
|
|
208
|
+
findings.select { |finding| notable?(finding) }
|
|
209
|
+
.sort_by { |finding| sort_key(finding) }
|
|
210
|
+
.first(LIMIT)
|
|
211
|
+
.map { |finding| finding_metadata(finding) }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private
|
|
215
|
+
|
|
216
|
+
attr_reader :findings, :target_name
|
|
217
|
+
|
|
218
|
+
def notable?(finding)
|
|
219
|
+
INCLUDED_CONFIDENCES.include?(triage_metadata(finding)["confidence"])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def sort_key(finding)
|
|
223
|
+
Commands::Scan::ProjectAnalyzerTriagePriority.sort_key(triage_metadata(finding)) +
|
|
224
|
+
[finding.rule_id.to_s, finding.message.to_s]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def finding_metadata(finding)
|
|
228
|
+
triage_metadata(finding).merge(identity_metadata(finding), detail_metadata(finding)).compact
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def identity_metadata(finding)
|
|
232
|
+
{ "target" => target_name, "rule_id" => finding.rule_id, "message" => finding.message }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def detail_metadata(finding)
|
|
236
|
+
{ "category" => category_for(finding), "metadata" => analyzer_metadata(finding),
|
|
237
|
+
"occurrence" => occurrence_metadata(finding) }
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def triage_metadata(finding)
|
|
241
|
+
Commands::Scan::ProjectAnalyzerMetadata.triage_metadata(finding)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def analyzer_metadata(finding)
|
|
245
|
+
Commands::Scan::ProjectAnalyzerMetadata.analyzer_metadata(finding)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def category_for(finding)
|
|
249
|
+
Commands::Scan::ProjectAnalyzerMetadata.category_for(finding)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def occurrence_metadata(finding)
|
|
253
|
+
occurrence = Array(finding.report_occurrences).compact.first
|
|
254
|
+
return unless occurrence
|
|
255
|
+
|
|
256
|
+
{ "path" => occurrence.path, "line" => occurrence_line(occurrence),
|
|
257
|
+
"context" => occurrence_context(occurrence) }.compact
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def occurrence_line(occurrence)
|
|
261
|
+
return occurrence.report_line if occurrence.respond_to?(:report_line)
|
|
262
|
+
return occurrence.line if occurrence.respond_to?(:line)
|
|
263
|
+
|
|
264
|
+
nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def occurrence_context(occurrence)
|
|
268
|
+
occurrence.context if occurrence.respond_to?(:context)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
class NoScanIndex
|
|
273
|
+
def backend_name = :none
|
|
274
|
+
|
|
275
|
+
def available? = false
|
|
276
|
+
|
|
277
|
+
def reason = "no top-level app/ or lib/ scan paths under calibration target"
|
|
278
|
+
|
|
279
|
+
def indexed_files = []
|
|
280
|
+
|
|
281
|
+
def index_errors = []
|
|
282
|
+
|
|
283
|
+
def diagnostics = []
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
class IndexMetadata
|
|
287
|
+
def initialize(index)
|
|
288
|
+
@index = index
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def to_h
|
|
292
|
+
availability_fields.merge(count_fields).compact
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
private
|
|
296
|
+
|
|
297
|
+
attr_reader :index
|
|
298
|
+
|
|
299
|
+
def availability_fields
|
|
300
|
+
{ "backend" => index.backend_name.to_s, "available" => index.available?,
|
|
301
|
+
"reason" => index.reason }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def count_fields
|
|
305
|
+
{ "indexed_file_count" => index.indexed_files.size, "index_error_count" => index.index_errors.size,
|
|
306
|
+
"diagnostic_count" => index.diagnostics.size }
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
class GitMetadata
|
|
311
|
+
def initialize(target)
|
|
312
|
+
@target = target
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def to_h
|
|
316
|
+
{ "branch" => git_capture("branch", "--show-current"),
|
|
317
|
+
"revision" => git_capture("rev-parse", "HEAD") }.compact
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
attr_reader :target
|
|
323
|
+
|
|
324
|
+
def git_capture(*)
|
|
325
|
+
stdout, _stderr, status = Open3.capture3("git", "-C", target, *)
|
|
326
|
+
return unless status.success?
|
|
327
|
+
|
|
328
|
+
stdout.strip.then { |value| value unless value.empty? }
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module MetzScan
|
|
6
|
+
module Calibration
|
|
7
|
+
module ProjectAnalyzerEvidenceRunner
|
|
8
|
+
class TargetSet
|
|
9
|
+
DEFAULT_SCAN_CHILDREN = %w[app lib].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(paths:, default_apps_path:, targets_file: nil)
|
|
12
|
+
@paths = Array(paths)
|
|
13
|
+
@default_apps_path = default_apps_path
|
|
14
|
+
@targets_file = targets_file && File.expand_path(targets_file)
|
|
15
|
+
reject_mixed_target_inputs!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :targets_file
|
|
19
|
+
|
|
20
|
+
def paths = raw_targets.map { |path| File.expand_path(path) }.uniq.sort
|
|
21
|
+
|
|
22
|
+
def ensure_present!
|
|
23
|
+
missing = paths.reject { |path| File.exist?(path) }
|
|
24
|
+
raise Error, "calibration target missing: #{missing.first}" unless missing.empty?
|
|
25
|
+
|
|
26
|
+
missing_scan_path = manifest_scan_paths.flatten.find { |path| !File.directory?(path) }
|
|
27
|
+
raise Error, "calibration scan path missing: #{missing_scan_path}" if missing_scan_path
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def scan_paths_for(target)
|
|
31
|
+
manifest_paths = manifest_scan_paths_by_target[File.expand_path(target)]
|
|
32
|
+
return manifest_paths if manifest_paths
|
|
33
|
+
|
|
34
|
+
scan_paths = scan_children_for(target)
|
|
35
|
+
scan_paths = [target] if scan_paths.empty? && explicit_targets?
|
|
36
|
+
scan_paths.map { |path| File.expand_path(path) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
attr_reader :default_apps_path
|
|
42
|
+
|
|
43
|
+
def explicit_targets?
|
|
44
|
+
!@paths.empty?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def raw_targets
|
|
48
|
+
return manifest_target_roots if targets_file
|
|
49
|
+
|
|
50
|
+
@paths.empty? ? discovered_default_targets : @paths
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def discovered_default_targets
|
|
54
|
+
return [default_apps_path] unless File.directory?(default_apps_path)
|
|
55
|
+
|
|
56
|
+
default_target_children.empty? ? [default_apps_path] : default_target_children
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def default_target_children
|
|
60
|
+
Dir.children(default_apps_path)
|
|
61
|
+
.map { |child| File.join(default_apps_path, child) }
|
|
62
|
+
.select { |path| File.directory?(path) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def scan_children_for(target)
|
|
66
|
+
DEFAULT_SCAN_CHILDREN.map { |child| File.join(target, child) }
|
|
67
|
+
.select { |path| File.directory?(path) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def reject_mixed_target_inputs!
|
|
71
|
+
return unless targets_file && explicit_targets?
|
|
72
|
+
|
|
73
|
+
raise Error, "cannot combine --targets-file with explicit PATH targets"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def manifest_target_roots
|
|
77
|
+
manifest_entries.map { |entry| entry.fetch("root") }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def manifest_scan_paths_by_target
|
|
81
|
+
return {} unless targets_file
|
|
82
|
+
|
|
83
|
+
@manifest_scan_paths_by_target ||= manifest_entries.to_h do |entry|
|
|
84
|
+
root = File.expand_path(entry.fetch("root"))
|
|
85
|
+
[root, Array(entry.fetch("scan_paths")).map { |path| File.expand_path(path, root) }]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def manifest_scan_paths
|
|
90
|
+
return [] unless targets_file
|
|
91
|
+
|
|
92
|
+
manifest_scan_paths_by_target.values
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def manifest_entries
|
|
96
|
+
@manifest_entries ||= parse_manifest_entries
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def parse_manifest_entries
|
|
100
|
+
target_entries.map { |entry| normalize_manifest_entry(entry) }
|
|
101
|
+
rescue Psych::Exception => e
|
|
102
|
+
raise Error, "invalid calibration targets file: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def target_entries
|
|
106
|
+
manifest_document.fetch("targets").tap do |entries|
|
|
107
|
+
raise Error, "calibration targets file must contain a targets list" unless entries.is_a?(Array)
|
|
108
|
+
end
|
|
109
|
+
rescue KeyError
|
|
110
|
+
raise Error, "calibration targets file must contain a targets list"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def manifest_document
|
|
114
|
+
raise Error, "calibration targets file missing: #{targets_file}" unless File.file?(targets_file)
|
|
115
|
+
|
|
116
|
+
YAML.safe_load_file(targets_file, aliases: false) || {}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def normalize_manifest_entry(entry)
|
|
120
|
+
raise Error, "calibration target entry must be a mapping" unless entry.is_a?(Hash)
|
|
121
|
+
|
|
122
|
+
{ "root" => manifest_entry_root(entry), "scan_paths" => manifest_entry_scan_paths(entry) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def manifest_entry_root(entry)
|
|
126
|
+
entry.fetch("root") { raise Error, "calibration target entry missing root" }.to_s
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def manifest_entry_scan_paths(entry)
|
|
130
|
+
scan_paths = entry.fetch("scan_paths") { raise Error, "calibration target entry missing scan_paths" }
|
|
131
|
+
raise Error, "calibration target scan_paths must be a list" unless scan_paths.is_a?(Array)
|
|
132
|
+
raise Error, "calibration target scan_paths must not be empty" if scan_paths.empty?
|
|
133
|
+
|
|
134
|
+
scan_paths.map(&:to_s)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "project_analyzer_evidence_runner/artifact_writer"
|
|
4
|
+
require_relative "project_analyzer_evidence_runner/baseline_delta"
|
|
5
|
+
require_relative "project_analyzer_evidence_runner/collaborators"
|
|
6
|
+
require_relative "project_analyzer_evidence_runner/markdown_renderer"
|
|
7
|
+
require_relative "project_analyzer_evidence_runner/summary"
|
|
8
|
+
require_relative "project_analyzer_evidence_runner/target_set"
|
|
9
|
+
|
|
10
|
+
module MetzScan
|
|
11
|
+
module Calibration
|
|
12
|
+
module ProjectAnalyzerEvidenceRunner
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
15
|
+
DEFAULT_APPS_PATH = File.join("tmp", "project-analyzer-calibration", "apps").freeze
|
|
16
|
+
DEFAULT_RESULTS_PATH = File.join("tmp", "project-analyzer-calibration", "results").freeze
|
|
17
|
+
|
|
18
|
+
def self.default_apps_path = File.expand_path(DEFAULT_APPS_PATH)
|
|
19
|
+
|
|
20
|
+
def self.default_results_path = File.expand_path(DEFAULT_RESULTS_PATH)
|
|
21
|
+
|
|
22
|
+
def self.summarize(**options)
|
|
23
|
+
options = summarize_options(options)
|
|
24
|
+
targets = target_set(options[:paths], options[:targets_file])
|
|
25
|
+
resolved_summary_options = summary_options(targets, options)
|
|
26
|
+
targets.ensure_present!
|
|
27
|
+
Summary.new(resolved_summary_options).to_h
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.write_artifacts(summary, output_dir: default_results_path, run_id: nil)
|
|
31
|
+
ArtifactWriter.new(summary: summary, output_dir: output_dir, run_id: run_id).call
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.target_set(paths, targets_file)
|
|
35
|
+
TargetSet.new(paths: paths, default_apps_path: default_apps_path, targets_file: targets_file)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.summarize_options(options)
|
|
39
|
+
{ paths: nil, default_output: false, analyzer_names: nil, targets_file: nil,
|
|
40
|
+
baseline_file: nil, index_builder: nil, finding_runner: nil }.merge(options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.summary_options(targets, options)
|
|
44
|
+
analyzer_names = Array(options[:analyzer_names]).compact
|
|
45
|
+
analyzers = AnalyzerSelection.new(analyzer_names).call
|
|
46
|
+
base_summary_options(targets, options[:default_output], analyzer_names, options[:baseline_file])
|
|
47
|
+
.merge(collaborator_options(options, analyzers))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.collaborator_options(options, analyzers)
|
|
51
|
+
{ index_builder: options[:index_builder] || ProjectIndex.method(:build),
|
|
52
|
+
finding_runner: options[:finding_runner] || FindingRunner.new(analyzers: analyzers) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.base_summary_options(targets, default_output, analyzer_names, baseline_file)
|
|
56
|
+
{ targets: targets.paths, target_set: targets,
|
|
57
|
+
default_output: default_output, fixture_root: default_apps_path,
|
|
58
|
+
analyzer_names: analyzer_names, targets_file: targets.targets_file, baseline_file: baseline_file }
|
|
59
|
+
end
|
|
60
|
+
private_class_method :target_set, :summary_options
|
|
61
|
+
private_class_method :base_summary_options
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|