metz-scan 0.5.3
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/LICENSE +21 -0
- data/bin/metz-scan +10 -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
- data/metz-scan.gemspec +41 -0
- metadata +160 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module MetzScan
|
|
6
|
+
module Calibration
|
|
7
|
+
class RubydexDriftFormatter
|
|
8
|
+
def initialize(summary, analyzers:)
|
|
9
|
+
@summary = summary
|
|
10
|
+
@analyzers = analyzers
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def json
|
|
14
|
+
JSON.pretty_generate(payload)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def text
|
|
18
|
+
[header_lines, rule_lines, breakdown_lines].flatten.compact.join("\n")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :summary, :analyzers
|
|
24
|
+
|
|
25
|
+
def payload
|
|
26
|
+
compact_identity.merge(compact_results)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def compact_identity
|
|
30
|
+
{ "targets_file" => summary["targets_file"],
|
|
31
|
+
"targets" => target_names,
|
|
32
|
+
"analyzers" => analyzers }.compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def compact_results
|
|
36
|
+
{ "finding_count" => summary.fetch("finding_count"),
|
|
37
|
+
"offense_count" => summary.fetch("offense_count"),
|
|
38
|
+
"rules" => rules,
|
|
39
|
+
"breakdowns" => summary.fetch("breakdowns", {}) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def header_lines
|
|
43
|
+
["rubydex drift check",
|
|
44
|
+
targets_file_line,
|
|
45
|
+
"targets: #{target_names.join(', ')}",
|
|
46
|
+
"analyzers: #{analyzers.join(', ')}",
|
|
47
|
+
"findings: #{summary.fetch('finding_count')} (offenses: #{summary.fetch('offense_count')})"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def target_names
|
|
51
|
+
summary.fetch("targets").map { |target| target.fetch("name") }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def targets_file_line
|
|
55
|
+
"targets file: #{summary['targets_file']}" if summary["targets_file"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def rule_lines
|
|
59
|
+
return ["rules: none"] if rules.empty?
|
|
60
|
+
|
|
61
|
+
["rules:", *rules.map { |rule| rule_line(rule) }]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def rules
|
|
65
|
+
summary.fetch("project_analyzers", {}).fetch("rules", [])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def rule_line(rule)
|
|
69
|
+
" - #{rule.fetch('cop_name')}: #{rule.fetch('finding_count')} finding(s), " \
|
|
70
|
+
"#{rule.fetch('offense_count')} offense(s), status=#{rule.fetch('status', '?')}, " \
|
|
71
|
+
"confidence=#{rule.fetch('confidence', '?')}, severity=#{rule.fetch('triage_severity', '?')}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def breakdown_lines
|
|
75
|
+
lines = [breakdown_line("confidence", breakdowns["confidence"]),
|
|
76
|
+
breakdown_line("severity", breakdowns["triage_severity"])].compact
|
|
77
|
+
lines.empty? ? [] : ["breakdowns:", *lines]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def breakdowns
|
|
81
|
+
summary.fetch("breakdowns", {})
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def breakdown_line(label, values)
|
|
85
|
+
return if values.to_a.empty?
|
|
86
|
+
|
|
87
|
+
" - #{label}: #{values.map { |value| "#{value.fetch('value')}=#{value.fetch('finding_count')}" }.join(', ')}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
require_relative "version"
|
|
6
|
+
|
|
7
|
+
module MetzScan
|
|
8
|
+
class CLI
|
|
9
|
+
SUBCOMMAND_SUMMARIES = {
|
|
10
|
+
"rules" => "List all Metz/* cops with their one-line rationale",
|
|
11
|
+
"explain" => "Print full metadata for a single Metz cop",
|
|
12
|
+
"scan" => "Run rubocop-metz against PATH and render a report",
|
|
13
|
+
"report" => "Re-render an existing rubocop-metz JSON report",
|
|
14
|
+
"project-analyzers" => "List project analyzers with status and default-output eligibility"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
SUBCOMMANDS = SUBCOMMAND_SUMMARIES.keys.freeze
|
|
18
|
+
|
|
19
|
+
def self.start(argv = ARGV, stdout: $stdout, stderr: $stderr)
|
|
20
|
+
new(stdout: stdout, stderr: stderr).run(argv)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(stdout: $stdout, stderr: $stderr)
|
|
24
|
+
@stdout = stdout
|
|
25
|
+
@stderr = stderr
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run(argv)
|
|
29
|
+
args = argv.dup
|
|
30
|
+
return show_help_and_fail if args.empty?
|
|
31
|
+
|
|
32
|
+
handle_global_flag(args.first) || dispatch(args)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :stdout, :stderr
|
|
38
|
+
|
|
39
|
+
def handle_global_flag(arg)
|
|
40
|
+
case arg
|
|
41
|
+
when "-v", "--version" then print_version
|
|
42
|
+
when "-h", "--help" then print_help_and_succeed
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def print_version
|
|
47
|
+
stdout.puts MetzScan::VERSION
|
|
48
|
+
0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def print_help_and_succeed
|
|
52
|
+
stdout.puts help_text
|
|
53
|
+
0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def show_help_and_fail
|
|
57
|
+
stderr.puts help_text
|
|
58
|
+
1
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def dispatch(args)
|
|
62
|
+
name = args.shift
|
|
63
|
+
return unknown_subcommand(name) unless SUBCOMMANDS.include?(name)
|
|
64
|
+
|
|
65
|
+
handler = subcommand_handler(name)
|
|
66
|
+
handler ? handler.run(args, stdout: stdout, stderr: stderr) : stub_subcommand(name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
SUBCOMMAND_HANDLERS = { "rules" => "Rules", "explain" => "Explain", "scan" => "Scan",
|
|
70
|
+
"report" => "Report", "project-analyzers" => "ProjectAnalyzers" }.freeze
|
|
71
|
+
private_constant :SUBCOMMAND_HANDLERS
|
|
72
|
+
|
|
73
|
+
def subcommand_handler(name)
|
|
74
|
+
klass_name = SUBCOMMAND_HANDLERS[name]
|
|
75
|
+
return unless klass_name
|
|
76
|
+
|
|
77
|
+
require_relative "commands/#{name.tr('-', '_')}"
|
|
78
|
+
Commands.const_get(klass_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def stub_subcommand(name)
|
|
82
|
+
stderr.puts "metz-scan: subcommand '#{name}' is not yet implemented."
|
|
83
|
+
1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def unknown_subcommand(name)
|
|
87
|
+
stderr.puts "metz-scan: unknown subcommand '#{name}'."
|
|
88
|
+
stderr.puts help_text
|
|
89
|
+
1
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def help_text
|
|
93
|
+
option_parser.help
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def option_parser
|
|
97
|
+
OptionParser.new do |opts|
|
|
98
|
+
opts.banner = "Usage: metz-scan [--version] [--help] <subcommand> [<args>]"
|
|
99
|
+
append_subcommands(opts)
|
|
100
|
+
append_options(opts)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
SUBCOMMAND_ROW = " %<name>-9s %<summary>s"
|
|
105
|
+
private_constant :SUBCOMMAND_ROW
|
|
106
|
+
|
|
107
|
+
def append_subcommands(opts)
|
|
108
|
+
opts.separator ""
|
|
109
|
+
opts.separator "Subcommands:"
|
|
110
|
+
SUBCOMMAND_SUMMARIES.each { |name, summary| opts.separator(format(SUBCOMMAND_ROW, name: name, summary: summary)) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def append_options(opts)
|
|
114
|
+
opts.separator ""
|
|
115
|
+
opts.separator "Options:"
|
|
116
|
+
opts.on("-v", "--version", "Print the metz-scan version and exit")
|
|
117
|
+
opts.on("-h", "--help", "Show this help message and exit")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module MetzScan
|
|
7
|
+
module Commands
|
|
8
|
+
class Explain
|
|
9
|
+
USAGE = "Usage: metz-scan explain Metz/CopName"
|
|
10
|
+
NON_KNOB_KEYS = %w[Description VersionAdded StyleGuide Reference].freeze
|
|
11
|
+
|
|
12
|
+
def self.run(argv, stdout:, stderr:)
|
|
13
|
+
new(stdout: stdout, stderr: stderr).run(argv)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(stdout:, stderr:)
|
|
17
|
+
@stdout = stdout
|
|
18
|
+
@stderr = stderr
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run(argv)
|
|
22
|
+
args = parse_options(argv.dup)
|
|
23
|
+
return missing_argument_error if args.empty?
|
|
24
|
+
|
|
25
|
+
emit_or_error(args.first)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def emit_or_error(cop_name)
|
|
29
|
+
cop = lookup_metz_cop(cop_name)
|
|
30
|
+
return unknown_cop_error(cop_name) unless cop
|
|
31
|
+
|
|
32
|
+
emit(cop)
|
|
33
|
+
0
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :stdout, :stderr
|
|
39
|
+
|
|
40
|
+
def parse_options(argv)
|
|
41
|
+
OptionParser.new { |opts| opts.banner = USAGE }.parse!(argv)
|
|
42
|
+
argv
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def lookup_metz_cop(name)
|
|
46
|
+
require "rubocop-metz"
|
|
47
|
+
cop = RuboCop::Cop::Registry.global.find_by_cop_name(name)
|
|
48
|
+
cop if cop && metz_cop?(cop)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def metz_cop?(cop)
|
|
52
|
+
cop.cop_name.start_with?("Metz/") && cop.respond_to?(:metz_metadata)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def missing_argument_error
|
|
56
|
+
stderr.puts "metz-scan explain: missing required COP_NAME argument."
|
|
57
|
+
stderr.puts USAGE
|
|
58
|
+
stderr.puts "Run `metz-scan rules` to see the list of available Metz cops."
|
|
59
|
+
1
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def unknown_cop_error(name)
|
|
63
|
+
stderr.puts "metz-scan explain: no such cop '#{name}'."
|
|
64
|
+
stderr.puts "Run `metz-scan rules` to see the list of available Metz cops."
|
|
65
|
+
1
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def emit(cop)
|
|
69
|
+
emit_heading(cop)
|
|
70
|
+
emit_why(cop)
|
|
71
|
+
emit_fix_safety(cop)
|
|
72
|
+
emit_suggested_next_moves(cop)
|
|
73
|
+
emit_configuration(cop)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def emit_heading(cop)
|
|
77
|
+
stdout.puts cop.cop_name
|
|
78
|
+
stdout.puts("=" * cop.cop_name.length)
|
|
79
|
+
stdout.puts
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def emit_why(cop)
|
|
83
|
+
stdout.puts "Why it matters:"
|
|
84
|
+
stdout.puts " #{cop.why_it_matters}"
|
|
85
|
+
stdout.puts
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def emit_fix_safety(cop)
|
|
89
|
+
stdout.puts "Fix safety: #{cop.fix_safety}"
|
|
90
|
+
stdout.puts
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def emit_suggested_next_moves(cop)
|
|
94
|
+
stdout.puts "Suggested next moves:"
|
|
95
|
+
cop.suggested_next_moves.each { |move| stdout.puts " - #{move}" }
|
|
96
|
+
stdout.puts
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def emit_configuration(cop)
|
|
100
|
+
knobs = configuration_knobs(cop.cop_name)
|
|
101
|
+
return if knobs.empty?
|
|
102
|
+
|
|
103
|
+
stdout.puts "Configuration:"
|
|
104
|
+
knobs.each { |key, value| stdout.puts " #{key}: #{format_value(value)}" }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def configuration_knobs(cop_name)
|
|
108
|
+
entry = default_config[cop_name] || {}
|
|
109
|
+
entry.except(*NON_KNOB_KEYS)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def default_config
|
|
113
|
+
@default_config ||= YAML.load_file(default_config_path)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def default_config_path
|
|
117
|
+
RuboCop::Metz::Plugin.new.rules(nil).value.to_s
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def format_value(value)
|
|
121
|
+
case value
|
|
122
|
+
when Array then value.empty? ? "[]" : value.join(", ")
|
|
123
|
+
else value.to_s
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
require_relative "scan/project_analyzer_runner"
|
|
7
|
+
|
|
8
|
+
module MetzScan
|
|
9
|
+
module Commands
|
|
10
|
+
class ProjectAnalyzers
|
|
11
|
+
USAGE = "Usage: metz-scan project-analyzers [--json]"
|
|
12
|
+
|
|
13
|
+
def self.run(argv, stdout:, stderr:)
|
|
14
|
+
new(stdout: stdout, stderr: stderr).run(argv)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(stdout:, stderr:)
|
|
18
|
+
@stdout = stdout
|
|
19
|
+
@stderr = stderr
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run(argv)
|
|
23
|
+
options = parse_options(argv)
|
|
24
|
+
return 1 unless options
|
|
25
|
+
|
|
26
|
+
entries = analyzer_entries
|
|
27
|
+
options[:json] ? emit_json(entries) : emit_table(entries)
|
|
28
|
+
0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :stdout, :stderr
|
|
34
|
+
|
|
35
|
+
def parse_options(argv)
|
|
36
|
+
options = { json: false }
|
|
37
|
+
OptionParser.new { |opts| configure_options(opts, options) }.parse!(argv)
|
|
38
|
+
options
|
|
39
|
+
rescue OptionParser::ParseError => e
|
|
40
|
+
parse_error(e)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def parse_error(error)
|
|
44
|
+
stderr.puts error.message
|
|
45
|
+
stderr.puts option_parser.help
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def configure_options(opts, options)
|
|
50
|
+
opts.banner = USAGE
|
|
51
|
+
opts.on("--json", "Emit a JSON array of project analyzers") { options[:json] = true }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def option_parser
|
|
55
|
+
OptionParser.new { |opts| configure_options(opts, {}) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def analyzer_entries
|
|
59
|
+
Scan::ProjectAnalyzerRunner::ANALYZERS.map { |analyzer| analyzer_entry(analyzer) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def analyzer_entry(analyzer)
|
|
63
|
+
{ name: analyzer::RULE_ID, status: analyzer::PROJECT_ANALYZER_STATUS,
|
|
64
|
+
default_output: Scan::ProjectAnalyzerRunner.default_output_analyzer?(analyzer),
|
|
65
|
+
confidence: analyzer::CONFIDENCE, triage_severity: analyzer::TRIAGE_SEVERITY,
|
|
66
|
+
triage_summary: analyzer::TRIAGE_SUMMARY, why_it_matters: analyzer::WHY,
|
|
67
|
+
suggested_next_moves: analyzer::SUGGESTED_NEXT_MOVES }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def emit_json(entries)
|
|
71
|
+
stdout.puts JSON.generate(entries)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def emit_table(entries)
|
|
75
|
+
widths = column_widths(entries)
|
|
76
|
+
stdout.puts row(%w[ANALYZER STATUS DEFAULT CONFIDENCE TRIAGE], widths)
|
|
77
|
+
stdout.puts row(widths.map { |_key, width| "-" * width }, widths)
|
|
78
|
+
entries.each { |entry| stdout.puts entry_row(entry, widths) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def column_widths(entries)
|
|
82
|
+
{ name: max_width(entries, :name, "ANALYZER"), status: max_width(entries, :status, "STATUS"),
|
|
83
|
+
default: "DEFAULT".length, confidence: max_width(entries, :confidence, "CONFIDENCE"),
|
|
84
|
+
triage: max_width(entries, :triage_severity, "TRIAGE") }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def max_width(entries, key, header)
|
|
88
|
+
[header.length, entries.map { |entry| entry.fetch(key).length }.max].max
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def entry_row(entry, widths)
|
|
92
|
+
row([entry.fetch(:name), entry.fetch(:status), default_label(entry),
|
|
93
|
+
entry.fetch(:confidence), entry.fetch(:triage_severity)], widths)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def default_label(entry)
|
|
97
|
+
entry.fetch(:default_output) ? "yes" : "no"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def row(values, widths)
|
|
101
|
+
values.zip(widths.values).map { |value, width| format("%-#{width}s", value) }.join(" ").rstrip
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
require_relative "scan/runner"
|
|
7
|
+
require_relative "scan/compliance_scorecard"
|
|
8
|
+
require_relative "scan/text_renderer"
|
|
9
|
+
require_relative "scan/sarif_renderer"
|
|
10
|
+
require_relative "scan/github_annotations_renderer"
|
|
11
|
+
|
|
12
|
+
module MetzScan
|
|
13
|
+
module Commands
|
|
14
|
+
class Report
|
|
15
|
+
USAGE = "Usage: metz-scan report PATH/TO/JSON [--format text|json|sarif|gh-annotations]"
|
|
16
|
+
VALID_FORMATS = %w[text json sarif gh-annotations].freeze
|
|
17
|
+
RENDERERS = { "sarif" => Scan::SarifRenderer, "gh-annotations" => Scan::GithubAnnotationsRenderer }.freeze
|
|
18
|
+
DEFAULT_FORMAT = "text"
|
|
19
|
+
|
|
20
|
+
Options = Struct.new(:format, :path, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
def self.run(argv, stdout:, stderr:)
|
|
23
|
+
new(stdout: stdout, stderr: stderr).run(argv)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(stdout:, stderr:)
|
|
27
|
+
@stdout = stdout
|
|
28
|
+
@stderr = stderr
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run(argv)
|
|
32
|
+
options = parse_options(argv)
|
|
33
|
+
validate(options) || emit(options)
|
|
34
|
+
rescue OptionParser::ParseError => e
|
|
35
|
+
parser_error(e)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
attr_reader :stdout, :stderr
|
|
41
|
+
|
|
42
|
+
def parse_options(argv)
|
|
43
|
+
flags = { format: DEFAULT_FORMAT }
|
|
44
|
+
rest = OptionParser.new { |opts| configure_parser(opts, flags) }.parse(argv)
|
|
45
|
+
Options.new(**flags, path: rest.first)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def configure_parser(opts, flags)
|
|
49
|
+
opts.banner = USAGE
|
|
50
|
+
opts.on("--format FORMAT", "Output format: text (default), json, sarif, gh-annotations") do |format|
|
|
51
|
+
flags[:format] = format
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate(options)
|
|
56
|
+
return missing_path_arg unless options.path
|
|
57
|
+
return missing_file(options.path) unless File.file?(options.path)
|
|
58
|
+
|
|
59
|
+
invalid_format(options.format) unless VALID_FORMATS.include?(options.format)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def emit(options)
|
|
63
|
+
parsed = load_json(options.path)
|
|
64
|
+
return parsed if parsed.is_a?(Integer)
|
|
65
|
+
|
|
66
|
+
render(parsed, options.format)
|
|
67
|
+
Scan::Runner.exit_code_for(parsed)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def load_json(path)
|
|
71
|
+
JSON.parse(File.read(path))
|
|
72
|
+
rescue JSON::ParserError => e
|
|
73
|
+
invalid_json(path, e.message)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render(parsed, format)
|
|
77
|
+
return render_json(parsed) if format == "json"
|
|
78
|
+
|
|
79
|
+
RENDERERS.fetch(format, Scan::TextRenderer).new(stdout, parsed).render
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def render_json(parsed)
|
|
83
|
+
Scan::ComplianceScorecard.add_to_summary!(parsed)
|
|
84
|
+
stdout.puts JSON.generate(parsed)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def parser_error(err)
|
|
88
|
+
stderr.puts "metz-scan report: #{err.message}"
|
|
89
|
+
stderr.puts USAGE
|
|
90
|
+
1
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def missing_path_arg
|
|
94
|
+
stderr.puts "metz-scan report: missing PATH argument."
|
|
95
|
+
stderr.puts USAGE
|
|
96
|
+
1
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def missing_file(path)
|
|
100
|
+
stderr.puts "metz-scan report: no such file: #{path}"
|
|
101
|
+
1
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def invalid_format(fmt)
|
|
105
|
+
stderr.puts "metz-scan report: invalid --format '#{fmt}'. Valid formats: text, json, sarif, gh-annotations."
|
|
106
|
+
1
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def invalid_json(path, detail)
|
|
110
|
+
stderr.puts "metz-scan report: invalid JSON in #{path}: #{detail}"
|
|
111
|
+
1
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
|
|
6
|
+
module MetzScan
|
|
7
|
+
module Commands
|
|
8
|
+
class Rules
|
|
9
|
+
ANSI_RESET = "\e[0m"
|
|
10
|
+
ANSI_BOLD = "\e[1m"
|
|
11
|
+
ANSI_CYAN = "\e[36m"
|
|
12
|
+
ANSI_GREY = "\e[90m"
|
|
13
|
+
|
|
14
|
+
USAGE = "Usage: metz-scan rules [--json]"
|
|
15
|
+
|
|
16
|
+
def self.run(argv, stdout:, stderr:)
|
|
17
|
+
new(stdout: stdout, stderr: stderr).run(argv)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(stdout:, stderr:)
|
|
21
|
+
@stdout = stdout
|
|
22
|
+
@stderr = stderr
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def run(argv)
|
|
26
|
+
options = parse_options(argv)
|
|
27
|
+
cops = load_cops
|
|
28
|
+
options[:json] ? emit_json(cops) : emit_table(cops)
|
|
29
|
+
0
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :stdout, :stderr
|
|
35
|
+
|
|
36
|
+
def parse_options(argv)
|
|
37
|
+
options = { json: false }
|
|
38
|
+
OptionParser.new { |opts| configure_options(opts, options) }.parse!(argv)
|
|
39
|
+
options
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def configure_options(opts, options)
|
|
43
|
+
opts.banner = USAGE
|
|
44
|
+
opts.on("--json", "Emit a JSON array of all Metz cops") { options[:json] = true }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def load_cops
|
|
48
|
+
require "rubocop-metz"
|
|
49
|
+
RuboCop::Cop::Registry.global.cops.select { |cop| cop.cop_name.start_with?("Metz/") }.sort_by(&:cop_name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def emit_json(cops)
|
|
53
|
+
stdout.puts JSON.generate(cops.map { |cop| json_entry(cop) })
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def json_entry(cop)
|
|
57
|
+
{ name: cop.cop_name, why_it_matters: cop.why_it_matters,
|
|
58
|
+
fix_safety: cop.fix_safety.to_s, suggested_next_moves: Array(cop.suggested_next_moves) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def emit_table(cops)
|
|
62
|
+
name_width = cops.map { |cop| cop.cop_name.length }.max
|
|
63
|
+
colorize = tty?
|
|
64
|
+
stdout.puts header_row(name_width, colorize)
|
|
65
|
+
stdout.puts separator_row(name_width, colorize)
|
|
66
|
+
cops.each { |cop| stdout.puts data_row(cop, name_width, colorize) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def tty?
|
|
70
|
+
stdout.respond_to?(:tty?) && stdout.tty?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def header_row(name_width, colorize)
|
|
74
|
+
decorate(format("%-#{name_width}s %s", "COP", "WHY IT MATTERS"), ANSI_BOLD, colorize)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def separator_row(name_width, colorize)
|
|
78
|
+
dashes = ("-" * name_width)
|
|
79
|
+
decorate("#{dashes} #{'-' * 14}", ANSI_GREY, colorize)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def data_row(cop, name_width, colorize)
|
|
83
|
+
colored_name = decorate(format("%-#{name_width}s", cop.cop_name), ANSI_CYAN, colorize)
|
|
84
|
+
"#{colored_name} #{cop.why_it_matters}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def decorate(text, code, colorize)
|
|
88
|
+
colorize ? "#{code}#{text}#{ANSI_RESET}" : text
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|