necropsy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/Rakefile +8 -0
- data/exe/necropsy +6 -0
- data/lib/necropsy/analyzer.rb +19 -0
- data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +196 -0
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +69 -0
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +376 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +18 -0
- data/lib/necropsy/analyzers/static/cha.rb +106 -0
- data/lib/necropsy/analyzers/static/name_resolution.rb +53 -0
- data/lib/necropsy/analyzers/static/rta.rb +79 -0
- data/lib/necropsy/ast_scanner.rb +616 -0
- data/lib/necropsy/bench/evaluator.rb +127 -0
- data/lib/necropsy/cache/scan_cache.rb +158 -0
- data/lib/necropsy/cli.rb +287 -0
- data/lib/necropsy/confidence/scorer.rb +158 -0
- data/lib/necropsy/configuration.rb +116 -0
- data/lib/necropsy/coverage_runtime.rb +13 -0
- data/lib/necropsy/entry_points/plain.rb +36 -0
- data/lib/necropsy/entry_points/rails.rb +335 -0
- data/lib/necropsy/entry_points/test.rb +13 -0
- data/lib/necropsy/graph/call_graph.rb +199 -0
- data/lib/necropsy/guardrail/baseline.rb +47 -0
- data/lib/necropsy/guardrail/diff.rb +16 -0
- data/lib/necropsy/guardrail/quarantine.rb +46 -0
- data/lib/necropsy/models.rb +164 -0
- data/lib/necropsy/project.rb +65 -0
- data/lib/necropsy/reachability/engine.rb +42 -0
- data/lib/necropsy/report.rb +50 -0
- data/lib/necropsy/reporter.rb +112 -0
- data/lib/necropsy/runner.rb +74 -0
- data/lib/necropsy/version.rb +5 -0
- data/lib/necropsy.rb +46 -0
- metadata +95 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Necropsy
|
|
4
|
+
class Reporter
|
|
5
|
+
def initialize(report)
|
|
6
|
+
@report = report
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def render(format: :human, min_confidence: :low)
|
|
10
|
+
case format.to_sym
|
|
11
|
+
when :json
|
|
12
|
+
report.to_json
|
|
13
|
+
when :sarif
|
|
14
|
+
render_sarif(min_confidence)
|
|
15
|
+
when :github, :annotations
|
|
16
|
+
render_github_annotations(min_confidence)
|
|
17
|
+
when :yaml, :yml
|
|
18
|
+
report.to_yaml
|
|
19
|
+
else
|
|
20
|
+
render_human(min_confidence)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :report
|
|
27
|
+
|
|
28
|
+
def render_human(min_confidence)
|
|
29
|
+
findings = report.dead_methods(min_confidence: min_confidence)
|
|
30
|
+
lines = [
|
|
31
|
+
'Necropsy report',
|
|
32
|
+
"Root: #{report.root}",
|
|
33
|
+
"Nodes: #{report.summary['nodes']}, Edges: #{report.summary['edges']}, Entry points: #{report.summary['entry_points']}",
|
|
34
|
+
"Findings: #{findings.length}"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
findings.group_by(&:classification).sort_by do |classification, _|
|
|
38
|
+
classification.to_s
|
|
39
|
+
end.each do |classification, group|
|
|
40
|
+
lines << ''
|
|
41
|
+
lines << "#{classification} (#{group.length})"
|
|
42
|
+
group.sort_by { |finding| [finding.node.file, finding.node.line, finding.node.id] }.each do |finding|
|
|
43
|
+
lines << " [#{finding.confidence}] #{finding.node.id} #{finding.node.file}:#{finding.node.line}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
lines.join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_github_annotations(min_confidence)
|
|
51
|
+
report.dead_methods(min_confidence: min_confidence).map do |finding|
|
|
52
|
+
message = "#{finding.classification} #{finding.node.id} confidence=#{finding.confidence}"
|
|
53
|
+
escaped = message.gsub('%', '%25').gsub("\n", '%0A').gsub("\r", '%0D')
|
|
54
|
+
"::warning file=#{finding.node.file},line=#{finding.node.line},title=Necropsy #{finding.confidence}::#{escaped}"
|
|
55
|
+
end.join("\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def render_sarif(min_confidence)
|
|
59
|
+
findings = report.dead_methods(min_confidence: min_confidence)
|
|
60
|
+
{
|
|
61
|
+
'version' => '2.1.0',
|
|
62
|
+
'$schema' => 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
63
|
+
'runs' => [
|
|
64
|
+
{
|
|
65
|
+
'tool' => {
|
|
66
|
+
'driver' => {
|
|
67
|
+
'name' => 'Necropsy',
|
|
68
|
+
'informationUri' => 'https://github.com/ydah/necropsy',
|
|
69
|
+
'rules' => sarif_rules(findings)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
'results' => findings.map { |finding| sarif_result(finding) }
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}.to_json
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def sarif_rules(findings)
|
|
79
|
+
findings.map(&:classification).uniq.map do |classification|
|
|
80
|
+
{
|
|
81
|
+
'id' => classification.to_s,
|
|
82
|
+
'name' => classification.to_s,
|
|
83
|
+
'shortDescription' => { 'text' => "Necropsy #{classification}" }
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def sarif_result(finding)
|
|
89
|
+
{
|
|
90
|
+
'ruleId' => finding.classification.to_s,
|
|
91
|
+
'level' => sarif_level(finding),
|
|
92
|
+
'message' => { 'text' => "#{finding.node.id} is #{finding.classification} (#{finding.confidence})" },
|
|
93
|
+
'locations' => [
|
|
94
|
+
{
|
|
95
|
+
'physicalLocation' => {
|
|
96
|
+
'artifactLocation' => { 'uri' => finding.node.file },
|
|
97
|
+
'region' => { 'startLine' => finding.node.line }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
'partialFingerprints' => { 'necropsy' => finding.fingerprint }
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def sarif_level(finding)
|
|
106
|
+
return 'error' if %i[certain high].include?(finding.confidence)
|
|
107
|
+
return 'warning' if finding.confidence == :medium
|
|
108
|
+
|
|
109
|
+
'note'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Necropsy
|
|
4
|
+
class Runner
|
|
5
|
+
attr_reader :root, :config, :analyzers
|
|
6
|
+
|
|
7
|
+
def initialize(root:, config_path: nil, analyzers: nil)
|
|
8
|
+
@root = File.expand_path(root)
|
|
9
|
+
@config = Configuration.load(root: @root, path: config_path)
|
|
10
|
+
@analyzers = analyzers
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def analyze
|
|
14
|
+
project = Project.new(root: root, config: config)
|
|
15
|
+
graph = CallGraph.new(project.scan_result)
|
|
16
|
+
|
|
17
|
+
apply_entry_points(graph, project)
|
|
18
|
+
configured_analyzers.each do |analyzer|
|
|
19
|
+
graph.add_profile(analyzer.profile)
|
|
20
|
+
graph.apply_result(analyzer.analyze(graph, project))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
reachability = Reachability::Engine.new(graph).call
|
|
24
|
+
findings = Confidence::Scorer.new(graph: graph, reachability: reachability, project: project).findings
|
|
25
|
+
Report.new(root: root, graph: graph, findings: findings)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def apply_entry_points(graph, project)
|
|
31
|
+
EntryPoints::Plain.new.apply(graph, project)
|
|
32
|
+
EntryPoints::Rails.new.apply(graph, project)
|
|
33
|
+
EntryPoints::Test.new.apply(graph, project)
|
|
34
|
+
graph.entrypoint_hints.each { |entry| graph.add_entry_point(entry.node_id, entry.reason) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def configured_analyzers
|
|
38
|
+
return analyzers if analyzers
|
|
39
|
+
|
|
40
|
+
static = config.static_analyzers.filter_map do |name|
|
|
41
|
+
case name
|
|
42
|
+
when 'name_resolution'
|
|
43
|
+
Analyzers::Static::NameResolution.new
|
|
44
|
+
when 'cha'
|
|
45
|
+
Analyzers::Static::CHA.new
|
|
46
|
+
when 'rta'
|
|
47
|
+
Analyzers::Static::RTA.new
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
dynamic = []
|
|
52
|
+
coverage_config = config.dynamic_config(:coverage)
|
|
53
|
+
dynamic << Analyzers::Dynamic::CoverageImporter.new(coverage_config) if coverage_config['source']
|
|
54
|
+
coverband_config = config.dynamic_config(:coverband)
|
|
55
|
+
dynamic << Analyzers::Dynamic::CoverbandImporter.new(coverband_config) if coverband_config['source']
|
|
56
|
+
trace_point_config = config.dynamic_config(:trace_point)
|
|
57
|
+
dynamic << Analyzers::Dynamic::TracePointImporter.new(trace_point_config) if trace_point_config['source']
|
|
58
|
+
|
|
59
|
+
static + dynamic + custom_analyzers
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def custom_analyzers
|
|
63
|
+
config.custom_analyzers.map do |class_name|
|
|
64
|
+
constantize(class_name).new
|
|
65
|
+
rescue NameError => e
|
|
66
|
+
raise Error, "Could not load custom analyzer #{class_name}: #{e.message}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def constantize(class_name)
|
|
71
|
+
class_name.split('::').reduce(Object) { |constant, name| constant.const_get(name) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/necropsy.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'necropsy/version'
|
|
4
|
+
require_relative 'necropsy/models'
|
|
5
|
+
require_relative 'necropsy/configuration'
|
|
6
|
+
require_relative 'necropsy/ast_scanner'
|
|
7
|
+
require_relative 'necropsy/cache/scan_cache'
|
|
8
|
+
require_relative 'necropsy/project'
|
|
9
|
+
require_relative 'necropsy/graph/call_graph'
|
|
10
|
+
require_relative 'necropsy/analyzer'
|
|
11
|
+
require_relative 'necropsy/analyzers/static/name_resolution'
|
|
12
|
+
require_relative 'necropsy/analyzers/static/cha'
|
|
13
|
+
require_relative 'necropsy/analyzers/static/rta'
|
|
14
|
+
require_relative 'necropsy/analyzers/dynamic/coverage_importer'
|
|
15
|
+
require_relative 'necropsy/analyzers/dynamic/coverage_collector'
|
|
16
|
+
require_relative 'necropsy/analyzers/dynamic/coverband_importer'
|
|
17
|
+
require_relative 'necropsy/analyzers/dynamic/trace_point_importer'
|
|
18
|
+
require_relative 'necropsy/analyzers/dynamic/trace_point_collector'
|
|
19
|
+
require_relative 'necropsy/entry_points/plain'
|
|
20
|
+
require_relative 'necropsy/entry_points/rails'
|
|
21
|
+
require_relative 'necropsy/entry_points/test'
|
|
22
|
+
require_relative 'necropsy/reachability/engine'
|
|
23
|
+
require_relative 'necropsy/confidence/scorer'
|
|
24
|
+
require_relative 'necropsy/report'
|
|
25
|
+
require_relative 'necropsy/reporter'
|
|
26
|
+
require_relative 'necropsy/guardrail/baseline'
|
|
27
|
+
require_relative 'necropsy/guardrail/diff'
|
|
28
|
+
require_relative 'necropsy/guardrail/quarantine'
|
|
29
|
+
require_relative 'necropsy/bench/evaluator'
|
|
30
|
+
require_relative 'necropsy/runner'
|
|
31
|
+
|
|
32
|
+
module Necropsy
|
|
33
|
+
class Error < StandardError; end
|
|
34
|
+
|
|
35
|
+
def self.analyze(root: '.', config_path: nil, analyzers: nil)
|
|
36
|
+
Runner.new(root: root, config_path: config_path, analyzers: analyzers).analyze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.default_analyzers
|
|
40
|
+
[
|
|
41
|
+
Analyzers::Static::NameResolution.new,
|
|
42
|
+
Analyzers::Static::CHA.new,
|
|
43
|
+
Analyzers::Static::RTA.new
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: necropsy
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
description: Necropsy combines static call-graph reachability, optional dynamic evidence,
|
|
27
|
+
and CI guardrails for Ruby dead-code detection.
|
|
28
|
+
email:
|
|
29
|
+
- t.yudai92@gmail.com
|
|
30
|
+
executables:
|
|
31
|
+
- necropsy
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- LICENSE.txt
|
|
36
|
+
- README.md
|
|
37
|
+
- Rakefile
|
|
38
|
+
- exe/necropsy
|
|
39
|
+
- lib/necropsy.rb
|
|
40
|
+
- lib/necropsy/analyzer.rb
|
|
41
|
+
- lib/necropsy/analyzers/dynamic/coverage_collector.rb
|
|
42
|
+
- lib/necropsy/analyzers/dynamic/coverage_importer.rb
|
|
43
|
+
- lib/necropsy/analyzers/dynamic/coverband_importer.rb
|
|
44
|
+
- lib/necropsy/analyzers/dynamic/trace_point_collector.rb
|
|
45
|
+
- lib/necropsy/analyzers/dynamic/trace_point_importer.rb
|
|
46
|
+
- lib/necropsy/analyzers/static/cha.rb
|
|
47
|
+
- lib/necropsy/analyzers/static/name_resolution.rb
|
|
48
|
+
- lib/necropsy/analyzers/static/rta.rb
|
|
49
|
+
- lib/necropsy/ast_scanner.rb
|
|
50
|
+
- lib/necropsy/bench/evaluator.rb
|
|
51
|
+
- lib/necropsy/cache/scan_cache.rb
|
|
52
|
+
- lib/necropsy/cli.rb
|
|
53
|
+
- lib/necropsy/confidence/scorer.rb
|
|
54
|
+
- lib/necropsy/configuration.rb
|
|
55
|
+
- lib/necropsy/coverage_runtime.rb
|
|
56
|
+
- lib/necropsy/entry_points/plain.rb
|
|
57
|
+
- lib/necropsy/entry_points/rails.rb
|
|
58
|
+
- lib/necropsy/entry_points/test.rb
|
|
59
|
+
- lib/necropsy/graph/call_graph.rb
|
|
60
|
+
- lib/necropsy/guardrail/baseline.rb
|
|
61
|
+
- lib/necropsy/guardrail/diff.rb
|
|
62
|
+
- lib/necropsy/guardrail/quarantine.rb
|
|
63
|
+
- lib/necropsy/models.rb
|
|
64
|
+
- lib/necropsy/project.rb
|
|
65
|
+
- lib/necropsy/reachability/engine.rb
|
|
66
|
+
- lib/necropsy/report.rb
|
|
67
|
+
- lib/necropsy/reporter.rb
|
|
68
|
+
- lib/necropsy/runner.rb
|
|
69
|
+
- lib/necropsy/version.rb
|
|
70
|
+
homepage: https://github.com/ydah/necropsy
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata:
|
|
74
|
+
allowed_push_host: https://rubygems.org
|
|
75
|
+
homepage_uri: https://github.com/ydah/necropsy
|
|
76
|
+
source_code_uri: https://github.com/ydah/necropsy.git
|
|
77
|
+
rubygems_mfa_required: 'true'
|
|
78
|
+
rdoc_options: []
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: 3.2.0
|
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
requirements: []
|
|
92
|
+
rubygems_version: 4.0.6
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Hybrid dead-code detection for Ruby projects.
|
|
95
|
+
test_files: []
|