deadfinder 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/deadfinder/cli.rb +3 -1
- data/lib/deadfinder/runner.rb +19 -0
- data/lib/deadfinder/version.rb +1 -1
- data/lib/deadfinder.rb +73 -7
- metadata +26 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c4f184329cd4314f65259f1f4b316c50ad3953bd52eeb81fbac45907eca25ab
|
4
|
+
data.tar.gz: d6e046f05ca1088d3a9a593fce99cc52dc582687e9982b68b1b604d3e1573918
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c8dd4b7c1047808c8ac57228f9b1ab7ea02b948e8f42c37766a189058b7b44cb50822b4d4632faf45b27046364bdb08b0cc679c7ed2c8a95f2dacef818085aa
|
7
|
+
data.tar.gz: af74057d1c8e03e69e47253b07f685796f3978e9c5da75a33efd33ae7d8f004f147555a03c529c33bd871fd7830b887cc27c1a9e851f93d8725daa24036cefef
|
data/lib/deadfinder/cli.rb
CHANGED
@@ -15,7 +15,7 @@ module DeadFinder
|
|
15
15
|
class_option :headers, aliases: :H, default: [], type: :array,
|
16
16
|
desc: 'Custom HTTP headers to send with initial request'
|
17
17
|
class_option :worker_headers, default: [], type: :array, desc: 'Custom HTTP headers to send with worker requests'
|
18
|
-
class_option :user_agent, default: 'Mozilla/5.0 (compatible; DeadFinder/1.
|
18
|
+
class_option :user_agent, default: 'Mozilla/5.0 (compatible; DeadFinder/1.8.0;)', type: :string,
|
19
19
|
desc: 'User-Agent string to use for requests'
|
20
20
|
class_option :proxy, aliases: :p, default: '', type: :string, desc: 'Proxy server to use for requests'
|
21
21
|
class_option :proxy_auth, default: '', type: :string, desc: 'Proxy server authentication credentials'
|
@@ -24,6 +24,8 @@ module DeadFinder
|
|
24
24
|
class_option :silent, aliases: :s, default: false, type: :boolean, desc: 'Silent mode'
|
25
25
|
class_option :verbose, aliases: :v, default: false, type: :boolean, desc: 'Verbose mode'
|
26
26
|
class_option :debug, default: false, type: :boolean, desc: 'Debug mode'
|
27
|
+
class_option :limit, default: 0, type: :numeric, desc: 'Limit the number of URLs to scan'
|
28
|
+
class_option :coverage, default: false, type: :boolean, desc: 'Enable coverage tracking and reporting'
|
27
29
|
|
28
30
|
def self.exit_on_failure?
|
29
31
|
true
|
data/lib/deadfinder/runner.rb
CHANGED
@@ -83,6 +83,15 @@ module DeadFinder
|
|
83
83
|
jobs.close
|
84
84
|
|
85
85
|
(1..jobs_size).each { ~results }
|
86
|
+
|
87
|
+
# Log coverage summary if tracking was enabled
|
88
|
+
if options['coverage'] && DeadFinder.coverage_data[target] && DeadFinder.coverage_data[target][:total] > 0
|
89
|
+
total = DeadFinder.coverage_data[target][:total]
|
90
|
+
dead = DeadFinder.coverage_data[target][:dead]
|
91
|
+
percentage = ((dead.to_f / total) * 100).round(2)
|
92
|
+
DeadFinder::Logger.sub_info "Coverage: #{dead}/#{total} URLs are dead links (#{percentage}%)"
|
93
|
+
end
|
94
|
+
|
86
95
|
DeadFinder::Logger.sub_complete 'Task completed'
|
87
96
|
rescue StandardError => e
|
88
97
|
DeadFinder::Logger.error "[#{e}] #{target}"
|
@@ -94,6 +103,12 @@ module DeadFinder
|
|
94
103
|
# Skip if already cached
|
95
104
|
else
|
96
105
|
CACHE_SET[j] = true
|
106
|
+
# Track total URLs tested for coverage calculation (only if coverage flag is enabled)
|
107
|
+
if options['coverage']
|
108
|
+
DeadFinder.coverage_data[target] ||= { total: 0, dead: 0 }
|
109
|
+
DeadFinder.coverage_data[target][:total] += 1
|
110
|
+
end
|
111
|
+
|
97
112
|
begin
|
98
113
|
CACHE_QUE[j] = true
|
99
114
|
uri = URI.parse(j)
|
@@ -114,11 +129,15 @@ module DeadFinder
|
|
114
129
|
CACHE_QUE[j] = false
|
115
130
|
DeadFinder.output[target] ||= []
|
116
131
|
DeadFinder.output[target] << j
|
132
|
+
# Track dead URLs for coverage calculation (only if coverage flag is enabled)
|
133
|
+
DeadFinder.coverage_data[target][:dead] += 1 if options['coverage']
|
117
134
|
else
|
118
135
|
DeadFinder::Logger.verbose_ok "[#{status_code}] #{j}" if options['verbose']
|
119
136
|
end
|
120
137
|
rescue StandardError => e
|
121
138
|
DeadFinder::Logger.verbose "[#{e}] #{j}" if options['verbose']
|
139
|
+
# Consider errored URLs as dead for coverage calculation (only if coverage flag is enabled)
|
140
|
+
DeadFinder.coverage_data[target][:dead] += 1 if options['coverage']
|
122
141
|
end
|
123
142
|
end
|
124
143
|
results << j
|
data/lib/deadfinder/version.rb
CHANGED
data/lib/deadfinder.rb
CHANGED
@@ -29,6 +29,15 @@ module DeadFinder
|
|
29
29
|
@output = val
|
30
30
|
end
|
31
31
|
|
32
|
+
@coverage_data = {}
|
33
|
+
def self.coverage_data
|
34
|
+
@coverage_data
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.coverage_data=(val)
|
38
|
+
@coverage_data = val
|
39
|
+
end
|
40
|
+
|
32
41
|
def self.run_pipe(options)
|
33
42
|
run_with_input(options) { $stdin.gets&.chomp }
|
34
43
|
end
|
@@ -40,6 +49,7 @@ module DeadFinder
|
|
40
49
|
def self.run_url(url, options)
|
41
50
|
DeadFinder::Logger.apply_options(options)
|
42
51
|
run_with_target(url, options)
|
52
|
+
gen_output(options)
|
43
53
|
end
|
44
54
|
|
45
55
|
def self.run_sitemap(sitemap_url, options)
|
@@ -47,8 +57,10 @@ module DeadFinder
|
|
47
57
|
app = Runner.new
|
48
58
|
base_uri = URI(sitemap_url)
|
49
59
|
sitemap = SitemapParser.new(sitemap_url, recurse: true)
|
50
|
-
|
51
|
-
|
60
|
+
urls = sitemap.to_a
|
61
|
+
urls = urls.first(options['limit']) if options['limit'].positive?
|
62
|
+
DeadFinder::Logger.info "Found #{urls.size} URLs from #{sitemap_url}"
|
63
|
+
urls.each do |url|
|
52
64
|
turl = generate_url(url, base_uri)
|
53
65
|
run_with_target(turl, options, app)
|
54
66
|
end
|
@@ -59,7 +71,9 @@ module DeadFinder
|
|
59
71
|
DeadFinder::Logger.apply_options(options)
|
60
72
|
DeadFinder::Logger.info 'Reading input'
|
61
73
|
app = Runner.new
|
62
|
-
Array(yield)
|
74
|
+
targets = Array(yield)
|
75
|
+
targets = targets.first(options['limit']) if options['limit'].positive?
|
76
|
+
targets.each do |target|
|
63
77
|
run_with_target(target, options, app)
|
64
78
|
end
|
65
79
|
gen_output(options)
|
@@ -70,30 +84,82 @@ module DeadFinder
|
|
70
84
|
app.run(target, options)
|
71
85
|
end
|
72
86
|
|
87
|
+
def self.calculate_coverage
|
88
|
+
coverage_summary = {}
|
89
|
+
total_all_tested = 0
|
90
|
+
total_all_dead = 0
|
91
|
+
|
92
|
+
coverage_data.each do |target, data|
|
93
|
+
total = data[:total]
|
94
|
+
dead = data[:dead]
|
95
|
+
coverage_percentage = total.positive? ? ((dead.to_f / total) * 100).round(2) : 0.0
|
96
|
+
|
97
|
+
coverage_summary[target] = {
|
98
|
+
total_tested: total,
|
99
|
+
dead_links: dead,
|
100
|
+
coverage_percentage: coverage_percentage
|
101
|
+
}
|
102
|
+
|
103
|
+
total_all_tested += total
|
104
|
+
total_all_dead += dead
|
105
|
+
end
|
106
|
+
|
107
|
+
overall_coverage = total_all_tested.positive? ? ((total_all_dead.to_f / total_all_tested) * 100).round(2) : 0.0
|
108
|
+
|
109
|
+
{
|
110
|
+
targets: coverage_summary,
|
111
|
+
summary: {
|
112
|
+
total_tested: total_all_tested,
|
113
|
+
total_dead: total_all_dead,
|
114
|
+
overall_coverage_percentage: overall_coverage
|
115
|
+
}
|
116
|
+
}
|
117
|
+
end
|
118
|
+
|
73
119
|
def self.gen_output(options)
|
74
120
|
return if options['output'].empty?
|
75
121
|
|
76
122
|
output_data = DeadFinder.output.to_h
|
77
123
|
format = options['output_format'].to_s.downcase
|
78
124
|
|
125
|
+
# Include coverage data only if coverage flag is enabled and data exists
|
126
|
+
coverage_info = calculate_coverage if options['coverage'] && coverage_data.any? && coverage_data.values.any? { |v| v[:total].positive? }
|
127
|
+
|
79
128
|
content = case format
|
80
129
|
when 'yaml', 'yml'
|
81
|
-
output_data
|
130
|
+
output_with_coverage = coverage_info ? { 'dead_links' => output_data, 'coverage' => coverage_info } : output_data
|
131
|
+
output_with_coverage.to_yaml
|
82
132
|
when 'csv'
|
83
|
-
generate_csv(output_data)
|
133
|
+
generate_csv(output_data, coverage_info)
|
84
134
|
else
|
85
|
-
|
135
|
+
output_with_coverage = coverage_info ? { 'dead_links' => output_data, 'coverage' => coverage_info } : output_data
|
136
|
+
JSON.pretty_generate(output_with_coverage)
|
86
137
|
end
|
87
138
|
|
88
139
|
File.write(options['output'], content)
|
89
140
|
end
|
90
141
|
|
91
|
-
def self.generate_csv(output_data)
|
142
|
+
def self.generate_csv(output_data, coverage_info = nil)
|
92
143
|
CSV.generate do |csv|
|
93
144
|
csv << %w[target url]
|
94
145
|
output_data.each do |target, urls|
|
95
146
|
Array(urls).each { |url| csv << [target, url] }
|
96
147
|
end
|
148
|
+
|
149
|
+
# Add coverage information as additional rows if available
|
150
|
+
if coverage_info
|
151
|
+
csv << [] # Empty row separator
|
152
|
+
csv << ['Coverage Report']
|
153
|
+
csv << %w[target total_tested dead_links coverage_percentage]
|
154
|
+
coverage_info[:targets].each do |target, data|
|
155
|
+
csv << [target, data[:total_tested], data[:dead_links], "#{data[:coverage_percentage]}%"]
|
156
|
+
end
|
157
|
+
csv << [] # Empty row separator
|
158
|
+
csv << ['Overall Summary']
|
159
|
+
csv << %w[total_tested total_dead overall_coverage_percentage]
|
160
|
+
summary = coverage_info[:summary]
|
161
|
+
csv << [summary[:total_tested], summary[:total_dead], "#{summary[:overall_coverage_percentage]}%"]
|
162
|
+
end
|
97
163
|
end
|
98
164
|
end
|
99
165
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: deadfinder
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- hahwul
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-08-23 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: colorize
|
@@ -53,42 +53,42 @@ dependencies:
|
|
53
53
|
name: json
|
54
54
|
requirement: !ruby/object:Gem::Requirement
|
55
55
|
requirements:
|
56
|
-
- - "~>"
|
57
|
-
- !ruby/object:Gem::Version
|
58
|
-
version: 2.6.0
|
59
56
|
- - ">="
|
60
57
|
- !ruby/object:Gem::Version
|
61
|
-
version: 2.6
|
58
|
+
version: '2.6'
|
59
|
+
- - "<"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '2.11'
|
62
62
|
type: :runtime
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 2.6.0
|
69
66
|
- - ">="
|
70
67
|
- !ruby/object:Gem::Version
|
71
|
-
version: 2.6
|
68
|
+
version: '2.6'
|
69
|
+
- - "<"
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '2.11'
|
72
72
|
- !ruby/object:Gem::Dependency
|
73
73
|
name: nokogiri
|
74
74
|
requirement: !ruby/object:Gem::Requirement
|
75
75
|
requirements:
|
76
|
-
- - "~>"
|
77
|
-
- !ruby/object:Gem::Version
|
78
|
-
version: 1.13.0
|
79
76
|
- - ">="
|
80
77
|
- !ruby/object:Gem::Version
|
81
|
-
version: 1.13
|
78
|
+
version: '1.13'
|
79
|
+
- - "<"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '1.19'
|
82
82
|
type: :runtime
|
83
83
|
prerelease: false
|
84
84
|
version_requirements: !ruby/object:Gem::Requirement
|
85
85
|
requirements:
|
86
|
-
- - "~>"
|
87
|
-
- !ruby/object:Gem::Version
|
88
|
-
version: 1.13.0
|
89
86
|
- - ">="
|
90
87
|
- !ruby/object:Gem::Version
|
91
|
-
version: 1.13
|
88
|
+
version: '1.13'
|
89
|
+
- - "<"
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '1.19'
|
92
92
|
- !ruby/object:Gem::Dependency
|
93
93
|
name: open-uri
|
94
94
|
requirement: !ruby/object:Gem::Requirement
|
@@ -153,22 +153,22 @@ dependencies:
|
|
153
153
|
name: thor
|
154
154
|
requirement: !ruby/object:Gem::Requirement
|
155
155
|
requirements:
|
156
|
-
- - "~>"
|
157
|
-
- !ruby/object:Gem::Version
|
158
|
-
version: 1.2.0
|
159
156
|
- - ">="
|
160
157
|
- !ruby/object:Gem::Version
|
161
|
-
version: 1.2
|
158
|
+
version: '1.2'
|
159
|
+
- - "<"
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '1.5'
|
162
162
|
type: :runtime
|
163
163
|
prerelease: false
|
164
164
|
version_requirements: !ruby/object:Gem::Requirement
|
165
165
|
requirements:
|
166
|
-
- - "~>"
|
167
|
-
- !ruby/object:Gem::Version
|
168
|
-
version: 1.2.0
|
169
166
|
- - ">="
|
170
167
|
- !ruby/object:Gem::Version
|
171
|
-
version: 1.2
|
168
|
+
version: '1.2'
|
169
|
+
- - "<"
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '1.5'
|
172
172
|
- !ruby/object:Gem::Dependency
|
173
173
|
name: rspec
|
174
174
|
requirement: !ruby/object:Gem::Requirement
|