deadfinder 1.7.1 → 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 +16 -5
- data/lib/deadfinder/runner.rb +27 -0
- data/lib/deadfinder/version.rb +1 -1
- data/lib/deadfinder/visualizer.rb +109 -0
- data/lib/deadfinder.rb +88 -15
- metadata +46 -11
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'
|
@@ -24,6 +24,9 @@ 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'
|
29
|
+
class_option :visualize, default: '', type: :string, desc: 'Generate a visualization of the scan results (e.g., report.png)'
|
27
30
|
|
28
31
|
def self.exit_on_failure?
|
29
32
|
true
|
@@ -31,22 +34,30 @@ module DeadFinder
|
|
31
34
|
|
32
35
|
desc 'pipe', 'Scan the URLs from STDIN. (e.g., cat urls.txt | deadfinder pipe)'
|
33
36
|
def pipe
|
34
|
-
|
37
|
+
opts = options.dup
|
38
|
+
opts['coverage'] = true if opts['visualize'] && !opts['visualize'].empty?
|
39
|
+
DeadFinder.run_pipe opts
|
35
40
|
end
|
36
41
|
|
37
42
|
desc 'file <FILE>', 'Scan the URLs from File. (e.g., deadfinder file urls.txt)'
|
38
43
|
def file(filename)
|
39
|
-
|
44
|
+
opts = options.dup
|
45
|
+
opts['coverage'] = true if opts['visualize'] && !opts['visualize'].empty?
|
46
|
+
DeadFinder.run_file filename, opts
|
40
47
|
end
|
41
48
|
|
42
49
|
desc 'url <URL>', 'Scan the Single URL.'
|
43
50
|
def url(url)
|
44
|
-
|
51
|
+
opts = options.dup
|
52
|
+
opts['coverage'] = true if opts['visualize'] && !opts['visualize'].empty?
|
53
|
+
DeadFinder.run_url url, opts
|
45
54
|
end
|
46
55
|
|
47
56
|
desc 'sitemap <SITEMAP-URL>', 'Scan the URLs from sitemap.'
|
48
57
|
def sitemap(sitemap)
|
49
|
-
|
58
|
+
opts = options.dup
|
59
|
+
opts['coverage'] = true if opts['visualize'] && !opts['visualize'].empty?
|
60
|
+
DeadFinder.run_sitemap sitemap, opts
|
50
61
|
end
|
51
62
|
|
52
63
|
desc 'completion <SHELL>', 'Generate completion script for shell.'
|
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, status_counts: Hash.new(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,23 @@ 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
|
+
if options['coverage']
|
134
|
+
DeadFinder.coverage_data[target][:dead] += 1
|
135
|
+
DeadFinder.coverage_data[target][:status_counts][status_code] += 1
|
136
|
+
end
|
117
137
|
else
|
118
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']
|
119
141
|
end
|
120
142
|
rescue StandardError => e
|
121
143
|
DeadFinder::Logger.verbose "[#{e}] #{j}" if options['verbose']
|
144
|
+
# Consider errored URLs as dead for coverage calculation (only if coverage flag is enabled)
|
145
|
+
if options['coverage']
|
146
|
+
DeadFinder.coverage_data[target][:dead] += 1
|
147
|
+
DeadFinder.coverage_data[target][:status_counts]['error'] += 1
|
148
|
+
end
|
122
149
|
end
|
123
150
|
end
|
124
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'
|
@@ -29,6 +30,15 @@ module DeadFinder
|
|
29
30
|
@output = val
|
30
31
|
end
|
31
32
|
|
33
|
+
@coverage_data = {}
|
34
|
+
def self.coverage_data
|
35
|
+
@coverage_data
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.coverage_data=(val)
|
39
|
+
@coverage_data = val
|
40
|
+
end
|
41
|
+
|
32
42
|
def self.run_pipe(options)
|
33
43
|
run_with_input(options) { $stdin.gets&.chomp }
|
34
44
|
end
|
@@ -48,8 +58,10 @@ module DeadFinder
|
|
48
58
|
app = Runner.new
|
49
59
|
base_uri = URI(sitemap_url)
|
50
60
|
sitemap = SitemapParser.new(sitemap_url, recurse: true)
|
51
|
-
|
52
|
-
|
61
|
+
urls = sitemap.to_a
|
62
|
+
urls = urls.first(options['limit']) if options['limit'].positive?
|
63
|
+
DeadFinder::Logger.info "Found #{urls.size} URLs from #{sitemap_url}"
|
64
|
+
urls.each do |url|
|
53
65
|
turl = generate_url(url, base_uri)
|
54
66
|
run_with_target(turl, options, app)
|
55
67
|
end
|
@@ -60,7 +72,9 @@ module DeadFinder
|
|
60
72
|
DeadFinder::Logger.apply_options(options)
|
61
73
|
DeadFinder::Logger.info 'Reading input'
|
62
74
|
app = Runner.new
|
63
|
-
Array(yield)
|
75
|
+
targets = Array(yield)
|
76
|
+
targets = targets.first(options['limit']) if options['limit'].positive?
|
77
|
+
targets.each do |target|
|
64
78
|
run_with_target(target, options, app)
|
65
79
|
end
|
66
80
|
gen_output(options)
|
@@ -71,30 +85,89 @@ module DeadFinder
|
|
71
85
|
app.run(target, options)
|
72
86
|
end
|
73
87
|
|
74
|
-
def self.
|
75
|
-
|
88
|
+
def self.calculate_coverage
|
89
|
+
coverage_summary = {}
|
90
|
+
total_all_tested = 0
|
91
|
+
total_all_dead = 0
|
92
|
+
overall_status_counts = Hash.new(0)
|
93
|
+
|
94
|
+
coverage_data.each do |target, data|
|
95
|
+
total = data[:total]
|
96
|
+
dead = data[:dead]
|
97
|
+
status_counts = data[:status_counts] || {}
|
98
|
+
coverage_percentage = total.positive? ? ((dead.to_f / total) * 100).round(2) : 0.0
|
99
|
+
|
100
|
+
coverage_summary[target] = {
|
101
|
+
total_tested: total,
|
102
|
+
dead_links: dead,
|
103
|
+
coverage_percentage: coverage_percentage,
|
104
|
+
status_counts: status_counts
|
105
|
+
}
|
106
|
+
|
107
|
+
total_all_tested += total
|
108
|
+
total_all_dead += dead
|
109
|
+
status_counts.each { |code, count| overall_status_counts[code] += count }
|
110
|
+
end
|
76
111
|
|
112
|
+
overall_coverage = total_all_tested.positive? ? ((total_all_dead.to_f / total_all_tested) * 100).round(2) : 0.0
|
113
|
+
|
114
|
+
{
|
115
|
+
targets: coverage_summary,
|
116
|
+
summary: {
|
117
|
+
total_tested: total_all_tested,
|
118
|
+
total_dead: total_all_dead,
|
119
|
+
overall_coverage_percentage: overall_coverage,
|
120
|
+
overall_status_counts: overall_status_counts
|
121
|
+
}
|
122
|
+
}
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.gen_output(options)
|
77
126
|
output_data = DeadFinder.output.to_h
|
78
127
|
format = options['output_format'].to_s.downcase
|
128
|
+
# Include coverage data only if coverage flag is enabled and data exists
|
129
|
+
coverage_info = calculate_coverage if options['coverage'] && coverage_data.any? && coverage_data.values.any? { |v| v[:total].positive? }
|
79
130
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
88
146
|
|
89
|
-
|
147
|
+
DeadFinder::Visualizer.generate(coverage_info, options['visualize'])
|
90
148
|
end
|
91
149
|
|
92
|
-
def self.generate_csv(output_data)
|
150
|
+
def self.generate_csv(output_data, coverage_info = nil)
|
93
151
|
CSV.generate do |csv|
|
94
152
|
csv << %w[target url]
|
95
153
|
output_data.each do |target, urls|
|
96
154
|
Array(urls).each { |url| csv << [target, url] }
|
97
155
|
end
|
156
|
+
|
157
|
+
# Add coverage information as additional rows if available
|
158
|
+
if coverage_info
|
159
|
+
csv << [] # Empty row separator
|
160
|
+
csv << ['Coverage Report']
|
161
|
+
csv << %w[target total_tested dead_links coverage_percentage]
|
162
|
+
coverage_info[:targets].each do |target, data|
|
163
|
+
csv << [target, data[:total_tested], data[:dead_links], "#{data[:coverage_percentage]}%"]
|
164
|
+
end
|
165
|
+
csv << [] # Empty row separator
|
166
|
+
csv << ['Overall Summary']
|
167
|
+
csv << %w[total_tested total_dead overall_coverage_percentage]
|
168
|
+
summary = coverage_info[:summary]
|
169
|
+
csv << [summary[:total_tested], summary[:total_dead], "#{summary[:overall_coverage_percentage]}%"]
|
170
|
+
end
|
98
171
|
end
|
99
172
|
end
|
100
173
|
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.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
|
@@ -151,38 +151,72 @@ dependencies:
|
|
151
151
|
version: 0.5.0
|
152
152
|
- !ruby/object:Gem::Dependency
|
153
153
|
name: thor
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '1.2'
|
159
|
+
- - "<"
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '1.5'
|
162
|
+
type: :runtime
|
163
|
+
prerelease: false
|
164
|
+
version_requirements: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '1.2'
|
169
|
+
- - "<"
|
170
|
+
- !ruby/object:Gem::Version
|
171
|
+
version: '1.5'
|
172
|
+
- !ruby/object:Gem::Dependency
|
173
|
+
name: chunky_png
|
154
174
|
requirement: !ruby/object:Gem::Requirement
|
155
175
|
requirements:
|
156
176
|
- - "~>"
|
157
177
|
- !ruby/object:Gem::Version
|
158
|
-
version: 1.
|
178
|
+
version: 1.4.0
|
159
179
|
- - ">="
|
160
180
|
- !ruby/object:Gem::Version
|
161
|
-
version: 1.
|
181
|
+
version: 1.4.0
|
162
182
|
type: :runtime
|
163
183
|
prerelease: false
|
164
184
|
version_requirements: !ruby/object:Gem::Requirement
|
165
185
|
requirements:
|
166
186
|
- - "~>"
|
167
187
|
- !ruby/object:Gem::Version
|
168
|
-
version: 1.
|
188
|
+
version: 1.4.0
|
169
189
|
- - ">="
|
170
190
|
- !ruby/object:Gem::Version
|
171
|
-
version: 1.
|
191
|
+
version: 1.4.0
|
172
192
|
- !ruby/object:Gem::Dependency
|
173
193
|
name: rspec
|
174
194
|
requirement: !ruby/object:Gem::Requirement
|
175
195
|
requirements:
|
176
|
-
- - "
|
196
|
+
- - "~>"
|
177
197
|
- !ruby/object:Gem::Version
|
178
|
-
version: '
|
198
|
+
version: '3.12'
|
179
199
|
type: :development
|
180
200
|
prerelease: false
|
181
201
|
version_requirements: !ruby/object:Gem::Requirement
|
182
202
|
requirements:
|
183
|
-
- - "
|
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
|
+
- - "~>"
|
184
218
|
- !ruby/object:Gem::Version
|
185
|
-
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: []
|