deadfinder 1.8.0 → 1.9.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 +14 -5
- data/lib/deadfinder/runner.rb +14 -6
- data/lib/deadfinder/version.rb +1 -1
- data/lib/deadfinder/visualizer.rb +109 -0
- data/lib/deadfinder.rb +25 -17
- metadata +42 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d72852a66ecdb612a93361fa42c367d9c2505966016bf4ca0a76a099e6ab235
|
4
|
+
data.tar.gz: 19116106505f9c3060dce40406b8bc478f4ce01e49eccfddaa192fc981adcef2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b00c09cbe273376dd5cc60fb964d1a74146e6d98e5a21656d91202b792b6bcac3576ba825b3fc3d144f58f2e76a34ad08658f925d0469824b420fac1e2ea153
|
7
|
+
data.tar.gz: 4ae07f18471a1aa71018533259e14d99af53799e1f65479a6d953f44d7d5f182c97393747cb502a910878311ff70ec85a6e007c63aea20c8c58b78798f03b3cc
|
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.9.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'
|
@@ -26,6 +26,7 @@ module DeadFinder
|
|
26
26
|
class_option :debug, default: false, type: :boolean, desc: 'Debug mode'
|
27
27
|
class_option :limit, default: 0, type: :numeric, desc: 'Limit the number of URLs to scan'
|
28
28
|
class_option :coverage, default: false, type: :boolean, desc: 'Enable coverage tracking and reporting'
|
29
|
+
class_option :visualize, default: '', type: :string, desc: 'Generate a visualization of the scan results (e.g., report.png)'
|
29
30
|
|
30
31
|
def self.exit_on_failure?
|
31
32
|
true
|
@@ -33,22 +34,30 @@ module DeadFinder
|
|
33
34
|
|
34
35
|
desc 'pipe', 'Scan the URLs from STDIN. (e.g., cat urls.txt | deadfinder pipe)'
|
35
36
|
def pipe
|
36
|
-
|
37
|
+
opts = options.dup
|
38
|
+
opts['coverage'] = true if opts['visualize'] && !opts['visualize'].empty?
|
39
|
+
DeadFinder.run_pipe opts
|
37
40
|
end
|
38
41
|
|
39
42
|
desc 'file <FILE>', 'Scan the URLs from File. (e.g., deadfinder file urls.txt)'
|
40
43
|
def file(filename)
|
41
|
-
|
44
|
+
opts = options.dup
|
45
|
+
opts['coverage'] = true if opts['visualize'] && !opts['visualize'].empty?
|
46
|
+
DeadFinder.run_file filename, opts
|
42
47
|
end
|
43
48
|
|
44
49
|
desc 'url <URL>', 'Scan the Single URL.'
|
45
50
|
def url(url)
|
46
|
-
|
51
|
+
opts = options.dup
|
52
|
+
opts['coverage'] = true if opts['visualize'] && !opts['visualize'].empty?
|
53
|
+
DeadFinder.run_url url, opts
|
47
54
|
end
|
48
55
|
|
49
56
|
desc 'sitemap <SITEMAP-URL>', 'Scan the URLs from sitemap.'
|
50
57
|
def sitemap(sitemap)
|
51
|
-
|
58
|
+
opts = options.dup
|
59
|
+
opts['coverage'] = true if opts['visualize'] && !opts['visualize'].empty?
|
60
|
+
DeadFinder.run_sitemap sitemap, opts
|
52
61
|
end
|
53
62
|
|
54
63
|
desc 'completion <SHELL>', 'Generate completion script for shell.'
|
data/lib/deadfinder/runner.rb
CHANGED
@@ -83,7 +83,7 @@ module DeadFinder
|
|
83
83
|
jobs.close
|
84
84
|
|
85
85
|
(1..jobs_size).each { ~results }
|
86
|
-
|
86
|
+
|
87
87
|
# Log coverage summary if tracking was enabled
|
88
88
|
if options['coverage'] && DeadFinder.coverage_data[target] && DeadFinder.coverage_data[target][:total] > 0
|
89
89
|
total = DeadFinder.coverage_data[target][:total]
|
@@ -91,7 +91,7 @@ module DeadFinder
|
|
91
91
|
percentage = ((dead.to_f / total) * 100).round(2)
|
92
92
|
DeadFinder::Logger.sub_info "Coverage: #{dead}/#{total} URLs are dead links (#{percentage}%)"
|
93
93
|
end
|
94
|
-
|
94
|
+
|
95
95
|
DeadFinder::Logger.sub_complete 'Task completed'
|
96
96
|
rescue StandardError => e
|
97
97
|
DeadFinder::Logger.error "[#{e}] #{target}"
|
@@ -105,10 +105,10 @@ module DeadFinder
|
|
105
105
|
CACHE_SET[j] = true
|
106
106
|
# Track total URLs tested for coverage calculation (only if coverage flag is enabled)
|
107
107
|
if options['coverage']
|
108
|
-
DeadFinder.coverage_data[target] ||= { total: 0, dead: 0 }
|
108
|
+
DeadFinder.coverage_data[target] ||= { total: 0, dead: 0, status_counts: Hash.new(0) }
|
109
109
|
DeadFinder.coverage_data[target][:total] += 1
|
110
110
|
end
|
111
|
-
|
111
|
+
|
112
112
|
begin
|
113
113
|
CACHE_QUE[j] = true
|
114
114
|
uri = URI.parse(j)
|
@@ -130,14 +130,22 @@ module DeadFinder
|
|
130
130
|
DeadFinder.output[target] ||= []
|
131
131
|
DeadFinder.output[target] << j
|
132
132
|
# Track dead URLs for coverage calculation (only if coverage flag is enabled)
|
133
|
-
|
133
|
+
if options['coverage']
|
134
|
+
DeadFinder.coverage_data[target][:dead] += 1
|
135
|
+
DeadFinder.coverage_data[target][:status_counts][status_code] += 1
|
136
|
+
end
|
134
137
|
else
|
135
138
|
DeadFinder::Logger.verbose_ok "[#{status_code}] #{j}" if options['verbose']
|
139
|
+
# Track status for successful URLs
|
140
|
+
DeadFinder.coverage_data[target][:status_counts][status_code] += 1 if options['coverage']
|
136
141
|
end
|
137
142
|
rescue StandardError => e
|
138
143
|
DeadFinder::Logger.verbose "[#{e}] #{j}" if options['verbose']
|
139
144
|
# Consider errored URLs as dead for coverage calculation (only if coverage flag is enabled)
|
140
|
-
|
145
|
+
if options['coverage']
|
146
|
+
DeadFinder.coverage_data[target][:dead] += 1
|
147
|
+
DeadFinder.coverage_data[target][:status_counts]['error'] += 1
|
148
|
+
end
|
141
149
|
end
|
142
150
|
end
|
143
151
|
results << j
|
data/lib/deadfinder/version.rb
CHANGED
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chunky_png'
|
4
|
+
|
5
|
+
module DeadFinder
|
6
|
+
# Visualizer module for generating images from scan results
|
7
|
+
module Visualizer
|
8
|
+
def self.generate(data, output_path)
|
9
|
+
# Extract summary data
|
10
|
+
summary = data[:summary]
|
11
|
+
return if summary.nil?
|
12
|
+
|
13
|
+
total_tested = summary[:total_tested]
|
14
|
+
return if total_tested.nil? || total_tested.zero?
|
15
|
+
|
16
|
+
# Create a new image with transparent background
|
17
|
+
png = ChunkyPNG::Image.new(500, 300, ChunkyPNG::Color::TRANSPARENT)
|
18
|
+
|
19
|
+
# Draw stacked bar chart for status code distribution
|
20
|
+
status_counts = data[:summary][:overall_status_counts] || {}
|
21
|
+
bar_height = 70
|
22
|
+
current_y = 110
|
23
|
+
|
24
|
+
# Sort statuses by count descending
|
25
|
+
sorted_statuses = status_counts.sort_by { |_, v| -v }
|
26
|
+
|
27
|
+
sorted_statuses.each do |status, count|
|
28
|
+
height = (count.to_f / total_tested * bar_height).to_i
|
29
|
+
next if height.zero?
|
30
|
+
|
31
|
+
color = case status.to_s
|
32
|
+
when '200' then ChunkyPNG::Color.rgb(0, 255, 0) # Green for 200
|
33
|
+
when /^3\d{2}$/ then ChunkyPNG::Color.rgb(255, 165, 0) # Orange for 3xx
|
34
|
+
when /^4\d{2}$/ then ChunkyPNG::Color.rgb(255, 0, 0) # Red for 4xx
|
35
|
+
when /^5\d{2}$/ then ChunkyPNG::Color.rgb(128, 0, 128) # Purple for 5xx
|
36
|
+
else ChunkyPNG::Color.rgb(128, 128, 128) # Gray for others/error
|
37
|
+
end
|
38
|
+
|
39
|
+
(current_y..(current_y + height - 1)).each do |y|
|
40
|
+
(20..480).each do |x|
|
41
|
+
png[x, y] = color
|
42
|
+
end
|
43
|
+
end
|
44
|
+
current_y += height
|
45
|
+
end
|
46
|
+
|
47
|
+
# Draw rounded outline around the bar area
|
48
|
+
r = 10
|
49
|
+
x1 = 10
|
50
|
+
y1 = 100
|
51
|
+
x2 = 490
|
52
|
+
y2 = 190
|
53
|
+
|
54
|
+
# Top line
|
55
|
+
((x1 + r)..(x2 - r)).each do |x|
|
56
|
+
png[x, y1] = ChunkyPNG::Color.rgba(0, 0, 0, 128)
|
57
|
+
# Bottom line
|
58
|
+
png[x, y2] = ChunkyPNG::Color.rgba(0, 0, 0, 128)
|
59
|
+
end
|
60
|
+
# Left line
|
61
|
+
((y1 + r)..(y2 - r)).each do |y|
|
62
|
+
png[x1, y] = ChunkyPNG::Color.rgba(0, 0, 0, 128)
|
63
|
+
# Right line
|
64
|
+
png[x2, y] = ChunkyPNG::Color.rgba(0, 0, 0, 128)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Corners: quarter circles
|
68
|
+
# Top-left
|
69
|
+
(0..90).each do |angle|
|
70
|
+
rad = angle * Math::PI / 180
|
71
|
+
cx = x1 + r
|
72
|
+
cy = y1 + r
|
73
|
+
px = cx + (r * Math.cos(rad))
|
74
|
+
py = cy + (r * Math.sin(rad))
|
75
|
+
png[px.to_i, py.to_i] = ChunkyPNG::Color.rgba(0, 0, 0, 128) if px >= x1 && py >= y1
|
76
|
+
end
|
77
|
+
# Top-right
|
78
|
+
(90..180).each do |angle|
|
79
|
+
rad = angle * Math::PI / 180
|
80
|
+
cx = x2 - r
|
81
|
+
cy = y1 + r
|
82
|
+
px = cx + (r * Math.cos(rad))
|
83
|
+
py = cy + (r * Math.sin(rad))
|
84
|
+
png[px.to_i, py.to_i] = ChunkyPNG::Color.rgba(0, 0, 0, 128) if px <= x2 && py >= y1
|
85
|
+
end
|
86
|
+
# Bottom-left
|
87
|
+
(270..360).each do |angle|
|
88
|
+
rad = angle * Math::PI / 180
|
89
|
+
cx = x1 + r
|
90
|
+
cy = y2 - r
|
91
|
+
px = cx + (r * Math.cos(rad))
|
92
|
+
py = cy + (r * Math.sin(rad))
|
93
|
+
png[px.to_i, py.to_i] = ChunkyPNG::Color.rgba(0, 0, 0, 128) if px >= x1 && py <= y2
|
94
|
+
end
|
95
|
+
# Bottom-right
|
96
|
+
(180..270).each do |angle|
|
97
|
+
rad = angle * Math::PI / 180
|
98
|
+
cx = x2 - r
|
99
|
+
cy = y2 - r
|
100
|
+
px = cx + (r * Math.cos(rad))
|
101
|
+
py = cy + (r * Math.sin(rad))
|
102
|
+
png[px.to_i, py.to_i] = ChunkyPNG::Color.rgba(0, 0, 0, 128) if px <= x2 && py <= y2
|
103
|
+
end
|
104
|
+
|
105
|
+
# Save the image
|
106
|
+
png.save(output_path, :fast_rgba)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/deadfinder.rb
CHANGED
@@ -7,6 +7,7 @@ require 'nokogiri'
|
|
7
7
|
require 'deadfinder/utils'
|
8
8
|
require 'deadfinder/logger'
|
9
9
|
require 'deadfinder/runner'
|
10
|
+
require 'deadfinder/visualizer'
|
10
11
|
require 'deadfinder/cli'
|
11
12
|
require 'deadfinder/version'
|
12
13
|
require 'concurrent-edge'
|
@@ -88,20 +89,24 @@ module DeadFinder
|
|
88
89
|
coverage_summary = {}
|
89
90
|
total_all_tested = 0
|
90
91
|
total_all_dead = 0
|
92
|
+
overall_status_counts = Hash.new(0)
|
91
93
|
|
92
94
|
coverage_data.each do |target, data|
|
93
95
|
total = data[:total]
|
94
96
|
dead = data[:dead]
|
97
|
+
status_counts = data[:status_counts] || {}
|
95
98
|
coverage_percentage = total.positive? ? ((dead.to_f / total) * 100).round(2) : 0.0
|
96
99
|
|
97
100
|
coverage_summary[target] = {
|
98
101
|
total_tested: total,
|
99
102
|
dead_links: dead,
|
100
|
-
coverage_percentage: coverage_percentage
|
103
|
+
coverage_percentage: coverage_percentage,
|
104
|
+
status_counts: status_counts
|
101
105
|
}
|
102
106
|
|
103
107
|
total_all_tested += total
|
104
108
|
total_all_dead += dead
|
109
|
+
status_counts.each { |code, count| overall_status_counts[code] += count }
|
105
110
|
end
|
106
111
|
|
107
112
|
overall_coverage = total_all_tested.positive? ? ((total_all_dead.to_f / total_all_tested) * 100).round(2) : 0.0
|
@@ -111,32 +116,35 @@ module DeadFinder
|
|
111
116
|
summary: {
|
112
117
|
total_tested: total_all_tested,
|
113
118
|
total_dead: total_all_dead,
|
114
|
-
overall_coverage_percentage: overall_coverage
|
119
|
+
overall_coverage_percentage: overall_coverage,
|
120
|
+
overall_status_counts: overall_status_counts
|
115
121
|
}
|
116
122
|
}
|
117
123
|
end
|
118
124
|
|
119
125
|
def self.gen_output(options)
|
120
|
-
return if options['output'].empty?
|
121
|
-
|
122
126
|
output_data = DeadFinder.output.to_h
|
123
127
|
format = options['output_format'].to_s.downcase
|
124
|
-
|
125
128
|
# Include coverage data only if coverage flag is enabled and data exists
|
126
129
|
coverage_info = calculate_coverage if options['coverage'] && coverage_data.any? && coverage_data.values.any? { |v| v[:total].positive? }
|
127
130
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
131
|
+
unless options['output'].to_s.empty?
|
132
|
+
content = case format
|
133
|
+
when 'yaml', 'yml'
|
134
|
+
output_with_coverage = coverage_info ? { 'dead_links' => output_data, 'coverage' => coverage_info } : output_data
|
135
|
+
output_with_coverage.to_yaml
|
136
|
+
when 'csv'
|
137
|
+
generate_csv(output_data, coverage_info)
|
138
|
+
else
|
139
|
+
output_with_coverage = coverage_info ? { 'dead_links' => output_data, 'coverage' => coverage_info } : output_data
|
140
|
+
JSON.pretty_generate(output_with_coverage)
|
141
|
+
end
|
142
|
+
File.write(options['output'], content)
|
143
|
+
end
|
144
|
+
|
145
|
+
return unless options['visualize'] && !options['visualize'].empty? && coverage_info
|
146
|
+
|
147
|
+
DeadFinder::Visualizer.generate(coverage_info, options['visualize'])
|
140
148
|
end
|
141
149
|
|
142
150
|
def self.generate_csv(output_data, coverage_info = nil)
|
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.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- hahwul
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date:
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: colorize
|
@@ -170,19 +170,53 @@ dependencies:
|
|
170
170
|
- !ruby/object:Gem::Version
|
171
171
|
version: '1.5'
|
172
172
|
- !ruby/object:Gem::Dependency
|
173
|
-
name:
|
173
|
+
name: chunky_png
|
174
174
|
requirement: !ruby/object:Gem::Requirement
|
175
175
|
requirements:
|
176
|
+
- - "~>"
|
177
|
+
- !ruby/object:Gem::Version
|
178
|
+
version: 1.4.0
|
176
179
|
- - ">="
|
177
180
|
- !ruby/object:Gem::Version
|
178
|
-
version:
|
179
|
-
type: :
|
181
|
+
version: 1.4.0
|
182
|
+
type: :runtime
|
180
183
|
prerelease: false
|
181
184
|
version_requirements: !ruby/object:Gem::Requirement
|
182
185
|
requirements:
|
186
|
+
- - "~>"
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
version: 1.4.0
|
183
189
|
- - ">="
|
184
190
|
- !ruby/object:Gem::Version
|
185
|
-
version:
|
191
|
+
version: 1.4.0
|
192
|
+
- !ruby/object:Gem::Dependency
|
193
|
+
name: rspec
|
194
|
+
requirement: !ruby/object:Gem::Requirement
|
195
|
+
requirements:
|
196
|
+
- - "~>"
|
197
|
+
- !ruby/object:Gem::Version
|
198
|
+
version: '3.12'
|
199
|
+
type: :development
|
200
|
+
prerelease: false
|
201
|
+
version_requirements: !ruby/object:Gem::Requirement
|
202
|
+
requirements:
|
203
|
+
- - "~>"
|
204
|
+
- !ruby/object:Gem::Version
|
205
|
+
version: '3.12'
|
206
|
+
- !ruby/object:Gem::Dependency
|
207
|
+
name: cyclonedx-ruby
|
208
|
+
requirement: !ruby/object:Gem::Requirement
|
209
|
+
requirements:
|
210
|
+
- - "~>"
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: '1.4'
|
213
|
+
type: :development
|
214
|
+
prerelease: false
|
215
|
+
version_requirements: !ruby/object:Gem::Requirement
|
216
|
+
requirements:
|
217
|
+
- - "~>"
|
218
|
+
- !ruby/object:Gem::Version
|
219
|
+
version: '1.4'
|
186
220
|
description: Find dead-links (broken links). Dead link (broken link) means a link
|
187
221
|
within a web page that cannot be connected. These links can have a negative impact
|
188
222
|
to SEO and Security. This tool makes it easy to identify and modify.
|
@@ -202,6 +236,7 @@ files:
|
|
202
236
|
- lib/deadfinder/url_pattern_matcher.rb
|
203
237
|
- lib/deadfinder/utils.rb
|
204
238
|
- lib/deadfinder/version.rb
|
239
|
+
- lib/deadfinder/visualizer.rb
|
205
240
|
homepage: https://www.hahwul.com/projects/deadfinder/
|
206
241
|
licenses:
|
207
242
|
- MIT
|
@@ -222,7 +257,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
222
257
|
- !ruby/object:Gem::Version
|
223
258
|
version: '0'
|
224
259
|
requirements: []
|
225
|
-
rubygems_version: 3.6.
|
260
|
+
rubygems_version: 3.6.9
|
226
261
|
specification_version: 4
|
227
262
|
summary: Find dead-links (broken links)
|
228
263
|
test_files: []
|